Initial ui redo progress (#1)
Complete UI rewrite using shadcn-svelte for consistency and better design Reviewed-on: #1 Co-authored-by: June <self@breadone.net> Co-committed-by: June <self@breadone.net>
This commit was merged in pull request #1.
This commit is contained in:
16
components.json
Normal file
16
components.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/routes/layout.css",
|
||||
"baseColor": "neutral"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
}
|
||||
@@ -17,17 +17,24 @@
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.11.0",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@sveltejs/adapter-node": "^5.5.2",
|
||||
"@sveltejs/kit": "^2.50.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^22",
|
||||
"bits-ui": "^2.15.5",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"svelte": "^5.48.2",
|
||||
"svelte-check": "^4.3.5",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
@@ -41,6 +48,7 @@
|
||||
"@tiptap/pm": "^3.18.0",
|
||||
"@tiptap/starter-kit": "^3.18.0",
|
||||
"marked": "^17.0.1",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"postgres": "^3.4.8",
|
||||
"tiptap": "^1.32.2"
|
||||
}
|
||||
|
||||
204
pnpm-lock.yaml
generated
204
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
||||
marked:
|
||||
specifier: ^17.0.1
|
||||
version: 17.0.1
|
||||
mode-watcher:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(svelte@5.49.0)
|
||||
postgres:
|
||||
specifier: ^3.4.8
|
||||
version: 3.4.8
|
||||
@@ -42,6 +45,12 @@ importers:
|
||||
specifier: ^1.32.2
|
||||
version: 1.32.2(vue-template-compiler@2.7.16)(vue@2.7.16)
|
||||
devDependencies:
|
||||
'@internationalized/date':
|
||||
specifier: ^3.11.0
|
||||
version: 3.11.0
|
||||
'@lucide/svelte':
|
||||
specifier: ^0.561.0
|
||||
version: 0.561.0(svelte@5.49.0)
|
||||
'@sveltejs/adapter-node':
|
||||
specifier: ^5.5.2
|
||||
version: 5.5.2(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))
|
||||
@@ -60,6 +69,12 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22
|
||||
version: 22.19.7
|
||||
bits-ui:
|
||||
specifier: ^2.15.5
|
||||
version: 2.15.5(@internationalized/date@3.11.0)(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
drizzle-kit:
|
||||
specifier: ^0.31.8
|
||||
version: 0.31.8
|
||||
@@ -72,9 +87,18 @@ importers:
|
||||
svelte-check:
|
||||
specifier: ^4.3.5
|
||||
version: 4.3.5(picomatch@4.0.3)(svelte@5.49.0)(typescript@5.9.3)
|
||||
tailwind-merge:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
tailwind-variants:
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18)
|
||||
tailwindcss:
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
tw-animate-css:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
@@ -578,6 +602,9 @@ packages:
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.20
|
||||
|
||||
'@internationalized/date@3.11.0':
|
||||
resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
@@ -594,6 +621,11 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@lucide/svelte@0.561.0':
|
||||
resolution: {integrity: sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==}
|
||||
peerDependencies:
|
||||
svelte: ^5
|
||||
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
@@ -805,6 +837,9 @@ packages:
|
||||
svelte: ^5.0.0
|
||||
vite: ^6.3.0 || ^7.0.0
|
||||
|
||||
'@swc/helpers@0.5.18':
|
||||
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
|
||||
|
||||
'@tailwindcss/forms@0.5.11':
|
||||
resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==}
|
||||
peerDependencies:
|
||||
@@ -1085,6 +1120,13 @@ packages:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
bits-ui@2.15.5:
|
||||
resolution: {integrity: sha512-WhS+P+E//ClLfKU6KqjKC17nGDRLnz+vkwoP6ClFUPd5m1fFVDxTElPX8QVsduLj5V1KFDxlnv6sW2G5Lqk+vw==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
'@internationalized/date': ^3.8.1
|
||||
svelte: ^5.33.0
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
@@ -1125,6 +1167,10 @@ packages:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1304,6 +1350,9 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
inline-style-parser@0.2.7:
|
||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||
|
||||
is-core-module@2.16.1:
|
||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1404,6 +1453,10 @@ packages:
|
||||
locate-character@3.0.0:
|
||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||
|
||||
lz-string@1.5.0:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@@ -1423,6 +1476,11 @@ packages:
|
||||
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
|
||||
hasBin: true
|
||||
|
||||
mode-watcher@1.1.0:
|
||||
resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==}
|
||||
peerDependencies:
|
||||
svelte: ^5.27.0
|
||||
|
||||
mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1553,6 +1611,25 @@ packages:
|
||||
rope-sequence@1.3.4:
|
||||
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
|
||||
|
||||
runed@0.23.4:
|
||||
resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==}
|
||||
peerDependencies:
|
||||
svelte: ^5.7.0
|
||||
|
||||
runed@0.25.0:
|
||||
resolution: {integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==}
|
||||
peerDependencies:
|
||||
svelte: ^5.7.0
|
||||
|
||||
runed@0.35.1:
|
||||
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^2.21.0
|
||||
svelte: ^5.7.0
|
||||
peerDependenciesMeta:
|
||||
'@sveltejs/kit':
|
||||
optional: true
|
||||
|
||||
sade@1.8.1:
|
||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1575,6 +1652,9 @@ packages:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
style-to-object@1.0.14:
|
||||
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1587,10 +1667,38 @@ packages:
|
||||
svelte: ^4.0.0 || ^5.0.0-next.0
|
||||
typescript: '>=5.0.0'
|
||||
|
||||
svelte-toolbelt@0.10.6:
|
||||
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
|
||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||
peerDependencies:
|
||||
svelte: ^5.30.2
|
||||
|
||||
svelte-toolbelt@0.7.1:
|
||||
resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==}
|
||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
svelte@5.49.0:
|
||||
resolution: {integrity: sha512-Fn2mCc3XX0gnnbBYzWOTrZHi5WnF9KvqmB1+KGlUWoJkdioPmFYtg2ALBr6xl2dcnFTz3Vi7/mHpbKSVg/imVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tabbable@6.4.0:
|
||||
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
|
||||
|
||||
tailwind-merge@3.4.0:
|
||||
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
||||
|
||||
tailwind-variants@3.2.2:
|
||||
resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==}
|
||||
engines: {node: '>=16.x', pnpm: '>=7.x'}
|
||||
peerDependencies:
|
||||
tailwind-merge: '>=3.0.0'
|
||||
tailwindcss: '*'
|
||||
peerDependenciesMeta:
|
||||
tailwind-merge:
|
||||
optional: true
|
||||
|
||||
tailwindcss@4.1.18:
|
||||
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
|
||||
|
||||
@@ -1618,6 +1726,12 @@ packages:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tw-animate-css@1.4.0:
|
||||
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -1979,6 +2093,10 @@ snapshots:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
'@fullcalendar/daygrid': 6.1.20(@fullcalendar/core@6.1.20)
|
||||
|
||||
'@internationalized/date@3.11.0':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.18
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -1998,6 +2116,10 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@lucide/svelte@0.561.0(svelte@5.49.0)':
|
||||
dependencies:
|
||||
svelte: 5.49.0
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@remirror/core-constants@3.0.0': {}
|
||||
@@ -2165,6 +2287,10 @@ snapshots:
|
||||
vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)
|
||||
vitefu: 1.1.1(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||
|
||||
'@swc/helpers@0.5.18':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@tailwindcss/forms@0.5.11(tailwindcss@4.1.18)':
|
||||
dependencies:
|
||||
mini-svg-data-uri: 1.4.4
|
||||
@@ -2437,6 +2563,19 @@ snapshots:
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
||||
bits-ui@2.15.5(@internationalized/date@3.11.0)(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0):
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.4
|
||||
'@floating-ui/dom': 1.7.5
|
||||
'@internationalized/date': 3.11.0
|
||||
esm-env: 1.2.2
|
||||
runed: 0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)
|
||||
svelte: 5.49.0
|
||||
svelte-toolbelt: 0.10.6(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)
|
||||
tabbable: 6.4.0
|
||||
transitivePeerDependencies:
|
||||
- '@sveltejs/kit'
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
chokidar@4.0.3:
|
||||
@@ -2461,6 +2600,8 @@ snapshots:
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
devalue@5.6.2: {}
|
||||
@@ -2618,6 +2759,8 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
inline-style-parser@0.2.7: {}
|
||||
|
||||
is-core-module@2.16.1:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
@@ -2693,6 +2836,8 @@ snapshots:
|
||||
|
||||
locate-character@3.0.0: {}
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -2712,6 +2857,12 @@ snapshots:
|
||||
|
||||
mini-svg-data-uri@1.4.4: {}
|
||||
|
||||
mode-watcher@1.1.0(svelte@5.49.0):
|
||||
dependencies:
|
||||
runed: 0.25.0(svelte@5.49.0)
|
||||
svelte: 5.49.0
|
||||
svelte-toolbelt: 0.7.1(svelte@5.49.0)
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
@@ -2891,6 +3042,25 @@ snapshots:
|
||||
|
||||
rope-sequence@1.3.4: {}
|
||||
|
||||
runed@0.23.4(svelte@5.49.0):
|
||||
dependencies:
|
||||
esm-env: 1.2.2
|
||||
svelte: 5.49.0
|
||||
|
||||
runed@0.25.0(svelte@5.49.0):
|
||||
dependencies:
|
||||
esm-env: 1.2.2
|
||||
svelte: 5.49.0
|
||||
|
||||
runed@0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
esm-env: 1.2.2
|
||||
lz-string: 1.5.0
|
||||
svelte: 5.49.0
|
||||
optionalDependencies:
|
||||
'@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||
|
||||
sade@1.8.1:
|
||||
dependencies:
|
||||
mri: 1.2.0
|
||||
@@ -2912,6 +3082,10 @@ snapshots:
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
style-to-object@1.0.14:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.7
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.49.0)(typescript@5.9.3):
|
||||
@@ -2926,6 +3100,22 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- picomatch
|
||||
|
||||
svelte-toolbelt@0.10.6(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0):
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
runed: 0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)
|
||||
style-to-object: 1.0.14
|
||||
svelte: 5.49.0
|
||||
transitivePeerDependencies:
|
||||
- '@sveltejs/kit'
|
||||
|
||||
svelte-toolbelt@0.7.1(svelte@5.49.0):
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
runed: 0.23.4(svelte@5.49.0)
|
||||
style-to-object: 1.0.14
|
||||
svelte: 5.49.0
|
||||
|
||||
svelte@5.49.0:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
@@ -2944,6 +3134,16 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
zimmerframe: 1.1.4
|
||||
|
||||
tabbable@6.4.0: {}
|
||||
|
||||
tailwind-merge@3.4.0: {}
|
||||
|
||||
tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18):
|
||||
dependencies:
|
||||
tailwindcss: 4.1.18
|
||||
optionalDependencies:
|
||||
tailwind-merge: 3.4.0
|
||||
|
||||
tailwindcss@4.1.18: {}
|
||||
|
||||
tapable@2.3.0: {}
|
||||
@@ -2986,6 +3186,10 @@ snapshots:
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tw-animate-css@1.4.0: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
17
src/lib/components/ThemeModeButton.svelte
Normal file
17
src/lib/components/ThemeModeButton.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import SunIcon from "@lucide/svelte/icons/sun";
|
||||
import MoonIcon from "@lucide/svelte/icons/moon";
|
||||
|
||||
import { toggleMode } from "mode-watcher";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
</script>
|
||||
|
||||
<Button onclick={toggleMode} variant="outline" size="icon">
|
||||
<SunIcon
|
||||
class="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 !transition-all dark:scale-0 dark:-rotate-90"
|
||||
/>
|
||||
<MoonIcon
|
||||
class="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 !transition-all dark:scale-100 dark:rotate-0"
|
||||
/>
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
@@ -1,309 +1,67 @@
|
||||
<script lang="ts">
|
||||
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
|
||||
import CalendarDay from "$lib/components/ui/calendar/calendar-day.svelte";
|
||||
import { CalendarDate, type DateValue } from "@internationalized/date";
|
||||
|
||||
let {
|
||||
year = $bindable("2026"),
|
||||
month = $bindable("0"),
|
||||
dayClickCallback = $bindable(() => {}),
|
||||
entries,
|
||||
value = $bindable<CalendarDate | undefined>(
|
||||
new CalendarDate(2026, 2, 1),
|
||||
),
|
||||
} = $props();
|
||||
|
||||
const headers = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
const monthNames = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
let placeholder = $state<CalendarDate | undefined>(undefined);
|
||||
let imageMap = $state<Map<string, string>>(new Map());
|
||||
|
||||
let days = $state([]);
|
||||
let expanded = $state(false);
|
||||
let currentYear = $state(parseInt(year));
|
||||
let currentMonth = $state(parseInt(month));
|
||||
|
||||
function toISODate(year: number, month: number, date: number) {
|
||||
return `${year}-${String(month + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function getEntryForDay(year: number, month: number, date: number) {
|
||||
if (!entries) return undefined;
|
||||
const dateStr = toISODate(year, month, date);
|
||||
return entries.find((e) => e.date.split("T")[0] === dateStr);
|
||||
}
|
||||
|
||||
function createDayObject(date: Date, isCurrentMonth: boolean) {
|
||||
const today = new Date();
|
||||
const entry = getEntryForDay(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate(),
|
||||
);
|
||||
return {
|
||||
date: date.getDate(),
|
||||
month: date.getMonth(),
|
||||
year: date.getFullYear(),
|
||||
iso: toISODate(date.getFullYear(), date.getMonth(), date.getDate()),
|
||||
dayName: headers[date.getDay() === 0 ? 6 : date.getDay() - 1],
|
||||
isCurrentMonth,
|
||||
isToday: date.toDateString() === today.toDateString(),
|
||||
entry,
|
||||
};
|
||||
}
|
||||
|
||||
function getCurrentWeek() {
|
||||
const today = new Date();
|
||||
const diff = today.getDay() === 0 ? -6 : 1 - today.getDay();
|
||||
const monday = new Date(today);
|
||||
monday.setDate(today.getDate() + diff);
|
||||
|
||||
return Array.from({ length: 7 }, (_, i) => {
|
||||
const day = new Date(monday);
|
||||
day.setDate(monday.getDate() + i);
|
||||
return createDayObject(day, day.getMonth() === today.getMonth());
|
||||
});
|
||||
}
|
||||
|
||||
function generateMonth(year: number, month: number) {
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const startDay = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1;
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const prevMonthDays = new Date(year, month, 0).getDate();
|
||||
const days = [];
|
||||
|
||||
// Previous month
|
||||
for (let i = startDay - 1; i >= 0; i--) {
|
||||
const date = new Date(year, month, 0);
|
||||
date.setDate(prevMonthDays - i);
|
||||
days.push(createDayObject(date, false));
|
||||
}
|
||||
|
||||
// Current month
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
days.push(createDayObject(new Date(year, month, i), true));
|
||||
}
|
||||
|
||||
// Next month
|
||||
const remaining = 42 - days.length;
|
||||
for (let i = 1; i <= remaining; i++) {
|
||||
days.push(createDayObject(new Date(year, month + 1, i), false));
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
let displayedMonth = $derived(placeholder ?? value);
|
||||
|
||||
$effect(() => {
|
||||
days = expanded
|
||||
? generateMonth(currentYear, currentMonth)
|
||||
: getCurrentWeek();
|
||||
if (displayedMonth) {
|
||||
const month = `${displayedMonth.year}-${String(displayedMonth.month).padStart(2, "0")}`;
|
||||
fetchMonthImages(month);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleExpanded() {
|
||||
expanded = !expanded;
|
||||
if (expanded) {
|
||||
const today = new Date();
|
||||
currentYear = today.getFullYear();
|
||||
currentMonth = today.getMonth();
|
||||
async function fetchMonthImages(month: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/entry?month=${month}`);
|
||||
const entries = await res.json();
|
||||
const newMap = new Map<string, string>();
|
||||
for (const entry of entries) {
|
||||
if (entry.image) {
|
||||
const d = new Date(entry.date);
|
||||
const key = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
||||
newMap.set(key, entry.image);
|
||||
}
|
||||
}
|
||||
imageMap = newMap;
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch month images:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function previousMonth() {
|
||||
if (!expanded) return;
|
||||
currentMonth === 0
|
||||
? ((currentMonth = 11), currentYear--)
|
||||
: currentMonth--;
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
if (!expanded) return;
|
||||
currentMonth === 11
|
||||
? ((currentMonth = 0), currentYear++)
|
||||
: currentMonth++;
|
||||
function getImageForDate(day: DateValue): string | null {
|
||||
const key = `${day.year}-${day.month}-${day.day}`;
|
||||
return imageMap.get(key) ?? null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">
|
||||
<button
|
||||
class="expand-btn"
|
||||
onclick={toggleExpanded}
|
||||
title={expanded ? "Show week view" : "Show month view"}
|
||||
>
|
||||
{expanded ? "−" : "+"}
|
||||
</button>
|
||||
{#if expanded}
|
||||
<button class="nav-btn" onclick={previousMonth}>‹</button>
|
||||
<span class="month-year"
|
||||
>{monthNames[currentMonth]} {currentYear}</span
|
||||
>
|
||||
<button class="nav-btn" onclick={nextMonth}>›</button>
|
||||
{:else}
|
||||
<span class="month-year">Current Week</span>
|
||||
<Calendar
|
||||
type="single"
|
||||
bind:value
|
||||
bind:placeholder
|
||||
class="rounded-lg border shadow-sm md:[--cell-size:--spacing(14)]"
|
||||
>
|
||||
{#snippet day({ day })}
|
||||
<CalendarDay>
|
||||
{@const img = getImageForDate(day)}
|
||||
{#if img}
|
||||
<img
|
||||
src={img}
|
||||
alt=""
|
||||
class="absolute inset-0 h-full w-full rounded-sm object-cover hover:opacity-20"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="calendar">
|
||||
{#each days as day, index}
|
||||
<button
|
||||
class="day hover:brightness-80"
|
||||
class:other-month={!day.isCurrentMonth}
|
||||
class:today={day.isToday}
|
||||
class:has-image={day.entry?.image}
|
||||
onclick={day.entry?.id
|
||||
? () => dayClickCallback(true, day.entry.id)
|
||||
: () => dayClickCallback(false, day.iso)}
|
||||
style={day.entry?.image
|
||||
? `background-image: url(${day.entry.image}); background-size: cover; background-position: center;`
|
||||
: ""}
|
||||
>
|
||||
{#if day.entry?.image}
|
||||
<div class="image-overlay"></div>
|
||||
{/if}
|
||||
<div class="day-header">
|
||||
{#if index < 7}
|
||||
<span class="day-name">{day.dayName}</span>
|
||||
{/if}
|
||||
<span class="date">{day.date}</span>
|
||||
{#if day.entry && !day.entry.image}
|
||||
<span class="entry-indicator"></span>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.calendar-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: rgba(166, 168, 179, 0.05);
|
||||
border-bottom: 1px solid rgba(166, 168, 179, 0.12);
|
||||
}
|
||||
|
||||
.expand-btn,
|
||||
.nav-btn {
|
||||
border: 1px solid rgba(166, 168, 179, 0.2);
|
||||
background: transparent;
|
||||
color: #009fb7;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.expand-btn:hover,
|
||||
.nav-btn:hover {
|
||||
background: rgba(0, 159, 183, 0.1);
|
||||
border-color: #009fb7;
|
||||
}
|
||||
|
||||
.month-year {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #009fb7;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(7, minmax(120px, 1fr));
|
||||
grid-auto-rows: 120px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.day {
|
||||
border-bottom: 1px solid rgba(166, 168, 179, 0.12);
|
||||
border-right: 1px solid rgba(166, 168, 179, 0.12);
|
||||
padding: 14px 20px;
|
||||
box-sizing: border-box;
|
||||
color: #98a0a6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.day.has-image {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.image-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.day-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.day-name {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: #009fb7;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.entry-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #4a9eff;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.day.has-image .day-name,
|
||||
.day.has-image .date {
|
||||
color: white;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.day.other-month {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.day.today {
|
||||
background: rgba(0, 159, 183, 0.1);
|
||||
}
|
||||
|
||||
.day.today .date {
|
||||
color: #009fb7;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
<span class="relative z-10">{day.day}</span>
|
||||
</CalendarDay>
|
||||
{/snippet}
|
||||
</Calendar>
|
||||
|
||||
319
src/lib/components/editor.svelte
Normal file
319
src/lib/components/editor.svelte
Normal file
@@ -0,0 +1,319 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
CalendarDate,
|
||||
today,
|
||||
getLocalTimeZone,
|
||||
} from "@internationalized/date";
|
||||
import { onMount } from "svelte";
|
||||
import { formatDate } from "$lib/date";
|
||||
import { createEntry, updateEntry, deleteEntry } from "$lib/upload";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { Upload, X } from "@lucide/svelte";
|
||||
import { marked } from "marked";
|
||||
|
||||
let { dateValue = $bindable<CalendarDate>(today(getLocalTimeZone())) } =
|
||||
$props();
|
||||
|
||||
let edit = $state<boolean>(false);
|
||||
let isLoading = $state<boolean>(false);
|
||||
let error = $state<string>("");
|
||||
let previewImage = $state<string | null>(null);
|
||||
let fileInput = $state<HTMLInputElement | undefined>(undefined);
|
||||
|
||||
let entry = $state({
|
||||
id: "",
|
||||
date: dateValue.toString(),
|
||||
image: "",
|
||||
content: "",
|
||||
});
|
||||
|
||||
let editingEntry = $state({
|
||||
id: "",
|
||||
date: dateValue.toString(),
|
||||
image: "",
|
||||
content: "",
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
await loadEntry();
|
||||
});
|
||||
|
||||
async function loadEntry() {
|
||||
try {
|
||||
error = "";
|
||||
const res = await fetch(`/api/entry?date=${dateValue.toString()}`);
|
||||
const data = await res.json();
|
||||
if (data && data.length > 0) {
|
||||
entry = { ...data[0], date: dateValue.toString() };
|
||||
previewImage = entry.image || null;
|
||||
} else {
|
||||
entry = {
|
||||
id: "",
|
||||
date: dateValue.toString(),
|
||||
image: "",
|
||||
content: "",
|
||||
};
|
||||
previewImage = null;
|
||||
}
|
||||
} catch (err) {
|
||||
error = "Failed to load entry";
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit() {
|
||||
editingEntry = { ...entry };
|
||||
previewImage = entry.image || null;
|
||||
edit = true;
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
edit = false;
|
||||
error = "";
|
||||
previewImage = entry.image || null;
|
||||
}
|
||||
|
||||
function handleImageSelect(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const files = target.files;
|
||||
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith("image/")) {
|
||||
error = "Please select a valid image file";
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (5MB max)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
error = "Image must be smaller than 5MB";
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const result = event.target?.result as string;
|
||||
editingEntry.image = result;
|
||||
previewImage = result;
|
||||
error = "";
|
||||
};
|
||||
reader.onerror = () => {
|
||||
error = "Failed to read image file";
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
editingEntry.image = "";
|
||||
previewImage = null;
|
||||
if (fileInput) {
|
||||
fileInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
isLoading = true;
|
||||
error = "";
|
||||
|
||||
editingEntry.date = dateValue.toString();
|
||||
|
||||
if (entry.id) {
|
||||
editingEntry.id = entry.id;
|
||||
await updateEntry(editingEntry);
|
||||
} else {
|
||||
delete editingEntry.id;
|
||||
await createEntry(editingEntry);
|
||||
}
|
||||
|
||||
// Reload the entry to get updated data
|
||||
await loadEntry();
|
||||
edit = false;
|
||||
} catch (err) {
|
||||
error = "Failed to save entry. Please try again.";
|
||||
console.error(err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm("Are you sure you want to delete this entry?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = "";
|
||||
await deleteEntry(entry);
|
||||
await loadEntry();
|
||||
edit = false;
|
||||
} catch (err) {
|
||||
error = "Failed to delete entry. Please try again.";
|
||||
console.error(err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full max-w-2xl">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-row items-center justify-between mb-6">
|
||||
<h1 class="text-3xl font-bold">{formatDate(entry.date)}</h1>
|
||||
|
||||
<div class="flex gap-2">
|
||||
{#if !edit}
|
||||
<Button variant="secondary" onclick={startEdit}>Edit</Button>
|
||||
{:else}
|
||||
<Button
|
||||
variant="destructive"
|
||||
onclick={handleDelete}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onclick={handleSave}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={cancelEdit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div
|
||||
class="mb-4 p-3 bg-destructive/10 border border-destructive/20 rounded-md text-destructive text-sm"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- View Mode -->
|
||||
{#if !edit}
|
||||
<div class="space-y-4">
|
||||
<!-- Image Display -->
|
||||
{#if entry.image}
|
||||
<div class="rounded-lg overflow-hidden border border-input">
|
||||
<img
|
||||
src={entry.image}
|
||||
alt="Entry"
|
||||
class="w-full h-auto max-h-128 object-cover"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="rounded-lg border-2 border-dashed border-muted-foreground/25 bg-muted/50 h-48 flex items-center justify-center text-muted-foreground text-sm"
|
||||
>
|
||||
No image
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Content Display -->
|
||||
{#if entry.content}
|
||||
<div class="rounded-lg border border-input bg-muted/30 p-4">
|
||||
<div class="text-base whitespace-pre-wrap text-foreground">
|
||||
{@html marked(entry.content)}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="rounded-lg border border-dashed border-muted-foreground/25 bg-muted/50 p-4 text-muted-foreground text-sm text-center"
|
||||
>
|
||||
No content yet
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Edit Mode -->
|
||||
{:else}
|
||||
<div class="space-y-6">
|
||||
<!-- Image Picker -->
|
||||
<div class="space-y-3">
|
||||
<!-- <label for="image-input" class="text-sm font-medium">
|
||||
Image
|
||||
</label> -->
|
||||
|
||||
{#if previewImage}
|
||||
<div
|
||||
class="relative rounded-lg overflow-hidden border border-input"
|
||||
>
|
||||
<img
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
class="w-full h-auto max-h-128 object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={removeImage}
|
||||
class="absolute top-2 right-2 p-2 bg-destructive text-white rounded-md hover:bg-destructive/90 transition-colors"
|
||||
title="Remove image"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="rounded-lg border-2 border-dashed border-muted-foreground/25 bg-muted/50 p-8 flex flex-col items-center justify-center gap-3 cursor-pointer hover:border-muted-foreground/50 transition-colors"
|
||||
role="button"
|
||||
tabindex={0}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
fileInput?.click();
|
||||
}
|
||||
}}
|
||||
onclick={() => fileInput?.click()}
|
||||
>
|
||||
<Upload size={32} class="text-muted-foreground" />
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Click to upload or drag and drop
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
PNG, JPG, GIF up to 5MB
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
id="image-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onchange={handleImageSelect}
|
||||
class="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Content Editor -->
|
||||
<div class="space-y-3">
|
||||
<!-- <label for="content-input" class="text-sm font-medium">
|
||||
Content
|
||||
</label> -->
|
||||
<Textarea
|
||||
id="content-input"
|
||||
bind:value={editingEntry.content}
|
||||
placeholder="How was your day?"
|
||||
class="min-h-64"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{editingEntry.content.length} characters
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,24 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { generateHTML } from "@tiptap/html";
|
||||
import * as Item from "$lib/components/ui/item/index.js";
|
||||
import { CalendarDate, parseDate } from "@internationalized/date";
|
||||
|
||||
import { marked } from 'marked';
|
||||
import { StarterKit } from "@tiptap/starter-kit";
|
||||
import { formatDate } from "$lib/date.ts";
|
||||
|
||||
const { entry, clickCB } = $props();
|
||||
let {
|
||||
entry,
|
||||
value = $bindable<CalendarDate | undefined>(
|
||||
new CalendarDate(2026, 2, 1),
|
||||
),
|
||||
} = $props()
|
||||
|
||||
const formattedDate = formatDate(entry.date);
|
||||
|
||||
function select() { value = parseDate(entry.date) }
|
||||
</script>
|
||||
|
||||
<button class="bg-white/10 p-4 rounded-xl w-full hover:brightness-80 transition-all mb-3" onclick={clickCB}>
|
||||
<div class="flex flex-row">
|
||||
<div class="text-start">
|
||||
<p class="text-sm text-white/60">{formattedDate}</p>
|
||||
{@html marked(entry.content)}
|
||||
</div>
|
||||
<button class="border border-white/30 rounded-xl w-full hover:brightness-80 transition-all mb-3" onclick={select}>
|
||||
<Item.Root>
|
||||
<Item.Content class="text-left">
|
||||
<Item.Description>{formattedDate}</Item.Description>
|
||||
<Item.Title>{@html marked(entry.content)}</Item.Title>
|
||||
</Item.Content>
|
||||
|
||||
<Item.Media>
|
||||
<img
|
||||
class="max-h-30 min-h-15 max-w-1/2 rounded-xl object-cover ml-auto"
|
||||
src={entry.image}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</Item.Media>
|
||||
</Item.Root>
|
||||
|
||||
</button>
|
||||
@@ -1,3 +1,9 @@
|
||||
<script lang="ts">
|
||||
import ThemeModeButton from "$lib/components/ThemeModeButton.svelte";
|
||||
</script>
|
||||
|
||||
<div class="flex w-full h-14 px-2 text-2xl rounded-xl mb-4 bg-[#009FB7] place-items-center">
|
||||
<p>Memento</p>
|
||||
<p class="mr-auto">Memento</p>
|
||||
|
||||
<ThemeModeButton />
|
||||
</div>
|
||||
82
src/lib/components/ui/button/button.svelte
Normal file
82
src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
|
||||
outline:
|
||||
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
17
src/lib/components/ui/button/index.ts
Normal file
17
src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
76
src/lib/components/ui/calendar/calendar-caption.svelte
Normal file
76
src/lib/components/ui/calendar/calendar-caption.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type Calendar from "./calendar.svelte";
|
||||
import CalendarMonthSelect from "./calendar-month-select.svelte";
|
||||
import CalendarYearSelect from "./calendar-year-select.svelte";
|
||||
import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date";
|
||||
|
||||
let {
|
||||
captionLayout,
|
||||
months,
|
||||
monthFormat,
|
||||
years,
|
||||
yearFormat,
|
||||
month,
|
||||
locale,
|
||||
placeholder = $bindable(),
|
||||
monthIndex = 0,
|
||||
}: {
|
||||
captionLayout: ComponentProps<typeof Calendar>["captionLayout"];
|
||||
months: ComponentProps<typeof CalendarMonthSelect>["months"];
|
||||
monthFormat: ComponentProps<typeof CalendarMonthSelect>["monthFormat"];
|
||||
years: ComponentProps<typeof CalendarYearSelect>["years"];
|
||||
yearFormat: ComponentProps<typeof CalendarYearSelect>["yearFormat"];
|
||||
month: DateValue;
|
||||
placeholder: DateValue | undefined;
|
||||
locale: string;
|
||||
monthIndex: number;
|
||||
} = $props();
|
||||
|
||||
function formatYear(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear());
|
||||
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
|
||||
}
|
||||
|
||||
function formatMonth(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1);
|
||||
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet MonthSelect()}
|
||||
<CalendarMonthSelect
|
||||
{months}
|
||||
{monthFormat}
|
||||
value={month.month}
|
||||
onchange={(e) => {
|
||||
if (!placeholder) return;
|
||||
const v = Number.parseInt(e.currentTarget.value);
|
||||
const newPlaceholder = placeholder.set({ month: v });
|
||||
placeholder = newPlaceholder.subtract({ months: monthIndex });
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet YearSelect()}
|
||||
<CalendarYearSelect {years} {yearFormat} value={month.year} />
|
||||
{/snippet}
|
||||
|
||||
{#if captionLayout === "dropdown"}
|
||||
{@render MonthSelect()}
|
||||
{@render YearSelect()}
|
||||
{:else if captionLayout === "dropdown-months"}
|
||||
{@render MonthSelect()}
|
||||
{#if placeholder}
|
||||
{formatYear(placeholder)}
|
||||
{/if}
|
||||
{:else if captionLayout === "dropdown-years"}
|
||||
{#if placeholder}
|
||||
{formatMonth(placeholder)}
|
||||
{/if}
|
||||
{@render YearSelect()}
|
||||
{:else}
|
||||
{formatMonth(month)} {formatYear(month)}
|
||||
{/if}
|
||||
19
src/lib/components/ui/calendar/calendar-cell.svelte
Normal file
19
src/lib/components/ui/calendar/calendar-cell.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.CellProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Cell
|
||||
bind:ref
|
||||
class={cn(
|
||||
"relative flex-1 aspect-square p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-md [&:last-child[data-selected]_[data-bits-day]]:rounded-e-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
35
src/lib/components/ui/calendar/calendar-day.svelte
Normal file
35
src/lib/components/ui/calendar/calendar-day.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.DayProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Day
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"relative flex flex-1 flex-col items-center justify-center gap-1 p-0 leading-none font-normal whitespace-nowrap select-none",
|
||||
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground",
|
||||
"data-[selected]:bg-primary dark:data-[selected]:hover:bg-accent/50 data-[selected]:text-primary-foreground",
|
||||
// Outside months
|
||||
"[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground",
|
||||
// Disabled
|
||||
"data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
// Unavailable
|
||||
"data-[unavailable]:text-muted-foreground data-[unavailable]:line-through",
|
||||
// hover
|
||||
"dark:hover:text-accent-foreground",
|
||||
// focus
|
||||
"focus:border-ring focus:ring-ring/50 focus:relative",
|
||||
// inner spans
|
||||
"[&>span]:text-xs [&>span]:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
12
src/lib/components/ui/calendar/calendar-grid-body.svelte
Normal file
12
src/lib/components/ui/calendar/calendar-grid-body.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridBodyProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />
|
||||
12
src/lib/components/ui/calendar/calendar-grid-head.svelte
Normal file
12
src/lib/components/ui/calendar/calendar-grid-head.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridHeadProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />
|
||||
12
src/lib/components/ui/calendar/calendar-grid-row.svelte
Normal file
12
src/lib/components/ui/calendar/calendar-grid-row.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridRowProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.GridRow bind:ref class={cn("flex flex-1 gap-1", className)} {...restProps} />
|
||||
16
src/lib/components/ui/calendar/calendar-grid.svelte
Normal file
16
src/lib/components/ui/calendar/calendar-grid.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Grid
|
||||
bind:ref
|
||||
class={cn("mt-4 flex w-full border-collapse flex-col gap-1", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
19
src/lib/components/ui/calendar/calendar-head-cell.svelte
Normal file
19
src/lib/components/ui/calendar/calendar-head-cell.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.HeadCellProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.HeadCell
|
||||
bind:ref
|
||||
class={cn(
|
||||
"text-muted-foreground w-(--cell-size) rounded-md text-[0.8rem] font-normal",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
19
src/lib/components/ui/calendar/calendar-header.svelte
Normal file
19
src/lib/components/ui/calendar/calendar-header.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.HeaderProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Header
|
||||
bind:ref
|
||||
class={cn(
|
||||
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
16
src/lib/components/ui/calendar/calendar-heading.svelte
Normal file
16
src/lib/components/ui/calendar/calendar-heading.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.HeadingProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Heading
|
||||
bind:ref
|
||||
class={cn("px-(--cell-size) text-sm font-medium", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
48
src/lib/components/ui/calendar/calendar-month-select.svelte
Normal file
48
src/lib/components/ui/calendar/calendar-month-select.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
onchange,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarPrimitive.MonthSelect
|
||||
bind:ref
|
||||
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet child({ props, monthItems, selectedMonthItem })}
|
||||
<select {...props} {value} {onchange}>
|
||||
{#each monthItems as monthItem (monthItem.value)}
|
||||
<option
|
||||
value={monthItem.value}
|
||||
selected={value !== undefined
|
||||
? monthItem.value === value
|
||||
: monthItem.value === selectedMonthItem.value}
|
||||
>
|
||||
{monthItem.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span
|
||||
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</span>
|
||||
{/snippet}
|
||||
</CalendarPrimitive.MonthSelect>
|
||||
</span>
|
||||
15
src/lib/components/ui/calendar/calendar-month.svelte
Normal file
15
src/lib/components/ui/calendar/calendar-month.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { type WithElementRef, cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div {...restProps} bind:this={ref} class={cn("flex w-full flex-col", className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
src/lib/components/ui/calendar/calendar-months.svelte
Normal file
19
src/lib/components/ui/calendar/calendar-months.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("relative flex flex-col gap-4 md:flex-row", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
src/lib/components/ui/calendar/calendar-nav.svelte
Normal file
19
src/lib/components/ui/calendar/calendar-nav.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<nav
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", className)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</nav>
|
||||
31
src/lib/components/ui/calendar/calendar-next-button.svelte
Normal file
31
src/lib/components/ui/calendar/calendar-next-button.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = "ghost",
|
||||
...restProps
|
||||
}: CalendarPrimitive.NextButtonProps & {
|
||||
variant?: ButtonVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronRightIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<CalendarPrimitive.NextButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant }),
|
||||
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
31
src/lib/components/ui/calendar/calendar-prev-button.svelte
Normal file
31
src/lib/components/ui/calendar/calendar-prev-button.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
|
||||
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = "ghost",
|
||||
...restProps
|
||||
}: CalendarPrimitive.PrevButtonProps & {
|
||||
variant?: ButtonVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<CalendarPrimitive.PrevButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant }),
|
||||
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
47
src/lib/components/ui/calendar/calendar-year-select.svelte
Normal file
47
src/lib/components/ui/calendar/calendar-year-select.svelte
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarPrimitive.YearSelect
|
||||
bind:ref
|
||||
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet child({ props, yearItems, selectedYearItem })}
|
||||
<select {...props} {value}>
|
||||
{#each yearItems as yearItem (yearItem.value)}
|
||||
<option
|
||||
value={yearItem.value}
|
||||
selected={value !== undefined
|
||||
? yearItem.value === value
|
||||
: yearItem.value === selectedYearItem.value}
|
||||
>
|
||||
{yearItem.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span
|
||||
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</span>
|
||||
{/snippet}
|
||||
</CalendarPrimitive.YearSelect>
|
||||
</span>
|
||||
115
src/lib/components/ui/calendar/calendar.svelte
Normal file
115
src/lib/components/ui/calendar/calendar.svelte
Normal file
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import * as Calendar from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ButtonVariant } from "../button/button.svelte";
|
||||
import { isEqualMonth, type DateValue } from "@internationalized/date";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
placeholder = $bindable(),
|
||||
class: className,
|
||||
weekdayFormat = "short",
|
||||
buttonVariant = "ghost",
|
||||
captionLayout = "label",
|
||||
locale = "en-NZ",
|
||||
months: monthsProp,
|
||||
years,
|
||||
monthFormat: monthFormatProp,
|
||||
yearFormat = "numeric",
|
||||
day,
|
||||
disableDaysOutsideMonth = false,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
|
||||
buttonVariant?: ButtonVariant;
|
||||
captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label";
|
||||
months?: CalendarPrimitive.MonthSelectProps["months"];
|
||||
years?: CalendarPrimitive.YearSelectProps["years"];
|
||||
monthFormat?: CalendarPrimitive.MonthSelectProps["monthFormat"];
|
||||
yearFormat?: CalendarPrimitive.YearSelectProps["yearFormat"];
|
||||
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
|
||||
} = $props();
|
||||
|
||||
const monthFormat = $derived.by(() => {
|
||||
if (monthFormatProp) return monthFormatProp;
|
||||
if (captionLayout.startsWith("dropdown")) return "short";
|
||||
return "long";
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Discriminated Unions + Destructing (required for bindable) do not
|
||||
get along, so we shut typescript up by casting `value` to `never`.
|
||||
-->
|
||||
<CalendarPrimitive.Root
|
||||
bind:value={value as never}
|
||||
bind:ref
|
||||
bind:placeholder
|
||||
{weekdayFormat}
|
||||
{disableDaysOutsideMonth}
|
||||
class={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{locale}
|
||||
{monthFormat}
|
||||
{yearFormat}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ months, weekdays })}
|
||||
<Calendar.Months>
|
||||
<Calendar.Nav>
|
||||
<Calendar.PrevButton variant={buttonVariant} />
|
||||
<Calendar.NextButton variant={buttonVariant} />
|
||||
</Calendar.Nav>
|
||||
{#each months as month, monthIndex (month)}
|
||||
<Calendar.Month>
|
||||
<Calendar.Header>
|
||||
<Calendar.Caption
|
||||
{captionLayout}
|
||||
months={monthsProp}
|
||||
{monthFormat}
|
||||
{years}
|
||||
{yearFormat}
|
||||
month={month.value}
|
||||
bind:placeholder
|
||||
{locale}
|
||||
{monthIndex}
|
||||
/>
|
||||
</Calendar.Header>
|
||||
<Calendar.Grid>
|
||||
<Calendar.GridHead>
|
||||
<Calendar.GridRow class="select-none">
|
||||
{#each weekdays as weekday (weekday)}
|
||||
<Calendar.HeadCell>
|
||||
{weekday.slice(0, 2)}
|
||||
</Calendar.HeadCell>
|
||||
{/each}
|
||||
</Calendar.GridRow>
|
||||
</Calendar.GridHead>
|
||||
<Calendar.GridBody>
|
||||
{#each month.weeks as weekDates (weekDates)}
|
||||
<Calendar.GridRow class="mt-2 w-full">
|
||||
{#each weekDates as date (date)}
|
||||
<Calendar.Cell {date} month={month.value}>
|
||||
{#if day}
|
||||
{@render day({
|
||||
day: date,
|
||||
outsideMonth: !isEqualMonth(date, month.value),
|
||||
})}
|
||||
{:else}
|
||||
<Calendar.Day />
|
||||
{/if}
|
||||
</Calendar.Cell>
|
||||
{/each}
|
||||
</Calendar.GridRow>
|
||||
{/each}
|
||||
</Calendar.GridBody>
|
||||
</Calendar.Grid>
|
||||
</Calendar.Month>
|
||||
{/each}
|
||||
</Calendar.Months>
|
||||
{/snippet}
|
||||
</CalendarPrimitive.Root>
|
||||
40
src/lib/components/ui/calendar/index.ts
Normal file
40
src/lib/components/ui/calendar/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import Root from "./calendar.svelte";
|
||||
import Cell from "./calendar-cell.svelte";
|
||||
import Day from "./calendar-day.svelte";
|
||||
import Grid from "./calendar-grid.svelte";
|
||||
import Header from "./calendar-header.svelte";
|
||||
import Months from "./calendar-months.svelte";
|
||||
import GridRow from "./calendar-grid-row.svelte";
|
||||
import Heading from "./calendar-heading.svelte";
|
||||
import GridBody from "./calendar-grid-body.svelte";
|
||||
import GridHead from "./calendar-grid-head.svelte";
|
||||
import HeadCell from "./calendar-head-cell.svelte";
|
||||
import NextButton from "./calendar-next-button.svelte";
|
||||
import PrevButton from "./calendar-prev-button.svelte";
|
||||
import MonthSelect from "./calendar-month-select.svelte";
|
||||
import YearSelect from "./calendar-year-select.svelte";
|
||||
import Month from "./calendar-month.svelte";
|
||||
import Nav from "./calendar-nav.svelte";
|
||||
import Caption from "./calendar-caption.svelte";
|
||||
|
||||
export {
|
||||
Day,
|
||||
Cell,
|
||||
Grid,
|
||||
Header,
|
||||
Months,
|
||||
GridRow,
|
||||
Heading,
|
||||
GridBody,
|
||||
GridHead,
|
||||
HeadCell,
|
||||
NextButton,
|
||||
PrevButton,
|
||||
Nav,
|
||||
Month,
|
||||
YearSelect,
|
||||
MonthSelect,
|
||||
Caption,
|
||||
//
|
||||
Root as Calendar,
|
||||
};
|
||||
2
src/lib/components/ui/input/index.ts
Normal file
2
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Input } from "./input.svelte";
|
||||
export type { InputProps } from "./input.svelte";
|
||||
25
src/lib/components/ui/input/input.svelte
Normal file
25
src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
|
||||
export type InputProps = WithElementRef<HTMLInputAttributes>;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
type = "text",
|
||||
ref = $bindable(null),
|
||||
...restProps
|
||||
}: InputProps = $props();
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={ref}
|
||||
{type}
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
34
src/lib/components/ui/item/index.ts
Normal file
34
src/lib/components/ui/item/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Root from "./item.svelte";
|
||||
import Group from "./item-group.svelte";
|
||||
import Separator from "./item-separator.svelte";
|
||||
import Header from "./item-header.svelte";
|
||||
import Footer from "./item-footer.svelte";
|
||||
import Content from "./item-content.svelte";
|
||||
import Title from "./item-title.svelte";
|
||||
import Description from "./item-description.svelte";
|
||||
import Actions from "./item-actions.svelte";
|
||||
import Media from "./item-media.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Separator,
|
||||
Header,
|
||||
Footer,
|
||||
Content,
|
||||
Title,
|
||||
Description,
|
||||
Actions,
|
||||
Media,
|
||||
//
|
||||
Root as Item,
|
||||
Group as ItemGroup,
|
||||
Separator as ItemSeparator,
|
||||
Header as ItemHeader,
|
||||
Footer as ItemFooter,
|
||||
Content as ItemContent,
|
||||
Title as ItemTitle,
|
||||
Description as ItemDescription,
|
||||
Actions as ItemActions,
|
||||
Media as ItemMedia,
|
||||
};
|
||||
20
src/lib/components/ui/item/item-actions.svelte
Normal file
20
src/lib/components/ui/item/item-actions.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="item-actions"
|
||||
class={cn("flex items-center gap-2", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/item/item-content.svelte
Normal file
20
src/lib/components/ui/item/item-content.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="item-content"
|
||||
class={cn("flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
24
src/lib/components/ui/item/item-description.svelte
Normal file
24
src/lib/components/ui/item/item-description.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="item-description"
|
||||
class={cn(
|
||||
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
20
src/lib/components/ui/item/item-footer.svelte
Normal file
20
src/lib/components/ui/item/item-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="item-footer"
|
||||
class={cn("flex basis-full items-center justify-between gap-2", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
21
src/lib/components/ui/item/item-group.svelte
Normal file
21
src/lib/components/ui/item/item-group.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
role="list"
|
||||
data-slot="item-group"
|
||||
class={cn("group/item-group flex flex-col", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/item/item-header.svelte
Normal file
20
src/lib/components/ui/item/item-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="item-header"
|
||||
class={cn("flex basis-full items-center justify-between gap-2", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
42
src/lib/components/ui/item/item-media.svelte
Normal file
42
src/lib/components/ui/item/item-media.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
export const itemMediaVariants = tv({
|
||||
base: "flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
icon: "bg-muted size-8 rounded-sm border [&_svg:not([class*='size-'])]:size-4",
|
||||
image: "size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ItemMediaVariant = VariantProps<typeof itemMediaVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = "default",
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: ItemMediaVariant } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="item-media"
|
||||
data-variant={variant}
|
||||
class={cn(itemMediaVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
src/lib/components/ui/item/item-separator.svelte
Normal file
19
src/lib/components/ui/item/item-separator.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Separator> = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="item-separator"
|
||||
orientation="horizontal"
|
||||
class={cn("my-0", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/item/item-title.svelte
Normal file
20
src/lib/components/ui/item/item-title.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="item-title"
|
||||
class={cn("flex w-fit items-center gap-2 text-sm leading-snug font-medium", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
60
src/lib/components/ui/item/item.svelte
Normal file
60
src/lib/components/ui/item/item.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
export const itemVariants = tv({
|
||||
base: "group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-md border border-transparent text-sm transition-colors duration-100 outline-none focus-visible:ring-[3px] [a]:transition-colors",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline: "border-border",
|
||||
muted: "bg-muted/50",
|
||||
},
|
||||
size: {
|
||||
default: "gap-4 p-4",
|
||||
sm: "gap-2.5 px-4 py-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ItemSize = VariantProps<typeof itemVariants>["size"];
|
||||
export type ItemVariant = VariantProps<typeof itemVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
child,
|
||||
variant,
|
||||
size,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
variant?: ItemVariant;
|
||||
size?: ItemSize;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(itemVariants({ variant, size }), className),
|
||||
"data-slot": "item",
|
||||
"data-variant": variant,
|
||||
"data-size": size,
|
||||
...restProps,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div bind:this={ref} {...mergedProps}>
|
||||
{@render mergedProps.children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
7
src/lib/components/ui/separator/index.ts
Normal file
7
src/lib/components/ui/separator/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
21
src/lib/components/ui/separator/separator.svelte
Normal file
21
src/lib/components/ui/separator/separator.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "separator",
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:min-h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
2
src/lib/components/ui/textarea/index.ts
Normal file
2
src/lib/components/ui/textarea/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Textarea } from "./textarea.svelte";
|
||||
export type { TextareaProps } from "./textarea.svelte";
|
||||
28
src/lib/components/ui/textarea/textarea.svelte
Normal file
28
src/lib/components/ui/textarea/textarea.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||
|
||||
export type TextareaProps = WithElementRef<HTMLTextareaAttributes> & {
|
||||
class?: string;
|
||||
value?: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
value = $bindable(""),
|
||||
ref = $bindable(null),
|
||||
...restProps
|
||||
}: TextareaProps = $props();
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:this={ref}
|
||||
bind:value
|
||||
class={cn(
|
||||
"flex min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
></textarea>
|
||||
@@ -1,8 +1,8 @@
|
||||
import { text, integer, pgTable, json, timestamp } from "drizzle-orm/pg-core";
|
||||
import { text, integer, pgTable, json, date } from "drizzle-orm/pg-core";
|
||||
|
||||
export const entryTable = pgTable("entries", {
|
||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||
date: timestamp({ mode: 'date', withTimezone: true }).notNull().defaultNow(),
|
||||
date: date().notNull().defaultNow(),
|
||||
location: json(),
|
||||
content: text(),
|
||||
image: text()
|
||||
|
||||
@@ -55,3 +55,13 @@ export async function updateEntry(entry) {
|
||||
body: JSON.stringify(entry),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteEntry(entry) {
|
||||
await fetch(`/api/entry/delete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: entry.id })
|
||||
});
|
||||
}
|
||||
13
src/lib/utils.ts
Normal file
13
src/lib/utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
4
src/params/date.ts
Normal file
4
src/params/date.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
export function match(value) {
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(value)
|
||||
}
|
||||
@@ -1,14 +1,59 @@
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import Header from '$lib/components/header.svelte';
|
||||
import "./layout.css";
|
||||
import favicon from "$lib/assets/favicon.svg";
|
||||
import { ModeWatcher } from "mode-watcher";
|
||||
import {
|
||||
CalendarDate,
|
||||
today,
|
||||
getLocalTimeZone,
|
||||
} from "@internationalized/date";
|
||||
|
||||
let { children } = $props();
|
||||
import Header from "$lib/components/header.svelte";
|
||||
import Calendar from "$lib/components/calendar.svelte";
|
||||
import EntrySummaryView from "$lib/components/entrySummaryView.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
let { children, data } = $props();
|
||||
let dateValue = $derived(data.date);
|
||||
let entries = $derived(data.entries);
|
||||
let title = $state("Memento")
|
||||
|
||||
$effect(() => {
|
||||
// Navigate when dateValue changes
|
||||
goto(`/${dateValue}`);
|
||||
title = `Memento: ${dateValue}`
|
||||
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// Sync dateValue with data.date when it changes
|
||||
dateValue = data.date;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
<ModeWatcher />
|
||||
|
||||
<div class="md:max-w-2/3 mx-auto">
|
||||
<div
|
||||
class="flex flex-col md:flex-row space-y-4 w-full mt-6 mx-auto md:max-w-3/4"
|
||||
>
|
||||
<div class="md:w-1/2 md:order-2">
|
||||
<Header />
|
||||
|
||||
{#key dateValue}
|
||||
{@render children()}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<div class="md:w-1/2 lg:max-w-1/4 space-y-4 md:mr-4">
|
||||
<Calendar bind:value={dateValue} />
|
||||
<ul class="space-y-4">
|
||||
{#each entries as entry}
|
||||
<EntrySummaryView {entry} bind:value={dateValue} />
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
22
src/routes/+layout.ts
Normal file
22
src/routes/+layout.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { today, getLocalTimeZone, CalendarDate } from "@internationalized/date";
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
let dateValue;
|
||||
|
||||
if (params.date) {
|
||||
const [year, month, day] = params.date.split("-").map(Number);
|
||||
dateValue = new CalendarDate(year, month, day);
|
||||
} else {
|
||||
dateValue = today(getLocalTimeZone());
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`/api/entry?month=${dateValue.year}-${dateValue.month}`,
|
||||
);
|
||||
const entries = await res.json();
|
||||
|
||||
return {
|
||||
entries: entries,
|
||||
date: dateValue,
|
||||
};
|
||||
}
|
||||
@@ -1,54 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { CalendarDate } from "@internationalized/date";
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
import Header from '$lib/components/header.svelte';
|
||||
|
||||
import Calendar from "$lib/components/calendar.svelte";
|
||||
import Editor from "$lib/components/editor/index.svelte";
|
||||
import Editor from "$lib/components/editor.svelte";
|
||||
import EntrySummaryView from "$lib/components/entrySummaryView.svelte";
|
||||
import AddEntryCover from "$lib/components/addEntryCover.svelte";
|
||||
import { today } from "$lib/date";
|
||||
|
||||
let edit = $state(false);
|
||||
let selectedEntry = $state("");
|
||||
let selectedDate = $state(today());
|
||||
let dateValue = $state(new CalendarDate(2026, 2, 1));
|
||||
|
||||
let selectEntry = async (hasEntry: boolean, data: string | null) => {
|
||||
// data should be the entryID if hasEntry == true, or the date if there is no entry
|
||||
if (hasEntry) {
|
||||
const res = await fetch(`/api/entry?id=${data}`);
|
||||
selectedEntry = await res.json();
|
||||
selectedDate = selectedEntry.date;
|
||||
} else {
|
||||
selectedEntry = "";
|
||||
selectedDate = data as string;
|
||||
}
|
||||
};
|
||||
|
||||
// $effect(() => {selectedEntry = ''})
|
||||
</script>
|
||||
|
||||
<Calendar entries={data.all} dayClickCallback={selectEntry} />
|
||||
<!-- <Editor {dateValue} /> -->
|
||||
|
||||
<div class="flex flex-col md:flex-row space-y-4 w-full mt-6">
|
||||
<div>
|
||||
Click on a date to view or edit an entry.
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <div class="flex flex-col md:flex-row space-y-4 w-full mt-6">
|
||||
<div class="md:w-1/2 md:order-2">
|
||||
{#if selectedEntry}
|
||||
{#key selectedEntry.id}
|
||||
<Editor bind:entry={selectedEntry} />
|
||||
<Header />
|
||||
|
||||
{#key dateValue}
|
||||
<Editor {dateValue} />
|
||||
{/key}
|
||||
{:else}
|
||||
{#key selectedDate}
|
||||
<AddEntryCover bind:date={selectedDate} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="md:w-1/2 md:mr-4">
|
||||
<ul>
|
||||
<div class="md:w-1/2 lg:max-w-1/4 space-y-4 md:mr-4">
|
||||
<Calendar bind:value={dateValue} />
|
||||
<ul class="space-y-4">
|
||||
{#each data.all as entry}
|
||||
<EntrySummaryView
|
||||
clickCB={async () => await selectEntry(true, entry.id)}
|
||||
{entry}
|
||||
bind:value={dateValue}
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export const load = async ({ fetch, params }) => {
|
||||
const res = await fetch("/api/entry/all");
|
||||
const allEntries = await res.json();
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayEntry = await fetch(`/api/entry?date=${today}`);
|
||||
const todayEntryData = await todayEntry.json();
|
||||
|
||||
return {
|
||||
all: allEntries,
|
||||
today: todayEntryData,
|
||||
};
|
||||
};
|
||||
10
src/routes/[date=date]/+page.svelte
Normal file
10
src/routes/[date=date]/+page.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import Editor from "$lib/components/editor.svelte";
|
||||
|
||||
let {
|
||||
data
|
||||
} = $props()
|
||||
|
||||
</script>
|
||||
|
||||
<Editor dateValue={data.date} />
|
||||
6
src/routes/[date=date]/+page.ts
Normal file
6
src/routes/[date=date]/+page.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
const res = await fetch(`/api/entry?date=${params.date}`);
|
||||
const entries = await res.json();
|
||||
return { entry: entries[0] };
|
||||
}
|
||||
@@ -61,7 +61,7 @@ async function getEntryByMonth(monthString: string) {
|
||||
const endDate = new Date(year, month, 1, 0, 0, 0, 0)
|
||||
|
||||
const entries = await db.select().from(entryTable).where(
|
||||
sql`${entryTable.date} >= ${startDate.toISOString()}::timestamp AND ${entryTable.date} < ${endDate.toISOString()}::timestamp`
|
||||
sql`${entryTable.date} >= ${startDate.toISOString()}::timestamp AND ${entryTable.date} < ${endDate.toISOString()}::timestamp ORDER BY ${entryTable.date} DESC`
|
||||
)
|
||||
|
||||
return httpResponse(entries, 200)
|
||||
|
||||
@@ -9,7 +9,6 @@ export async function POST({ request }) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const entry: typeof entryTable.$inferInsert = body
|
||||
entry.date = new Date(body.date)
|
||||
|
||||
await db.insert(entryTable).values(entry)
|
||||
|
||||
|
||||
@@ -1,16 +1,121 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/forms';
|
||||
@import "tailwindcss";
|
||||
|
||||
/*@import 'quill/dist/quill.core.css';
|
||||
@import 'quill/dist/quill.bubble.css';
|
||||
@import 'quill/dist/quill.snow.css';*/
|
||||
@import "tw-animate-css";
|
||||
|
||||
html {
|
||||
@apply py-3 px-4;
|
||||
@apply bg-[#252525] text-white
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
html body {
|
||||
/*@apply font-[AmericanTypewriter];*/
|
||||
/* @apply w-full md:w-[68%] mx-auto h-full; */
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user