diff --git a/web/package.json b/web/package.json index 1ea3aaa..8fe0f0f 100644 --- a/web/package.json +++ b/web/package.json @@ -14,9 +14,19 @@ }, "dependencies": { "@fontsource-variable/figtree": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.0.0", + "@fontsource/google-sans-code": "^5.0.0", + "@fontsource/monaspace-argon": "^5.0.0", + "@fontsource/monaspace-krypton": "^5.0.0", + "@fontsource/monaspace-neon": "^5.0.0", + "@fontsource/monaspace-radon": "^5.0.0", + "@fontsource/monaspace-xenon": "^5.0.0", + "@shikijs/rehype": "^3.0.0", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.4", "@tailwindcss/vite": "^4.1.18", + "@textcomplete/core": "^0.1.0", + "@textcomplete/textarea": "^0.1.0", "clsx": "^2.1.1", "motion": "^12.23.26", "rehype-external-links": "^3.0.0", @@ -24,6 +34,7 @@ "rehype-stringify": "^10.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", + "shiki": "^3.0.0", "solid-js": "^1.9.10", "solid-motionone": "^1.0.4", "tailwind-merge": "^3.4.0", @@ -37,6 +48,7 @@ "@iconify-json/ri": "^1.2.7", "@resvg/resvg-js": "^2.6.2", "@solidjs/testing-library": "^0.8.10", + "@tailwindcss/forms": "^0.5.11", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@types/node": "^24.10.1", @@ -53,5 +65,9 @@ "vite-plugin-solid": "^2.11.10", "vitest": "^4.0.16" }, - "pnpm": { "overrides": { "vite": "npm:rolldown-vite@7.2.5" } } + "pnpm": { + "overrides": { + "vite": "npm:rolldown-vite@7.2.5" + } + } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index a4ccb56..7129af2 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -14,6 +14,30 @@ importers: '@fontsource-variable/figtree': specifier: ^5.2.8 version: 5.2.10 + '@fontsource-variable/jetbrains-mono': + specifier: ^5.0.0 + version: 5.2.8 + '@fontsource/google-sans-code': + specifier: ^5.0.0 + version: 5.2.3 + '@fontsource/monaspace-argon': + specifier: ^5.0.0 + version: 5.2.5 + '@fontsource/monaspace-krypton': + specifier: ^5.0.0 + version: 5.2.5 + '@fontsource/monaspace-neon': + specifier: ^5.0.0 + version: 5.2.5 + '@fontsource/monaspace-radon': + specifier: ^5.0.0 + version: 5.2.5 + '@fontsource/monaspace-xenon': + specifier: ^5.0.0 + version: 5.2.5 + '@shikijs/rehype': + specifier: ^3.0.0 + version: 3.20.0 '@solidjs/meta': specifier: ^0.29.4 version: 0.29.4(solid-js@1.9.10) @@ -23,6 +47,12 @@ importers: '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(rolldown-vite@7.2.5(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.21.0)) + '@textcomplete/core': + specifier: ^0.1.0 + version: 0.1.13 + '@textcomplete/textarea': + specifier: ^0.1.0 + version: 0.1.13(@textcomplete/core@0.1.13) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -44,6 +74,9 @@ importers: remark-rehype: specifier: ^11.1.2 version: 11.1.2 + shiki: + specifier: ^3.0.0 + version: 3.20.0 solid-js: specifier: ^1.9.10 version: 1.9.10 @@ -78,6 +111,9 @@ importers: '@solidjs/testing-library': specifier: ^0.8.10 version: 0.8.10(@solidjs/router@0.15.4(solid-js@1.9.10))(solid-js@1.9.10) + '@tailwindcss/forms': + specifier: ^0.5.11 + version: 0.5.11(tailwindcss@4.1.18) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -484,6 +520,27 @@ packages: '@fontsource-variable/figtree@5.2.10': resolution: {integrity: sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA==} + '@fontsource-variable/jetbrains-mono@5.2.8': + resolution: {integrity: sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==} + + '@fontsource/google-sans-code@5.2.3': + resolution: {integrity: sha512-bCbCXFWtGIVNEe8O/mE7YpIuVjc3E4CV6pToJ0IZc9XcXKfa74yntZa3UgyzBRgabnx5bm6ciQVQvWGnwi4Rqw==} + + '@fontsource/monaspace-argon@5.2.5': + resolution: {integrity: sha512-EJ+jq1Smm3BB+8RK/gwB1uzjrSKdycZkKm8OZGCYvqJiTChKcGe7b2lmj0PmQbb/+lAEdCBIAcFLlPaWhtRANA==} + + '@fontsource/monaspace-krypton@5.2.5': + resolution: {integrity: sha512-dOWSb0DtSMfjbptckxTme2FKNfRkFtmTuNRQ86sBki/CanR4ajSrE+GuJoCm9Kl3352CvafoRt2fDo8FU+Gz4A==} + + '@fontsource/monaspace-neon@5.2.5': + resolution: {integrity: sha512-TjSSuHC37DroyhP5YCKRCAxdUb3In/uqHzqqW/8Ul+JWilz37d8lr8jFeapnbD3QdYXgiQoU2Rf/CmTdyl06DA==} + + '@fontsource/monaspace-radon@5.2.5': + resolution: {integrity: sha512-mG6ltrFW5BYmcoRvVIKboCORZu8BeGg6h5zV5rnwEmFcjv8Bltua3KOj6aOxWbrR4PYnKyR17lNqSsjR8kx4bQ==} + + '@fontsource/monaspace-xenon@5.2.5': + resolution: {integrity: sha512-k10RCF5nek9ee/rKSALaeYgOcJjjvzxxGkIlPwkSAoasjrsEeVKOSJ6ii1dMXOZwL9ZtW3p56Dxhgaw0Gtwvaw==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -718,6 +775,30 @@ packages: '@rolldown/pluginutils@1.0.0-beta.50': resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==} + '@shikijs/core@3.20.0': + resolution: {integrity: sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g==} + + '@shikijs/engine-javascript@3.20.0': + resolution: {integrity: sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg==} + + '@shikijs/engine-oniguruma@3.20.0': + resolution: {integrity: sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==} + + '@shikijs/langs@3.20.0': + resolution: {integrity: sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==} + + '@shikijs/rehype@3.20.0': + resolution: {integrity: sha512-/sqob3V/lJK0m2mZ64nkcWPN88im0D9atkI3S3PUBvtJZTHnJXVwZhHQFRDyObgEIa37IpHYHR3CuFtXB5bT2g==} + + '@shikijs/themes@3.20.0': + resolution: {integrity: sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==} + + '@shikijs/types@3.20.0': + resolution: {integrity: sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@shuding/opentype.js@1.4.0-beta.0': resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} engines: {node: '>= 8.0.0'} @@ -766,6 +847,11 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} @@ -870,6 +956,17 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@textcomplete/core@0.1.13': + resolution: {integrity: sha512-C4S+ihQU5HsKQ/TbsmS0e7hfPZtLZbEXj5NDUgRnhu/1Nezpu892bjNZGeErZm+R8iyDIT6wDu6EgIhng4M8eQ==} + + '@textcomplete/textarea@0.1.13': + resolution: {integrity: sha512-GNathnXpV361YuZrBVXvVqFYZ5NQZsjGC7Bt2sCUA/RTWlIgxHxC0ruDChYyRDx4siQZiZZOO5pWz+z1x8pZFQ==} + peerDependencies: + '@textcomplete/core': ^0.1.12 + + '@textcomplete/utils@0.1.13': + resolution: {integrity: sha512-5UW9Ee0WEX1s9K8MFffo5sfUjYm3YVhtqRhAor/ih7p0tnnpaMB7AwMRDKwhSIQL6O+g1fmEkxCeO8WqjPzjUA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1310,6 +1407,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -1414,6 +1514,9 @@ packages: hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -1735,6 +1838,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1782,6 +1889,12 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.4: + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1869,6 +1982,15 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + rehype-external-links@3.0.0: resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} @@ -1975,6 +2097,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@3.20.0: + resolution: {integrity: sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2038,6 +2163,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + textarea-caret@3.1.0: + resolution: {integrity: sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -2110,6 +2238,9 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + undate@0.3.0: + resolution: {integrity: sha512-ssH8QTNBY6B+2fRr3stSQ+9m2NT8qTaun3ExTx5ibzYQvP7yX4+BnX0McNxFCvh6S5ia/DYu6bsCKQx/U4nb/Q==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -2580,6 +2711,20 @@ snapshots: '@fontsource-variable/figtree@5.2.10': {} + '@fontsource-variable/jetbrains-mono@5.2.8': {} + + '@fontsource/google-sans-code@5.2.3': {} + + '@fontsource/monaspace-argon@5.2.5': {} + + '@fontsource/monaspace-krypton@5.2.5': {} + + '@fontsource/monaspace-neon@5.2.5': {} + + '@fontsource/monaspace-radon@5.2.5': {} + + '@fontsource/monaspace-xenon@5.2.5': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -2776,6 +2921,48 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.50': {} + '@shikijs/core@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + + '@shikijs/engine-oniguruma@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + + '@shikijs/rehype@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + '@types/hast': 3.0.4 + hast-util-to-string: 3.0.1 + shiki: 3.20.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + '@shikijs/themes@3.20.0': + dependencies: + '@shikijs/types': 3.20.0 + + '@shikijs/types@3.20.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@shuding/opentype.js@1.4.0-beta.0': dependencies: fflate: 0.7.4 @@ -2816,6 +3003,11 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tailwindcss/forms@0.5.11(tailwindcss@4.1.18)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 4.1.18 + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 @@ -2908,6 +3100,19 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@textcomplete/core@0.1.13': + dependencies: + eventemitter3: 5.0.1 + + '@textcomplete/textarea@0.1.13(@textcomplete/core@0.1.13)': + dependencies: + '@textcomplete/core': 0.1.13 + '@textcomplete/utils': 0.1.13 + textarea-caret: 3.1.0 + undate: 0.3.0 + + '@textcomplete/utils@0.1.13': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -3413,6 +3618,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.1: {} + expect-type@1.3.0: {} exsolve@1.0.8: {} @@ -3500,6 +3707,10 @@ snapshots: stringify-entities: 4.0.4 zwitch: 2.0.4 + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -3882,6 +4093,8 @@ snapshots: min-indent@1.0.1: {} + mini-svg-data-uri@1.4.4: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -3918,6 +4131,14 @@ snapshots: obug@2.1.1: {} + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.4: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4007,6 +4228,16 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + rehype-external-links@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -4119,6 +4350,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@3.20.0: + dependencies: + '@shikijs/core': 3.20.0 + '@shikijs/engine-javascript': 3.20.0 + '@shikijs/engine-oniguruma': 3.20.0 + '@shikijs/langs': 3.20.0 + '@shikijs/themes': 3.20.0 + '@shikijs/types': 3.20.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + siginfo@2.0.0: {} solid-js@1.9.10: @@ -4183,6 +4425,8 @@ snapshots: tapable@2.3.0: {} + textarea-caret@3.1.0: {} + tiny-inflate@1.0.3: {} tinybench@2.9.0: {} @@ -4246,6 +4490,8 @@ snapshots: ufo@1.6.1: {} + undate@0.3.0: {} + undici-types@7.16.0: {} unicode-trie@2.0.0: diff --git a/web/src/App.tsx b/web/src/App.tsx index a4fdd29..614d26b 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -16,6 +16,7 @@ import LectureImport from "$pages/LectureImport"; import Library from "$pages/Library"; import Login from "$pages/Login"; import LoginSuccess from "$pages/LoginSuccess"; +import NoteEdit from "$pages/NoteEdit"; import NoteNew from "$pages/NoteNew"; import Notes from "$pages/Notes"; import NoteView from "$pages/NoteView"; @@ -97,6 +98,7 @@ const App: Component = () => { + diff --git a/web/src/components/NoteEditor.tsx b/web/src/components/NoteEditor.tsx index 28c1e39..5fef9ba 100644 --- a/web/src/components/NoteEditor.tsx +++ b/web/src/components/NoteEditor.tsx @@ -1,17 +1,30 @@ /* eslint-disable solid/no-innerhtml */ +import { EditorToolbar } from "$components/notes/EditorToolbar"; import { api } from "$lib/api"; +import type { Note } from "$lib/model"; import { toast } from "$lib/toast"; import { Button } from "$ui/Button"; +import rehypeShiki from "@shikijs/rehype"; +import { Textcomplete } from "@textcomplete/core"; +import { TextareaEditor } from "@textcomplete/textarea"; import rehypeExternalLinks from "rehype-external-links"; -import rehypeSanitize from "rehype-sanitize"; import rehypeStringify from "rehype-stringify"; import remarkParse from "remark-parse"; import remarkRehype from "remark-rehype"; -import { createEffect, createSignal, Show } from "solid-js"; +import { createEffect, createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"; import { unified } from "unified"; +export type EditorFont = "neon" | "argon" | "krypton" | "radon" | "xenon" | "jetbrains" | "google"; + type NoteEditorProps = { noteId?: string; initialTitle?: string; initialContent?: string }; +type EditorTab = "write" | "preview"; + +const processor = unified().use(remarkParse).use(remarkRehype).use(rehypeShiki, { theme: "vitesse-dark" }).use( + rehypeExternalLinks, + { target: "_blank", rel: ["nofollow"] }, +).use(rehypeStringify); + export function NoteEditor(props: NoteEditorProps) { const [title, setTitle] = createSignal(props.initialTitle || ""); const [content, setContent] = createSignal(props.initialContent || ""); @@ -19,22 +32,121 @@ export function NoteEditor(props: NoteEditorProps) { const [tags, setTags] = createSignal(""); const [visibilityType, setVisibilityType] = createSignal("Private"); const [sharedWith, setSharedWith] = createSignal(""); + const [showLineNumbers, setShowLineNumbers] = createSignal(true); + const [editorFont, setEditorFont] = createSignal("jetbrains"); + const [activeTab, setActiveTab] = createSignal("write"); + + let textareaRef: HTMLTextAreaElement | undefined; + let textcomplete: Textcomplete | undefined; - const processor = unified().use(remarkParse).use(remarkRehype).use(rehypeSanitize).use(rehypeExternalLinks, { - target: "_blank", - rel: ["nofollow"], - }).use(rehypeStringify); + const [allNotes] = createResource(async (): Promise => { + const res = await api.getNotes(); + if (!res.ok) return []; + return res.json(); + }); + + const updatePreviewContent = async () => { + const file = await processor.process(content()); + setPreview(String(file)); + }; createEffect(() => { - processor.process(content()).then((file) => setPreview(String(file))).catch((e) => console.error(e)); + updatePreviewContent().catch(e => console.error(`Preview error: ${e instanceof Error ? e.message : e}`)); + }); + + onMount(() => { + if (!textareaRef) return; + + const editor = new TextareaEditor(textareaRef); + textcomplete = new Textcomplete(editor, [{ + match: /\[\[([^\]]*)/, + search: (term: string, callback: (results: string[]) => void) => { + const notes = allNotes() ?? []; + const filtered = notes.filter((n) => n.title.toLowerCase().includes(term.toLowerCase())).slice(0, 10).map((n) => + n.title + ); + callback(filtered); + }, + replace: (title: string) => `[[${title}]]`, + template: (title: string) => title, + }]); + }); + + onCleanup(() => { + textcomplete?.destroy(); + }); + + const fontValue = createMemo(() => { + switch (editorFont()) { + case "neon": + return "Monaspace Neon"; + case "argon": + return "Monaspace Argon"; + case "krypton": + return "Monaspace Krypton"; + case "radon": + return "Monaspace Radon"; + case "xenon": + return "Monaspace Xenon"; + case "google": + return "Google Sans Code"; + default: + return "JetBrains Mono"; + } }); + const insertAtCursor = (before: string, after: string = "") => { + if (!textareaRef) return; + const start = textareaRef.selectionStart; + const end = textareaRef.selectionEnd; + const text = content(); + const selectedText = text.substring(start, end); + const newText = text.substring(0, start) + before + selectedText + after + text.substring(end); + setContent(newText); + setTimeout(() => { + textareaRef!.focus(); + textareaRef!.setSelectionRange(start + before.length, start + before.length + selectedText.length); + }, 0); + }; + + const handleBold = () => insertAtCursor("**", "**"); + const handleItalic = () => insertAtCursor("*", "*"); + const handleLink = () => insertAtCursor("[", "](url)"); + const handleCode = () => insertAtCursor("`", "`"); + const handleCodeBlock = () => insertAtCursor("```\n", "\n```"); + const handleWikilink = () => insertAtCursor("[[", "]]"); + const handleList = () => insertAtCursor("- "); + + const handleHeading = (level: 1 | 2 | 3 | 4 | 5 | 6) => { + const prefix = "#".repeat(level) + " "; + insertAtCursor(prefix); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.metaKey || e.ctrlKey) { + switch (e.key) { + case "b": + e.preventDefault(); + handleBold(); + break; + case "i": + e.preventDefault(); + handleItalic(); + break; + case "k": + e.preventDefault(); + handleLink(); + break; + } + } + }; + const handleSubmit = async (e: Event) => { e.preventDefault(); try { let visibility; if (visibilityType() === "SharedWith") { - visibility = { type: "SharedWith", content: sharedWith().split(",").map(s => s.trim()).filter(s => s) }; + visibility = { type: "SharedWith", content: sharedWith().split(",").map((s) => s.trim()).filter((s) => s) }; } else { visibility = { type: visibilityType() }; } @@ -42,11 +154,12 @@ export function NoteEditor(props: NoteEditorProps) { const payload = { title: title(), body: content(), - tags: tags().split(",").map(t => t.trim()).filter(t => t), + tags: tags().split(",").map((t) => t.trim()).filter((t) => t), visibility, }; - const res = await api.post("/notes", payload); + const res = props.noteId ? await api.updateNote(props.noteId, payload) : await api.post("/notes", payload); + if (res.ok) { toast.success("Note saved!"); if (!props.noteId) { @@ -65,79 +178,156 @@ export function NoteEditor(props: NoteEditorProps) { } }; + const lineNumbers = () => Array.from({ length: content().split("\n").length }, (_, i) => i + 1); + return ( -
-
-

Note Editor

+
+
+

{props.noteId ? "Edit Note" : "New Note"}

-
+
+ + +
+
+ + +
+ + setTitle(e.target.value)} + class="w-full bg-slate-800 border border-slate-700 text-white rounded-lg p-3 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Note Title" + required /> +
+ +
+
+ + +
+ + +
+ +
+ +
+ {(num) =>
{num}
}
+
+
+