feat: better components
This commit is contained in:
parent
398ddde169
commit
cdeaa13d92
@ -4,6 +4,7 @@
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"tailwindFunctions": ["tv", "cn", "clsx"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
|
2385
client/package-lock.json
generated
2385
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,35 +14,38 @@
|
||||
"lint": "prettier --check . && eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/protovalidate": "^0.1.0",
|
||||
"@connectrpc/connect": "^2.0.2",
|
||||
"@connectrpc/connect-web": "^2.0.2",
|
||||
"@eslint/compat": "^1.2.8",
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@lucide/svelte": "^0.479.0",
|
||||
"@scalar/api-reference": "^1.28.22",
|
||||
"@scalar/api-reference": "^1.28.32",
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.20.7",
|
||||
"@sveltejs/kit": "^2.20.8",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"bits-ui": "^1.3.19",
|
||||
"cbor2": "^1.12.0",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"bits-ui": "^1.4.7",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.5.1",
|
||||
"globals": "^16.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"globals": "^16.1.0",
|
||||
"mode-watcher": "^1.0.7",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.27.3",
|
||||
"svelte-check": "^4.1.6",
|
||||
"svelte": "^5.28.2",
|
||||
"svelte-check": "^4.1.7",
|
||||
"svelte-sonner": "^0.3.28",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.0.13",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.1"
|
||||
"typescript-eslint": "^8.32.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,105 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
@import "tw-animate-css";
|
||||
|
||||
@theme {
|
||||
--color-crust: #11111b;
|
||||
--color-mantle: #181825;
|
||||
--color-base: #1e1e2e;
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
--color-surface-0: #313244;
|
||||
--color-surface-1: #45475a;
|
||||
--color-surface-2: #585b70;
|
||||
@theme inline {
|
||||
--spacing-body: calc(100vh - 180px);
|
||||
|
||||
--color-overlay-0: #6c7086;
|
||||
--color-overlay-1: #7f849c;
|
||||
--color-overlay-2: #9399b2;
|
||||
--color-accent: var(--sky);
|
||||
|
||||
--color-subtext-0: #a6adc8;
|
||||
--color-subtext-1: #bac2de;
|
||||
|
||||
--color-text: #cdd6f4;
|
||||
|
||||
--color-sky: #89dceb;
|
||||
--color-red: #f38ba8;
|
||||
--color-rosewater: var(--rosewater);
|
||||
--color-flamingo: var(--flamingo);
|
||||
--color-pink: var(--pink);
|
||||
--color-mauve: var(--mauve);
|
||||
--color-red: var(--red);
|
||||
--color-maroon: var(--maroon);
|
||||
--color-peach: var(--peach);
|
||||
--color-yellow: var(--yellow);
|
||||
--color-green: var(--green);
|
||||
--color-teal: var(--teal);
|
||||
--color-sky: var(--sky);
|
||||
--color-sapphire: var(--sapphire);
|
||||
--color-blue: var(--blue);
|
||||
--color-lavender: var(--lavender);
|
||||
--color-text: var(--text);
|
||||
--color-subtext-1: var(--subtext-1);
|
||||
--color-subtext: var(--subtext);
|
||||
--color-overlay-2: var(--overlay-2);
|
||||
--color-overlay-1: var(--overlay-1);
|
||||
--color-overlay: var(--overlay);
|
||||
--color-surface-2: var(--surface-2);
|
||||
--color-surface-1: var(--surface-1);
|
||||
--color-surface: var(--surface);
|
||||
--color-based: var(--based);
|
||||
--color-mantle: var(--mantle);
|
||||
--color-crust: var(--crust);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--rosewater: hsl(11deg, 59%, 67%);
|
||||
--flamingo: hsl(0deg, 60%, 67%);
|
||||
--pink: hsl(316deg, 73%, 69%);
|
||||
--mauve: hsl(266deg, 85%, 58%);
|
||||
--red: hsl(347deg, 87%, 44%);
|
||||
--maroon: hsl(355deg, 76%, 59%);
|
||||
--peach: hsl(22deg, 99%, 52%);
|
||||
--yellow: hsl(35deg, 77%, 49%);
|
||||
--green: hsl(109deg, 58%, 40%);
|
||||
--teal: hsl(183deg, 74%, 35%);
|
||||
--sky: hsl(197deg, 97%, 46%);
|
||||
--sapphire: hsl(189deg, 70%, 42%);
|
||||
--blue: hsl(220deg, 91%, 54%);
|
||||
--lavender: hsl(231deg, 97%, 72%);
|
||||
--text: hsl(234deg, 16%, 35%);
|
||||
--subtext-1: hsl(233deg, 13%, 41%);
|
||||
--subtext: hsl(233deg, 10%, 47%);
|
||||
--overlay-2: hsl(232deg, 10%, 53%);
|
||||
--overlay-1: hsl(231deg, 10%, 59%);
|
||||
--overlay: hsl(228deg, 11%, 65%);
|
||||
--surface-2: hsl(227deg, 12%, 71%);
|
||||
--surface-1: hsl(225deg, 14%, 77%);
|
||||
--surface: hsl(223deg, 16%, 83%);
|
||||
--based: hsl(220deg, 23%, 95%);
|
||||
--mantle: hsl(220deg, 22%, 92%);
|
||||
--crust: hsl(220deg, 21%, 89%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--rosewater: hsl(10deg, 56%, 91%);
|
||||
--flamingo: hsl(0deg, 59%, 88%);
|
||||
--pink: hsl(316deg, 72%, 86%);
|
||||
--mauve: hsl(267deg, 84%, 81%);
|
||||
--red: hsl(343deg, 81%, 75%);
|
||||
--maroon: hsl(350deg, 65%, 77%);
|
||||
--peach: hsl(23deg, 92%, 75%);
|
||||
--yellow: hsl(41deg, 86%, 83%);
|
||||
--green: hsl(115deg, 54%, 76%);
|
||||
--teal: hsl(170deg, 57%, 73%);
|
||||
--sky: hsl(189deg, 71%, 73%);
|
||||
--sapphire: hsl(199deg, 76%, 69%);
|
||||
--blue: hsl(217deg, 92%, 76%);
|
||||
--lavender: hsl(232deg, 97%, 85%);
|
||||
--text: hsl(226deg, 64%, 88%);
|
||||
--subtext-1: hsl(227deg, 35%, 80%);
|
||||
--subtext: hsl(228deg, 24%, 72%);
|
||||
--overlay-2: hsl(228deg, 17%, 64%);
|
||||
--overlay-1: hsl(230deg, 13%, 55%);
|
||||
--overlay: hsl(231deg, 11%, 47%);
|
||||
--surface-2: hsl(233deg, 12%, 39%);
|
||||
--surface-1: hsl(234deg, 13%, 31%);
|
||||
--surface: hsl(237deg, 16%, 23%);
|
||||
--based: hsl(240deg, 21%, 15%);
|
||||
--mantle: hsl(240deg, 21%, 12%);
|
||||
--crust: hsl(240deg, 23%, 9%);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
-- Cheat sheet --
|
||||
|
||||
Focus Outline: blue
|
||||
Border: surface-1
|
||||
Hover: bump color by 2 (eg crust -> based), if accent color drop opacity (eg blue -> blue/90)
|
||||
*/
|
@ -8,7 +8,7 @@
|
||||
<title>TrevStack</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="tap" class="bg-base text-text min-h-screen">
|
||||
<body data-sveltekit-preload-data="tap" class="bg-crust text-text min-h-screen">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
4678
client/src/lib/connect/buf/validate/validate_pb.ts
Normal file
4678
client/src/lib/connect/buf/validate/validate_pb.ts
Normal file
File diff suppressed because one or more lines are too long
@ -1,9 +1,10 @@
|
||||
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.2.5 with parameter "target=ts"
|
||||
// @generated from file item/v1/item.proto (package item.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1";
|
||||
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
|
||||
import type { Timestamp } from "@bufbuild/protobuf/wkt";
|
||||
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
@ -12,7 +13,7 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file item/v1/item.proto.
|
||||
*/
|
||||
export const file_item_v1_item: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEigQEKBEl0ZW0SCgoCaWQYASABKAMSDAoEbmFtZRgCIAEoCRIpCgVhZGRlZBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEwoLZGVzY3JpcHRpb24YBCABKAkSDQoFcHJpY2UYBSABKAISEAoIcXVhbnRpdHkYBiABKAUiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKAMiLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKAVIA4gBARITCgZvZmZzZXQYBSABKAVIBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAMiVwoRQ3JlYXRlSXRlbVJlcXVlc3QSDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRINCgVwcmljZRgDIAEoAhIQCghxdWFudGl0eRgEIAEoBSJLChJDcmVhdGVJdGVtUmVzcG9uc2USCgoCaWQYASABKAMSKQoFYWRkZWQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIqcBChFVcGRhdGVJdGVtUmVxdWVzdBIKCgJpZBgBIAEoAxIRCgRuYW1lGAIgASgJSACIAQESGAoLZGVzY3JpcHRpb24YAyABKAlIAYgBARISCgVwcmljZRgEIAEoAkgCiAEBEhUKCHF1YW50aXR5GAUgASgFSAOIAQFCBwoFX25hbWVCDgoMX2Rlc2NyaXB0aW9uQggKBl9wcmljZUILCglfcXVhbnRpdHkiFAoSVXBkYXRlSXRlbVJlc3BvbnNlIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgDIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKcAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkVnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL2Nvbm5lY3QvaXRlbS92MTtpdGVtdjGiAgNJWFiqAgdJdGVtLlYxygIHSXRlbVxWMeICE0l0ZW1cVjFcR1BCTWV0YWRhdGHqAghJdGVtOjpWMWIGcHJvdG8z", [file_google_protobuf_timestamp]);
|
||||
fileDesc("ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEiigEKBEl0ZW0SCgoCaWQYASABKAMSFQoEbmFtZRgCIAEoCUIHukgEcgIQAxIpCgVhZGRlZBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEwoLZGVzY3JpcHRpb24YBCABKAkSDQoFcHJpY2UYBSABKAISEAoIcXVhbnRpdHkYBiABKAUiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKAMiLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKAVIA4gBARITCgZvZmZzZXQYBSABKAVIBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAMiVwoRQ3JlYXRlSXRlbVJlcXVlc3QSDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRINCgVwcmljZRgDIAEoAhIQCghxdWFudGl0eRgEIAEoBSJLChJDcmVhdGVJdGVtUmVzcG9uc2USCgoCaWQYASABKAMSKQoFYWRkZWQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIqcBChFVcGRhdGVJdGVtUmVxdWVzdBIKCgJpZBgBIAEoAxIRCgRuYW1lGAIgASgJSACIAQESGAoLZGVzY3JpcHRpb24YAyABKAlIAYgBARISCgVwcmljZRgEIAEoAkgCiAEBEhUKCHF1YW50aXR5GAUgASgFSAOIAQFCBwoFX25hbWVCDgoMX2Rlc2NyaXB0aW9uQggKBl9wcmljZUILCglfcXVhbnRpdHkiFAoSVXBkYXRlSXRlbVJlc3BvbnNlIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgDIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKcAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkVnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL2Nvbm5lY3QvaXRlbS92MTtpdGVtdjGiAgNJWFiqAgdJdGVtLlYxygIHSXRlbVxWMeICE0l0ZW1cVjFcR1BCTWV0YWRhdGHqAghJdGVtOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp]);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.Item
|
||||
|
@ -1,16 +1,17 @@
|
||||
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.2.5 with parameter "target=ts"
|
||||
// @generated from file user/v1/auth.proto (package user.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1";
|
||||
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file user/v1/auth.proto.
|
||||
*/
|
||||
export const file_user_v1_auth: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChJ1c2VyL3YxL2F1dGgucHJvdG8SB3VzZXIudjEiMgoMTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIh4KDUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkiTQoNU2lnblVwUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCRIYChBjb25maXJtX3Bhc3N3b3JkGAMgASgJIhAKDlNpZ25VcFJlc3BvbnNlIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UywQEKC0F1dGhTZXJ2aWNlEjgKBUxvZ2luEhUudXNlci52MS5Mb2dpblJlcXVlc3QaFi51c2VyLnYxLkxvZ2luUmVzcG9uc2UiABI7CgZTaWduVXASFi51c2VyLnYxLlNpZ25VcFJlcXVlc3QaFy51c2VyLnYxLlNpZ25VcFJlc3BvbnNlIgASOwoGTG9nb3V0EhYudXNlci52MS5Mb2dvdXRSZXF1ZXN0GhcudXNlci52MS5Mb2dvdXRSZXNwb25zZSIAQpwBCgtjb20udXNlci52MUIJQXV0aFByb3RvUAFaRWdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvY29ubmVjdC91c2VyL3YxO3VzZXJ2MaICA1VYWKoCB1VzZXIuVjHKAgdVc2VyXFYx4gITVXNlclxWMVxHUEJNZXRhZGF0YeoCCFVzZXI6OlYxYgZwcm90bzM");
|
||||
fileDesc("ChJ1c2VyL3YxL2F1dGgucHJvdG8SB3VzZXIudjEiOwoMTG9naW5SZXF1ZXN0EhkKCHVzZXJuYW1lGAEgASgJQge6SARyAhADEhAKCHBhc3N3b3JkGAIgASgJIh4KDUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkiVgoNU2lnblVwUmVxdWVzdBIZCgh1c2VybmFtZRgBIAEoCUIHukgEcgIQAxIQCghwYXNzd29yZBgCIAEoCRIYChBjb25maXJtX3Bhc3N3b3JkGAMgASgJIhAKDlNpZ25VcFJlc3BvbnNlIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiLAoYQmVnaW5QYXNza2V5TG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJIjEKGUJlZ2luUGFzc2tleUxvZ2luUmVzcG9uc2USFAoMb3B0aW9uc19qc29uGAEgASgJIkIKGUZpbmlzaFBhc3NrZXlMb2dpblJlcXVlc3QSEAoIdXNlcm5hbWUYASABKAkSEwoLYXR0ZXN0YXRpb24YAiABKAkiKwoaRmluaXNoUGFzc2tleUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkygAMKC0F1dGhTZXJ2aWNlEjgKBUxvZ2luEhUudXNlci52MS5Mb2dpblJlcXVlc3QaFi51c2VyLnYxLkxvZ2luUmVzcG9uc2UiABI7CgZTaWduVXASFi51c2VyLnYxLlNpZ25VcFJlcXVlc3QaFy51c2VyLnYxLlNpZ25VcFJlc3BvbnNlIgASOwoGTG9nb3V0EhYudXNlci52MS5Mb2dvdXRSZXF1ZXN0GhcudXNlci52MS5Mb2dvdXRSZXNwb25zZSIAElwKEUJlZ2luUGFzc2tleUxvZ2luEiEudXNlci52MS5CZWdpblBhc3NrZXlMb2dpblJlcXVlc3QaIi51c2VyLnYxLkJlZ2luUGFzc2tleUxvZ2luUmVzcG9uc2UiABJfChJGaW5pc2hQYXNza2V5TG9naW4SIi51c2VyLnYxLkZpbmlzaFBhc3NrZXlMb2dpblJlcXVlc3QaIy51c2VyLnYxLkZpbmlzaFBhc3NrZXlMb2dpblJlc3BvbnNlIgBCnAEKC2NvbS51c2VyLnYxQglBdXRoUHJvdG9QAVpFZ2l0aHViLmNvbS9zcG90ZGVtbzQvdHJldnN0YWNrL3NlcnZlci9pbnRlcm5hbC9jb25uZWN0L3VzZXIvdjE7dXNlcnYxogIDVVhYqgIHVXNlci5WMcoCB1VzZXJcVjHiAhNVc2VyXFYxXEdQQk1ldGFkYXRh6gIIVXNlcjo6VjFiBnByb3RvMw", [file_buf_validate_validate]);
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.LoginRequest
|
||||
@ -117,6 +118,79 @@ export type LogoutResponse = Message<"user.v1.LogoutResponse"> & {
|
||||
export const LogoutResponseSchema: GenMessage<LogoutResponse> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_auth, 5);
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.BeginPasskeyLoginRequest
|
||||
*/
|
||||
export type BeginPasskeyLoginRequest = Message<"user.v1.BeginPasskeyLoginRequest"> & {
|
||||
/**
|
||||
* @generated from field: string username = 1;
|
||||
*/
|
||||
username: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message user.v1.BeginPasskeyLoginRequest.
|
||||
* Use `create(BeginPasskeyLoginRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const BeginPasskeyLoginRequestSchema: GenMessage<BeginPasskeyLoginRequest> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_auth, 6);
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.BeginPasskeyLoginResponse
|
||||
*/
|
||||
export type BeginPasskeyLoginResponse = Message<"user.v1.BeginPasskeyLoginResponse"> & {
|
||||
/**
|
||||
* @generated from field: string options_json = 1;
|
||||
*/
|
||||
optionsJson: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message user.v1.BeginPasskeyLoginResponse.
|
||||
* Use `create(BeginPasskeyLoginResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const BeginPasskeyLoginResponseSchema: GenMessage<BeginPasskeyLoginResponse> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_auth, 7);
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.FinishPasskeyLoginRequest
|
||||
*/
|
||||
export type FinishPasskeyLoginRequest = Message<"user.v1.FinishPasskeyLoginRequest"> & {
|
||||
/**
|
||||
* @generated from field: string username = 1;
|
||||
*/
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string attestation = 2;
|
||||
*/
|
||||
attestation: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message user.v1.FinishPasskeyLoginRequest.
|
||||
* Use `create(FinishPasskeyLoginRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const FinishPasskeyLoginRequestSchema: GenMessage<FinishPasskeyLoginRequest> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_auth, 8);
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.FinishPasskeyLoginResponse
|
||||
*/
|
||||
export type FinishPasskeyLoginResponse = Message<"user.v1.FinishPasskeyLoginResponse"> & {
|
||||
/**
|
||||
* @generated from field: string token = 1;
|
||||
*/
|
||||
token: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message user.v1.FinishPasskeyLoginResponse.
|
||||
* Use `create(FinishPasskeyLoginResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const FinishPasskeyLoginResponseSchema: GenMessage<FinishPasskeyLoginResponse> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_auth, 9);
|
||||
|
||||
/**
|
||||
* @generated from service user.v1.AuthService
|
||||
*/
|
||||
@ -145,6 +219,22 @@ export const AuthService: GenService<{
|
||||
input: typeof LogoutRequestSchema;
|
||||
output: typeof LogoutResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc user.v1.AuthService.BeginPasskeyLogin
|
||||
*/
|
||||
beginPasskeyLogin: {
|
||||
methodKind: "unary";
|
||||
input: typeof BeginPasskeyLoginRequestSchema;
|
||||
output: typeof BeginPasskeyLoginResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc user.v1.AuthService.FinishPasskeyLogin
|
||||
*/
|
||||
finishPasskeyLogin: {
|
||||
methodKind: "unary";
|
||||
input: typeof FinishPasskeyLoginRequestSchema;
|
||||
output: typeof FinishPasskeyLoginResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_user_v1_auth, 0);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v2.2.5 with parameter "target=ts"
|
||||
// @generated from file user/v1/user.proto (package user.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file user/v1/user.proto.
|
||||
*/
|
||||
export const file_user_v1_user: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiXAoEVXNlchIKCgJpZBgBIAEoAxIQCgh1c2VybmFtZRgCIAEoCRIfChJwcm9maWxlX3BpY3R1cmVfaWQYAyABKANIAIgBAUIVChNfcHJvZmlsZV9waWN0dXJlX2lkIhAKDkdldFVzZXJSZXF1ZXN0Ii4KD0dldFVzZXJSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIl0KFVVwZGF0ZVBhc3N3b3JkUmVxdWVzdBIUCgxvbGRfcGFzc3dvcmQYASABKAkSFAoMbmV3X3Bhc3N3b3JkGAIgASgJEhgKEGNvbmZpcm1fcGFzc3dvcmQYAyABKAkiNQoWVXBkYXRlUGFzc3dvcmRSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIj4KEEdldEFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIgChFHZXRBUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkiPgobVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXF1ZXN0EhEKCWZpbGVfbmFtZRgBIAEoCRIMCgRkYXRhGAIgASgMIjsKHFVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2USGwoEdXNlchgBIAEoCzINLnVzZXIudjEuVXNlcjLPAgoLVXNlclNlcnZpY2USPgoHR2V0VXNlchIXLnVzZXIudjEuR2V0VXNlclJlcXVlc3QaGC51c2VyLnYxLkdldFVzZXJSZXNwb25zZSIAElMKDlVwZGF0ZVBhc3N3b3JkEh4udXNlci52MS5VcGRhdGVQYXNzd29yZFJlcXVlc3QaHy51c2VyLnYxLlVwZGF0ZVBhc3N3b3JkUmVzcG9uc2UiABJECglHZXRBUElLZXkSGS51c2VyLnYxLkdldEFQSUtleVJlcXVlc3QaGi51c2VyLnYxLkdldEFQSUtleVJlc3BvbnNlIgASZQoUVXBkYXRlUHJvZmlsZVBpY3R1cmUSJC51c2VyLnYxLlVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVxdWVzdBolLnVzZXIudjEuVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXNwb25zZSIAQpwBCgtjb20udXNlci52MUIJVXNlclByb3RvUAFaRWdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvY29ubmVjdC91c2VyL3YxO3VzZXJ2MaICA1VYWKoCB1VzZXIuVjHKAgdVc2VyXFYx4gITVXNlclxWMVxHUEJNZXRhZGF0YeoCCFVzZXI6OlYxYgZwcm90bzM");
|
||||
fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiXAoEVXNlchIKCgJpZBgBIAEoAxIQCgh1c2VybmFtZRgCIAEoCRIfChJwcm9maWxlX3BpY3R1cmVfaWQYAyABKANIAIgBAUIVChNfcHJvZmlsZV9waWN0dXJlX2lkIhAKDkdldFVzZXJSZXF1ZXN0Ii4KD0dldFVzZXJSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIl0KFVVwZGF0ZVBhc3N3b3JkUmVxdWVzdBIUCgxvbGRfcGFzc3dvcmQYASABKAkSFAoMbmV3X3Bhc3N3b3JkGAIgASgJEhgKEGNvbmZpcm1fcGFzc3dvcmQYAyABKAkiNQoWVXBkYXRlUGFzc3dvcmRSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIj4KEEdldEFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIgChFHZXRBUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkiPgobVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXF1ZXN0EhEKCWZpbGVfbmFtZRgBIAEoCRIMCgRkYXRhGAIgASgMIjsKHFVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2USGwoEdXNlchgBIAEoCzINLnVzZXIudjEuVXNlciIhCh9CZWdpblBhc3NrZXlSZWdpc3RyYXRpb25SZXF1ZXN0IjgKIEJlZ2luUGFzc2tleVJlZ2lzdHJhdGlvblJlc3BvbnNlEhQKDG9wdGlvbnNfanNvbhgBIAEoCSI3CiBGaW5pc2hQYXNza2V5UmVnaXN0cmF0aW9uUmVxdWVzdBITCgthdHRlc3RhdGlvbhgBIAEoCSIjCiFGaW5pc2hQYXNza2V5UmVnaXN0cmF0aW9uUmVzcG9uc2UyuAQKC1VzZXJTZXJ2aWNlEj4KB0dldFVzZXISFy51c2VyLnYxLkdldFVzZXJSZXF1ZXN0GhgudXNlci52MS5HZXRVc2VyUmVzcG9uc2UiABJTCg5VcGRhdGVQYXNzd29yZBIeLnVzZXIudjEuVXBkYXRlUGFzc3dvcmRSZXF1ZXN0Gh8udXNlci52MS5VcGRhdGVQYXNzd29yZFJlc3BvbnNlIgASRAoJR2V0QVBJS2V5EhkudXNlci52MS5HZXRBUElLZXlSZXF1ZXN0GhoudXNlci52MS5HZXRBUElLZXlSZXNwb25zZSIAEmUKFFVwZGF0ZVByb2ZpbGVQaWN0dXJlEiQudXNlci52MS5VcGRhdGVQcm9maWxlUGljdHVyZVJlcXVlc3QaJS51c2VyLnYxLlVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2UiABJxChhCZWdpblBhc3NrZXlSZWdpc3RyYXRpb24SKC51c2VyLnYxLkJlZ2luUGFzc2tleVJlZ2lzdHJhdGlvblJlcXVlc3QaKS51c2VyLnYxLkJlZ2luUGFzc2tleVJlZ2lzdHJhdGlvblJlc3BvbnNlIgASdAoZRmluaXNoUGFzc2tleVJlZ2lzdHJhdGlvbhIpLnVzZXIudjEuRmluaXNoUGFzc2tleVJlZ2lzdHJhdGlvblJlcXVlc3QaKi51c2VyLnYxLkZpbmlzaFBhc3NrZXlSZWdpc3RyYXRpb25SZXNwb25zZSIAQpwBCgtjb20udXNlci52MUIJVXNlclByb3RvUAFaRWdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvY29ubmVjdC91c2VyL3YxO3VzZXJ2MaICA1VYWKoCB1VzZXIuVjHKAgdVc2VyXFYx4gITVXNlclxWMVxHUEJNZXRhZGF0YeoCCFVzZXI6OlYxYgZwcm90bzM");
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.User
|
||||
@ -191,6 +191,66 @@ export type UpdateProfilePictureResponse = Message<"user.v1.UpdateProfilePicture
|
||||
export const UpdateProfilePictureResponseSchema: GenMessage<UpdateProfilePictureResponse> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_user, 8);
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.BeginPasskeyRegistrationRequest
|
||||
*/
|
||||
export type BeginPasskeyRegistrationRequest = Message<"user.v1.BeginPasskeyRegistrationRequest"> & {
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message user.v1.BeginPasskeyRegistrationRequest.
|
||||
* Use `create(BeginPasskeyRegistrationRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const BeginPasskeyRegistrationRequestSchema: GenMessage<BeginPasskeyRegistrationRequest> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_user, 9);
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.BeginPasskeyRegistrationResponse
|
||||
*/
|
||||
export type BeginPasskeyRegistrationResponse = Message<"user.v1.BeginPasskeyRegistrationResponse"> & {
|
||||
/**
|
||||
* @generated from field: string options_json = 1;
|
||||
*/
|
||||
optionsJson: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message user.v1.BeginPasskeyRegistrationResponse.
|
||||
* Use `create(BeginPasskeyRegistrationResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const BeginPasskeyRegistrationResponseSchema: GenMessage<BeginPasskeyRegistrationResponse> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_user, 10);
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.FinishPasskeyRegistrationRequest
|
||||
*/
|
||||
export type FinishPasskeyRegistrationRequest = Message<"user.v1.FinishPasskeyRegistrationRequest"> & {
|
||||
/**
|
||||
* @generated from field: string attestation = 1;
|
||||
*/
|
||||
attestation: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message user.v1.FinishPasskeyRegistrationRequest.
|
||||
* Use `create(FinishPasskeyRegistrationRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const FinishPasskeyRegistrationRequestSchema: GenMessage<FinishPasskeyRegistrationRequest> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_user, 11);
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.FinishPasskeyRegistrationResponse
|
||||
*/
|
||||
export type FinishPasskeyRegistrationResponse = Message<"user.v1.FinishPasskeyRegistrationResponse"> & {
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message user.v1.FinishPasskeyRegistrationResponse.
|
||||
* Use `create(FinishPasskeyRegistrationResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const FinishPasskeyRegistrationResponseSchema: GenMessage<FinishPasskeyRegistrationResponse> = /*@__PURE__*/
|
||||
messageDesc(file_user_v1_user, 12);
|
||||
|
||||
/**
|
||||
* @generated from service user.v1.UserService
|
||||
*/
|
||||
@ -227,6 +287,22 @@ export const UserService: GenService<{
|
||||
input: typeof UpdateProfilePictureRequestSchema;
|
||||
output: typeof UpdateProfilePictureResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc user.v1.UserService.BeginPasskeyRegistration
|
||||
*/
|
||||
beginPasskeyRegistration: {
|
||||
methodKind: "unary";
|
||||
input: typeof BeginPasskeyRegistrationRequestSchema;
|
||||
output: typeof BeginPasskeyRegistrationResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc user.v1.UserService.FinishPasskeyRegistration
|
||||
*/
|
||||
finishPasskeyRegistration: {
|
||||
methodKind: "unary";
|
||||
input: typeof FinishPasskeyRegistrationRequestSchema;
|
||||
output: typeof FinishPasskeyRegistrationResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_user_v1_user, 0);
|
||||
|
||||
|
7
client/src/lib/coolforms/conststate.svelte.ts
Normal file
7
client/src/lib/coolforms/conststate.svelte.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
export function newState<T>(s: T) {
|
||||
const state = $state(s);
|
||||
|
||||
return state;
|
||||
}
|
175
client/src/lib/coolforms/coolforms.svelte.ts
Normal file
175
client/src/lib/coolforms/coolforms.svelte.ts
Normal file
@ -0,0 +1,175 @@
|
||||
import { create, type DescMessage, type DescService, type MessageShape, type MessageInitShape } from "@bufbuild/protobuf";
|
||||
import { ValidationError, type Violation } from "@bufbuild/protovalidate";
|
||||
import { Validator } from "../transport";
|
||||
import { ConnectError, type Client } from "@connectrpc/connect";
|
||||
import type { Action } from "svelte/action";
|
||||
|
||||
type Options<Input extends DescMessage, Output extends DescMessage> = {
|
||||
init?: MessageInitShape<Input>,
|
||||
start?: boolean,
|
||||
reset?: boolean,
|
||||
onSubmit?: (formData: FormData, input: MessageShape<Input>) => Promise<MessageShape<Input>>,
|
||||
onResult?: (result: MessageShape<Output>) => void,
|
||||
onError?: (err: Violation[] | ConnectError) => void
|
||||
}
|
||||
|
||||
type Violations<Field> = {
|
||||
[field in keyof Field]?: Violation[];
|
||||
};
|
||||
|
||||
export function coolForm<
|
||||
Service extends DescService,
|
||||
Method extends Service['methods'][number]
|
||||
>(
|
||||
client: Client<Service>,
|
||||
method: Method,
|
||||
options?: Options<Method['input'], Method['output']>
|
||||
) {
|
||||
const input = $state(create(method.input as Method['input'], options?.init));
|
||||
const output = $state(create(method.output as Method['output']));
|
||||
const errors: Violations<Method['input']['field']> & {
|
||||
form?: ConnectError
|
||||
} = $state({});
|
||||
let loading = $state(false);
|
||||
|
||||
const validate = () => {
|
||||
// Delete existing errors
|
||||
for (const key in errors) {
|
||||
delete errors[key];
|
||||
}
|
||||
|
||||
try {
|
||||
Validator.validate(method['input'], input);
|
||||
} catch (e) {
|
||||
if (!(e instanceof ValidationError)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Map violation errors to errors rune
|
||||
for (const violation of e.violations) {
|
||||
for (const field of violation.field) {
|
||||
if (!("localName" in field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create localName property if it doesn't exist
|
||||
if (!errors[field.localName]) {
|
||||
Object.assign(errors, {
|
||||
[field.localName]: [violation]
|
||||
})
|
||||
} else {
|
||||
errors[field.localName]?.push(violation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return e.violations;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
// When a request is successful
|
||||
const success = (response: MessageShape<Method['output']>) => {
|
||||
loading = false;
|
||||
|
||||
// Send the response up
|
||||
options?.onResult?.(response);
|
||||
|
||||
// Set the response
|
||||
Object.assign(output, response);
|
||||
|
||||
// If we want to reset the input
|
||||
if (options && (options.reset == undefined || options.reset)) {
|
||||
const cleared = create(method.input as Method['input'], options?.init)
|
||||
Object.assign(input, cleared);
|
||||
}
|
||||
}
|
||||
|
||||
// When a request fails
|
||||
const fail = (err: Violation[] | ConnectError | any) => {
|
||||
loading = false;
|
||||
|
||||
// It's a Violation[]
|
||||
if (Array.isArray(err)) {
|
||||
// Send the error up
|
||||
options?.onError?.(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a ConnectError
|
||||
if (err instanceof ConnectError) {
|
||||
// Assign it to the form
|
||||
errors.form = err;
|
||||
|
||||
// Send the error up
|
||||
options?.onError?.(err);
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
loading = true;
|
||||
|
||||
// Validate
|
||||
const validationErrors = validate();
|
||||
if (validationErrors.length > 0) {
|
||||
fail(validationErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send response
|
||||
if (method.methodKind == "unary") {
|
||||
// @ts-ignore I can't figure out how to make this typescript compliant
|
||||
const response = client[method.localName]($state.snapshot(input)) as Promise<MessageShape<Method['output']>>
|
||||
|
||||
response
|
||||
.then((resp) => {
|
||||
success(resp);
|
||||
}).catch(err => {
|
||||
fail(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// A nice action to give to forms to run submit() on submit
|
||||
const impair: Action<HTMLFormElement> = (form) => {
|
||||
$effect(() => {
|
||||
form.onsubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (options?.onSubmit) {
|
||||
const formData = new FormData(form);
|
||||
options.onSubmit(formData, input).then((i) => {
|
||||
Object.assign(input, i);
|
||||
submit();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
submit();
|
||||
}
|
||||
|
||||
return () => {
|
||||
form.onsubmit = () => { };
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
if (options?.start) {
|
||||
submit();
|
||||
}
|
||||
|
||||
return {
|
||||
input,
|
||||
output,
|
||||
errors,
|
||||
loading: () => loading,
|
||||
submit,
|
||||
validate,
|
||||
impair
|
||||
}
|
||||
}
|
7
client/src/lib/coolforms/index.ts
Normal file
7
client/src/lib/coolforms/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { coolForm } from "./coolforms.svelte";
|
||||
import { newState } from "./conststate.svelte";
|
||||
|
||||
export {
|
||||
coolForm,
|
||||
newState
|
||||
};
|
@ -1,9 +1,11 @@
|
||||
import { createConnectTransport } from '@connectrpc/connect-web';
|
||||
import { createValidator } from "@bufbuild/protovalidate";
|
||||
import { Code, ConnectError, createClient, type Interceptor } from '@connectrpc/connect';
|
||||
import { AuthService } from '$lib/connect/user/v1/auth_pb';
|
||||
import { UserService } from '$lib/connect/user/v1/user_pb';
|
||||
import { ItemService } from '$lib/connect/item/v1/item_pb';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const redirector: Interceptor = (next) => async (req) => {
|
||||
try {
|
||||
@ -11,7 +13,11 @@ const redirector: Interceptor = (next) => async (req) => {
|
||||
} catch (e) {
|
||||
const error = ConnectError.from(e);
|
||||
if (error.code === Code.Unauthenticated) {
|
||||
await goto('/auth');
|
||||
const redirectURL = new URL(page.url);
|
||||
redirectURL.pathname = '/auth';
|
||||
redirectURL.searchParams.append('redir', encodeURIComponent(page.url.pathname + page.url.search));
|
||||
|
||||
await goto(redirectURL);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@ -25,3 +31,5 @@ const transport = createConnectTransport({
|
||||
export const AuthClient = createClient(AuthService, transport);
|
||||
export const UserClient = createClient(UserService, transport);
|
||||
export const ItemClient = createClient(ItemService, transport);
|
||||
|
||||
export const Validator = createValidator();
|
||||
|
@ -1,15 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { userState } from '$lib/sharedState.svelte';
|
||||
import { Avatar } from 'bits-ui';
|
||||
</script>
|
||||
|
||||
<Avatar.Root class="flex h-full w-full items-center justify-center">
|
||||
<Avatar.Image
|
||||
src={userState.user?.profilePictureId ? '/file/' + userState.user.profilePictureId : null}
|
||||
alt={`${userState.user?.username}'s avatar`}
|
||||
class="rounded-full"
|
||||
/>
|
||||
<Avatar.Fallback class="font-medium uppercase"
|
||||
>{userState.user?.username.substring(0, 2)}</Avatar.Fallback
|
||||
>
|
||||
</Avatar.Root>
|
@ -1,32 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type me = MouseEvent & { currentTarget: EventTarget & HTMLButtonElement };
|
||||
|
||||
let {
|
||||
className,
|
||||
type,
|
||||
onclick,
|
||||
children
|
||||
}: {
|
||||
className?: string;
|
||||
type?: 'submit' | 'reset' | 'button' | null;
|
||||
onclick?: (e: me) => void;
|
||||
children?: Snippet<[]>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Button.Root
|
||||
{type}
|
||||
class={cn(
|
||||
'bg-sky text-crust focus:outline-sky flex w-fit cursor-pointer items-center justify-center rounded p-2 px-4 text-sm font-medium transition-all hover:brightness-120 focus:outline-2 focus:outline-offset-1',
|
||||
className
|
||||
)}
|
||||
onclick={(e: me) => {
|
||||
onclick?.(e);
|
||||
}}
|
||||
>
|
||||
{@render children?.()}
|
||||
</Button.Root>
|
@ -1,170 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { ArrowLeft, ArrowRight, Minus, Calendar, X } from '@lucide/svelte';
|
||||
import { DateRangePicker, type DateRange } from 'bits-ui';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { getLocalTimeZone } from '@internationalized/date';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
let {
|
||||
className,
|
||||
start = $bindable(),
|
||||
end = $bindable(),
|
||||
onchange
|
||||
}: {
|
||||
className?: string;
|
||||
start?: Date;
|
||||
end?: Date;
|
||||
onchange?: (start?: Date, end?: Date) => void;
|
||||
} = $props();
|
||||
|
||||
let daterange: DateRange = $state({
|
||||
start: undefined,
|
||||
end: undefined
|
||||
});
|
||||
let rerender = $state(false);
|
||||
</script>
|
||||
|
||||
<!-- Need to rerender because setting to undefined doesn't work -->
|
||||
{#key rerender}
|
||||
<DateRangePicker.Root
|
||||
bind:value={daterange}
|
||||
onValueChange={(v) => {
|
||||
if (v.start && v.end) {
|
||||
start = v.start.toDate(getLocalTimeZone());
|
||||
end = v.end.toDate(getLocalTimeZone());
|
||||
if (onchange) {
|
||||
onchange(start, end);
|
||||
}
|
||||
}
|
||||
}}
|
||||
class={cn(className)}
|
||||
>
|
||||
<div
|
||||
class="bg-mantle border-surface-0 hover:border-surface-2 flex items-center rounded border pl-2 text-sm drop-shadow-md transition-all"
|
||||
>
|
||||
<div class="flex grow items-center justify-center">
|
||||
{#each ['start', 'end'] as const as type (type)}
|
||||
<DateRangePicker.Input {type}>
|
||||
{#snippet children({ segments })}
|
||||
{#each segments as seg (seg)}
|
||||
<div class="inline-block select-none">
|
||||
{#if seg.part === 'literal'}
|
||||
<DateRangePicker.Segment part={seg.part} class="text-overlay-0 p-1">
|
||||
{seg.value}
|
||||
</DateRangePicker.Segment>
|
||||
{:else}
|
||||
<DateRangePicker.Segment
|
||||
part={seg.part}
|
||||
class="aria-[valuetext=Empty]:text-overlay-0 hover:bg-surface-0 focus:bg-surface-0 focus:outline-sky rounded p-0.5 transition-all focus:outline focus:outline-offset-1"
|
||||
>
|
||||
{seg.value}
|
||||
</DateRangePicker.Segment>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</DateRangePicker.Input>
|
||||
{#if type === 'start'}
|
||||
<div aria-hidden="true" class="px-1">
|
||||
<Minus size="10" />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<DateRangePicker.Trigger
|
||||
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky ml-1 flex grow cursor-pointer items-center justify-center p-2 transition-all focus:outline focus:outline-offset-1"
|
||||
>
|
||||
<Calendar size="20" />
|
||||
</DateRangePicker.Trigger>
|
||||
<button
|
||||
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded-r p-2 transition-all focus:outline focus:outline-offset-1"
|
||||
onclick={() => {
|
||||
if (daterange) {
|
||||
daterange.end = undefined;
|
||||
daterange.start = undefined;
|
||||
}
|
||||
start = undefined;
|
||||
end = undefined;
|
||||
if (onchange) {
|
||||
onchange(start, end);
|
||||
}
|
||||
rerender = !rerender;
|
||||
}}
|
||||
>
|
||||
<X size="20" />
|
||||
</button>
|
||||
</div>
|
||||
<DateRangePicker.Content forceMount>
|
||||
{#snippet child({ props, open })}
|
||||
{#if open}
|
||||
<div
|
||||
{...props}
|
||||
class="absolute z-50"
|
||||
transition:fade={{
|
||||
duration: 100
|
||||
}}
|
||||
>
|
||||
<DateRangePicker.Calendar
|
||||
class="border-surface-0 bg-mantle mt-1 rounded border p-3 drop-shadow-md"
|
||||
>
|
||||
{#snippet children({ months, weekdays })}
|
||||
<DateRangePicker.Header class="flex items-center justify-between">
|
||||
<DateRangePicker.PrevButton
|
||||
class="hover:bg-surface-0 inline-flex size-10 cursor-pointer items-center justify-center rounded transition-all active:scale-[0.98]"
|
||||
>
|
||||
<ArrowLeft />
|
||||
</DateRangePicker.PrevButton>
|
||||
<DateRangePicker.Heading class="font-medium select-none" />
|
||||
<DateRangePicker.NextButton
|
||||
class="hover:bg-surface-0 inline-flex size-10 cursor-pointer items-center justify-center rounded transition-all active:scale-[0.98]"
|
||||
>
|
||||
<ArrowRight />
|
||||
</DateRangePicker.NextButton>
|
||||
</DateRangePicker.Header>
|
||||
<div class="flex flex-col space-y-4 pt-4 sm:flex-row sm:space-y-0 sm:space-x-4">
|
||||
{#each months as month, i (i)}
|
||||
<DateRangePicker.Grid class="w-full border-collapse space-y-1 select-none">
|
||||
<DateRangePicker.GridHead>
|
||||
<DateRangePicker.GridRow class="mb-1 flex w-full justify-between">
|
||||
{#each weekdays as day, i (i)}
|
||||
<DateRangePicker.HeadCell
|
||||
class="text-overlay-0 w-10 rounded text-xs font-normal!"
|
||||
>
|
||||
{day.slice(0, 2)}
|
||||
</DateRangePicker.HeadCell>
|
||||
{/each}
|
||||
</DateRangePicker.GridRow>
|
||||
</DateRangePicker.GridHead>
|
||||
<DateRangePicker.GridBody>
|
||||
{#each month.weeks as weekDates, i (i)}
|
||||
<DateRangePicker.GridRow class="flex w-full">
|
||||
{#each weekDates as date, i (i)}
|
||||
<DateRangePicker.Cell
|
||||
{date}
|
||||
month={month.value}
|
||||
class="relative m-0 size-10 overflow-visible p-0! text-center text-sm focus-within:relative focus-within:z-20"
|
||||
>
|
||||
<DateRangePicker.Day
|
||||
class="hover:border-sky focus-visible:ring-foreground! data-highlighted:bg-surface-0 data-selected:bg-surface-1 data-selection-end:bg-surface-2 data-selection-start:bg-surface-2 data-disabled:text-text/30 data-unavailable:text-overlay-0 group relative inline-flex size-10 items-center justify-center overflow-visible rounded border border-transparent bg-transparent p-0 text-sm font-normal whitespace-nowrap transition-all data-disabled:pointer-events-none data-highlighted:rounded-none data-outside-month:pointer-events-none data-selected:rounded-none data-selection-end:rounded-r data-selection-start:rounded-l data-unavailable:line-through"
|
||||
>
|
||||
<div
|
||||
class="bg-sky group-data-selected:bg-background absolute top-[5px] hidden size-1 rounded-full transition-all group-data-today:block"
|
||||
></div>
|
||||
{date.day}
|
||||
</DateRangePicker.Day>
|
||||
</DateRangePicker.Cell>
|
||||
{/each}
|
||||
</DateRangePicker.GridRow>
|
||||
{/each}
|
||||
</DateRangePicker.GridBody>
|
||||
</DateRangePicker.Grid>
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
</DateRangePicker.Calendar>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DateRangePicker.Content>
|
||||
</DateRangePicker.Root>
|
||||
{/key}
|
@ -1,51 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { X } from '@lucide/svelte';
|
||||
|
||||
let {
|
||||
name,
|
||||
value = $bindable(''),
|
||||
type = 'text',
|
||||
placeholder,
|
||||
className,
|
||||
onchange
|
||||
}: {
|
||||
name?: string;
|
||||
value?: string | number;
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
onchange?: (e: Event) => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
'border-surface-0 hover:border-surface-2 flex items-center justify-between gap-1 rounded border p-0 drop-shadow-md transition-all',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<input
|
||||
id={name}
|
||||
{name}
|
||||
{type}
|
||||
{placeholder}
|
||||
class="focus:outline-sky grow rounded-l p-2 text-sm transition-all focus:outline focus:outline-offset-1"
|
||||
bind:value
|
||||
{onchange}
|
||||
/>
|
||||
<button
|
||||
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded-r p-2 transition-all focus:outline focus:outline-offset-1"
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
if (value) {
|
||||
value = '';
|
||||
if (onchange) {
|
||||
onchange(e);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X size="20" />
|
||||
</button>
|
||||
</div>
|
@ -1,92 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { X } from '@lucide/svelte';
|
||||
import { Dialog } from 'bits-ui';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { type Snippet } from 'svelte';
|
||||
import { pushState } from '$app/navigation';
|
||||
|
||||
let {
|
||||
trigger,
|
||||
title,
|
||||
content,
|
||||
open = $bindable(false)
|
||||
}: {
|
||||
trigger: Snippet<[Record<string, unknown>]>;
|
||||
title: Snippet;
|
||||
content: Snippet;
|
||||
open?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
onpopstate={() => {
|
||||
if (open) {
|
||||
open = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Dialog.Root
|
||||
bind:open
|
||||
onOpenChange={(e) => {
|
||||
if (e) {
|
||||
pushState('', '#modal');
|
||||
} else {
|
||||
history.back();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog.Trigger>
|
||||
{#snippet child({ props })}
|
||||
{@render trigger(props)}
|
||||
{/snippet}
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay forceMount>
|
||||
{#snippet child({ props, open })}
|
||||
{#if open}
|
||||
<div
|
||||
{...props}
|
||||
transition:fade={{
|
||||
duration: 100
|
||||
}}
|
||||
>
|
||||
<div class="fixed inset-0 z-50 mt-[50px] bg-black/50 transition-all"></div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Dialog.Overlay>
|
||||
<Dialog.Content forceMount>
|
||||
{#snippet child({ props, open: propopen })}
|
||||
{#if propopen}
|
||||
<div
|
||||
{...props}
|
||||
transition:fade={{
|
||||
duration: 100
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="bg-mantle border-surface-0 fixed inset-0 top-[50%] left-[50%] z-50 size-fit w-96 -translate-x-1/2 -translate-y-1/2 transform overflow-y-auto rounded-xl border pb-1 drop-shadow-md"
|
||||
>
|
||||
<div class="border-surface-0 flex justify-between border-b p-2">
|
||||
<h1 class="grow truncate p-1 text-center text-xl font-bold">
|
||||
{@render title()}
|
||||
</h1>
|
||||
<button
|
||||
tabindex="-1"
|
||||
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded p-1 transition-all focus:outline focus:outline-offset-1"
|
||||
onclick={() => {
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
{@render content()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
@ -1,86 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
||||
import { Pagination } from 'bits-ui';
|
||||
import { pushState, replaceState } from '$app/navigation';
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
let {
|
||||
count = $bindable(),
|
||||
limit = $bindable(),
|
||||
offset = $bindable(0),
|
||||
className,
|
||||
onchange
|
||||
}: {
|
||||
count: number;
|
||||
limit: number;
|
||||
offset?: number;
|
||||
className?: string;
|
||||
onchange?: (e: number) => void;
|
||||
} = $props();
|
||||
|
||||
let page: number = $state(1);
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
replaceState('', `${page}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
onpopstate={(e) => {
|
||||
const lastPage: number = Number(e.state['sveltekit:states']);
|
||||
if (!isNaN(lastPage)) {
|
||||
page = lastPage;
|
||||
offset = (lastPage - 1) * limit;
|
||||
window.scrollTo(0, 0);
|
||||
onchange?.(lastPage);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{#key count && limit}
|
||||
<Pagination.Root
|
||||
{count}
|
||||
bind:page
|
||||
perPage={limit}
|
||||
onPageChange={(e) => {
|
||||
offset = (e - 1) * limit;
|
||||
window.scrollTo(0, 0);
|
||||
pushState('', `${e}`);
|
||||
onchange?.(e);
|
||||
}}
|
||||
>
|
||||
{#snippet children({ pages, range })}
|
||||
<div class={cn('mb-2 flex items-center justify-center gap-2', className)}>
|
||||
<Pagination.PrevButton
|
||||
class="hover:bg-surface-0 disabled:text-overlay-0 inline-flex cursor-pointer items-center justify-center rounded p-2 transition-all disabled:cursor-not-allowed hover:disabled:bg-transparent"
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Pagination.PrevButton>
|
||||
<div class="flex items-center gap-2">
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === 'ellipsis'}
|
||||
<div class="font-medium select-none">...</div>
|
||||
{:else}
|
||||
<Pagination.Page
|
||||
{page}
|
||||
class="hover:bg-surface-0 data-selected:bg-surface-0 data-selected:text-background inline-flex size-10 cursor-pointer items-center justify-center rounded bg-transparent font-medium transition-all select-none disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent"
|
||||
>
|
||||
{page.value}
|
||||
</Pagination.Page>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<Pagination.NextButton
|
||||
class="hover:bg-surface-0 disabled:text-overlay-0 inline-flex cursor-pointer items-center justify-center rounded p-2 transition-all disabled:cursor-not-allowed hover:disabled:bg-transparent"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Pagination.NextButton>
|
||||
</div>
|
||||
<p class="text-overlay-2 text-center text-sm">
|
||||
Showing {range.start} - {range.end}
|
||||
</p>
|
||||
{/snippet}
|
||||
</Pagination.Root>
|
||||
{/key}
|
@ -1,94 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { Check, ChevronsDown, ChevronsUp, ChevronsUpDown, X } from '@lucide/svelte';
|
||||
import { Select } from 'bits-ui';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let {
|
||||
value = $bindable('10'),
|
||||
placeholder = 'Select an item',
|
||||
items = [],
|
||||
defaultValue = '',
|
||||
className,
|
||||
onchange
|
||||
}: {
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
items: { value: string; label: string; disabled?: boolean }[];
|
||||
defaultValue?: string;
|
||||
className?: string;
|
||||
onchange?: (e: string) => void;
|
||||
} = $props();
|
||||
|
||||
const selectedLabel = $derived(value ? items.find((i) => i.value === value)?.label : placeholder);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
'border-surface-0 bg-mantle hover:border-surface-2 flex items-center justify-between rounded border p-0 drop-shadow-md transition-all',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Select.Root type="single" {items} bind:value onValueChange={onchange}>
|
||||
<Select.Trigger
|
||||
class="focus:outline-sky data-placeholder:text-overlay-0 inline-flex grow cursor-pointer items-center justify-between gap-2 rounded-l py-2 pl-2 text-sm transition-colors select-none focus:outline focus:outline-offset-1"
|
||||
aria-label={placeholder}
|
||||
>
|
||||
{selectedLabel}
|
||||
<ChevronsUpDown class="text-overlay-0" size="20" />
|
||||
</Select.Trigger>
|
||||
<Select.Portal>
|
||||
<Select.Content forceMount>
|
||||
{#snippet child({ wrapperProps, props, open })}
|
||||
{#if open}
|
||||
<div {...wrapperProps}>
|
||||
<div
|
||||
{...props}
|
||||
class="border-surface-0 bg-mantle shadow-popover z-50 mt-1 rounded border p-1 outline-hidden select-none"
|
||||
transition:fade={{
|
||||
duration: 100
|
||||
}}
|
||||
>
|
||||
<Select.ScrollUpButton class="flex w-full items-center justify-center">
|
||||
<ChevronsUp size="20" />
|
||||
</Select.ScrollUpButton>
|
||||
<Select.Viewport class="p-1">
|
||||
{#each items as item, i (i + item.value)}
|
||||
<Select.Item
|
||||
class="data-highlighted:bg-surface-0 flex h-10 w-full cursor-pointer items-center gap-4 rounded px-5 py-3 text-sm capitalize outline-hidden select-none data-disabled:cursor-not-allowed data-disabled:opacity-50"
|
||||
value={item.value}
|
||||
label={item.label}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{#snippet children({ selected })}
|
||||
{item.label}
|
||||
{#if selected}
|
||||
<div class="ml-auto">
|
||||
<Check size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Viewport>
|
||||
<Select.ScrollDownButton class="flex w-full items-center justify-center">
|
||||
<ChevronsDown size="20" />
|
||||
</Select.ScrollDownButton>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
<button
|
||||
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded-r p-2 transition-all focus:outline focus:outline-offset-1"
|
||||
type="button"
|
||||
onclick={() => {
|
||||
value = defaultValue;
|
||||
onchange?.(value);
|
||||
}}
|
||||
>
|
||||
<X size="20" />
|
||||
</button>
|
||||
</div>
|
20
client/src/lib/ui/avatar/avatar-fallback.svelte
Normal file
20
client/src/lib/ui/avatar/avatar-fallback.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.FallbackProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Fallback
|
||||
bind:ref
|
||||
data-slot="avatar-fallback"
|
||||
class={cn(
|
||||
'bg-surface outline-surface-2 flex size-full select-none items-center justify-center rounded-full text-sm transition-all',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
17
client/src/lib/ui/avatar/avatar-image.svelte
Normal file
17
client/src/lib/ui/avatar/avatar-image.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.ImageProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Image
|
||||
bind:ref
|
||||
data-slot="avatar-image"
|
||||
class={cn("aspect-square size-full", className)}
|
||||
{...restProps}
|
||||
/>
|
17
client/src/lib/ui/avatar/avatar.svelte
Normal file
17
client/src/lib/ui/avatar/avatar.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Avatar as AvatarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AvatarPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AvatarPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="avatar"
|
||||
class={cn("relative outline outline-offset-2 outline-surface-1 flex size-9 shrink-0 overflow-hidden rounded-full", className)}
|
||||
{...restProps}
|
||||
/>
|
13
client/src/lib/ui/avatar/index.ts
Normal file
13
client/src/lib/ui/avatar/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import Root from "./avatar.svelte";
|
||||
import Image from "./avatar-image.svelte";
|
||||
import Fallback from "./avatar-fallback.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Image,
|
||||
Fallback,
|
||||
//
|
||||
Root as Avatar,
|
||||
Image as AvatarImage,
|
||||
Fallback as AvatarFallback,
|
||||
};
|
95
client/src/lib/ui/button/button.svelte
Normal file
95
client/src/lib/ui/button/button.svelte
Normal file
@ -0,0 +1,95 @@
|
||||
<script lang="ts" module>
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
import { cn } from '$lib/utils';
|
||||
import { LoaderCircle } from '@lucide/svelte';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: cn(
|
||||
'shadow-xs inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all',
|
||||
|
||||
// Focus
|
||||
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
|
||||
// Disabled
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
|
||||
// Images
|
||||
"[&_svg:not([class*='size-'])]:size-5 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
),
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'text-crust bg-accent hover:bg-accent/90 shadow-xs',
|
||||
red: 'text-crust bg-red hover:bg-red/90 shadow-xs',
|
||||
outline: 'text-text border-surface-1 hover:bg-surface shadow-xs border bg-transparent',
|
||||
input: 'text-text border-surface-1 hover:border-overlay shadow-xs border bg-transparent',
|
||||
ghost: 'text-text hover:bg-surface shadow-xs'
|
||||
},
|
||||
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: 'h-9 min-w-9 px-3'
|
||||
}
|
||||
},
|
||||
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;
|
||||
loading?: boolean;
|
||||
scan?: boolean;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = 'button',
|
||||
loading,
|
||||
scan,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{href}
|
||||
{...restProps}
|
||||
>
|
||||
{#if loading}
|
||||
<LoaderCircle class="animate-spin" />
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{...restProps}
|
||||
>
|
||||
{#if loading}
|
||||
<LoaderCircle class="animate-spin" />
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
17
client/src/lib/ui/button/index.ts
Normal file
17
client/src/lib/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,
|
||||
};
|
17
client/src/lib/ui/card/card.svelte
Normal file
17
client/src/lib/ui/card/card.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>>;
|
||||
|
||||
let { ref = $bindable(null), class: className, children, ...restProps }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn('bg-based border-surface-1 rounded border p-5 shadow-md', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
7
client/src/lib/ui/card/index.ts
Normal file
7
client/src/lib/ui/card/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Root from "./card.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Card,
|
||||
};
|
76
client/src/lib/ui/daterangepicker/daterangepicker.svelte
Normal file
76
client/src/lib/ui/daterangepicker/daterangepicker.svelte
Normal file
@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import CalendarIcon from '@lucide/svelte/icons/calendar';
|
||||
import type { DateRange } from 'bits-ui';
|
||||
import {
|
||||
CalendarDate,
|
||||
DateFormatter,
|
||||
type DateValue,
|
||||
getLocalTimeZone
|
||||
} from '@internationalized/date';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { buttonVariants } from '$lib/ui/button';
|
||||
import { RangeCalendar } from '$lib/ui/range-calendar';
|
||||
import * as Popover from '$lib/ui/popover';
|
||||
|
||||
let {
|
||||
className,
|
||||
start,
|
||||
end,
|
||||
onchange
|
||||
}: {
|
||||
className?: string;
|
||||
start?: Date;
|
||||
end?: Date;
|
||||
onchange?: (start: Date, end: Date) => void;
|
||||
} = $props();
|
||||
|
||||
let value: DateRange = $state({
|
||||
start: start
|
||||
? new CalendarDate(start.getFullYear(), start.getMonth(), start.getDate())
|
||||
: undefined,
|
||||
end: end ? new CalendarDate(end.getFullYear(), end.getMonth(), end.getDate()) : undefined
|
||||
});
|
||||
|
||||
const df = new DateFormatter('en-US', {
|
||||
dateStyle: 'medium'
|
||||
});
|
||||
|
||||
let startValue: DateValue | undefined = $state(undefined);
|
||||
</script>
|
||||
|
||||
<div class={cn('grid gap-2', className)}>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger
|
||||
class={cn(buttonVariants({ variant: 'input' }), 'bg-based', !value && 'text-subtext')}
|
||||
>
|
||||
<CalendarIcon class="mr-2 size-4" />
|
||||
{#if value && value.start}
|
||||
{#if value.end}
|
||||
{df.format(value.start.toDate(getLocalTimeZone()))} - {df.format(
|
||||
value.end.toDate(getLocalTimeZone())
|
||||
)}
|
||||
{:else}
|
||||
{df.format(value.start.toDate(getLocalTimeZone()))}
|
||||
{/if}
|
||||
{:else if startValue}
|
||||
{df.format(startValue.toDate(getLocalTimeZone()))}
|
||||
{:else}
|
||||
Pick a date range
|
||||
{/if}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-auto p-0 bg-based" align="start">
|
||||
<RangeCalendar
|
||||
bind:value
|
||||
onStartValueChange={(v) => {
|
||||
startValue = v;
|
||||
}}
|
||||
onValueChange={(v) => {
|
||||
if (v.start && v.end) {
|
||||
onchange?.(v.start.toDate(getLocalTimeZone()), v.end.toDate(getLocalTimeZone()));
|
||||
}
|
||||
}}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
6
client/src/lib/ui/daterangepicker/index.ts
Normal file
6
client/src/lib/ui/daterangepicker/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import Root from "./daterangepicker.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Root as DateRangePicker
|
||||
};
|
7
client/src/lib/ui/dialog/dialog-close.svelte
Normal file
7
client/src/lib/ui/dialog/dialog-close.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
50
client/src/lib/ui/dialog/dialog-content.svelte
Normal file
50
client/src/lib/ui/dialog/dialog-content.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import type { Snippet } from 'svelte';
|
||||
import * as Dialog from './index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
'bg-mantle text-text border-surface-1 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg',
|
||||
|
||||
// Animations
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 duration-200',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<DialogPrimitive.Close
|
||||
class={cn(
|
||||
'text-text absolute top-4 right-4 cursor-pointer p-1 rounded hover:bg-crust transition-all disabled:pointer-events-none',
|
||||
|
||||
// Focus
|
||||
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
|
||||
// Images
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
)}
|
||||
>
|
||||
<X />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
17
client/src/lib/ui/dialog/dialog-description.svelte
Normal file
17
client/src/lib/ui/dialog/dialog-description.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
21
client/src/lib/ui/dialog/dialog-footer.svelte
Normal file
21
client/src/lib/ui/dialog/dialog-footer.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
21
client/src/lib/ui/dialog/dialog-header.svelte
Normal file
21
client/src/lib/ui/dialog/dialog-header.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
20
client/src/lib/ui/dialog/dialog-overlay.svelte
Normal file
20
client/src/lib/ui/dialog/dialog-overlay.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
17
client/src/lib/ui/dialog/dialog-title.svelte
Normal file
17
client/src/lib/ui/dialog/dialog-title.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("text-lg font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
/>
|
7
client/src/lib/ui/dialog/dialog-trigger.svelte
Normal file
7
client/src/lib/ui/dialog/dialog-trigger.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
28
client/src/lib/ui/dialog/dialog.svelte
Normal file
28
client/src/lib/ui/dialog/dialog.svelte
Normal file
@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { pushState } from '$app/navigation';
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
|
||||
let { open = $bindable(), onOpenChange, ...restProps }: DialogPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
onpopstate={() => {
|
||||
if (open) {
|
||||
open = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<DialogPrimitive.Root
|
||||
onOpenChange={(v) => {
|
||||
if (v) {
|
||||
pushState('', '#dialog');
|
||||
} else {
|
||||
history.back();
|
||||
}
|
||||
|
||||
onOpenChange?.(v);
|
||||
}}
|
||||
bind:open
|
||||
{...restProps}
|
||||
/>
|
37
client/src/lib/ui/dialog/index.ts
Normal file
37
client/src/lib/ui/dialog/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
import Root from './dialog.svelte';
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Close from "./dialog-close.svelte";
|
||||
|
||||
const Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import Check from "@lucide/svelte/icons/check";
|
||||
import Minus from "@lucide/svelte/icons/minus";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
class={cn(
|
||||
"focus:bg-surface outline-hidden relative flex cursor-pointer select-none items-center gap-2 rounded-sm py-2 pl-8 pr-2 text-sm",
|
||||
|
||||
// Disabled
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
|
||||
// Images
|
||||
"[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
{#if indeterminate}
|
||||
<Minus class="size-4" />
|
||||
{:else}
|
||||
<Check class={cn("size-4", !checked && "text-transparent")} />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
31
client/src/lib/ui/dropdown-menu/dropdown-menu-content.svelte
Normal file
31
client/src/lib/ui/dropdown-menu/dropdown-menu-content.svelte
Normal file
@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ContentProps & {
|
||||
portalProps?: DropdownMenuPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Portal {...portalProps}>
|
||||
<DropdownMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-content"
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
'bg-based text-text border-surface z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||
|
||||
// Animations
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
17
client/src/lib/ui/dropdown-menu/dropdown-menu-group.svelte
Normal file
17
client/src/lib/ui/dropdown-menu/dropdown-menu-group.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Group
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-group"
|
||||
class={cn('border-b border-surface first:pt-0 last:pb-0 last:border-none', className)}
|
||||
{...restProps}
|
||||
/>
|
27
client/src/lib/ui/dropdown-menu/dropdown-menu-item.svelte
Normal file
27
client/src/lib/ui/dropdown-menu/dropdown-menu-item.svelte
Normal file
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ItemProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-item"
|
||||
class={cn(
|
||||
'focus:bg-surface text-text relative flex cursor-pointer items-center gap-2 px-4 py-2 text-sm outline-hidden transition-all select-none',
|
||||
|
||||
// Disabled
|
||||
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
|
||||
// Images
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
24
client/src/lib/ui/dropdown-menu/dropdown-menu-label.svelte
Normal file
24
client/src/lib/ui/dropdown-menu/dropdown-menu-label.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { type WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-label"
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
22
client/src/lib/ui/dropdown-menu/dropdown-menu-link.svelte
Normal file
22
client/src/lib/ui/dropdown-menu/dropdown-menu-link.svelte
Normal file
@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import Item from './dropdown-menu-item.svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
href,
|
||||
children,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.ItemProps & {
|
||||
href?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Item bind:ref class={className} {...restProps}>
|
||||
{#snippet child({ props })}
|
||||
<a {...props} {href}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item>
|
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...restProps}
|
||||
/>
|
@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from 'bits-ui';
|
||||
import Circle from '@lucide/svelte/icons/circle';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
class={cn(
|
||||
"focus:bg-surface text-text relative flex cursor-pointer items-center gap-2 py-2 pr-2 pl-8 text-sm select-none",
|
||||
|
||||
// Disabled
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
|
||||
// Images
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
{#if checked}
|
||||
<Circle class="size-2 fill-current" />
|
||||
{:else}
|
||||
<Circle class="size-2" />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-separator"
|
||||
class={cn("bg-surface-1 -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { type WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
class={cn("text-text ml-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubContentProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
class={cn(
|
||||
'bg-based text-text origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
|
||||
// Animations
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
class={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRight class="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
23
client/src/lib/ui/dropdown-menu/dropdown-menu-trigger.svelte
Normal file
23
client/src/lib/ui/dropdown-menu/dropdown-menu-trigger.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DropdownMenuPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-trigger"
|
||||
class={cn(
|
||||
'flex cursor-pointer items-center gap-1 transition-all',
|
||||
|
||||
// Focus
|
||||
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
50
client/src/lib/ui/dropdown-menu/index.ts
Normal file
50
client/src/lib/ui/dropdown-menu/index.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||
import Content from "./dropdown-menu-content.svelte";
|
||||
import Group from "./dropdown-menu-group.svelte";
|
||||
import Item from "./dropdown-menu-item.svelte";
|
||||
import Link from './dropdown-menu-link.svelte';
|
||||
import Label from "./dropdown-menu-label.svelte";
|
||||
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||
import Separator from "./dropdown-menu-separator.svelte";
|
||||
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||
import Trigger from "./dropdown-menu-trigger.svelte";
|
||||
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||
|
||||
const Sub = DropdownMenuPrimitive.Sub;
|
||||
const Root = DropdownMenuPrimitive.Root;
|
||||
|
||||
export {
|
||||
CheckboxItem,
|
||||
Content,
|
||||
Root as DropdownMenu,
|
||||
CheckboxItem as DropdownMenuCheckboxItem,
|
||||
Content as DropdownMenuContent,
|
||||
Group as DropdownMenuGroup,
|
||||
Item as DropdownMenuItem,
|
||||
Link as DropdownMenuLink,
|
||||
Label as DropdownMenuLabel,
|
||||
RadioGroup as DropdownMenuRadioGroup,
|
||||
RadioItem as DropdownMenuRadioItem,
|
||||
Separator as DropdownMenuSeparator,
|
||||
Shortcut as DropdownMenuShortcut,
|
||||
Sub as DropdownMenuSub,
|
||||
SubContent as DropdownMenuSubContent,
|
||||
SubTrigger as DropdownMenuSubTrigger,
|
||||
Trigger as DropdownMenuTrigger,
|
||||
Group,
|
||||
Item,
|
||||
Link,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
Root,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Sub,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
Trigger,
|
||||
};
|
31
client/src/lib/ui/form/context.svelte.ts
Normal file
31
client/src/lib/ui/form/context.svelte.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { getContext, hasContext, setContext } from 'svelte';
|
||||
|
||||
type Item = {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const key = 'form';
|
||||
export function setFormContext(id: string, name: string) {
|
||||
const item = getFormContext();
|
||||
if (!item) {
|
||||
const item: Item = $state({
|
||||
id,
|
||||
name,
|
||||
});
|
||||
setContext(key, item);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
item.id = id;
|
||||
item.name = name;
|
||||
}
|
||||
|
||||
export function getFormContext() {
|
||||
if (!hasContext(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getContext(key) as Item;
|
||||
}
|
30
client/src/lib/ui/form/errors.svelte
Normal file
30
client/src/lib/ui/form/errors.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { getFormContext } from './context.svelte';
|
||||
import type { WithElementRef, WithoutChildren } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { Violation } from '@bufbuild/protovalidate';
|
||||
import type { ConnectError } from '@connectrpc/connect';
|
||||
|
||||
type Props = WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
|
||||
errors?: Violation[] | ConnectError;
|
||||
};
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
errors = $bindable(),
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
|
||||
const item = getFormContext();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn('text-red text-sm', className)} {...restProps}>
|
||||
{#if errors && Array.isArray(errors)}
|
||||
{#each errors as error}
|
||||
<label for={item?.id}>{error.message}</label>
|
||||
{/each}
|
||||
{:else if errors}
|
||||
<span>{errors.message}</span>
|
||||
{/if}
|
||||
</div>
|
21
client/src/lib/ui/form/field.svelte
Normal file
21
client/src/lib/ui/form/field.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { setFormContext } from './context.svelte';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
name?: string;
|
||||
};
|
||||
let { ref = $bindable(null), class: className, name, children, ...restProps }: Props = $props();
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
if (name) {
|
||||
setFormContext(uid, name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn('flex flex-col gap-1')} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
9
client/src/lib/ui/form/index.ts
Normal file
9
client/src/lib/ui/form/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import Field from './field.svelte';
|
||||
import Errors from './errors.svelte';
|
||||
import Label from './label.svelte';
|
||||
|
||||
export {
|
||||
Field,
|
||||
Errors,
|
||||
Label
|
||||
};
|
29
client/src/lib/ui/form/label.svelte
Normal file
29
client/src/lib/ui/form/label.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import { getFormContext } from './context.svelte';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type Props = WithElementRef<HTMLAttributes<HTMLLabelElement>>;
|
||||
let { ref = $bindable(null), class: className, children, ...restProps }: Props = $props();
|
||||
|
||||
const item = getFormContext();
|
||||
|
||||
function formatName(name: string) {
|
||||
// Replace _ with spaces
|
||||
name = name.replace('_', ' ');
|
||||
|
||||
// Capitalize first letter
|
||||
name = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
return name;
|
||||
}
|
||||
</script>
|
||||
|
||||
<label bind:this={ref} class={cn('text-sm', className)} for={item?.id} {...restProps}>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if item}
|
||||
{formatName(item.name)}
|
||||
{/if}
|
||||
</label>
|
7
client/src/lib/ui/input/index.ts
Normal file
7
client/src/lib/ui/input/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
78
client/src/lib/ui/input/input.svelte
Normal file
78
client/src/lib/ui/input/input.svelte
Normal file
@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { getFormContext } from '../form/context.svelte';
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, 'type'> &
|
||||
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined }) & {
|
||||
scan?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
scan,
|
||||
id,
|
||||
name,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
|
||||
const item = getFormContext();
|
||||
if (item && !id) {
|
||||
id = item.id;
|
||||
}
|
||||
if (item && !name) {
|
||||
name = item.name;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if type === 'file'}
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
'border-surface-1 file:bg-surface hover:border-overlay placeholder:text-subtext text-text shadow-xs flex h-9 w-full min-w-0 cursor-pointer rounded-md border text-sm font-medium transition-all file:mr-2 file:px-3 file:py-2 md:text-sm',
|
||||
|
||||
// Focus
|
||||
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
|
||||
// Disabled
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
'border-surface-1 hover:border-overlay placeholder:text-subtext text-text shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 transition-all md:text-sm',
|
||||
|
||||
// Focus
|
||||
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
|
||||
// Disabled
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
7
client/src/lib/ui/label/index.ts
Normal file
7
client/src/lib/ui/label/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
20
client/src/lib/ui/label/label.svelte
Normal file
20
client/src/lib/ui/label/label.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="label"
|
||||
class={cn(
|
||||
'flex items-center gap-2 text-sm text-text leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
5
client/src/lib/ui/pager/index.ts
Normal file
5
client/src/lib/ui/pager/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Pager from './pager.svelte'
|
||||
|
||||
export {
|
||||
Pager
|
||||
}
|
49
client/src/lib/ui/pager/pager.svelte
Normal file
49
client/src/lib/ui/pager/pager.svelte
Normal file
@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import * as Pagination from '$lib/ui/pagination';
|
||||
|
||||
type Props = {
|
||||
count: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
onsubmit?: (offset: number) => void;
|
||||
};
|
||||
let { count = $bindable(), limit = 10, offset = $bindable(0), onsubmit }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Pagination.Root
|
||||
{count}
|
||||
page={offset / limit + 1}
|
||||
perPage={limit}
|
||||
onPageChange={async (num) => {
|
||||
offset = num * limit - limit;
|
||||
scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
onsubmit?.(offset);
|
||||
}}
|
||||
>
|
||||
{#snippet children({ pages, currentPage })}
|
||||
<Pagination.Content>
|
||||
<Pagination.Item>
|
||||
<Pagination.PrevButton />
|
||||
</Pagination.Item>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === 'ellipsis'}
|
||||
<Pagination.Item class="hidden md:block">
|
||||
<Pagination.Ellipsis />
|
||||
</Pagination.Item>
|
||||
{:else}
|
||||
<Pagination.Item class="hidden md:block">
|
||||
<Pagination.Link {page} isActive={currentPage === page.value}>
|
||||
{page.value}
|
||||
</Pagination.Link>
|
||||
</Pagination.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.Item>
|
||||
<Pagination.NextButton />
|
||||
</Pagination.Item>
|
||||
</Pagination.Content>
|
||||
{/snippet}
|
||||
</Pagination.Root>
|
25
client/src/lib/ui/pagination/index.ts
Normal file
25
client/src/lib/ui/pagination/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import Root from "./pagination.svelte";
|
||||
import Content from "./pagination-content.svelte";
|
||||
import Item from "./pagination-item.svelte";
|
||||
import Link from "./pagination-link.svelte";
|
||||
import PrevButton from "./pagination-prev-button.svelte";
|
||||
import NextButton from "./pagination-next-button.svelte";
|
||||
import Ellipsis from "./pagination-ellipsis.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Item,
|
||||
Link,
|
||||
PrevButton,
|
||||
NextButton,
|
||||
Ellipsis,
|
||||
//
|
||||
Root as Pagination,
|
||||
Content as PaginationContent,
|
||||
Item as PaginationItem,
|
||||
Link as PaginationLink,
|
||||
PrevButton as PaginationPrevButton,
|
||||
NextButton as PaginationNextButton,
|
||||
Ellipsis as PaginationEllipsis,
|
||||
};
|
21
client/src/lib/ui/pagination/pagination-content.svelte
Normal file
21
client/src/lib/ui/pagination/pagination-content.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-slot="pagination-content"
|
||||
class={cn("flex flex-row items-center gap-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
23
client/src/lib/ui/pagination/pagination-ellipsis.svelte
Normal file
23
client/src/lib/ui/pagination/pagination-ellipsis.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Ellipsis from "@lucide/svelte/icons/ellipsis";
|
||||
import type { WithElementRef, WithoutChildren } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
aria-hidden="true"
|
||||
data-slot="pagination-ellipsis"
|
||||
class={cn("flex size-9 items-center justify-center text-text", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<Ellipsis class="size-4" />
|
||||
<span class="sr-only">More pages</span>
|
||||
</span>
|
14
client/src/lib/ui/pagination/pagination-item.svelte
Normal file
14
client/src/lib/ui/pagination/pagination-item.svelte
Normal file
@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLLiAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<li bind:this={ref} data-slot="pagination-item" {...restProps}>
|
||||
{@render children?.()}
|
||||
</li>
|
41
client/src/lib/ui/pagination/pagination-link.svelte
Normal file
41
client/src/lib/ui/pagination/pagination-link.svelte
Normal file
@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { type Props, buttonVariants } from "$lib/ui/button";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
size = "icon",
|
||||
isActive = false,
|
||||
page,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.PageProps &
|
||||
Props & {
|
||||
isActive: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
{page.value}
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.Page
|
||||
bind:ref
|
||||
{page}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: "ghost",
|
||||
size,
|
||||
}),
|
||||
'text-text',
|
||||
isActive && 'bg-surface-1',
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
34
client/src/lib/ui/pagination/pagination-next-button.svelte
Normal file
34
client/src/lib/ui/pagination/pagination-next-button.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||
import { buttonVariants } from "$lib/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.NextButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<span>Next</span>
|
||||
<ChevronRight class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<!-- TODO: Fix this error: Expression produces a union type that is too complex to represent. Note: Removing `Fallback` in children={children || Fallback} fixes, makes you wonder how/why `Fallback` is causing this. -->
|
||||
<PaginationPrimitive.NextButton
|
||||
bind:ref
|
||||
aria-label="Go to next page"
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
size: "default",
|
||||
variant: "ghost",
|
||||
class: "gap-1 px-2.5 sm:pr-2.5",
|
||||
}),
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
33
client/src/lib/ui/pagination/pagination-prev-button.svelte
Normal file
33
client/src/lib/ui/pagination/pagination-prev-button.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
|
||||
import { buttonVariants } from "$lib/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.PrevButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronLeft class="size-4" />
|
||||
<span>Previous</span>
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.PrevButton
|
||||
bind:ref
|
||||
aria-label="Go to previous page"
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
size: "default",
|
||||
variant: "ghost",
|
||||
class: "gap-1 px-2.5 sm:pl-2.5",
|
||||
}),
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
47
client/src/lib/ui/pagination/pagination.svelte
Normal file
47
client/src/lib/ui/pagination/pagination.svelte
Normal file
@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from 'bits-ui';
|
||||
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { pushState } from '$app/navigation';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
count = 0,
|
||||
perPage = 10,
|
||||
page = $bindable(1),
|
||||
siblingCount = 1,
|
||||
onPageChange,
|
||||
...restProps
|
||||
}: PaginationPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
onpopstate={(state) => {
|
||||
const sks = state.state['sveltekit:states'] as {} | string;
|
||||
if (typeof sks === 'string' && sks.includes('#pagination-')) {
|
||||
page = Number(sks.split('#pagination-')[1]);
|
||||
onPageChange?.(page);
|
||||
} else {
|
||||
page = 1;
|
||||
onPageChange?.(page);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<PaginationPrimitive.Root
|
||||
bind:ref
|
||||
bind:page
|
||||
onPageChange={(p) => {
|
||||
pushState('', `#pagination-${p}`);
|
||||
onPageChange?.(p);
|
||||
}}
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
class={cn('mx-auto flex w-full justify-center', className)}
|
||||
{count}
|
||||
{perPage}
|
||||
{siblingCount}
|
||||
{...restProps}
|
||||
/>
|
17
client/src/lib/ui/popover/index.ts
Normal file
17
client/src/lib/ui/popover/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import Content from "./popover-content.svelte";
|
||||
import Trigger from "./popover-trigger.svelte";
|
||||
const Root = PopoverPrimitive.Root;
|
||||
const Close = PopoverPrimitive.Close;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Close,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
};
|
32
client/src/lib/ui/popover/popover-content.svelte
Normal file
32
client/src/lib/ui/popover/popover-content.svelte
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: PopoverPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="popover-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"bg-based text-text outline-hidden z-50 w-72 rounded-md border border-surface-1 p-4 shadow-md",
|
||||
|
||||
// Animation
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-popover-content-transform-origin)",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
17
client/src/lib/ui/popover/popover-trigger.svelte
Normal file
17
client/src/lib/ui/popover/popover-trigger.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: PopoverPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="popover-trigger"
|
||||
class={cn("", className)}
|
||||
{...restProps}
|
||||
/>
|
32
client/src/lib/ui/range-calendar/index.ts
Normal file
32
client/src/lib/ui/range-calendar/index.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import Root from "./range-calendar.svelte";
|
||||
import Cell from "./range-calendar-cell.svelte";
|
||||
import Day from "./range-calendar-day.svelte";
|
||||
import Grid from "./range-calendar-grid.svelte";
|
||||
import Header from "./range-calendar-header.svelte";
|
||||
import Months from "./range-calendar-months.svelte";
|
||||
import GridRow from "./range-calendar-grid-row.svelte";
|
||||
import Heading from "./range-calendar-heading.svelte";
|
||||
import HeadCell from "./range-calendar-head-cell.svelte";
|
||||
import NextButton from "./range-calendar-next-button.svelte";
|
||||
import PrevButton from "./range-calendar-prev-button.svelte";
|
||||
|
||||
const GridHead = RangeCalendarPrimitive.GridHead;
|
||||
const GridBody = RangeCalendarPrimitive.GridBody;
|
||||
|
||||
export {
|
||||
Day,
|
||||
Cell,
|
||||
Grid,
|
||||
Header,
|
||||
Months,
|
||||
GridRow,
|
||||
Heading,
|
||||
GridBody,
|
||||
GridHead,
|
||||
HeadCell,
|
||||
NextButton,
|
||||
PrevButton,
|
||||
//
|
||||
Root as RangeCalendar,
|
||||
};
|
19
client/src/lib/ui/range-calendar/range-calendar-cell.svelte
Normal file
19
client/src/lib/ui/range-calendar/range-calendar-cell.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.CellProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Cell
|
||||
bind:ref
|
||||
class={cn(
|
||||
"[&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50 relative size-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 first:[&:has([data-selected])]:rounded-l-md last:[&:has([data-selected])]:rounded-r-md [&:has([data-selected][data-selection-end])]:rounded-r-md [&:has([data-selected][data-selection-start])]:rounded-l-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
36
client/src/lib/ui/range-calendar/range-calendar-day.svelte
Normal file
36
client/src/lib/ui/range-calendar/range-calendar-day.svelte
Normal file
@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
|
||||
import { buttonVariants } from '$lib/ui/button/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.DayProps = $props();
|
||||
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Day
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'size-9 p-0 font-normal',
|
||||
'[&[data-today]:not([data-selected])]:bg-blue [&[data-today]:not([data-selected])]:text-crust [&[data-today]:not([data-selected])]:rounded-md',
|
||||
// Selected
|
||||
'data-[selected]:bg-surface data-[selected]:hover:bg-surface-1 data-[selected]:rounded-none data-[selected]:opacity-100',
|
||||
// Selection Start
|
||||
'data-[selection-start]:bg-surface-1 data-[selection-start]:text-text data-[selection-start]:hover:bg-surface-2 data-[selection-start]:rounded-l-md',
|
||||
// Selection End
|
||||
'data-[selection-end]:bg-surface-1 data-[selection-end]:text-text data-[selection-end]:hover:bg-surface-2 data-[selection-end]:rounded-r-md',
|
||||
// Outside months
|
||||
'data-[outside-month]:text-muted-foreground [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground data-[outside-month]:pointer-events-none data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:opacity-30',
|
||||
// Disabled
|
||||
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||
// Unavailable
|
||||
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.GridRowProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />
|
16
client/src/lib/ui/range-calendar/range-calendar-grid.svelte
Normal file
16
client/src/lib/ui/range-calendar/range-calendar-grid.svelte
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.GridProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Grid
|
||||
bind:ref
|
||||
class={cn("w-full border-collapse space-y-1", className)}
|
||||
{...restProps}
|
||||
/>
|
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.HeadCellProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.HeadCell
|
||||
bind:ref
|
||||
class={cn("text-muted-foreground w-9 rounded-md text-[0.8rem] font-normal", className)}
|
||||
{...restProps}
|
||||
/>
|
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.HeaderProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Header
|
||||
bind:ref
|
||||
class={cn("relative flex w-full items-center justify-between pt-1", className)}
|
||||
{...restProps}
|
||||
/>
|
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.HeadingProps = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Heading
|
||||
bind:ref
|
||||
class={cn("text-sm font-medium", className)}
|
||||
{...restProps}
|
||||
/>
|
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("mt-4 flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||
import { buttonVariants } from "$lib/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.NextButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronRight class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<RangeCalendarPrimitive.NextButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
||||
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
|
||||
import { buttonVariants } from "$lib/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: RangeCalendarPrimitive.PrevButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronLeft class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<RangeCalendarPrimitive.PrevButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
57
client/src/lib/ui/range-calendar/range-calendar.svelte
Normal file
57
client/src/lib/ui/range-calendar/range-calendar.svelte
Normal file
@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { RangeCalendar as RangeCalendarPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import * as RangeCalendar from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
placeholder = $bindable(),
|
||||
weekdayFormat = "short",
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<RangeCalendarPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<RangeCalendarPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
bind:placeholder
|
||||
{weekdayFormat}
|
||||
class={cn("p-3", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ months, weekdays })}
|
||||
<RangeCalendar.Header>
|
||||
<RangeCalendar.PrevButton />
|
||||
<RangeCalendar.Heading />
|
||||
<RangeCalendar.NextButton />
|
||||
</RangeCalendar.Header>
|
||||
<RangeCalendar.Months>
|
||||
{#each months as month (month)}
|
||||
<RangeCalendar.Grid>
|
||||
<RangeCalendar.GridHead>
|
||||
<RangeCalendar.GridRow class="flex">
|
||||
{#each weekdays as weekday (weekday)}
|
||||
<RangeCalendar.HeadCell>
|
||||
{weekday.slice(0, 2)}
|
||||
</RangeCalendar.HeadCell>
|
||||
{/each}
|
||||
</RangeCalendar.GridRow>
|
||||
</RangeCalendar.GridHead>
|
||||
<RangeCalendar.GridBody>
|
||||
{#each month.weeks as weekDates (weekDates)}
|
||||
<RangeCalendar.GridRow class="mt-2 w-full">
|
||||
{#each weekDates as date (date)}
|
||||
<RangeCalendar.Cell {date} month={month.value}>
|
||||
<RangeCalendar.Day />
|
||||
</RangeCalendar.Cell>
|
||||
{/each}
|
||||
</RangeCalendar.GridRow>
|
||||
{/each}
|
||||
</RangeCalendar.GridBody>
|
||||
</RangeCalendar.Grid>
|
||||
{/each}
|
||||
</RangeCalendar.Months>
|
||||
{/snippet}
|
||||
</RangeCalendarPrimitive.Root>
|
28
client/src/lib/ui/select/index.ts
Normal file
28
client/src/lib/ui/select/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
import Group from "./select-group.svelte";
|
||||
import Label from "./select-label.svelte";
|
||||
import Item from "./select-item.svelte";
|
||||
import Content from "./select-content.svelte";
|
||||
import Trigger from "./select-trigger.svelte";
|
||||
import Separator from "./select-separator.svelte";
|
||||
|
||||
const Root = SelectPrimitive.Root;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
};
|
33
client/src/lib/ui/select/select-content.svelte
Normal file
33
client/src/lib/ui/select/select-content.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive, type WithoutChild } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: SelectPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
'border-surface-1 bg-based relative z-50 max-h-[calc(var(--bits-select-content-available-height)-10px)] min-w-[var(--bits-select-anchor-width)] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||
|
||||
// Animations
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
7
client/src/lib/ui/select/select-group.svelte
Normal file
7
client/src/lib/ui/select/select-group.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group data-slot="select-group" {...restProps} />
|
38
client/src/lib/ui/select/select-item.svelte
Normal file
38
client/src/lib/ui/select/select-item.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import { Select as SelectPrimitive, type WithoutChild } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-surface text-text relative flex w-full cursor-pointer items-center gap-2 py-1.5 pr-8 pl-2 outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 md:text-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<Check class="size-4" />
|
||||
{/if}
|
||||
</span>
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
21
client/src/lib/ui/select/select-label.svelte
Normal file
21
client/src/lib/ui/select/select-label.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { type WithElementRef } from "bits-ui";
|
||||
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="select-label"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
18
client/src/lib/ui/select/select-separator.svelte
Normal file
18
client/src/lib/ui/select/select-separator.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { Separator } from "$lib/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
41
client/src/lib/ui/select/select-trigger.svelte
Normal file
41
client/src/lib/ui/select/select-trigger.svelte
Normal file
@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive, type WithoutChild } from 'bits-ui';
|
||||
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = 'default',
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: 'sm' | 'default';
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
'border-surface-1 bg-based hover:border-overlay text-text flex flex-row-reverse w-full cursor-pointer items-center justify-between gap-2 rounded-md border px-3 py-2 md:text-sm whitespace-nowrap shadow-xs transition-all',
|
||||
|
||||
// Focus
|
||||
'focus-visible:outline-accent focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
|
||||
// Disabled
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
|
||||
// Data
|
||||
'data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2',
|
||||
|
||||
// Image
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDown class="size-4 opacity-50" />
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Trigger>
|
7
client/src/lib/ui/separator/index.ts
Normal file
7
client/src/lib/ui/separator/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
24
client/src/lib/ui/separator/separator.svelte
Normal file
24
client/src/lib/ui/separator/separator.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="separator-root"
|
||||
{decorative}
|
||||
{orientation}
|
||||
class={cn(
|
||||
"bg-surface shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
36
client/src/lib/ui/sheet/index.ts
Normal file
36
client/src/lib/ui/sheet/index.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import Root from './sheet.svelte';
|
||||
import Trigger from "./sheet-trigger.svelte";
|
||||
import Close from "./sheet-close.svelte";
|
||||
import Overlay from "./sheet-overlay.svelte";
|
||||
import Content from "./sheet-content.svelte";
|
||||
import Header from "./sheet-header.svelte";
|
||||
import Footer from "./sheet-footer.svelte";
|
||||
import Title from "./sheet-title.svelte";
|
||||
import Description from "./sheet-description.svelte";
|
||||
|
||||
const Portal = SheetPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Close,
|
||||
Trigger,
|
||||
Portal,
|
||||
Overlay,
|
||||
Content,
|
||||
Header,
|
||||
Footer,
|
||||
Title,
|
||||
Description,
|
||||
//
|
||||
Root as Sheet,
|
||||
Close as SheetClose,
|
||||
Trigger as SheetTrigger,
|
||||
Portal as SheetPortal,
|
||||
Overlay as SheetOverlay,
|
||||
Content as SheetContent,
|
||||
Header as SheetHeader,
|
||||
Footer as SheetFooter,
|
||||
Title as SheetTitle,
|
||||
Description as SheetDescription,
|
||||
};
|
7
client/src/lib/ui/sheet/sheet-close.svelte
Normal file
7
client/src/lib/ui/sheet/sheet-close.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SheetPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps} />
|
60
client/src/lib/ui/sheet/sheet-content.svelte
Normal file
60
client/src/lib/ui/sheet/sheet-content.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
export const sheetVariants = tv({
|
||||
base: 'bg-mantle border-surface data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
variants: {
|
||||
side: {
|
||||
top: 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b',
|
||||
bottom:
|
||||
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t',
|
||||
left: 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||
right:
|
||||
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right max-w-2xs inset-y-0 right-0 h-full w-3/4 border-l'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right'
|
||||
}
|
||||
});
|
||||
|
||||
export type Side = VariantProps<typeof sheetVariants>['side'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SheetOverlay from './sheet-overlay.svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
side = 'right',
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
||||
portalProps?: SheetPrimitive.PortalProps;
|
||||
side?: Side;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Portal {...portalProps}>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="sheet-content"
|
||||
class={cn(sheetVariants({ side }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<SheetPrimitive.Close
|
||||
class="rounded-xs focus:outline-hidden absolute right-4 top-4 cursor-pointer opacity-70 transition-opacity hover:opacity-100 disabled:pointer-events-none"
|
||||
>
|
||||
<X class="size-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPrimitive.Portal>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user