diff --git a/package.json b/package.json
index 0e4c0d3..0fff0bc 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,11 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.2.2",
+ "@vueuse/core": "^14.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^1.0.0",
+ "reka-ui": "^2.9.3",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"vue": "^3.5.30"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f6c1285..6a92144 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
'@tailwindcss/vite':
specifier: ^4.2.2
version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1))
+ '@vueuse/core':
+ specifier: ^14.2.1
+ version: 14.2.1(vue@3.5.31(typescript@5.9.3))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -20,6 +23,9 @@ importers:
lucide-vue-next:
specifier: ^1.0.0
version: 1.0.0(vue@3.5.31(typescript@5.9.3))
+ reka-ui:
+ specifier: ^2.9.3
+ version: 2.9.3(vue@3.5.31(typescript@5.9.3))
tailwind-merge:
specifier: ^3.5.0
version: 3.5.0
@@ -80,6 +86,24 @@ packages:
'@emnapi/wasi-threads@1.2.0':
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
+ '@floating-ui/core@1.7.5':
+ resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
+
+ '@floating-ui/dom@1.7.6':
+ resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
+
+ '@floating-ui/utils@0.2.11':
+ resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+
+ '@floating-ui/vue@1.1.11':
+ resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==}
+
+ '@internationalized/date@3.12.0':
+ resolution: {integrity: sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==}
+
+ '@internationalized/number@3.6.5':
+ resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -206,6 +230,9 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.2':
resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==}
+ '@swc/helpers@0.5.20':
+ resolution: {integrity: sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==}
+
'@tailwindcss/node@4.2.2':
resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
@@ -300,12 +327,23 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
+ '@tanstack/virtual-core@3.13.23':
+ resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==}
+
+ '@tanstack/vue-virtual@3.13.23':
+ resolution: {integrity: sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ==}
+ peerDependencies:
+ vue: ^2.7.0 || ^3.0.0
+
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/node@24.12.0':
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
+ '@types/web-bluetooth@0.0.21':
+ resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
+
'@vitejs/plugin-vue@6.0.5':
resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -365,9 +403,26 @@ packages:
vue:
optional: true
+ '@vueuse/core@14.2.1':
+ resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==}
+ peerDependencies:
+ vue: ^3.5.0
+
+ '@vueuse/metadata@14.2.1':
+ resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==}
+
+ '@vueuse/shared@14.2.1':
+ resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==}
+ peerDependencies:
+ vue: ^3.5.0
+
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@@ -378,6 +433,9 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+ defu@6.1.6:
+ resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -504,6 +562,9 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ ohash@2.0.11:
+ resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
@@ -518,6 +579,11 @@ packages:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14}
+ reka-ui@2.9.3:
+ resolution: {integrity: sha512-C9lCVxsSC7uYD0Nbgik1+14FNndHNprZmf0zGQt0ZDYIt5KxXV3zD0hEqNcfRUsEEJvVmoRsUkJnASBVBeaaUw==}
+ peerDependencies:
+ vue: '>= 3.4.0'
+
rolldown@1.0.0-rc.12:
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -601,6 +667,17 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
+ vue-demi@0.14.10:
+ resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
+ engines: {node: '>=12'}
+ hasBin: true
+ peerDependencies:
+ '@vue/composition-api': ^1.0.0-rc.1
+ vue: ^3.0.0-0 || ^2.6.0
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+
vue-tsc@3.2.6:
resolution: {integrity: sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==}
hasBin: true
@@ -646,6 +723,34 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@floating-ui/core@1.7.5':
+ dependencies:
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/dom@1.7.6':
+ dependencies:
+ '@floating-ui/core': 1.7.5
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/utils@0.2.11': {}
+
+ '@floating-ui/vue@1.1.11(vue@3.5.31(typescript@5.9.3))':
+ dependencies:
+ '@floating-ui/dom': 1.7.6
+ '@floating-ui/utils': 0.2.11
+ vue-demi: 0.14.10(vue@3.5.31(typescript@5.9.3))
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ - vue
+
+ '@internationalized/date@3.12.0':
+ dependencies:
+ '@swc/helpers': 0.5.20
+
+ '@internationalized/number@3.6.5':
+ dependencies:
+ '@swc/helpers': 0.5.20
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -728,6 +833,10 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.2': {}
+ '@swc/helpers@0.5.20':
+ dependencies:
+ tslib: 2.8.1
+
'@tailwindcss/node@4.2.2':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -796,6 +905,13 @@ snapshots:
tailwindcss: 4.2.2
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1)
+ '@tanstack/virtual-core@3.13.23': {}
+
+ '@tanstack/vue-virtual@3.13.23(vue@3.5.31(typescript@5.9.3))':
+ dependencies:
+ '@tanstack/virtual-core': 3.13.23
+ vue: 3.5.31(typescript@5.9.3)
+
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@@ -805,6 +921,8 @@ snapshots:
dependencies:
undici-types: 7.16.0
+ '@types/web-bluetooth@0.0.21': {}
+
'@vitejs/plugin-vue@6.0.5(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.0)(jiti@2.6.1))(vue@3.5.31(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.2
@@ -892,8 +1010,25 @@ snapshots:
typescript: 5.9.3
vue: 3.5.31(typescript@5.9.3)
+ '@vueuse/core@14.2.1(vue@3.5.31(typescript@5.9.3))':
+ dependencies:
+ '@types/web-bluetooth': 0.0.21
+ '@vueuse/metadata': 14.2.1
+ '@vueuse/shared': 14.2.1(vue@3.5.31(typescript@5.9.3))
+ vue: 3.5.31(typescript@5.9.3)
+
+ '@vueuse/metadata@14.2.1': {}
+
+ '@vueuse/shared@14.2.1(vue@3.5.31(typescript@5.9.3))':
+ dependencies:
+ vue: 3.5.31(typescript@5.9.3)
+
alien-signals@3.1.2: {}
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@@ -902,6 +1037,8 @@ snapshots:
csstype@3.2.3: {}
+ defu@6.1.6: {}
+
detect-libc@2.1.2: {}
enhanced-resolve@5.20.1:
@@ -985,6 +1122,8 @@ snapshots:
nanoid@3.3.11: {}
+ ohash@2.0.11: {}
+
path-browserify@1.0.1: {}
picocolors@1.1.1: {}
@@ -997,6 +1136,22 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ reka-ui@2.9.3(vue@3.5.31(typescript@5.9.3)):
+ dependencies:
+ '@floating-ui/dom': 1.7.6
+ '@floating-ui/vue': 1.1.11(vue@3.5.31(typescript@5.9.3))
+ '@internationalized/date': 3.12.0
+ '@internationalized/number': 3.6.5
+ '@tanstack/vue-virtual': 3.13.23(vue@3.5.31(typescript@5.9.3))
+ '@vueuse/core': 14.2.1(vue@3.5.31(typescript@5.9.3))
+ '@vueuse/shared': 14.2.1(vue@3.5.31(typescript@5.9.3))
+ aria-hidden: 1.2.6
+ defu: 6.1.6
+ ohash: 2.0.11
+ vue: 3.5.31(typescript@5.9.3)
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+
rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1):
dependencies:
'@oxc-project/types': 0.122.0
@@ -1034,8 +1189,7 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
- tslib@2.8.1:
- optional: true
+ tslib@2.8.1: {}
tw-animate-css@1.4.0: {}
@@ -1060,6 +1214,10 @@ snapshots:
vscode-uri@3.1.0: {}
+ vue-demi@0.14.10(vue@3.5.31(typescript@5.9.3)):
+ dependencies:
+ vue: 3.5.31(typescript@5.9.3)
+
vue-tsc@3.2.6(typescript@5.9.3):
dependencies:
'@volar/typescript': 2.4.28
diff --git a/src/components/ui/badge/Badge.vue b/src/components/ui/badge/Badge.vue
new file mode 100644
index 0000000..d894dfe
--- /dev/null
+++ b/src/components/ui/badge/Badge.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/badge/index.ts b/src/components/ui/badge/index.ts
new file mode 100644
index 0000000..bbc0dfa
--- /dev/null
+++ b/src/components/ui/badge/index.ts
@@ -0,0 +1,26 @@
+import type { VariantProps } from "class-variance-authority"
+import { cva } from "class-variance-authority"
+
+export { default as Badge } from "./Badge.vue"
+
+export const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+)
+export type BadgeVariants = VariantProps
diff --git a/src/components/ui/button/Button.vue b/src/components/ui/button/Button.vue
new file mode 100644
index 0000000..3763470
--- /dev/null
+++ b/src/components/ui/button/Button.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/button/index.ts b/src/components/ui/button/index.ts
new file mode 100644
index 0000000..26e2c55
--- /dev/null
+++ b/src/components/ui/button/index.ts
@@ -0,0 +1,38 @@
+import type { VariantProps } from "class-variance-authority"
+import { cva } from "class-variance-authority"
+
+export { default as Button } from "./Button.vue"
+
+export const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ "default": "h-9 px-4 py-2 has-[>svg]:px-3",
+ "sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ "lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
+ "icon": "size-9",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+)
+export type ButtonVariants = VariantProps
diff --git a/src/components/ui/card/Card.vue b/src/components/ui/card/Card.vue
new file mode 100644
index 0000000..f5a0707
--- /dev/null
+++ b/src/components/ui/card/Card.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/card/CardAction.vue b/src/components/ui/card/CardAction.vue
new file mode 100644
index 0000000..c91638b
--- /dev/null
+++ b/src/components/ui/card/CardAction.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/card/CardContent.vue b/src/components/ui/card/CardContent.vue
new file mode 100644
index 0000000..dfbc552
--- /dev/null
+++ b/src/components/ui/card/CardContent.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/card/CardDescription.vue b/src/components/ui/card/CardDescription.vue
new file mode 100644
index 0000000..71c1b8d
--- /dev/null
+++ b/src/components/ui/card/CardDescription.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/card/CardFooter.vue b/src/components/ui/card/CardFooter.vue
new file mode 100644
index 0000000..9e3739e
--- /dev/null
+++ b/src/components/ui/card/CardFooter.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/card/CardHeader.vue b/src/components/ui/card/CardHeader.vue
new file mode 100644
index 0000000..4fe4da4
--- /dev/null
+++ b/src/components/ui/card/CardHeader.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/card/CardTitle.vue b/src/components/ui/card/CardTitle.vue
new file mode 100644
index 0000000..5f479e7
--- /dev/null
+++ b/src/components/ui/card/CardTitle.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/card/index.ts b/src/components/ui/card/index.ts
new file mode 100644
index 0000000..1627758
--- /dev/null
+++ b/src/components/ui/card/index.ts
@@ -0,0 +1,7 @@
+export { default as Card } from "./Card.vue"
+export { default as CardAction } from "./CardAction.vue"
+export { default as CardContent } from "./CardContent.vue"
+export { default as CardDescription } from "./CardDescription.vue"
+export { default as CardFooter } from "./CardFooter.vue"
+export { default as CardHeader } from "./CardHeader.vue"
+export { default as CardTitle } from "./CardTitle.vue"
diff --git a/src/components/ui/separator/Separator.vue b/src/components/ui/separator/Separator.vue
new file mode 100644
index 0000000..78d60ec
--- /dev/null
+++ b/src/components/ui/separator/Separator.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
diff --git a/src/components/ui/separator/index.ts b/src/components/ui/separator/index.ts
new file mode 100644
index 0000000..4407287
--- /dev/null
+++ b/src/components/ui/separator/index.ts
@@ -0,0 +1 @@
+export { default as Separator } from "./Separator.vue"
diff --git a/src/components/ui/skeleton/Skeleton.vue b/src/components/ui/skeleton/Skeleton.vue
new file mode 100644
index 0000000..0dadcef
--- /dev/null
+++ b/src/components/ui/skeleton/Skeleton.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/src/components/ui/skeleton/index.ts b/src/components/ui/skeleton/index.ts
new file mode 100644
index 0000000..e5ce72c
--- /dev/null
+++ b/src/components/ui/skeleton/index.ts
@@ -0,0 +1 @@
+export { default as Skeleton } from "./Skeleton.vue"
diff --git a/src/composables/useNostrAtmStatus.ts b/src/composables/useNostrAtmStatus.ts
new file mode 100644
index 0000000..d2e93a2
--- /dev/null
+++ b/src/composables/useNostrAtmStatus.ts
@@ -0,0 +1,123 @@
+import { ref, onMounted, onUnmounted } from 'vue'
+import { ATM_MACHINES, RELAY_URL, OFFLINE_THRESHOLD_SECONDS, type AtmConfig } from '@/config/atms'
+
+export interface AtmState {
+ config: AtmConfig
+ online: boolean
+ maintenance: boolean
+ cashIn: boolean
+ cashOut: boolean
+ cashLevel: 'none' | 'low' | 'good' | 'full'
+ lastSeen: number
+}
+
+function createInitialState(): Map {
+ const map = new Map()
+ for (const config of ATM_MACHINES) {
+ map.set(config.id, {
+ config,
+ online: false,
+ maintenance: false,
+ cashIn: false,
+ cashOut: false,
+ cashLevel: 'none',
+ lastSeen: 0,
+ })
+ }
+ return map
+}
+
+export function useNostrAtmStatus() {
+ const atms = ref