diff --git a/components.json b/components.json deleted file mode 100644 index 1df05563..00000000 --- a/components.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://shadcn-svelte.com/schema.json", - "tailwind": { - "css": "src/app.css", - "baseColor": "neutral" - }, - "aliases": { - "components": "$lib/components", - "utils": "$lib/utils/shadcn", - "ui": "$lib/components/ui", - "hooks": "$lib/hooks", - "lib": "$lib" - }, - "typescript": true, - "registry": "https://shadcn-svelte.com/registry" -} diff --git a/package.json b/package.json index 936045aa..97163485 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "vitest": "^4.1.9" }, "dependencies": { + "@openshock/svelte-core": "^0.2.2", "@opentelemetry/api-logs": "^0.219.0", "@opentelemetry/exporter-logs-otlp-http": "^0.219.0", "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bef1e545..b8fa887f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ importers: .: dependencies: + '@openshock/svelte-core': + specifier: ^0.2.2 + version: 0.2.2(2cd3459784afbbe2cb9606225751a11d) '@opentelemetry/api-logs': specifier: ^0.219.0 version: 0.219.0 @@ -750,6 +753,36 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@openshock/svelte-core@0.2.2': + resolution: {integrity: sha512-4B1bwH23v/gUuXW1dipjpac06N/FfhKYuih73FK21EqxnAcY7PgRE5XaST8Bl8WmVHTaEKqP9q61m10oOozRZQ==} + peerDependencies: + '@internationalized/date': ^3.12.2 + '@lucide/svelte': ^1.21.0 + '@tanstack/table-core': ^8.21.3 + bits-ui: 2.18.1 + clsx: ^2.1.1 + formsnap: ^2.0.1 + svelte: ^5.56.1 + svelte-sonner: ^1.1.1 + sveltekit-superforms: ^2.30.1 + tailwind-merge: ^3.6.0 + tailwind-variants: ^3.2.2 + temporal-polyfill: ^1.0.1 + vaul-svelte: 1.0.0-next.7 + peerDependenciesMeta: + '@internationalized/date': + optional: true + '@tanstack/table-core': + optional: true + formsnap: + optional: true + sveltekit-superforms: + optional: true + temporal-polyfill: + optional: true + vaul-svelte: + optional: true + '@opentelemetry/api-logs@0.219.0': resolution: {integrity: sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==} engines: {node: '>=8.0.0'} @@ -3442,6 +3475,23 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@openshock/svelte-core@0.2.2(2cd3459784afbbe2cb9606225751a11d)': + dependencies: + '@lucide/svelte': 1.21.0(svelte@5.56.3(@typescript-eslint/types@8.61.1)) + bits-ui: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.66.0(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.3)(jiti@2.7.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.1))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.3)(jiti@2.7.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.1)) + clsx: 2.1.1 + svelte: 5.56.3(@typescript-eslint/types@8.61.1) + svelte-sonner: 1.1.1(svelte@5.56.3(@typescript-eslint/types@8.61.1)) + tailwind-merge: 3.6.0 + tailwind-variants: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.1) + optionalDependencies: + '@internationalized/date': 3.12.2 + '@tanstack/table-core': 8.21.3 + formsnap: 2.0.1(svelte@5.56.3(@typescript-eslint/types@8.61.1))(sveltekit-superforms@2.30.1(@sveltejs/kit@2.66.0(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.3)(jiti@2.7.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.1))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.3)(jiti@2.7.0)))(@types/json-schema@7.0.15)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(typescript@6.0.3)) + sveltekit-superforms: 2.30.1(@sveltejs/kit@2.66.0(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.3)(jiti@2.7.0)))(svelte@5.56.3(@typescript-eslint/types@8.61.1))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(esbuild@0.27.3)(jiti@2.7.0)))(@types/json-schema@7.0.15)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(typescript@6.0.3) + temporal-polyfill: 1.0.1 + vaul-svelte: 1.0.0-next.7(svelte@5.56.3(@typescript-eslint/types@8.61.1)) + '@opentelemetry/api-logs@0.219.0': dependencies: '@opentelemetry/api': 1.9.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 402e0b1c..3cfc8e9a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,6 +6,7 @@ blockExoticSubdeps: true minimumReleaseAge: 4320 # 3 days minimumReleaseAgeExclude: + - '@openshock/*' - ws@8.20.1 onlyBuiltDependencies: diff --git a/scripts/update-shadcn.js b/scripts/update-shadcn.js deleted file mode 100644 index 120becd3..00000000 --- a/scripts/update-shadcn.js +++ /dev/null @@ -1,103 +0,0 @@ -import { execSync } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; - -const UI_DIR = 'src/lib/components/ui'; -const HOOKS_DIR = 'src/lib/hooks'; - -// Non-shadcn directories to keep -const EXCLUDE = ['multi-select-combobox']; - -// Unwanted dependencies that the CLI tends to add -const UNWANTED_DEPS = ['mode-watcher']; - -function run(command, options = {}) { - execSync(command, { stdio: 'inherit', shell: true, ...options }); -} - -// Collect shadcn component names -const components = fs - .readdirSync(UI_DIR, { withFileTypes: true }) - .filter((entry) => entry.isDirectory() && !EXCLUDE.includes(entry.name)) - .map((entry) => entry.name); - -if (components.length === 0) { - console.error('No shadcn components found to update.'); - process.exit(1); -} - -console.log(`Found ${components.length} shadcn components: ${components.join(' ')}`); - -// Delete all shadcn component directories -console.log('Deleting shadcn components...'); -for (const name of components) { - fs.rmSync(path.join(UI_DIR, name), { recursive: true, force: true }); -} -fs.rmSync(path.join(HOOKS_DIR, 'is-mobile'), { recursive: true, force: true }); - -// Re-add all components -console.log('Re-adding components via shadcn CLI...'); -run(`pnpm dlx shadcn-svelte@latest add --yes --overwrite ${components.join(' ')}`); - -// Remove unwanted dependencies -console.log('Cleaning up dependencies...'); -const packageJson = fs.readFileSync('package.json', 'utf-8'); -for (const dep of UNWANTED_DEPS) { - if (packageJson.includes(`"${dep}"`)) { - console.log(` Removing unwanted dependency: ${dep}`); - run(`pnpm remove ${dep}`); - } -} - -// Format to reduce diff noise -console.log('Formatting...'); -run('pnpm run format'); - -// Apply custom modifications -console.log('Applying custom modifications...'); - -function patch(filePath, replacements) { - let source = fs.readFileSync(filePath, 'utf-8'); - for (const [pattern, replacement] of replacements) { - source = source.replace(pattern, replacement); - } - fs.writeFileSync(filePath, source); -} - -// Sidebar: ease-in-out and duration-300 -patch(path.join(UI_DIR, 'sidebar/sidebar.svelte'), [ - [/duration-200 ease-linear/g, 'duration-300 ease-in-out'], -]); - -// Sidebar submenu: ml-* instead of mx-*, pl-* instead of px-* -patch(path.join(UI_DIR, 'sidebar/sidebar-menu-sub.svelte'), [ - [/mx-3\.5(.*)px-2\.5/g, 'ml-3.5$1pl-2.5'], -]); - -// Sonner: use color-scheme-state instead of mode-watcher -patch(path.join(UI_DIR, 'sonner/sonner.svelte'), [ - [ - 'import { mode } from "mode-watcher";', - "import { colorScheme } from '$lib/state/color-scheme-state.svelte';", - ], - ['theme={mode.current}', 'theme={colorScheme.value}'], -]); - -// Slider: add cursor-w-resize to thumb -patch(path.join(UI_DIR, 'slider/slider.svelte'), [ - [/select-none disabled:pointer/, 'cursor-w-resize select-none disabled:pointer'], -]); - -// Toggle group: suppress state_referenced_locally warnings -patch(path.join(UI_DIR, 'toggle-group/toggle-group.svelte'), [ - [/\tsetToggleGroupCtx/, '\t// svelte-ignore state_referenced_locally\n\tsetToggleGroupCtx'], -]); - -// Final format and check -console.log('Final format...'); -run('pnpm run format'); - -console.log('Running checks...'); -run('pnpm run check'); - -console.log('Done! Review the git diff before committing.'); diff --git a/src/app.css b/src/app.css index 92185e03..78be2016 100644 --- a/src/app.css +++ b/src/app.css @@ -1,255 +1,6 @@ @import 'tailwindcss'; @import 'tw-animate-css'; - -@custom-variant dark (&:is(.dark *)); - -:root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); - - --animate-accordion-up: accordion-up 0.2s ease-out; - --animate-accordion-down: accordion-down 0.2s ease-out; - --animate-caret-blink: caret-blink 1.25s ease-out infinite; -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } - - .hide-spinners { - /* Hide spin buttons in Chrome, Safari, Edge */ - -webkit-appearance: none; - -moz-appearance: textfield; /* Hide spinners in Firefox */ - appearance: textfield; - } - - /* Target WebKit browsers (Chrome, Safari, Edge) */ - .hide-spinners::-webkit-outer-spin-button, - .hide-spinners::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - button:not([disabled]), - [role='button']:not([disabled]) { - cursor: pointer; - } -} - -.transition-in-place { - display: grid; -} - -.transition-in-place > * { - grid-area: 1/1/2/2; -} - -@keyframes accordion-down { - from { - height: 0; - } - - to { - height: var(--bits-accordion-content-height); - } -} - -@keyframes accordion-up { - from { - height: var(--bits-accordion-content-height); - } - - to { - height: 0; - } -} - -@keyframes caret-blink { - 0%, - 70%, - 100% { - opacity: 1; - } - - 20%, - 50% { - opacity: 0; - } -} - -@custom-variant data-open { - &:where([data-state='open']), - &:where([data-open]:not([data-open='false'])) { - @slot; - } -} - -@custom-variant data-closed { - &:where([data-state='closed']), - &:where([data-closed]:not([data-closed='false'])) { - @slot; - } -} - -@custom-variant data-checked { - &:where([data-state='checked']), - &:where([data-checked]:not([data-checked='false'])) { - @slot; - } -} - -@custom-variant data-unchecked { - &:where([data-state='unchecked']), - &:where([data-unchecked]:not([data-unchecked='false'])) { - @slot; - } -} - -@custom-variant data-selected { - &:where([data-selected]) { - @slot; - } -} - -@custom-variant data-disabled { - &:where([data-disabled='true']), - &:where([data-disabled]:not([data-disabled='false'])) { - @slot; - } -} - -@custom-variant data-active { - &:where([data-state='active']), - &:where([data-active]:not([data-active='false'])) { - @slot; - } -} - -@custom-variant data-horizontal { - &:where([data-orientation='horizontal']) { - @slot; - } -} - -@custom-variant data-vertical { - &:where([data-orientation='vertical']) { - @slot; - } -} - -@utility no-scrollbar { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar { - display: none; - } -} +@import '@openshock/svelte-core/theme.css'; /* driver.js theme override — match the dark UI */ .driver-popover { diff --git a/src/hooks.client.ts b/src/hooks.client.ts index 23f7bb13..d02abe84 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -5,10 +5,10 @@ import { versionGetBackendInfo } from '$lib/api'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; import { authState, startAuthLifecycle } from '$lib/state/auth-state.svelte'; import { backendMetadata } from '$lib/state/backend-metadata-state.svelte'; -import { initializeColorScheme } from '$lib/state/color-scheme-state.svelte'; import { userState } from '$lib/state/user-state.svelte'; import { initTelemetry, log } from '$lib/telemetry/logger'; import { redirectLegacyHashRoute } from '$lib/utils/legacy-hash-redirect'; +import { initializeColorScheme } from '@openshock/svelte-core/state/color-scheme-state.svelte'; import type { HandleClientError } from '@sveltejs/kit'; /** Best-effort extraction of a message + stack from an unknown thrown value. */ diff --git a/src/lib/api/firmwareCDN.ts b/src/lib/api/firmwareCDN.ts index a746dc72..bf896989 100644 --- a/src/lib/api/firmwareCDN.ts +++ b/src/lib/api/firmwareCDN.ts @@ -1,4 +1,4 @@ -import { HashBuffer } from '$lib/utils/crypto'; +import { HashBuffer } from '@openshock/svelte-core/utils/crypto'; export const FirmwareChannels = ['stable', 'beta', 'develop'] as const; export type FirmwareChannel = (typeof FirmwareChannels)[number]; diff --git a/src/lib/api/next/transformers/LoginOkResponse.ts b/src/lib/api/next/transformers/LoginOkResponse.ts index 2b4d57d2..8723a422 100644 --- a/src/lib/api/next/transformers/LoginOkResponse.ts +++ b/src/lib/api/next/transformers/LoginOkResponse.ts @@ -1,5 +1,4 @@ -import { isObject } from '$lib/typeguards'; -import { HasBoolean, HasString, HasStringArray } from '../../../typeguards/propGuards'; +import { HasBoolean, HasString, HasStringArray, isObject } from '@openshock/svelte-core/typeguards'; import { TransformError } from '../TransformError'; import type { LoginOkResponse } from '../models'; import { IsRoleType } from './RoleType'; diff --git a/src/lib/api/next/transformers/OAuthSignupData.ts b/src/lib/api/next/transformers/OAuthSignupData.ts index 9b785e60..d425091a 100644 --- a/src/lib/api/next/transformers/OAuthSignupData.ts +++ b/src/lib/api/next/transformers/OAuthSignupData.ts @@ -1,5 +1,4 @@ -import { isObject } from '$lib/typeguards'; -import { HasString, HasStringOrNull } from '../../../typeguards/propGuards'; +import { HasString, HasStringOrNull, isObject } from '@openshock/svelte-core/typeguards'; import { TransformError } from '../TransformError'; import type { OAuthSignupData } from '../models/OAuthSignupData'; diff --git a/src/lib/api/pwnedPasswords.ts b/src/lib/api/pwnedPasswords.ts deleted file mode 100644 index 5fda02f1..00000000 --- a/src/lib/api/pwnedPasswords.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { HashString } from '$lib/utils/crypto'; - -export async function checkPwnedCount(password: string): Promise { - if (!password) { - throw new Error('Password cannot be empty'); - } - - const hash = await HashString(password, 'SHA-1'); - const hashPrefix = hash.substring(0, 5); - - let raw: string; - try { - const response = await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`); - - if (!response.ok) { - throw new Error(`Pwned passwords range request failed with status ${response.status}`); - } - - raw = await response.text(); - } catch (error) { - throw new Error('Error while fetching pwned passwords range', { cause: error }); - } - - const hashSuffix = hash.substring(5).toUpperCase(); - const match = raw.split('\n').find((line) => line.startsWith(hashSuffix)); - - if (match) { - const [, count] = match.split(':'); - - return Number.parseInt(count, 10); - } - - return 0; -} diff --git a/src/lib/components/Code.svelte b/src/lib/components/Code.svelte deleted file mode 100644 index c882be5e..00000000 --- a/src/lib/components/Code.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - {@render children?.()} - diff --git a/src/lib/components/ConfirmDeleteDialog.svelte b/src/lib/components/ConfirmDeleteDialog.svelte deleted file mode 100644 index 06faa36f..00000000 --- a/src/lib/components/ConfirmDeleteDialog.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - {title} - {@render description()} - - {@render children?.()} - - - diff --git a/src/lib/components/Container.svelte b/src/lib/components/Container.svelte deleted file mode 100644 index d8a15ad6..00000000 --- a/src/lib/components/Container.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
- {@render children()} -
diff --git a/src/lib/components/ControlModules/LiveButton.svelte b/src/lib/components/ControlModules/LiveButton.svelte index 444c6b93..a09f0321 100644 --- a/src/lib/components/ControlModules/LiveButton.svelte +++ b/src/lib/components/ControlModules/LiveButton.svelte @@ -1,6 +1,6 @@ - -
- {#if icon} - {@render icon()} - {/if} - - - - -
diff --git a/src/lib/components/DotGrid.svelte b/src/lib/components/DotGrid.svelte deleted file mode 100644 index 81580ace..00000000 --- a/src/lib/components/DotGrid.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - -
- - -
- - diff --git a/src/lib/components/EmptyState.svelte b/src/lib/components/EmptyState.svelte deleted file mode 100644 index b3dbbbb0..00000000 --- a/src/lib/components/EmptyState.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - {title} - {#if description} - - {description} - - {/if} - - {#if children} - - {@render children()} - - {/if} - diff --git a/src/lib/components/ExpirationPicker.svelte b/src/lib/components/ExpirationPicker.svelte index fe3eb504..4ea7e9eb 100644 --- a/src/lib/components/ExpirationPicker.svelte +++ b/src/lib/components/ExpirationPicker.svelte @@ -1,9 +1,9 @@ - - - {@render children?.()} - diff --git a/src/lib/components/LightSwitch.svelte b/src/lib/components/LightSwitch.svelte deleted file mode 100644 index 200f4be8..00000000 --- a/src/lib/components/LightSwitch.svelte +++ /dev/null @@ -1,95 +0,0 @@ - - - pendingScheme !== null, handleOpenChanged}> - - - Switch to light mode - - Warning: You are about to switch to light mode. -
- Are you sure you want to do this? -
-
- -
-
- - - - - - Toggle theme - - - {#each Object.values(ColorScheme) as value (value)} - evaluateLightSwitch(value)}>{value} - {/each} - - diff --git a/src/lib/components/PageHeader.svelte b/src/lib/components/PageHeader.svelte deleted file mode 100644 index f1819e47..00000000 --- a/src/lib/components/PageHeader.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - -
-
-

- {title} -

- {#if children} -
- {@render children()} -
- {/if} -
- - {#if subtitle} -

{subtitle}

- {/if} -
diff --git a/src/lib/components/Stepper.svelte b/src/lib/components/Stepper.svelte index f790092c..32ba2141 100644 --- a/src/lib/components/Stepper.svelte +++ b/src/lib/components/Stepper.svelte @@ -12,7 +12,7 @@ - - - - {#snippet child({ props })} - - {/snippet} - - - {@render children?.()} - - diff --git a/src/lib/components/TelemetryConsentToast.svelte b/src/lib/components/TelemetryConsentToast.svelte index ea2ddf1c..3de2ee76 100644 --- a/src/lib/components/TelemetryConsentToast.svelte +++ b/src/lib/components/TelemetryConsentToast.svelte @@ -1,6 +1,6 @@ - - - - - {date ? df.format(date.toDate()) : 'Pick a date'} - - -
- time && setTime(time)} /> -
- - -
-
diff --git a/src/lib/components/datetime-picker/time-picker-input.svelte b/src/lib/components/datetime-picker/time-picker-input.svelte deleted file mode 100644 index f6a04e56..00000000 --- a/src/lib/components/datetime-picker/time-picker-input.svelte +++ /dev/null @@ -1,140 +0,0 @@ - - - - - { - e.preventDefault(); - onchange?.(e); - }} - {type} - inputmode="decimal" - onkeydown={(e) => { - onkeydown?.(e); - handleKeyDown(e); - }} - {...restProps} -/> diff --git a/src/lib/components/datetime-picker/time-picker-utils.ts b/src/lib/components/datetime-picker/time-picker-utils.ts deleted file mode 100644 index a2d9a498..00000000 --- a/src/lib/components/datetime-picker/time-picker-utils.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Time } from '@internationalized/date'; - -/** - * regular expression to check for valid hour format (01-23) - */ -export function isValidHour(value: string) { - return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value); -} - -/** - * regular expression to check for valid 12 hour format (01-12) - */ -export function isValid12Hour(value: string) { - return /^(0[1-9]|1[0-2])$/.test(value); -} - -/** - * regular expression to check for valid minute format (00-59) - */ -export function isValidMinuteOrSecond(value: string) { - return /^[0-5][0-9]$/.test(value); -} - -type GetValidNumberConfig = { max: number; min?: number; loop?: boolean }; - -export function getValidNumber( - value: string, - { max, min = 0, loop = false }: GetValidNumberConfig -) { - let numericValue = Number.parseInt(value, 10); - - if (!isNaN(numericValue)) { - if (!loop) { - if (numericValue > max) numericValue = max; - if (numericValue < min) numericValue = min; - } else { - if (numericValue > max) numericValue = min; - if (numericValue < min) numericValue = max; - } - return numericValue.toString().padStart(2, '0'); - } - - return '00'; -} - -export function getValidHour(value: string) { - if (isValidHour(value)) return value; - return getValidNumber(value, { max: 23 }); -} - -export function getValid12Hour(value: string) { - if (isValid12Hour(value)) return value; - return getValidNumber(value, { min: 1, max: 12 }); -} - -export function getValidMinuteOrSecond(value: string) { - if (isValidMinuteOrSecond(value)) return value; - return getValidNumber(value, { max: 59 }); -} - -type GetValidArrowNumberConfig = { - min: number; - max: number; - step: number; -}; - -export function getValidArrowNumber(value: string, { min, max, step }: GetValidArrowNumberConfig) { - let numericValue = Number.parseInt(value, 10); - if (!isNaN(numericValue)) { - numericValue += step; - return getValidNumber(String(numericValue), { min, max, loop: true }); - } - return '00'; -} - -export function getValidArrowHour(value: string, step: number) { - return getValidArrowNumber(value, { min: 0, max: 23, step }); -} - -export function getValidArrow12Hour(value: string, step: number) { - return getValidArrowNumber(value, { min: 1, max: 12, step }); -} - -export function getValidArrowMinuteOrSecond(value: string, step: number) { - return getValidArrowNumber(value, { min: 0, max: 59, step }); -} - -export function setMinutes(time: Time, value: string) { - const minutes = getValidMinuteOrSecond(value); - return time.set({ minute: Number.parseInt(minutes, 10) }); -} - -export function setSeconds(time: Time, value: string) { - const seconds = getValidMinuteOrSecond(value); - return time.set({ second: Number.parseInt(seconds, 10) }); -} - -export function setHours(time: Time, value: string) { - const hours = getValidHour(value); - return time.set({ hour: Number.parseInt(hours, 10) }); -} - -export function set12Hours(time: Time, value: string, period: Period) { - const hours = Number.parseInt(getValid12Hour(value), 10); - const convertedHours = convert12HourTo24Hour(hours, period); - return time.set({ hour: convertedHours }); -} - -export type TimePickerType = 'minutes' | 'seconds' | 'hours' | '12hours'; -export type Period = 'AM' | 'PM'; - -export function setDateByType(time: Time, value: string, type: TimePickerType, period?: Period) { - switch (type) { - case 'minutes': - return setMinutes(time, value); - case 'seconds': - return setSeconds(time, value); - case 'hours': - return setHours(time, value); - case '12hours': { - if (!period) return time; - return set12Hours(time, value, period); - } - default: - return time; - } -} - -export function getDateByType(time: Time, type: TimePickerType) { - switch (type) { - case 'minutes': - return getValidMinuteOrSecond(String(time.minute)); - case 'seconds': - return getValidMinuteOrSecond(String(time.second)); - case 'hours': - return getValidHour(String(time.hour)); - case '12hours': { - const hours = display12HourValue(time.hour); - return getValid12Hour(String(hours)); - } - default: - return '00'; - } -} - -export function getArrowByType(value: string, step: number, type: TimePickerType) { - switch (type) { - case 'minutes': - return getValidArrowMinuteOrSecond(value, step); - case 'seconds': - return getValidArrowMinuteOrSecond(value, step); - case 'hours': - return getValidArrowHour(value, step); - case '12hours': - return getValidArrow12Hour(value, step); - default: - return '00'; - } -} - -/** - * handles value change of 12-hour input - * 12:00 PM is 12:00 - * 12:00 AM is 00:00 - */ -export function convert12HourTo24Hour(hour: number, period: Period) { - if (period === 'PM') { - if (hour <= 11) { - return hour + 12; - } else { - return hour; - } - } else if (period === 'AM') { - if (hour === 12) return 0; - return hour; - } - return hour; -} - -/** - * time is stored in the 24-hour form, - * but needs to be displayed to the user - * in its 12-hour representation - */ -export function display12HourValue(hours: number) { - if (hours === 0 || hours === 12) return '12'; - if (hours >= 22) return `${hours - 12}`; - if (hours % 12 > 9) return `${hours}`; - return `0${hours % 12}`; -} diff --git a/src/lib/components/datetime-picker/time-picker.svelte b/src/lib/components/datetime-picker/time-picker.svelte deleted file mode 100644 index a0ee4afe..00000000 --- a/src/lib/components/datetime-picker/time-picker.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - -
-
- {#if view === 'labels'} - - {/if} - - minuteRef?.focus()} - /> -
- - {#if view === 'dotted'} - : - {/if} - -
- {#if view === 'labels'} - - {/if} - - hourRef?.focus()} - onRightFocus={() => secondRef?.focus()} - /> -
- - {#if view === 'dotted'} - : - {/if} - - {#if showSeconds} -
- {#if view === 'labels'} - - {/if} - - minuteRef?.focus()} - /> -
- {/if} -
diff --git a/src/lib/components/dialog-manager/dialog-alert-content.svelte b/src/lib/components/dialog-manager/dialog-alert-content.svelte deleted file mode 100644 index e8b4ea1f..00000000 --- a/src/lib/components/dialog-manager/dialog-alert-content.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - {title} - {#if desc} - {desc} - {/if} - - - - diff --git a/src/lib/components/dialog-manager/dialog-confirm-content.svelte b/src/lib/components/dialog-manager/dialog-confirm-content.svelte deleted file mode 100644 index 450b7556..00000000 --- a/src/lib/components/dialog-manager/dialog-confirm-content.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - - - {title} - - {desc} - {#if descSnippet} - {@render descSnippet(data as T)} - {/if} - - - - - - diff --git a/src/lib/components/dialog-manager/dialog-custom-content.svelte b/src/lib/components/dialog-manager/dialog-custom-content.svelte deleted file mode 100644 index 38f91730..00000000 --- a/src/lib/components/dialog-manager/dialog-custom-content.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -{@render contentSnippet(renderProps)} diff --git a/src/lib/components/dialog-manager/dialog-manager.svelte b/src/lib/components/dialog-manager/dialog-manager.svelte deleted file mode 100644 index 5d4749c0..00000000 --- a/src/lib/components/dialog-manager/dialog-manager.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - -{#if dialogId && ctx} - {#key dialogId} - open, handleOpenChange}> - - - - - {/key} -{/if} diff --git a/src/lib/components/dialog-manager/dialog-store.svelte.ts b/src/lib/components/dialog-manager/dialog-store.svelte.ts deleted file mode 100644 index ce520d2d..00000000 --- a/src/lib/components/dialog-manager/dialog-store.svelte.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { SvelteMap } from 'svelte/reactivity'; -import DialogAlertContent from './dialog-alert-content.svelte'; -import DialogConfirmContent from './dialog-confirm-content.svelte'; -import DialogCustomContent from './dialog-custom-content.svelte'; -import type { - AlertDialogOptions, - ConfirmDialogOptions, - ConfirmResult, - CustomDialogOptions, - DialogContext, -} from './types'; - -// State -let dialogCount = $state(0); -const dialogs = new SvelteMap(); - -export function getOldestDialog(): [number, DialogContext] | null { - const firstEntry = dialogs.entries().next(); - return firstEntry.done ? null : firstEntry.value; -} - -export function removeDialog(id: number): void { - dialogs.delete(id); -} - -// Helper to create dialog with common setup -export function createDialog( - contextFactory: (resolve: (result: R) => void) => DialogContext -): Promise { - const { promise, resolve } = Promise.withResolvers(); - const id = ++dialogCount; - let resolved = false; - - const wrappedResolve = (result: R) => { - if (resolved) return; - resolved = true; - setTimeout(() => removeDialog(id), 150); - resolve(result); - }; - - dialogs.set(id, contextFactory(wrappedResolve) as DialogContext); - return promise; -} - -/** - * Opens a fully custom dialog with your own content snippet. - */ -export function open(options: CustomDialogOptions): Promise { - return createDialog((resolve) => ({ - content: DialogCustomContent, - props: { - data: options.data, - contentSnippet: options.contentSnippet, - resolve, - close: () => resolve(undefined as R), - }, - resolve, - })); -} - -/** - * Opens a confirm dialog with built-in confirm/cancel buttons. - */ -export function confirm(options: ConfirmDialogOptions): Promise> { - return createDialog>((resolve) => ({ - content: DialogConfirmContent, - props: { - ...options, - resolve, - close: () => resolve({ confirmed: false }), - }, - resolve, - })); -} - -/** - * Opens an alert dialog that the user acknowledges. - */ -export function alert(options: AlertDialogOptions): Promise { - return createDialog((resolve) => ({ - content: DialogAlertContent, - props: { - ...options, - resolve, - close: () => resolve(), - }, - resolve, - })); -} - -export const dialog = { - open, - confirm, - alert, - createDialog, -}; diff --git a/src/lib/components/dialog-manager/types.ts b/src/lib/components/dialog-manager/types.ts deleted file mode 100644 index b234e297..00000000 --- a/src/lib/components/dialog-manager/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { AnyComponent } from '$lib/types/AnyComponent'; -import type { Snippet } from 'svelte'; - -// Props passed to dialog content components -export interface DialogContentProps { - resolve: (result: R) => void; - close: () => void; -} - -// Props for custom dialog snippets -export interface DialogRenderProps extends DialogContentProps { - data: T; -} - -// Generic dialog context - stores a component to render -export interface DialogContext { - content: AnyComponent; - props: Record; - resolve: (result: R) => void; -} - -// Result types -export type ConfirmResult = { confirmed: true; data: T } | { confirmed: false }; - -// Options for each dialog type -export interface CustomDialogOptions { - data?: T; - contentSnippet: Snippet<[DialogRenderProps]>; -} - -export interface ConfirmDialogOptions { - title: string; - desc?: string; - data?: T; - confirmButtonText?: string; - cancelButtonText?: string; - descSnippet?: Snippet<[T]>; -} - -export interface AlertDialogOptions { - title: string; - desc?: string; - buttonText?: string; -} - -export interface AlertProps extends AlertDialogOptions { - resolve: () => void; - close: () => void; -} - -export interface ConfirmProps extends ConfirmDialogOptions { - resolve: (result: ConfirmResult) => void; - close: () => void; -} - -export interface CustomProps extends CustomDialogOptions { - resolve: (result: R) => void; - close: () => void; -} diff --git a/src/lib/components/input/DatePicker.svelte b/src/lib/components/input/DatePicker.svelte index 762d4cd1..5a274c32 100644 --- a/src/lib/components/input/DatePicker.svelte +++ b/src/lib/components/input/DatePicker.svelte @@ -1,10 +1,10 @@ - - diff --git a/src/lib/components/input/PasswordInput.svelte b/src/lib/components/input/PasswordInput.svelte deleted file mode 100644 index d7a8a8a5..00000000 --- a/src/lib/components/input/PasswordInput.svelte +++ /dev/null @@ -1,161 +0,0 @@ - - -{#snippet popup()} - -{/snippet} - - (showPopup = false)} - popup={showPopup ? (popup as Snippet) : undefined} -> - {#snippet labelSnippet(id: string)} -
- {label} - {#if showForget} - - Forgot your password? - - {/if} -
- {/snippet} - {#snippet after()} - - {/snippet} -
diff --git a/src/lib/components/input/TextInput.svelte b/src/lib/components/input/TextInput.svelte deleted file mode 100644 index a3692017..00000000 --- a/src/lib/components/input/TextInput.svelte +++ /dev/null @@ -1,130 +0,0 @@ - - - - {#if labelSnippet} - {@render labelSnippet(id)} - {/if} - - {#if label} - {label} - {/if} - -
- {#if Icon} - - {/if} - {#if after} - - - - {@render after()} - - - {:else} - - {/if} - {#if popup} - - {/if} -
- {#if validationResult?.message} -

- {validationResult.message} - {#if validationResult.link} - - - {validationResult.link.text} - - {/if} -

- {:else} - - {/if} -
diff --git a/src/lib/components/input/UsernameInput.svelte b/src/lib/components/input/UsernameInput.svelte index 151536fa..f52431a7 100644 --- a/src/lib/components/input/UsernameInput.svelte +++ b/src/lib/components/input/UsernameInput.svelte @@ -1,6 +1,6 @@ - - diff --git a/src/lib/components/metadata/BasicTags.svelte b/src/lib/components/metadata/BasicTags.svelte deleted file mode 100644 index 3f84afc0..00000000 --- a/src/lib/components/metadata/BasicTags.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - - {title} - {#if description} - - {/if} - diff --git a/src/lib/components/metadata/OpenGraphTags.svelte b/src/lib/components/metadata/OpenGraphTags.svelte deleted file mode 100644 index a08887f9..00000000 --- a/src/lib/components/metadata/OpenGraphTags.svelte +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - {#if image.type} - - {/if} - {#if image.width} - - {/if} - {#if image.height} - - {/if} - {#if image.alt} - - {/if} - - - {#if siteName} - - {/if} - {#if description} - - {/if} - {#if determiner} - - {/if} - - {#if metaLocale} - - {/if} - {#if locales} - {#each locales as locale (locale)} - - {/each} - {/if} - - {#if video} - - {#if video.type} - - {/if} - {#if video.width} - - {/if} - {#if video.height} - - {/if} - {/if} - - {#if audio} - - {#if audio.type} - - {/if} - {/if} - diff --git a/src/lib/components/metadata/Twitter/TwitterAppTags.svelte b/src/lib/components/metadata/Twitter/TwitterAppTags.svelte deleted file mode 100644 index 860175ff..00000000 --- a/src/lib/components/metadata/Twitter/TwitterAppTags.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - - {#if site} - - {/if} - {#if iphone} - - - - {/if} - {#if ipad} - - - - {/if} - {#if googleplay} - - - - {/if} - diff --git a/src/lib/components/metadata/Twitter/TwitterPlayerTags.svelte b/src/lib/components/metadata/Twitter/TwitterPlayerTags.svelte deleted file mode 100644 index ca046631..00000000 --- a/src/lib/components/metadata/Twitter/TwitterPlayerTags.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - - - - {#if site} - {#if isTwitterHandle(site)} - - {:else} - - {/if} - {/if} - {#if description} - - {/if} - {#if title} - - {/if} - {#if image} - - - {/if} - {#if player} - {#if player.type === 'iframe'} - - {/if} - - - {#if player.type === 'stream'} - - {/if} - {/if} - diff --git a/src/lib/components/metadata/Twitter/TwitterSummaryTags.svelte b/src/lib/components/metadata/Twitter/TwitterSummaryTags.svelte deleted file mode 100644 index 7c8286b1..00000000 --- a/src/lib/components/metadata/Twitter/TwitterSummaryTags.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - - - - {#if site} - {#if isTwitterHandle(site)} - - {:else} - - {/if} - {/if} - {#if creator} - {#if isTwitterHandle(creator)} - - {:else} - - {/if} - {/if} - {#if description} - - {/if} - {#if title} - - {/if} - {#if image} - - - {/if} - diff --git a/src/lib/components/metadata/Twitter/index.ts b/src/lib/components/metadata/Twitter/index.ts deleted file mode 100644 index dea413e3..00000000 --- a/src/lib/components/metadata/Twitter/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import TwitterAppTags from './TwitterAppTags.svelte'; -import TwitterPlayerTags from './TwitterPlayerTags.svelte'; -import TwitterSummaryTags from './TwitterSummaryTags.svelte'; - -export { TwitterAppTags, TwitterPlayerTags, TwitterSummaryTags }; diff --git a/src/lib/components/metadata/index.ts b/src/lib/components/metadata/index.ts deleted file mode 100644 index fc6adc3f..00000000 --- a/src/lib/components/metadata/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import BasicTags from './BasicTags.svelte'; -import OpenGraphTags from './OpenGraphTags.svelte'; -import { TwitterAppTags, TwitterPlayerTags, TwitterSummaryTags } from './Twitter'; - -export { BasicTags, OpenGraphTags, TwitterAppTags, TwitterPlayerTags, TwitterSummaryTags }; diff --git a/src/lib/components/shares/permission-tooltip.svelte b/src/lib/components/shares/permission-tooltip.svelte index 8d335658..1ab5246a 100644 --- a/src/lib/components/shares/permission-tooltip.svelte +++ b/src/lib/components/shares/permission-tooltip.svelte @@ -1,7 +1,7 @@ - - - - - Google Chrome - - - - - - - - - - - - - - - - - - - - diff --git a/src/lib/components/svg/CloudflareLogo.svelte b/src/lib/components/svg/CloudflareLogo.svelte deleted file mode 100644 index 3bc70c30..00000000 --- a/src/lib/components/svg/CloudflareLogo.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - Cloudflare - - - - - - - - - - - - - - - diff --git a/src/lib/components/svg/DiscordLogo.svelte b/src/lib/components/svg/DiscordLogo.svelte deleted file mode 100644 index b8858fbd..00000000 --- a/src/lib/components/svg/DiscordLogo.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - Discord - - diff --git a/src/lib/components/svg/EdgeLogo.svelte b/src/lib/components/svg/EdgeLogo.svelte deleted file mode 100644 index bd406c8f..00000000 --- a/src/lib/components/svg/EdgeLogo.svelte +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - Microsoft Edge - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/lib/components/svg/GithubIcon.svelte b/src/lib/components/svg/GithubIcon.svelte deleted file mode 100644 index d6882d0c..00000000 --- a/src/lib/components/svg/GithubIcon.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - Github - - diff --git a/src/lib/components/svg/GoogleLogo.svelte b/src/lib/components/svg/GoogleLogo.svelte deleted file mode 100644 index 84040e23..00000000 --- a/src/lib/components/svg/GoogleLogo.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - Google - - diff --git a/src/lib/components/svg/LiveControlIcon.svelte b/src/lib/components/svg/LiveControlIcon.svelte index 0e0399ea..7ec8a817 100644 --- a/src/lib/components/svg/LiveControlIcon.svelte +++ b/src/lib/components/svg/LiveControlIcon.svelte @@ -1,5 +1,5 @@ - - - - - Opera - - - - - - - - - - - - - - - - - - diff --git a/src/lib/components/svg/XLogo.svelte b/src/lib/components/svg/XLogo.svelte deleted file mode 100644 index 256c4da8..00000000 --- a/src/lib/components/svg/XLogo.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - X (formerly Twitter) - - diff --git a/src/lib/components/ui/README.md b/src/lib/components/ui/README.md deleted file mode 100644 index 42223f18..00000000 --- a/src/lib/components/ui/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Updating shadcn-svelte Components - -Everything in this directory comes from the shadcn-svelte registry **except** `multi-select-combobox`. - -## How to Update - -Run the update script from the project root: - -```bash -./scripts/update-shadcn.sh -``` - -This will automatically: -1. Delete all shadcn component directories (preserving `multi-select-combobox`) -2. Re-add them from the registry -3. Remove unwanted dependencies the CLI adds -4. Upgrade all dependencies to latest -5. Reapply project-specific customizations (see below) -6. Format and type-check the result - -Review the git diff before committing. - -## Custom Modifications - -These are automatically applied by the script, but documented here for reference. - -### Sidebar (`sidebar.svelte`) - -- Change `ease-linear` to `ease-in-out` -- Change `duration-200` to `duration-300` - -### Sidebar submenu (`sidebar-menu-sub.svelte`) - -- Change `mx-3.5` to `ml-3.5` -- Change `px-2.5` to `pl-2.5` - -### Sonner (`sonner.svelte`) - -- Uses our own `color-scheme-state` instead of `mode-watcher` - -### Slider (`slider.svelte`) - -- Add `cursor-w-resize` to the thumb - -### Toggle group (`toggle-group.svelte`) - -- Add `// svelte-ignore state_referenced_locally` to suppress false warnings diff --git a/src/lib/components/ui/accordion/accordion-content.svelte b/src/lib/components/ui/accordion/accordion-content.svelte deleted file mode 100644 index 7360caf5..00000000 --- a/src/lib/components/ui/accordion/accordion-content.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - -
- {@render children?.()} -
-
diff --git a/src/lib/components/ui/accordion/accordion-item.svelte b/src/lib/components/ui/accordion/accordion-item.svelte deleted file mode 100644 index e2b299fe..00000000 --- a/src/lib/components/ui/accordion/accordion-item.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/accordion/accordion-trigger.svelte b/src/lib/components/ui/accordion/accordion-trigger.svelte deleted file mode 100644 index cdff46f1..00000000 --- a/src/lib/components/ui/accordion/accordion-trigger.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - - {@render children?.()} - - - diff --git a/src/lib/components/ui/accordion/accordion.svelte b/src/lib/components/ui/accordion/accordion.svelte deleted file mode 100644 index 01ee847d..00000000 --- a/src/lib/components/ui/accordion/accordion.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/lib/components/ui/accordion/index.ts b/src/lib/components/ui/accordion/index.ts deleted file mode 100644 index ac343a10..00000000 --- a/src/lib/components/ui/accordion/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Root from "./accordion.svelte"; -import Content from "./accordion-content.svelte"; -import Item from "./accordion-item.svelte"; -import Trigger from "./accordion-trigger.svelte"; - -export { - Root, - Content, - Item, - Trigger, - // - Root as Accordion, - Content as AccordionContent, - Item as AccordionItem, - Trigger as AccordionTrigger, -}; diff --git a/src/lib/components/ui/avatar/avatar-badge.svelte b/src/lib/components/ui/avatar/avatar-badge.svelte deleted file mode 100644 index d64ff149..00000000 --- a/src/lib/components/ui/avatar/avatar-badge.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - -svg]:hidden", - "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", - "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", - className - )} - {...restProps} -> - {@render children?.()} - diff --git a/src/lib/components/ui/avatar/avatar-fallback.svelte b/src/lib/components/ui/avatar/avatar-fallback.svelte deleted file mode 100644 index 3b7a6e30..00000000 --- a/src/lib/components/ui/avatar/avatar-fallback.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/src/lib/components/ui/avatar/avatar-group-count.svelte b/src/lib/components/ui/avatar/avatar-group-count.svelte deleted file mode 100644 index c1283ffc..00000000 --- a/src/lib/components/ui/avatar/avatar-group-count.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", - className - )} - {...restProps} -> - {@render children?.()} -
diff --git a/src/lib/components/ui/avatar/avatar-group.svelte b/src/lib/components/ui/avatar/avatar-group.svelte deleted file mode 100644 index cec81e54..00000000 --- a/src/lib/components/ui/avatar/avatar-group.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/src/lib/components/ui/avatar/avatar-image.svelte b/src/lib/components/ui/avatar/avatar-image.svelte deleted file mode 100644 index 910a3378..00000000 --- a/src/lib/components/ui/avatar/avatar-image.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/avatar/avatar.svelte b/src/lib/components/ui/avatar/avatar.svelte deleted file mode 100644 index 5a1f954b..00000000 --- a/src/lib/components/ui/avatar/avatar.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/src/lib/components/ui/avatar/index.ts b/src/lib/components/ui/avatar/index.ts deleted file mode 100644 index 38ccef84..00000000 --- a/src/lib/components/ui/avatar/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Root from "./avatar.svelte"; -import Image from "./avatar-image.svelte"; -import Fallback from "./avatar-fallback.svelte"; -import Badge from "./avatar-badge.svelte"; -import Group from "./avatar-group.svelte"; -import GroupCount from "./avatar-group-count.svelte"; - -export { - Root, - Image, - Fallback, - Badge, - Group, - GroupCount, - // - Root as Avatar, - Image as AvatarImage, - Fallback as AvatarFallback, - Badge as AvatarBadge, - Group as AvatarGroup, - GroupCount as AvatarGroupCount, -}; diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte deleted file mode 100644 index 26281952..00000000 --- a/src/lib/components/ui/badge/badge.svelte +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - {@render children?.()} - diff --git a/src/lib/components/ui/badge/index.ts b/src/lib/components/ui/badge/index.ts deleted file mode 100644 index 64e0aa9b..00000000 --- a/src/lib/components/ui/badge/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Badge } from "./badge.svelte"; -export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte deleted file mode 100644 index 28d78fd0..00000000 --- a/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte deleted file mode 100644 index ecd73f9d..00000000 --- a/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
  • - {@render children?.()} -
  • diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte deleted file mode 100644 index 42da73fd..00000000 --- a/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -{#if child} - {@render child({ props: attrs })} -{:else} - - {@render children?.()} - -{/if} diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte deleted file mode 100644 index 4edc2636..00000000 --- a/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
      - {@render children?.()} -
    diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte deleted file mode 100644 index 8e371964..00000000 --- a/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - {@render children?.()} - diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte deleted file mode 100644 index 7d870b5f..00000000 --- a/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/src/lib/components/ui/breadcrumb/breadcrumb.svelte b/src/lib/components/ui/breadcrumb/breadcrumb.svelte deleted file mode 100644 index 78fc5209..00000000 --- a/src/lib/components/ui/breadcrumb/breadcrumb.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/src/lib/components/ui/breadcrumb/index.ts b/src/lib/components/ui/breadcrumb/index.ts deleted file mode 100644 index dc914ec3..00000000 --- a/src/lib/components/ui/breadcrumb/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Root from "./breadcrumb.svelte"; -import Ellipsis from "./breadcrumb-ellipsis.svelte"; -import Item from "./breadcrumb-item.svelte"; -import Separator from "./breadcrumb-separator.svelte"; -import Link from "./breadcrumb-link.svelte"; -import List from "./breadcrumb-list.svelte"; -import Page from "./breadcrumb-page.svelte"; - -export { - Root, - Ellipsis, - Item, - Separator, - Link, - List, - Page, - // - Root as Breadcrumb, - Ellipsis as BreadcrumbEllipsis, - Item as BreadcrumbItem, - Separator as BreadcrumbSeparator, - Link as BreadcrumbLink, - List as BreadcrumbList, - Page as BreadcrumbPage, -}; diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte deleted file mode 100644 index a2f724d8..00000000 --- a/src/lib/components/ui/button/button.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - - - -{#if href} - - {@render children?.()} - -{:else} - -{/if} diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts deleted file mode 100644 index fb585d76..00000000 --- a/src/lib/components/ui/button/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -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, -}; diff --git a/src/lib/components/ui/calendar/calendar-caption.svelte b/src/lib/components/ui/calendar/calendar-caption.svelte deleted file mode 100644 index 5c930370..00000000 --- a/src/lib/components/ui/calendar/calendar-caption.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - -{#snippet MonthSelect()} - { - if (!placeholder) return; - const v = Number.parseInt(e.currentTarget.value); - const newPlaceholder = placeholder.set({ month: v }); - placeholder = newPlaceholder.subtract({ months: monthIndex }); - }} - /> -{/snippet} - -{#snippet YearSelect()} - -{/snippet} - -{#if captionLayout === "dropdown"} - {@render MonthSelect()} - {@render YearSelect()} -{:else if captionLayout === "dropdown-months"} - {@render MonthSelect()} - {#if placeholder} - {formatYear(placeholder)} - {/if} -{:else if captionLayout === "dropdown-years"} - {#if placeholder} - {formatMonth(placeholder)} - {/if} - {@render YearSelect()} -{:else} - {formatMonth(month)} {formatYear(month)} -{/if} diff --git a/src/lib/components/ui/calendar/calendar-cell.svelte b/src/lib/components/ui/calendar/calendar-cell.svelte deleted file mode 100644 index c02eeab1..00000000 --- a/src/lib/components/ui/calendar/calendar-cell.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/lib/components/ui/calendar/calendar-day.svelte b/src/lib/components/ui/calendar/calendar-day.svelte deleted file mode 100644 index be3599d5..00000000 --- a/src/lib/components/ui/calendar/calendar-day.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - -span]:text-xs [&>span]:opacity-70", - className - )} - {...restProps} -/> diff --git a/src/lib/components/ui/calendar/calendar-grid-body.svelte b/src/lib/components/ui/calendar/calendar-grid-body.svelte deleted file mode 100644 index 6f6185fc..00000000 --- a/src/lib/components/ui/calendar/calendar-grid-body.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/lib/components/ui/calendar/calendar-grid-head.svelte b/src/lib/components/ui/calendar/calendar-grid-head.svelte deleted file mode 100644 index 58b44b01..00000000 --- a/src/lib/components/ui/calendar/calendar-grid-head.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/lib/components/ui/calendar/calendar-grid-row.svelte b/src/lib/components/ui/calendar/calendar-grid-row.svelte deleted file mode 100644 index 002470ca..00000000 --- a/src/lib/components/ui/calendar/calendar-grid-row.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/lib/components/ui/calendar/calendar-grid.svelte b/src/lib/components/ui/calendar/calendar-grid.svelte deleted file mode 100644 index fc0875e6..00000000 --- a/src/lib/components/ui/calendar/calendar-grid.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/lib/components/ui/calendar/calendar-head-cell.svelte b/src/lib/components/ui/calendar/calendar-head-cell.svelte deleted file mode 100644 index 475b4e35..00000000 --- a/src/lib/components/ui/calendar/calendar-head-cell.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/lib/components/ui/calendar/calendar-header.svelte b/src/lib/components/ui/calendar/calendar-header.svelte deleted file mode 100644 index b3bc4a0a..00000000 --- a/src/lib/components/ui/calendar/calendar-header.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/lib/components/ui/calendar/calendar-heading.svelte b/src/lib/components/ui/calendar/calendar-heading.svelte deleted file mode 100644 index 0c3ea1a3..00000000 --- a/src/lib/components/ui/calendar/calendar-heading.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/lib/components/ui/calendar/calendar-month-select.svelte b/src/lib/components/ui/calendar/calendar-month-select.svelte deleted file mode 100644 index 1545241d..00000000 --- a/src/lib/components/ui/calendar/calendar-month-select.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - - - - {#snippet child({ props, monthItems, selectedMonthItem })} - - - {/snippet} - - diff --git a/src/lib/components/ui/calendar/calendar-month.svelte b/src/lib/components/ui/calendar/calendar-month.svelte deleted file mode 100644 index 34bb5ec2..00000000 --- a/src/lib/components/ui/calendar/calendar-month.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/calendar/calendar-months.svelte b/src/lib/components/ui/calendar/calendar-months.svelte deleted file mode 100644 index 8125cdc4..00000000 --- a/src/lib/components/ui/calendar/calendar-months.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/calendar/calendar-nav.svelte b/src/lib/components/ui/calendar/calendar-nav.svelte deleted file mode 100644 index c0e99a77..00000000 --- a/src/lib/components/ui/calendar/calendar-nav.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/lib/components/ui/calendar/calendar-next-button.svelte b/src/lib/components/ui/calendar/calendar-next-button.svelte deleted file mode 100644 index c507d6b1..00000000 --- a/src/lib/components/ui/calendar/calendar-next-button.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -{#snippet Fallback()} - -{/snippet} - - - {#if children} - {@render children?.()} - {:else} - {@render Fallback()} - {/if} - diff --git a/src/lib/components/ui/calendar/calendar-prev-button.svelte b/src/lib/components/ui/calendar/calendar-prev-button.svelte deleted file mode 100644 index 3d6b5549..00000000 --- a/src/lib/components/ui/calendar/calendar-prev-button.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -{#snippet Fallback()} - -{/snippet} - - - {#if children} - {@render children?.()} - {:else} - {@render Fallback()} - {/if} - diff --git a/src/lib/components/ui/calendar/calendar-year-select.svelte b/src/lib/components/ui/calendar/calendar-year-select.svelte deleted file mode 100644 index 5956ddd8..00000000 --- a/src/lib/components/ui/calendar/calendar-year-select.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - - - - {#snippet child({ props, yearItems, selectedYearItem })} - - - {/snippet} - - diff --git a/src/lib/components/ui/calendar/calendar.svelte b/src/lib/components/ui/calendar/calendar.svelte deleted file mode 100644 index c70552c0..00000000 --- a/src/lib/components/ui/calendar/calendar.svelte +++ /dev/null @@ -1,115 +0,0 @@ - - - - - {#snippet children({ months, weekdays })} - - - - - - {#each months as month, monthIndex (month)} - - - - - - - - {#each weekdays as weekday, i (i)} - - {weekday.slice(0, 2)} - - {/each} - - - - {#each month.weeks as weekDates (weekDates)} - - {#each weekDates as date (date)} - - {#if day} - {@render day({ - day: date, - outsideMonth: !isEqualMonth(date, month.value), - })} - {:else} - - {/if} - - {/each} - - {/each} - - - - {/each} - - {/snippet} - diff --git a/src/lib/components/ui/calendar/index.ts b/src/lib/components/ui/calendar/index.ts deleted file mode 100644 index f3a16d2d..00000000 --- a/src/lib/components/ui/calendar/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Root from "./calendar.svelte"; -import Cell from "./calendar-cell.svelte"; -import Day from "./calendar-day.svelte"; -import Grid from "./calendar-grid.svelte"; -import Header from "./calendar-header.svelte"; -import Months from "./calendar-months.svelte"; -import GridRow from "./calendar-grid-row.svelte"; -import Heading from "./calendar-heading.svelte"; -import GridBody from "./calendar-grid-body.svelte"; -import GridHead from "./calendar-grid-head.svelte"; -import HeadCell from "./calendar-head-cell.svelte"; -import NextButton from "./calendar-next-button.svelte"; -import PrevButton from "./calendar-prev-button.svelte"; -import MonthSelect from "./calendar-month-select.svelte"; -import YearSelect from "./calendar-year-select.svelte"; -import Month from "./calendar-month.svelte"; -import Nav from "./calendar-nav.svelte"; -import Caption from "./calendar-caption.svelte"; - -export { - Day, - Cell, - Grid, - Header, - Months, - GridRow, - Heading, - GridBody, - GridHead, - HeadCell, - NextButton, - PrevButton, - Nav, - Month, - YearSelect, - MonthSelect, - Caption, - // - Root as Calendar, -}; diff --git a/src/lib/components/ui/card/card-action.svelte b/src/lib/components/ui/card/card-action.svelte deleted file mode 100644 index d49f1c34..00000000 --- a/src/lib/components/ui/card/card-action.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte deleted file mode 100644 index f3a4ace2..00000000 --- a/src/lib/components/ui/card/card-content.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte deleted file mode 100644 index 9449ce62..00000000 --- a/src/lib/components/ui/card/card-description.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -

    - {@render children?.()} -

    diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte deleted file mode 100644 index 00b5850e..00000000 --- a/src/lib/components/ui/card/card-footer.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte deleted file mode 100644 index 15d2c952..00000000 --- a/src/lib/components/ui/card/card-header.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte deleted file mode 100644 index f3b2a4e4..00000000 --- a/src/lib/components/ui/card/card-title.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte deleted file mode 100644 index 0bf33eec..00000000 --- a/src/lib/components/ui/card/card.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
    img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)} - {...restProps} -> - {@render children?.()} -
    diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts deleted file mode 100644 index 4d3fce48..00000000 --- a/src/lib/components/ui/card/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Root from "./card.svelte"; -import Content from "./card-content.svelte"; -import Description from "./card-description.svelte"; -import Footer from "./card-footer.svelte"; -import Header from "./card-header.svelte"; -import Title from "./card-title.svelte"; -import Action from "./card-action.svelte"; - -export { - Root, - Content, - Description, - Footer, - Header, - Title, - Action, - // - Root as Card, - Content as CardContent, - Description as CardDescription, - Footer as CardFooter, - Header as CardHeader, - Title as CardTitle, - Action as CardAction, -}; diff --git a/src/lib/components/ui/checkbox/checkbox.svelte b/src/lib/components/ui/checkbox/checkbox.svelte deleted file mode 100644 index 234154d0..00000000 --- a/src/lib/components/ui/checkbox/checkbox.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - - - {#snippet children({ checked, indeterminate })} -
    - {#if checked} - - {:else if indeterminate} - - {/if} -
    - {/snippet} -
    diff --git a/src/lib/components/ui/checkbox/index.ts b/src/lib/components/ui/checkbox/index.ts deleted file mode 100644 index 6d92d945..00000000 --- a/src/lib/components/ui/checkbox/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Root from "./checkbox.svelte"; -export { - Root, - // - Root as Checkbox, -}; diff --git a/src/lib/components/ui/collapsible/collapsible-content.svelte b/src/lib/components/ui/collapsible/collapsible-content.svelte deleted file mode 100644 index bdabb559..00000000 --- a/src/lib/components/ui/collapsible/collapsible-content.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/src/lib/components/ui/collapsible/collapsible-trigger.svelte deleted file mode 100644 index ece7ad68..00000000 --- a/src/lib/components/ui/collapsible/collapsible-trigger.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/collapsible/collapsible.svelte b/src/lib/components/ui/collapsible/collapsible.svelte deleted file mode 100644 index 39cdd4e4..00000000 --- a/src/lib/components/ui/collapsible/collapsible.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/src/lib/components/ui/collapsible/index.ts b/src/lib/components/ui/collapsible/index.ts deleted file mode 100644 index 169b4791..00000000 --- a/src/lib/components/ui/collapsible/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Root from "./collapsible.svelte"; -import Trigger from "./collapsible-trigger.svelte"; -import Content from "./collapsible-content.svelte"; - -export { - Root, - Content, - Trigger, - // - Root as Collapsible, - Content as CollapsibleContent, - Trigger as CollapsibleTrigger, -}; diff --git a/src/lib/components/ui/command/command-dialog.svelte b/src/lib/components/ui/command/command-dialog.svelte deleted file mode 100644 index ce6c1e03..00000000 --- a/src/lib/components/ui/command/command-dialog.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - - - - {title} - {description} - - - - - diff --git a/src/lib/components/ui/command/command-empty.svelte b/src/lib/components/ui/command/command-empty.svelte deleted file mode 100644 index 1afcf7c2..00000000 --- a/src/lib/components/ui/command/command-empty.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/command/command-group.svelte b/src/lib/components/ui/command/command-group.svelte deleted file mode 100644 index 74c948af..00000000 --- a/src/lib/components/ui/command/command-group.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - {#if heading} - - {heading} - - {/if} - - diff --git a/src/lib/components/ui/command/command-input.svelte b/src/lib/components/ui/command/command-input.svelte deleted file mode 100644 index 0dda6bf8..00000000 --- a/src/lib/components/ui/command/command-input.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -
    - - - {#snippet child({ props })} - - {/snippet} - - - - - -
    diff --git a/src/lib/components/ui/command/command-item.svelte b/src/lib/components/ui/command/command-item.svelte deleted file mode 100644 index 22930ed7..00000000 --- a/src/lib/components/ui/command/command-item.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - {@render children?.()} - - diff --git a/src/lib/components/ui/command/command-link-item.svelte b/src/lib/components/ui/command/command-link-item.svelte deleted file mode 100644 index ab8cd707..00000000 --- a/src/lib/components/ui/command/command-link-item.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/src/lib/components/ui/command/command-list.svelte b/src/lib/components/ui/command/command-list.svelte deleted file mode 100644 index f3016d44..00000000 --- a/src/lib/components/ui/command/command-list.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/command/command-loading.svelte b/src/lib/components/ui/command/command-loading.svelte deleted file mode 100644 index 19dd2985..00000000 --- a/src/lib/components/ui/command/command-loading.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/command/command-separator.svelte b/src/lib/components/ui/command/command-separator.svelte deleted file mode 100644 index 767fb56a..00000000 --- a/src/lib/components/ui/command/command-separator.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/command/command-shortcut.svelte b/src/lib/components/ui/command/command-shortcut.svelte deleted file mode 100644 index 19fc358e..00000000 --- a/src/lib/components/ui/command/command-shortcut.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - {@render children?.()} - diff --git a/src/lib/components/ui/command/command.svelte b/src/lib/components/ui/command/command.svelte deleted file mode 100644 index b87561e9..00000000 --- a/src/lib/components/ui/command/command.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/src/lib/components/ui/command/index.ts b/src/lib/components/ui/command/index.ts deleted file mode 100644 index 5435fbe8..00000000 --- a/src/lib/components/ui/command/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Root from "./command.svelte"; -import Loading from "./command-loading.svelte"; -import Dialog from "./command-dialog.svelte"; -import Empty from "./command-empty.svelte"; -import Group from "./command-group.svelte"; -import Item from "./command-item.svelte"; -import Input from "./command-input.svelte"; -import List from "./command-list.svelte"; -import Separator from "./command-separator.svelte"; -import Shortcut from "./command-shortcut.svelte"; -import LinkItem from "./command-link-item.svelte"; - -export { - Root, - Dialog, - Empty, - Group, - Item, - LinkItem, - Input, - List, - Separator, - Shortcut, - Loading, - // - Root as Command, - Dialog as CommandDialog, - Empty as CommandEmpty, - Group as CommandGroup, - Item as CommandItem, - LinkItem as CommandLinkItem, - Input as CommandInput, - List as CommandList, - Separator as CommandSeparator, - Shortcut as CommandShortcut, - Loading as CommandLoading, -}; diff --git a/src/lib/components/ui/data-table/data-table.svelte.ts b/src/lib/components/ui/data-table/data-table.svelte.ts deleted file mode 100644 index f3c30a7f..00000000 --- a/src/lib/components/ui/data-table/data-table.svelte.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - type RowData, - type TableOptions, - type TableOptionsResolved, - type TableState, - type Updater, - createTable, -} from "@tanstack/table-core"; - -/** - * Creates a reactive TanStack table object for Svelte. - * @param options Table options to create the table with. - * @returns A reactive table object. - * @example - * ```svelte - * - * - * - * - * {#each table.getHeaderGroups() as headerGroup} - * - * {#each headerGroup.headers as header} - * - * {/each} - * - * {/each} - * - * - *
    - * - *
    - * ``` - */ -export function createSvelteTable(options: TableOptions) { - const resolvedOptions: TableOptionsResolved = mergeObjects( - { - state: {}, - onStateChange() {}, - renderFallbackValue: null, - mergeOptions: ( - defaultOptions: TableOptions, - options: Partial> - ) => { - return mergeObjects(defaultOptions, options); - }, - }, - options - ); - - const table = createTable(resolvedOptions); - let state = $state(table.initialState); - - function updateOptions() { - table.setOptions(() => { - return mergeObjects(resolvedOptions, options, { - state: mergeObjects(state, options.state || {}), - - onStateChange: (updater: Updater) => { - if (updater instanceof Function) state = updater(state); - else state = mergeObjects(state, updater); - - options.onStateChange?.(updater); - }, - }); - }); - } - - updateOptions(); - - $effect.pre(() => { - updateOptions(); - }); - - return table; -} - -type MaybeThunk = T | (() => T | null | undefined); -type Intersection = (T extends [infer H, ...infer R] - ? H & Intersection - : unknown) & {}; - -/** - * Lazily merges several objects (or thunks) while preserving - * getter semantics from every source. - * - * Proxy-based to avoid known WebKit recursion issue. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function mergeObjects[]>( - ...sources: Sources -): Intersection<{ [K in keyof Sources]: Sources[K] }> { - const resolve = (src: MaybeThunk): T | undefined => - typeof src === "function" ? (src() ?? undefined) : src; - - const findSourceWithKey = (key: PropertyKey) => { - for (let i = sources.length - 1; i >= 0; i--) { - const obj = resolve(sources[i]); - if (obj && key in obj) return obj; - } - return undefined; - }; - - return new Proxy(Object.create(null), { - get(_, key) { - const src = findSourceWithKey(key); - - return src?.[key as never]; - }, - - has(_, key) { - return !!findSourceWithKey(key); - }, - - ownKeys(): (string | symbol)[] { - // eslint-disable-next-line svelte/prefer-svelte-reactivity - const all = new Set(); - for (const s of sources) { - const obj = resolve(s); - if (obj) { - for (const k of Reflect.ownKeys(obj) as (string | symbol)[]) { - all.add(k); - } - } - } - return [...all]; - }, - - getOwnPropertyDescriptor(_, key) { - const src = findSourceWithKey(key); - if (!src) return undefined; - return { - configurable: true, - enumerable: true, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: (src as any)[key], - writable: true, - }; - }, - }) as Intersection<{ [K in keyof Sources]: Sources[K] }>; -} diff --git a/src/lib/components/ui/data-table/flex-render.svelte b/src/lib/components/ui/data-table/flex-render.svelte deleted file mode 100644 index ac82a581..00000000 --- a/src/lib/components/ui/data-table/flex-render.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - -{#if typeof content === "string"} - {content} -{:else if content instanceof Function} - - - {@const result = content(context as any)} - {#if result instanceof RenderComponentConfig} - {@const { component: Component, props } = result} - - {:else if result instanceof RenderSnippetConfig} - {@const { snippet, params } = result} - {@render snippet({ ...params, attach })} - {:else} - {result} - {/if} -{/if} diff --git a/src/lib/components/ui/data-table/index.ts b/src/lib/components/ui/data-table/index.ts deleted file mode 100644 index 5f4e77ea..00000000 --- a/src/lib/components/ui/data-table/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as FlexRender } from "./flex-render.svelte"; -export { renderComponent, renderSnippet } from "./render-helpers.js"; -export { createSvelteTable } from "./data-table.svelte.js"; diff --git a/src/lib/components/ui/data-table/render-helpers.ts b/src/lib/components/ui/data-table/render-helpers.ts deleted file mode 100644 index fa036d62..00000000 --- a/src/lib/components/ui/data-table/render-helpers.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { Component, ComponentProps, Snippet } from "svelte"; - -/** - * A helper class to make it easy to identify Svelte components in - * `columnDef.cell` and `columnDef.header` properties. - * - * > NOTE: This class should only be used internally by the adapter. If you're - * reading this and you don't know what this is for, you probably don't need it. - * - * @example - * ```svelte - * {@const result = content(context as any)} - * {#if result instanceof RenderComponentConfig} - * {@const { component: Component, props } = result} - * - * {/if} - * ``` - */ -export class RenderComponentConfig { - component: TComponent; - props: ComponentProps | Record; - constructor( - component: TComponent, - props: ComponentProps | Record = {} - ) { - this.component = component; - this.props = props; - } -} - -/** - * A helper class to make it easy to identify Svelte Snippets in `columnDef.cell` and `columnDef.header` properties. - * - * > NOTE: This class should only be used internally by the adapter. If you're - * reading this and you don't know what this is for, you probably don't need it. - * - * @example - * ```svelte - * {@const result = content(context as any)} - * {#if result instanceof RenderSnippetConfig} - * {@const { snippet, params } = result} - * {@render snippet(params)} - * {/if} - * ``` - */ -export class RenderSnippetConfig { - snippet: Snippet<[TProps]>; - params: TProps; - constructor(snippet: Snippet<[TProps]>, params: TProps) { - this.snippet = snippet; - this.params = params; - } -} - -/** - * A helper function to help create cells from Svelte components through ColumnDef's `cell` and `header` properties. - * - * This is only to be used with Svelte Components - use `renderSnippet` for Svelte Snippets. - * - * @param component A Svelte component - * @param props The props to pass to `component` - * @returns A `RenderComponentConfig` object that helps svelte-table know how to render the header/cell component. - * @example - * ```ts - * // +page.svelte - * const defaultColumns = [ - * columnHelper.accessor('name', { - * header: header => renderComponent(SortHeader, { label: 'Name', header }), - * }), - * columnHelper.accessor('state', { - * header: header => renderComponent(SortHeader, { label: 'State', header }), - * }), - * ] - * ``` - * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} - */ -export function renderComponent< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends Component, - Props extends ComponentProps, ->(component: T, props: Props = {} as Props) { - return new RenderComponentConfig(component, props); -} - -/** - * A helper function to help create cells from Svelte Snippets through ColumnDef's `cell` and `header` properties. - * - * The snippet must only take one parameter. - * - * This is only to be used with Snippets - use `renderComponent` for Svelte Components. - * - * @param snippet - * @param params - * @returns - A `RenderSnippetConfig` object that helps svelte-table know how to render the header/cell snippet. - * @example - * ```ts - * // +page.svelte - * const defaultColumns = [ - * columnHelper.accessor('name', { - * cell: cell => renderSnippet(nameSnippet, { name: cell.row.name }), - * }), - * columnHelper.accessor('state', { - * cell: cell => renderSnippet(stateSnippet, { state: cell.row.state }), - * }), - * ] - * ``` - * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} - */ -export function renderSnippet(snippet: Snippet<[TProps]>, params: TProps = {} as TProps) { - return new RenderSnippetConfig(snippet, params); -} diff --git a/src/lib/components/ui/dialog/dialog-close.svelte b/src/lib/components/ui/dialog/dialog-close.svelte deleted file mode 100644 index de68f2f0..00000000 --- a/src/lib/components/ui/dialog/dialog-close.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/src/lib/components/ui/dialog/dialog-content.svelte b/src/lib/components/ui/dialog/dialog-content.svelte deleted file mode 100644 index 42bb07c5..00000000 --- a/src/lib/components/ui/dialog/dialog-content.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - {@render children?.()} - {#if showCloseButton} - - {#snippet child({ props })} - - {/snippet} - - {/if} - - diff --git a/src/lib/components/ui/dialog/dialog-description.svelte b/src/lib/components/ui/dialog/dialog-description.svelte deleted file mode 100644 index e8197975..00000000 --- a/src/lib/components/ui/dialog/dialog-description.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/dialog/dialog-footer.svelte b/src/lib/components/ui/dialog/dialog-footer.svelte deleted file mode 100644 index 1714a809..00000000 --- a/src/lib/components/ui/dialog/dialog-footer.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
    - {@render children?.()} - {#if showCloseButton} - - {#snippet child({ props })} - - {/snippet} - - {/if} -
    diff --git a/src/lib/components/ui/dialog/dialog-header.svelte b/src/lib/components/ui/dialog/dialog-header.svelte deleted file mode 100644 index 1c8f0ba9..00000000 --- a/src/lib/components/ui/dialog/dialog-header.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/dialog/dialog-overlay.svelte b/src/lib/components/ui/dialog/dialog-overlay.svelte deleted file mode 100644 index e3ad3be3..00000000 --- a/src/lib/components/ui/dialog/dialog-overlay.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/dialog/dialog-portal.svelte b/src/lib/components/ui/dialog/dialog-portal.svelte deleted file mode 100644 index ccfa79ca..00000000 --- a/src/lib/components/ui/dialog/dialog-portal.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/dialog/dialog-title.svelte b/src/lib/components/ui/dialog/dialog-title.svelte deleted file mode 100644 index 4d769a43..00000000 --- a/src/lib/components/ui/dialog/dialog-title.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/dialog/dialog-trigger.svelte b/src/lib/components/ui/dialog/dialog-trigger.svelte deleted file mode 100644 index 589ee0c3..00000000 --- a/src/lib/components/ui/dialog/dialog-trigger.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/src/lib/components/ui/dialog/dialog.svelte b/src/lib/components/ui/dialog/dialog.svelte deleted file mode 100644 index 211672c6..00000000 --- a/src/lib/components/ui/dialog/dialog.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/dialog/index.ts b/src/lib/components/ui/dialog/index.ts deleted file mode 100644 index 076cef52..00000000 --- a/src/lib/components/ui/dialog/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Root from "./dialog.svelte"; -import Portal from "./dialog-portal.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"; - -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, -}; diff --git a/src/lib/components/ui/drawer/drawer-close.svelte b/src/lib/components/ui/drawer/drawer-close.svelte deleted file mode 100644 index 95c24796..00000000 --- a/src/lib/components/ui/drawer/drawer-close.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/drawer/drawer-content.svelte b/src/lib/components/ui/drawer/drawer-content.svelte deleted file mode 100644 index ff1f5349..00000000 --- a/src/lib/components/ui/drawer/drawer-content.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - {@render children?.()} - - diff --git a/src/lib/components/ui/drawer/drawer-description.svelte b/src/lib/components/ui/drawer/drawer-description.svelte deleted file mode 100644 index ea0cbe9b..00000000 --- a/src/lib/components/ui/drawer/drawer-description.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/drawer/drawer-footer.svelte b/src/lib/components/ui/drawer/drawer-footer.svelte deleted file mode 100644 index 48479704..00000000 --- a/src/lib/components/ui/drawer/drawer-footer.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/drawer/drawer-header.svelte b/src/lib/components/ui/drawer/drawer-header.svelte deleted file mode 100644 index da89cb0a..00000000 --- a/src/lib/components/ui/drawer/drawer-header.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/drawer/drawer-nested.svelte b/src/lib/components/ui/drawer/drawer-nested.svelte deleted file mode 100644 index 834af946..00000000 --- a/src/lib/components/ui/drawer/drawer-nested.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/lib/components/ui/drawer/drawer-overlay.svelte b/src/lib/components/ui/drawer/drawer-overlay.svelte deleted file mode 100644 index 14c73c84..00000000 --- a/src/lib/components/ui/drawer/drawer-overlay.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/drawer/drawer-portal.svelte b/src/lib/components/ui/drawer/drawer-portal.svelte deleted file mode 100644 index 5a0dd740..00000000 --- a/src/lib/components/ui/drawer/drawer-portal.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/drawer/drawer-title.svelte b/src/lib/components/ui/drawer/drawer-title.svelte deleted file mode 100644 index b7891ea7..00000000 --- a/src/lib/components/ui/drawer/drawer-title.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/drawer/drawer-trigger.svelte b/src/lib/components/ui/drawer/drawer-trigger.svelte deleted file mode 100644 index f1877d8e..00000000 --- a/src/lib/components/ui/drawer/drawer-trigger.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/drawer/drawer.svelte b/src/lib/components/ui/drawer/drawer.svelte deleted file mode 100644 index 0cb57ff9..00000000 --- a/src/lib/components/ui/drawer/drawer.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/lib/components/ui/drawer/index.ts b/src/lib/components/ui/drawer/index.ts deleted file mode 100644 index 1656cac5..00000000 --- a/src/lib/components/ui/drawer/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Root from "./drawer.svelte"; -import Content from "./drawer-content.svelte"; -import Description from "./drawer-description.svelte"; -import Overlay from "./drawer-overlay.svelte"; -import Footer from "./drawer-footer.svelte"; -import Header from "./drawer-header.svelte"; -import Title from "./drawer-title.svelte"; -import NestedRoot from "./drawer-nested.svelte"; -import Close from "./drawer-close.svelte"; -import Trigger from "./drawer-trigger.svelte"; -import Portal from "./drawer-portal.svelte"; - -export { - Root, - NestedRoot, - Content, - Description, - Overlay, - Footer, - Header, - Title, - Trigger, - Portal, - Close, - - // - Root as Drawer, - NestedRoot as DrawerNestedRoot, - Content as DrawerContent, - Description as DrawerDescription, - Overlay as DrawerOverlay, - Footer as DrawerFooter, - Header as DrawerHeader, - Title as DrawerTitle, - Trigger as DrawerTrigger, - Portal as DrawerPortal, - Close as DrawerClose, -}; diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte deleted file mode 100644 index e0e19718..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte deleted file mode 100644 index d9d9db5d..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - - - {#snippet children({ checked, indeterminate })} - - {#if indeterminate} - - {:else if checked} - - {/if} - - {@render childrenProp?.()} - {/snippet} - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte deleted file mode 100644 index ee83a9bf..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte deleted file mode 100644 index 9532da06..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte deleted file mode 100644 index aca1f7bd..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte deleted file mode 100644 index 58349d3e..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte deleted file mode 100644 index 31e06a21..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte deleted file mode 100644 index 274cfef7..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte deleted file mode 100644 index 189aef40..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte deleted file mode 100644 index 05e0d532..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - - - {#snippet children({ checked })} - - {#if checked} - - {/if} - - {@render childrenProp?.({ checked })} - {/snippet} - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte deleted file mode 100644 index db1f0135..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte deleted file mode 100644 index 98bb6113..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - {@render children?.()} - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte deleted file mode 100644 index 5a6c135f..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte deleted file mode 100644 index 024bbc8f..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - {@render children?.()} - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte deleted file mode 100644 index f0445813..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte deleted file mode 100644 index cb053444..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte deleted file mode 100644 index cb4bc621..00000000 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts deleted file mode 100644 index 7850c6a3..00000000 --- a/src/lib/components/ui/dropdown-menu/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Root from "./dropdown-menu.svelte"; -import Sub from "./dropdown-menu-sub.svelte"; -import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; -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 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"; -import GroupHeading from "./dropdown-menu-group-heading.svelte"; -import Portal from "./dropdown-menu-portal.svelte"; - -export { - CheckboxGroup, - CheckboxItem, - Content, - Portal, - Root as DropdownMenu, - CheckboxGroup as DropdownMenuCheckboxGroup, - CheckboxItem as DropdownMenuCheckboxItem, - Content as DropdownMenuContent, - Portal as DropdownMenuPortal, - Group as DropdownMenuGroup, - Item as DropdownMenuItem, - 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, - GroupHeading as DropdownMenuGroupHeading, - Group, - GroupHeading, - Item, - Label, - RadioGroup, - RadioItem, - Root, - Separator, - Shortcut, - Sub, - SubContent, - SubTrigger, - Trigger, -}; diff --git a/src/lib/components/ui/empty/empty-content.svelte b/src/lib/components/ui/empty/empty-content.svelte deleted file mode 100644 index 520ac878..00000000 --- a/src/lib/components/ui/empty/empty-content.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/empty/empty-description.svelte b/src/lib/components/ui/empty/empty-description.svelte deleted file mode 100644 index e6d97cde..00000000 --- a/src/lib/components/ui/empty/empty-description.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
    a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", - className - )} - {...restProps} -> - {@render children?.()} -
    diff --git a/src/lib/components/ui/empty/empty-header.svelte b/src/lib/components/ui/empty/empty-header.svelte deleted file mode 100644 index 9085f955..00000000 --- a/src/lib/components/ui/empty/empty-header.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/empty/empty-media.svelte b/src/lib/components/ui/empty/empty-media.svelte deleted file mode 100644 index 6bc65c0f..00000000 --- a/src/lib/components/ui/empty/empty-media.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/empty/empty-title.svelte b/src/lib/components/ui/empty/empty-title.svelte deleted file mode 100644 index 3641794b..00000000 --- a/src/lib/components/ui/empty/empty-title.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/empty/empty.svelte b/src/lib/components/ui/empty/empty.svelte deleted file mode 100644 index e5f37460..00000000 --- a/src/lib/components/ui/empty/empty.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/empty/index.ts b/src/lib/components/ui/empty/index.ts deleted file mode 100644 index ae4c106e..00000000 --- a/src/lib/components/ui/empty/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Root from "./empty.svelte"; -import Header from "./empty-header.svelte"; -import Media from "./empty-media.svelte"; -import Title from "./empty-title.svelte"; -import Description from "./empty-description.svelte"; -import Content from "./empty-content.svelte"; - -export { - Root, - Header, - Media, - Title, - Description, - Content, - // - Root as Empty, - Header as EmptyHeader, - Media as EmptyMedia, - Title as EmptyTitle, - Description as EmptyDescription, - Content as EmptyContent, -}; diff --git a/src/lib/components/ui/field/field-content.svelte b/src/lib/components/ui/field/field-content.svelte deleted file mode 100644 index 694a885b..00000000 --- a/src/lib/components/ui/field/field-content.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/field/field-description.svelte b/src/lib/components/ui/field/field-description.svelte deleted file mode 100644 index 9580b775..00000000 --- a/src/lib/components/ui/field/field-description.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -

    a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", - className - )} - {...restProps} -> - {@render children?.()} -

    diff --git a/src/lib/components/ui/field/field-error.svelte b/src/lib/components/ui/field/field-error.svelte deleted file mode 100644 index 7abff78c..00000000 --- a/src/lib/components/ui/field/field-error.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -{#if hasContent} - -{/if} diff --git a/src/lib/components/ui/field/field-group.svelte b/src/lib/components/ui/field/field-group.svelte deleted file mode 100644 index eaf9a1c8..00000000 --- a/src/lib/components/ui/field/field-group.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/field/field-label.svelte b/src/lib/components/ui/field/field-label.svelte deleted file mode 100644 index 533866b8..00000000 --- a/src/lib/components/ui/field/field-label.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/src/lib/components/ui/field/field-legend.svelte b/src/lib/components/ui/field/field-legend.svelte deleted file mode 100644 index ec601fdb..00000000 --- a/src/lib/components/ui/field/field-legend.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - {@render children?.()} - diff --git a/src/lib/components/ui/field/field-separator.svelte b/src/lib/components/ui/field/field-separator.svelte deleted file mode 100644 index b4c4537d..00000000 --- a/src/lib/components/ui/field/field-separator.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - -
    - - {#if children} - - {@render children()} - - {/if} -
    diff --git a/src/lib/components/ui/field/field-set.svelte b/src/lib/components/ui/field/field-set.svelte deleted file mode 100644 index 7c621818..00000000 --- a/src/lib/components/ui/field/field-set.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col", className)} - {...restProps} -> - {@render children?.()} -
    diff --git a/src/lib/components/ui/field/field-title.svelte b/src/lib/components/ui/field/field-title.svelte deleted file mode 100644 index 3c52cf09..00000000 --- a/src/lib/components/ui/field/field-title.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/field/field.svelte b/src/lib/components/ui/field/field.svelte deleted file mode 100644 index 0c28dbd6..00000000 --- a/src/lib/components/ui/field/field.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - - - -
    - {@render children?.()} -
    diff --git a/src/lib/components/ui/field/index.ts b/src/lib/components/ui/field/index.ts deleted file mode 100644 index a644a956..00000000 --- a/src/lib/components/ui/field/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Field from "./field.svelte"; -import Set from "./field-set.svelte"; -import Legend from "./field-legend.svelte"; -import Group from "./field-group.svelte"; -import Content from "./field-content.svelte"; -import Label from "./field-label.svelte"; -import Title from "./field-title.svelte"; -import Description from "./field-description.svelte"; -import Separator from "./field-separator.svelte"; -import Error from "./field-error.svelte"; - -export { - Field, - Set, - Legend, - Group, - Content, - Label, - Title, - Description, - Separator, - Error, - // - Set as FieldSet, - Legend as FieldLegend, - Group as FieldGroup, - Content as FieldContent, - Label as FieldLabel, - Title as FieldTitle, - Description as FieldDescription, - Separator as FieldSeparator, - Error as FieldError, -}; diff --git a/src/lib/components/ui/form/form-button.svelte b/src/lib/components/ui/form/form-button.svelte deleted file mode 100644 index 48d39369..00000000 --- a/src/lib/components/ui/form/form-button.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/input-group/input-group-input.svelte b/src/lib/components/ui/input-group/input-group-input.svelte deleted file mode 100644 index 6e8a63e5..00000000 --- a/src/lib/components/ui/input-group/input-group-input.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/src/lib/components/ui/input-group/input-group-text.svelte b/src/lib/components/ui/input-group/input-group-text.svelte deleted file mode 100644 index 98e00d58..00000000 --- a/src/lib/components/ui/input-group/input-group-text.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - {@render children?.()} - diff --git a/src/lib/components/ui/input-group/input-group-textarea.svelte b/src/lib/components/ui/input-group/input-group-textarea.svelte deleted file mode 100644 index 9f8c53e7..00000000 --- a/src/lib/components/ui/input-group/input-group-textarea.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/src/lib/components/ui/toggle-group/index.ts b/src/lib/components/ui/toggle-group/index.ts deleted file mode 100644 index 12b14b96..00000000 --- a/src/lib/components/ui/toggle-group/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Root from "./toggle-group.svelte"; -import Item from "./toggle-group-item.svelte"; - -export { - Root, - Item, - // - Root as ToggleGroup, - Item as ToggleGroupItem, -}; diff --git a/src/lib/components/ui/toggle-group/toggle-group-item.svelte b/src/lib/components/ui/toggle-group/toggle-group-item.svelte deleted file mode 100644 index 263ed688..00000000 --- a/src/lib/components/ui/toggle-group/toggle-group-item.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/src/lib/components/ui/toggle-group/toggle-group.svelte b/src/lib/components/ui/toggle-group/toggle-group.svelte deleted file mode 100644 index be614494..00000000 --- a/src/lib/components/ui/toggle-group/toggle-group.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - diff --git a/src/lib/components/ui/toggle/index.ts b/src/lib/components/ui/toggle/index.ts deleted file mode 100644 index 8cb2936f..00000000 --- a/src/lib/components/ui/toggle/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Root from "./toggle.svelte"; -export { - toggleVariants, - type ToggleSize, - type ToggleVariant, - type ToggleVariants, -} from "./toggle.svelte"; - -export { - Root, - // - Root as Toggle, -}; diff --git a/src/lib/components/ui/toggle/toggle.svelte b/src/lib/components/ui/toggle/toggle.svelte deleted file mode 100644 index 44fc7059..00000000 --- a/src/lib/components/ui/toggle/toggle.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/src/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts deleted file mode 100644 index 17186042..00000000 --- a/src/lib/components/ui/tooltip/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Root from "./tooltip.svelte"; -import Trigger from "./tooltip-trigger.svelte"; -import Content from "./tooltip-content.svelte"; -import Provider from "./tooltip-provider.svelte"; -import Portal from "./tooltip-portal.svelte"; - -export { - Root, - Trigger, - Content, - Provider, - Portal, - // - Root as Tooltip, - Content as TooltipContent, - Trigger as TooltipTrigger, - Provider as TooltipProvider, - Portal as TooltipPortal, -}; diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte deleted file mode 100644 index 993742e9..00000000 --- a/src/lib/components/ui/tooltip/tooltip-content.svelte +++ /dev/null @@ -1,52 +0,0 @@ - - - - - {@render children?.()} - - {#snippet child({ props })} -
    - {/snippet} -
    -
    -
    diff --git a/src/lib/components/ui/tooltip/tooltip-portal.svelte b/src/lib/components/ui/tooltip/tooltip-portal.svelte deleted file mode 100644 index d234f7d7..00000000 --- a/src/lib/components/ui/tooltip/tooltip-portal.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/tooltip/tooltip-provider.svelte b/src/lib/components/ui/tooltip/tooltip-provider.svelte deleted file mode 100644 index 6dba9a67..00000000 --- a/src/lib/components/ui/tooltip/tooltip-provider.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/src/lib/components/ui/tooltip/tooltip-trigger.svelte deleted file mode 100644 index c4b21fc7..00000000 --- a/src/lib/components/ui/tooltip/tooltip-trigger.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/ui/tooltip/tooltip.svelte b/src/lib/components/ui/tooltip/tooltip.svelte deleted file mode 100644 index 03c9a3d9..00000000 --- a/src/lib/components/ui/tooltip/tooltip.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/lib/components/utils/MultiPauseToggle.svelte b/src/lib/components/utils/MultiPauseToggle.svelte index 4ef40694..6baa3211 100644 --- a/src/lib/components/utils/MultiPauseToggle.svelte +++ b/src/lib/components/utils/MultiPauseToggle.svelte @@ -2,8 +2,8 @@ import { shockerPauseShocker, shockerShockerShareCodePause } from '$lib/api'; import type { BooleanLegacyDataResponse } from '$lib/api'; import { Asterisk, Pause, Play } from '@lucide/svelte'; - import { Button } from '$lib/components/ui/button'; - import { Spinner } from '$lib/components/ui/spinner'; + import { Button } from '@openshock/svelte-core/ui/button'; + import { Spinner } from '@openshock/svelte-core/ui/spinner'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; interface Props { diff --git a/src/lib/components/utils/PauseToggle.svelte b/src/lib/components/utils/PauseToggle.svelte index 19415d30..78f50ce9 100644 --- a/src/lib/components/utils/PauseToggle.svelte +++ b/src/lib/components/utils/PauseToggle.svelte @@ -2,8 +2,8 @@ import { shockerPauseShocker, shockerShockerShareCodePause } from '$lib/api'; import type { BooleanLegacyDataResponse } from '$lib/api'; import { Pause, Play } from '@lucide/svelte'; - import { Button } from '$lib/components/ui/button'; - import { Spinner } from '$lib/components/ui/spinner'; + import { Button } from '@openshock/svelte-core/ui/button'; + import { Spinner } from '@openshock/svelte-core/ui/spinner'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; interface Props { diff --git a/src/lib/errorhandling/ProblemDetails.ts b/src/lib/errorhandling/ProblemDetails.ts index 11d19c0d..dc36d485 100644 --- a/src/lib/errorhandling/ProblemDetails.ts +++ b/src/lib/errorhandling/ProblemDetails.ts @@ -1,5 +1,4 @@ -import { isObject, isString } from '$lib/typeguards'; -import { HasNumber, HasString } from '$lib/typeguards/propGuards'; +import { HasNumber, HasString, isObject, isString } from '@openshock/svelte-core/typeguards'; export interface ProblemDetails { type: string; diff --git a/src/lib/errorhandling/ValidationProblemDetails.ts b/src/lib/errorhandling/ValidationProblemDetails.ts index 37b1b38f..31f1b338 100644 --- a/src/lib/errorhandling/ValidationProblemDetails.ts +++ b/src/lib/errorhandling/ValidationProblemDetails.ts @@ -1,5 +1,5 @@ -import { isObject } from '$lib/typeguards'; -import type { ValidationResult } from '$lib/types/ValidationResult'; +import { isObject } from '@openshock/svelte-core/typeguards'; +import type { ValidationResult } from '@openshock/svelte-core/types/ValidationResult'; import type { ProblemDetails } from './ProblemDetails'; export interface ValidationProblemDetails extends ProblemDetails { diff --git a/src/lib/hooks/is-mobile.svelte.ts b/src/lib/hooks/is-mobile.svelte.ts deleted file mode 100644 index e3802d38..00000000 --- a/src/lib/hooks/is-mobile.svelte.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { MediaQuery } from 'svelte/reactivity'; - -const DEFAULT_MOBILE_BREAKPOINT = 768; - -export class IsMobile extends MediaQuery { - constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { - super(`max-width: ${breakpoint - 1}px`); - } -} diff --git a/src/lib/inputvalidation/emailValidator.test.ts b/src/lib/inputvalidation/emailValidator.test.ts deleted file mode 100644 index f5c9ba16..00000000 --- a/src/lib/inputvalidation/emailValidator.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { isEmailAddress, validateEmail } from './emailValidator'; - -describe('isEmailAddress', () => { - it('returns false for empty string', () => { - expect(isEmailAddress('')).toBe(false); - }); - - it('returns false when missing "@" or has multiple "@"', () => { - expect(isEmailAddress('plainaddress')).toBe(false); - expect(isEmailAddress('a@b@c.com')).toBe(false); - }); - - it('returns false when total length exceeds 320 characters', () => { - const localPart = 'a'.repeat(64); - const domainLabel = 'b'.repeat(63); - const domain = Array(5).fill(domainLabel).join('.'); // 5×63 + 4 dots = 319 - const validEmail = `${localPart}@${domain}`; // 384 chars total - expect(validEmail.length).toBe(64 + 1 + 319); - expect(isEmailAddress(validEmail)).toBe(false); - }); - - it('returns false when local part exceeds 64 characters', () => { - const account = 'x'.repeat(65); - const address = 'domain.com'; - expect(isEmailAddress(`${account}@${address}`)).toBe(false); - }); - - it('returns false when domain part exceeds 255 characters', () => { - const account = 'user'; - const longLabel = 'd'.repeat(63); - // 4 labels of 63 chars + 3 dots = 4*63 + 3 = 255 - const domain = Array(4).fill(longLabel).join('.'); - expect(domain.length).toBe(255); - // Prepend one character to push domain over 255 - const tooLongDomain = 'e' + domain; - expect(isEmailAddress(`${account}@${tooLongDomain}`)).toBe(false); - }); - - it('returns false when any domain label exceeds 63 characters', () => { - const account = 'user'; - const longLabel = 'd'.repeat(64); - const address = `${longLabel}.com`; - expect(isEmailAddress(`${account}@${address}`)).toBe(false); - }); - - it('returns false for invalid characters or formats not matching regex', () => { - // starts with a dot - expect(isEmailAddress('.user@domain.com')).toBe(false); - // consecutive dots in local part - expect(isEmailAddress('user..name@domain.com')).toBe(false); - // hyphen at start of domain label - expect(isEmailAddress('user@-domain.com')).toBe(false); - // no TLD - expect(isEmailAddress('user@domain')).toBe(false); - }); - - it('returns true for valid standard email addresses', () => { - expect(isEmailAddress('simple@example.com')).toBe(true); - expect(isEmailAddress('very.common@example.co.uk')).toBe(true); - expect(isEmailAddress("o'reilly@example.io")).toBe(true); - expect(isEmailAddress('user_name-123@sub.domain.com')).toBe(true); - }); -}); - -describe('validateEmail', () => { - it('returns null for empty input', () => { - const result = validateEmail(''); - expect(result).toBeNull(); - }); - - it('returns valid: false and message "Invalid email" for syntactically invalid addresses', () => { - const invalids = [ - 'noatsign.com', - 'user@.com', - 'user@domain..com', - 'user@domain', - 'toolong' + 'a'.repeat(320) + '@example.com', - ]; - for (const addr of invalids) { - const result = validateEmail(addr); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Invalid email'); - } - }); - - it('returns valid: false and message "Email cannot contain aliases" when email contains "+"', () => { - const withAlias = 'user+alias@example.com'; - const result = validateEmail(withAlias); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Email cannot contain aliases'); - }); - - it('returns valid: true for valid email without "+"', () => { - const valid = 'firstname.lastname@domain.com'; - const result = validateEmail(valid); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(true); - expect(result).not.toHaveProperty('message'); - }); - - it('treats "+" anywhere in the local part as invalid alias', () => { - const examples = [ - 'plus+sign@domain.com', - '+start@domain.com', - 'end+@domain.com', - 'middle+more@sub.domain.org', - ]; - for (const addr of examples) { - const result = validateEmail(addr); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Email cannot contain aliases'); - } - }); - - it('does not reject valid emails with hyphens in domain or underscores in local part', () => { - const examples = [ - 'user_name@example-domain.com', // underscore in local, hyphen in domain - 'another.user-name@sub.domain.com', // hyphen in both portions - ]; - for (const addr of examples) { - const result = validateEmail(addr); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(true); - } - }); -}); - -describe('isEmailAddress boundary cases', () => { - it('accepts a 320-char address at the upper boundary', () => { - // 4 × 63-char alpha labels + 3 dots = 255-char domain (max). - // 64-char local + '@' + 255-char domain = 320 (max total). - const local = 'b'.repeat(64); - const label = 'a'.repeat(63); - const domain = [label, label, label, label].join('.'); - const addr = `${local}@${domain}`; - expect(domain.length).toBe(255); - expect(addr.length).toBe(320); - expect(isEmailAddress(addr)).toBe(true); - }); - - it('returns false for empty local part', () => { - expect(isEmailAddress('@example.com')).toBe(false); - }); - - it('returns false for empty domain part', () => { - expect(isEmailAddress('user@')).toBe(false); - }); - - it('returns false for "@" only', () => { - expect(isEmailAddress('@')).toBe(false); - }); - - it('returns false for trailing whitespace (regex anchors enforce this)', () => { - expect(isEmailAddress('user@example.com ')).toBe(false); - expect(isEmailAddress(' user@example.com')).toBe(false); - }); - - it('returns false when domain TLD is numeric', () => { - expect(isEmailAddress('user@example.123')).toBe(false); - }); - - it('returns false for a hyphen at the start of any domain label', () => { - expect(isEmailAddress('user@sub.-example.com')).toBe(false); - }); - - it('returns true at the exact local-part length of 64', () => { - const local = 'a'.repeat(64); - expect(isEmailAddress(`${local}@example.com`)).toBe(true); - }); -}); - -describe('validateEmail boundary cases', () => { - it('rejects "+" at the very start of local part as alias', () => { - const result = validateEmail('+plus@example.com'); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Email cannot contain aliases'); - }); - - it('rejects "+" at the end of local part as alias', () => { - const result = validateEmail('plus+@example.com'); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Email cannot contain aliases'); - }); - - it('rejects multiple "+" signs as alias', () => { - const result = validateEmail('a+b+c@example.com'); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Email cannot contain aliases'); - }); - - it('treats whitespace-only input as empty (returns null)', () => { - // value.length is 0 only for ''; non-empty whitespace falls through to invalid - expect(validateEmail('')).toBeNull(); - expect(validateEmail(' ')?.valid).toBe(false); - }); -}); diff --git a/src/lib/inputvalidation/emailValidator.ts b/src/lib/inputvalidation/emailValidator.ts deleted file mode 100644 index deebd9ec..00000000 --- a/src/lib/inputvalidation/emailValidator.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ValidationResult } from '$lib/types/ValidationResult'; - -const emailRegex = - /^[\w-!#$%&'*+/=?^`{|}~](\.?[\w-!#$%&'*+/=?^`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; - -export function isEmailAddress(value: string): boolean { - if (!value) return false; - if (value.length > 320) return false; - - const emailParts = value.split('@'); - - if (emailParts.length !== 2) return false; - - const [account, address] = emailParts; - - if (account.length > 64) return false; - if (address.length > 255) return false; - - const domainParts = address.split('.'); - - if (domainParts.some((part) => part.length > 63)) return false; - - return emailRegex.test(value); -} - -export function validateEmail(value: string): ValidationResult | null { - if (value.length == 0) { - return null; - } - - if (!isEmailAddress(value)) { - return { - valid: false, - message: 'Invalid email', - }; - } - - if (value.includes('+')) { - return { - valid: false, - message: 'Email cannot contain aliases', - }; - } - - return { valid: true }; -} diff --git a/src/lib/inputvalidation/passwordValidator.test.ts b/src/lib/inputvalidation/passwordValidator.test.ts deleted file mode 100644 index fe7126ac..00000000 --- a/src/lib/inputvalidation/passwordValidator.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getPasswordStrength, validatePassword, validatePasswordMatch } from './passwordValidator'; - -describe('validatePassword', () => { - it('returns null for empty password', () => { - const result = validatePassword(''); - expect(result).toBeNull(); - }); - - it('rejects passwords shorter than 12 characters', () => { - const result = validatePassword('shortPwd1!'); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Password is too short'); - }); - - it('rejects passwords longer than 256 characters', () => { - const longPwd = 'a'.repeat(257); - const result = validatePassword(longPwd); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(false); - expect(result?.message).toBe(`Seriously? ${longPwd.length} characters? That's too much`); - }); - - it('rejects passwords consisting only of whitespaces', () => { - const spaces = ' \r \n \t \r \n '; - const result = validatePassword(spaces); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Password cannot consist of only whitespaces'); - }); - - it('rejects passwords that start or end with whitespace', () => { - const leading = ' validPassword123'; - const trailing = 'validPassword123 '; - const both = ' validPassword123 '; - for (const pwd of [leading, trailing, both]) { - const result = validatePassword(pwd); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Password cannot start or end with whitespace'); - } - }); - - it('accepts valid passwords of acceptable length and no leading/trailing whitespace', () => { - const valid = 'ValidPassword123'; - const result = validatePassword(valid); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(true); - expect(result).not.toHaveProperty('message'); - }); -}); - -describe('validatePasswordMatch', () => { - it('returns null when original password is empty', () => { - const result = validatePasswordMatch('', 'anything'); - expect(result).toBeNull(); - }); - - it('rejects non-matching passwords', () => { - const result = validatePasswordMatch('password123', 'password124'); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Passwords do not match'); - }); - - it('accepts matching passwords', () => { - const result = validatePasswordMatch('password123', 'password123'); - expect(result).not.toBeNull(); - expect(result?.valid).toBe(true); - expect(result).not.toHaveProperty('message'); - }); - - it('rejects empty confirmation against non-empty password', () => { - const result = validatePasswordMatch('password123', ''); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Passwords do not match'); - }); - - it('treats unicode-equivalent strings as a literal mismatch (no NFC normalization)', () => { - // Composed "é" vs decomposed "e" + combining acute - const composed = 'pässword1234'; - const decomposed = 'pässword1234'; // combining diaeresis - expect(composed).not.toEqual(decomposed); - const result = validatePasswordMatch(composed, decomposed); - expect(result?.valid).toBe(false); - }); -}); - -describe('validatePassword boundary cases', () => { - it('accepts exactly 12 characters (lower boundary)', () => { - const pwd = 'a'.repeat(12); - const result = validatePassword(pwd); - expect(result?.valid).toBe(true); - }); - - it('rejects exactly 11 characters (just below)', () => { - const result = validatePassword('a'.repeat(11)); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Password is too short'); - }); - - it('accepts exactly 256 characters (upper boundary)', () => { - const result = validatePassword('a'.repeat(256)); - expect(result?.valid).toBe(true); - }); - - it('rejects exactly 257 characters (just above)', () => { - const result = validatePassword('a'.repeat(257)); - expect(result?.valid).toBe(false); - expect(result?.message).toMatch(/Seriously\?/); - }); - - it('accepts internal whitespace within otherwise valid password', () => { - const result = validatePassword('a strong pass!'); - expect(result?.valid).toBe(true); - }); - - it('rejects tab-only passwords as whitespace-only', () => { - const result = validatePassword('\t'.repeat(12)); - expect(result?.valid).toBe(false); - expect(result?.message).toBe('Password cannot consist of only whitespaces'); - }); -}); - -describe('getPasswordStrength', () => { - it('returns None/gray/0% for empty string', () => { - const { percent, text, color } = getPasswordStrength(''); - expect(percent).toBe(0); - expect(text).toBe('None'); - expect(color).toBe('gray'); - }); - - it('rates a repeated single-character password as Very weak (red)', () => { - // "aaaaaaaaaaaa" (12×'a'): uniqueLength=1 ⇒ entropy=0 ⇒ percent=0 ⇒ <30 - const pwd = 'a'.repeat(12); - const { percent, text, color } = getPasswordStrength(pwd); - expect(percent).toBe(0); - expect(text).toBe('Very weak'); - expect(color).toBe('red'); - }); - - it('rates a moderately varied 12-character password as Weak (orange)', () => { - // "abcABC123!@#": length=12, uniqueLength=12 ⇒ entropy≈43 ⇒ percent≈35.8 ⇒ <50 - const pwd = 'abcABC123!@#'; - const { percent, text, color } = getPasswordStrength(pwd); - expect(percent).toBeGreaterThanOrEqual(30); - expect(percent).toBeLessThan(50); - expect(text).toBe('Weak'); - expect(color).toBe('orange'); - }); - - it('rates a 60-character password with two unique chars as Fair (yellow)', () => { - // 60×(two-character alphabet) ⇒ entropy=60*1=60 ⇒ percent=50 ⇒ <60 - const pwd = 'ab'.repeat(30); - const { percent, text, color } = getPasswordStrength(pwd); - expect(percent).toBeCloseTo(50, 1); - expect(text).toBe('Fair'); - expect(color).toBe('yellow'); - }); - - it('rates a 72-character password with two unique chars as Strong (green)', () => { - // 72×(two-character alphabet) ⇒ entropy=72 ⇒ percent=60 ⇒ <99 - const pwd = 'ab'.repeat(36); - const { percent, text, color } = getPasswordStrength(pwd); - expect(percent).toBeCloseTo(60, 1); - expect(text).toBe('Strong'); - expect(color).toBe('green'); - }); - - it('caps strength at 100% and rates as Very strong (cyan) for high entropy', () => { - // 100 random characters - const randomBytes = new Uint8Array(100); - crypto.getRandomValues(randomBytes); - const pwd = Array.from(randomBytes, (b) => String.fromCharCode((b % 94) + 33)).join(''); - - const { percent, text, color } = getPasswordStrength(pwd); - expect(percent).toBe(100); - expect(text).toBe('Very strong'); - expect(color).toBe('cyan'); - }); -}); diff --git a/src/lib/inputvalidation/passwordValidator.ts b/src/lib/inputvalidation/passwordValidator.ts deleted file mode 100644 index 9040d449..00000000 --- a/src/lib/inputvalidation/passwordValidator.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { TwTextColor } from '$lib/types/Tailwind'; -import type { ValidationResult } from '$lib/types/ValidationResult'; -import { calculateStringEntropy } from '$lib/utils/entropy'; - -export function validatePassword(value: string): ValidationResult | null { - if (value.length == 0) { - return null; - } - - const result = { - valid: false, - message: '', - }; - - if (value.length < 12) { - result.message = 'Password is too short'; - return result; - } - - if (value.length > 256) { - result.message = `Seriously? ${value.length} characters? That's too much`; - return result; - } - - const trimmedLength = value.trim().length; - if (trimmedLength == 0) { - result.message = 'Password cannot consist of only whitespaces'; - return result; - } - - if (trimmedLength != value.length) { - result.message = 'Password cannot start or end with whitespace'; - return result; - } - - return { valid: true }; -} - -export function validatePasswordMatch( - password: string, - passwordConfirmation: string -): ValidationResult | null { - if (password.length == 0) { - return null; - } - - if (password != passwordConfirmation) { - return { - valid: false, - message: 'Passwords do not match', - }; - } - - return { valid: true }; -} - -export function getPasswordStrength(value: string): { - percent: number; - text: string; - color: TwTextColor; -} { - let percent: number; - let text: string; - let color: TwTextColor; - - if (value.length === 0) { - percent = 0; - text = 'None'; - color = 'gray'; - } else { - percent = Math.min((calculateStringEntropy(value) / 120) * 100, 100); - - if (percent < 30) { - text = 'Very weak'; - color = 'red'; - } else if (percent < 50) { - text = 'Weak'; - color = 'orange'; - } else if (percent < 60) { - text = 'Fair'; - color = 'yellow'; - } else if (percent < 99) { - text = 'Strong'; - color = 'green'; - } else { - text = 'Very strong'; - color = 'cyan'; - } - } - - return { percent, text, color }; -} diff --git a/src/lib/inputvalidation/usernameValidator.ts b/src/lib/inputvalidation/usernameValidator.ts index 3dd4ed1a..34cedec2 100644 --- a/src/lib/inputvalidation/usernameValidator.ts +++ b/src/lib/inputvalidation/usernameValidator.ts @@ -1,7 +1,7 @@ import type { UsernameCheckResponse } from '$lib/api'; import { UsernameAvailability } from '$lib/api'; -import { isEmailAddress } from '$lib/inputvalidation/emailValidator'; -import type { ValidationResult } from '$lib/types/ValidationResult'; +import { isEmailAddress } from '@openshock/svelte-core/inputvalidation/emailValidator'; +import type { ValidationResult } from '@openshock/svelte-core/types/ValidationResult'; const MultipleWhiteSpaceRegex = /\s{2,}/; diff --git a/src/lib/signalr/handlers/DeviceUpdate.ts b/src/lib/signalr/handlers/DeviceUpdate.ts index 51a87648..346d0ead 100644 --- a/src/lib/signalr/handlers/DeviceUpdate.ts +++ b/src/lib/signalr/handlers/DeviceUpdate.ts @@ -1,6 +1,6 @@ import { isHubUpdateType } from '$lib/signalr/models/HubUpdateType'; import { refreshOwnHubs } from '$lib/state/hubs-state.svelte'; -import { isString } from '$lib/typeguards'; +import { isString } from '@openshock/svelte-core/typeguards'; import { toast } from 'svelte-sonner'; export function handleSignalrDeviceUpdate(deviceId: unknown, updateType: unknown) { diff --git a/src/lib/signalr/handlers/OtaInstallFailed.ts b/src/lib/signalr/handlers/OtaInstallFailed.ts index eda2fcc8..6fde451e 100644 --- a/src/lib/signalr/handlers/OtaInstallFailed.ts +++ b/src/lib/signalr/handlers/OtaInstallFailed.ts @@ -1,5 +1,5 @@ import { onlineHubs } from '$lib/state/hubs-state.svelte'; -import { isBoolean, isNumber, isString } from '$lib/typeguards'; +import { isBoolean, isNumber, isString } from '@openshock/svelte-core/typeguards'; import { toast } from 'svelte-sonner'; /** diff --git a/src/lib/signalr/handlers/OtaInstallProgress.ts b/src/lib/signalr/handlers/OtaInstallProgress.ts index f1dbac7e..d4c7db01 100644 --- a/src/lib/signalr/handlers/OtaInstallProgress.ts +++ b/src/lib/signalr/handlers/OtaInstallProgress.ts @@ -1,6 +1,6 @@ import { isOtaUpdateProgressTask } from '$lib/signalr/models/OtaUpdateProgressTask'; import { onlineHubs } from '$lib/state/hubs-state.svelte'; -import { isNumber, isString } from '$lib/typeguards'; +import { isNumber, isString } from '@openshock/svelte-core/typeguards'; import { toast } from 'svelte-sonner'; /** diff --git a/src/lib/signalr/handlers/OtaInstallStarted.ts b/src/lib/signalr/handlers/OtaInstallStarted.ts index 3a7d72b0..60291379 100644 --- a/src/lib/signalr/handlers/OtaInstallStarted.ts +++ b/src/lib/signalr/handlers/OtaInstallStarted.ts @@ -1,5 +1,5 @@ import { onlineHubs } from '$lib/state/hubs-state.svelte'; -import { isNumber, isString } from '$lib/typeguards'; +import { isNumber, isString } from '@openshock/svelte-core/typeguards'; import { toast } from 'svelte-sonner'; /** diff --git a/src/lib/signalr/handlers/OtaInstallSucceeded.ts b/src/lib/signalr/handlers/OtaInstallSucceeded.ts index 81cb1e2e..4569479c 100644 --- a/src/lib/signalr/handlers/OtaInstallSucceeded.ts +++ b/src/lib/signalr/handlers/OtaInstallSucceeded.ts @@ -1,5 +1,5 @@ import { onlineHubs } from '$lib/state/hubs-state.svelte'; -import { isNumber, isString } from '$lib/typeguards'; +import { isNumber, isString } from '@openshock/svelte-core/typeguards'; import { toast } from 'svelte-sonner'; /** diff --git a/src/lib/signalr/handlers/OtaRollback.ts b/src/lib/signalr/handlers/OtaRollback.ts index de678c79..5a3c0b14 100644 --- a/src/lib/signalr/handlers/OtaRollback.ts +++ b/src/lib/signalr/handlers/OtaRollback.ts @@ -1,5 +1,5 @@ import { onlineHubs } from '$lib/state/hubs-state.svelte'; -import { isNumber, isString } from '$lib/typeguards'; +import { isNumber, isString } from '@openshock/svelte-core/typeguards'; import { toast } from 'svelte-sonner'; /** diff --git a/src/lib/signalr/models/ControlLog.ts b/src/lib/signalr/models/ControlLog.ts index 71cd7a84..3ccb4ec8 100644 --- a/src/lib/signalr/models/ControlLog.ts +++ b/src/lib/signalr/models/ControlLog.ts @@ -1,5 +1,4 @@ -import { isObject } from '$lib/typeguards'; -import { HasNumber, HasString } from '$lib/typeguards/propGuards'; +import { HasNumber, HasString, isObject } from '@openshock/svelte-core/typeguards'; import { ControlType, isControlType } from './ControlType'; interface ControlLogShockerInfo { diff --git a/src/lib/signalr/models/ControlLogSender.ts b/src/lib/signalr/models/ControlLogSender.ts index 105f9fd2..a165cfc5 100644 --- a/src/lib/signalr/models/ControlLogSender.ts +++ b/src/lib/signalr/models/ControlLogSender.ts @@ -1,5 +1,4 @@ -import { isObject } from '$lib/typeguards'; -import { HasObject, HasString, HasStringOrNull } from '$lib/typeguards/propGuards'; +import { HasObject, HasString, HasStringOrNull, isObject } from '@openshock/svelte-core/typeguards'; export interface ControlLogSender { connectionId: string; diff --git a/src/lib/signalr/models/ControlType.ts b/src/lib/signalr/models/ControlType.ts index d6c5aa7b..57158303 100644 --- a/src/lib/signalr/models/ControlType.ts +++ b/src/lib/signalr/models/ControlType.ts @@ -1,4 +1,4 @@ -import { isNumber } from '$lib/typeguards'; +import { isNumber } from '@openshock/svelte-core/typeguards'; export enum ControlType { Stop = 0, diff --git a/src/lib/signalr/models/DeviceOnlineState.ts b/src/lib/signalr/models/DeviceOnlineState.ts index 0cb5915e..6d45aa02 100644 --- a/src/lib/signalr/models/DeviceOnlineState.ts +++ b/src/lib/signalr/models/DeviceOnlineState.ts @@ -1,5 +1,9 @@ -import { isObject } from '$lib/typeguards'; -import { HasBoolean, HasString, HasStringOrNull } from '$lib/typeguards/propGuards'; +import { + HasBoolean, + HasString, + HasStringOrNull, + isObject, +} from '@openshock/svelte-core/typeguards'; export interface DeviceOnlineState { device: string; diff --git a/src/lib/signalr/models/HubUpdateType.ts b/src/lib/signalr/models/HubUpdateType.ts index 92d2eb63..352a6149 100644 --- a/src/lib/signalr/models/HubUpdateType.ts +++ b/src/lib/signalr/models/HubUpdateType.ts @@ -1,4 +1,4 @@ -import { isNumber } from '$lib/typeguards'; +import { isNumber } from '@openshock/svelte-core/typeguards'; export enum HubUpdateType { HubCreated = 0, diff --git a/src/lib/signalr/models/OtaUpdateProgressTask.ts b/src/lib/signalr/models/OtaUpdateProgressTask.ts index bf528193..570aab9c 100644 --- a/src/lib/signalr/models/OtaUpdateProgressTask.ts +++ b/src/lib/signalr/models/OtaUpdateProgressTask.ts @@ -1,4 +1,4 @@ -import { isNumber } from '$lib/typeguards'; +import { isNumber } from '@openshock/svelte-core/typeguards'; export enum OtaUpdateProgressTask { FetchingMetadata = 0, diff --git a/src/lib/state/auth-state.svelte.ts b/src/lib/state/auth-state.svelte.ts index 061f6944..a3741ba7 100644 --- a/src/lib/state/auth-state.svelte.ts +++ b/src/lib/state/auth-state.svelte.ts @@ -1,4 +1,3 @@ -import { dialog } from '$lib/components/dialog-manager/dialog-store.svelte'; import { registerOnUnauthorized } from '$lib/errorhandling/apiErrorHandling'; import { destroySignalR, initializeSignalR } from '$lib/signalr/user.svelte'; import { @@ -8,6 +7,7 @@ import { markTourCompleted, startWelcomeTour, } from '$lib/tour/welcome-tour'; +import { dialog } from '@openshock/svelte-core/components/dialog-manager'; import { userState } from './user-state.svelte'; export const AuthStatus = { diff --git a/src/lib/state/classes/persisted-state.svelte.ts b/src/lib/state/classes/persisted-state.svelte.ts deleted file mode 100644 index 1c56f1a6..00000000 --- a/src/lib/state/classes/persisted-state.svelte.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { browser } from '$app/environment'; -import { onDestroy } from 'svelte'; - -export type StorageType = 'local' | 'session'; - -export interface LocalStorageStateOptions { - /** Which Web Storage area to persist to. Defaults to `'local'`. */ - storage?: StorageType; -} - -const mockStorage: Storage = { - length: 0, - clear: () => {}, - getItem: () => null, - key: () => null, - removeItem: () => {}, - setItem: () => {}, -}; - -export class PersistedState { - readonly #key: string; - readonly defaultValue: T; - readonly #storage: Storage; - #value: T; - - constructor(key: string, defaultValue: T, options: LocalStorageStateOptions = {}) { - this.#key = key; - this.defaultValue = defaultValue; - this.#storage = browser - ? options.storage === 'session' - ? sessionStorage - : localStorage - : mockStorage; - - this.#value = $state(this.deserialize(this.#storage.getItem(key))); - - // Only localStorage fires storage events across tabs; sessionStorage is tab-scoped. - if (browser && options.storage !== 'session') { - window.addEventListener('storage', this.#onStorage); - } - } - - /** Detach the cross-tab `storage` listener. No-op if none was attached. */ - dispose() { - if (browser && this.#storage === localStorage) { - window.removeEventListener('storage', this.#onStorage); - } - } - - #onStorage = (event: StorageEvent) => { - if (event.storageArea !== this.#storage) return; - if (event.key !== this.#key) return; - this.#update(this.deserialize(event.newValue)); - }; - - #update(value: T) { - this.#value = value; - this.onChange(value); - } - - get value() { - return this.#value; - } - set value(value: T) { - this.#update(value); - this.#storage.setItem(this.#key, this.serialize(value)); - } - - reset() { - this.#update(this.defaultValue); - this.#storage.removeItem(this.#key); - } - - /** Hook called whenever the value changes, regardless of source (setter, reset, or storage event). */ - protected onChange(_value: T): void {} - - protected serialize(value: T): string { - return JSON.stringify(value); - } - - protected deserialize(raw: string | null): T { - if (raw === null) return this.defaultValue; - return JSON.parse(raw) as T; - } -} - -/** Component-aware `PersistedState` that detaches the storage listener on unmount. */ -export function usePersistedState( - key: string, - defaultValue: T, - options?: LocalStorageStateOptions -): PersistedState { - const state = new PersistedState(key, defaultValue, options); - onDestroy(() => state.dispose()); - return state; -} diff --git a/src/lib/state/classes/persisted-state.test.svelte.ts b/src/lib/state/classes/persisted-state.test.svelte.ts deleted file mode 100644 index 16d184b6..00000000 --- a/src/lib/state/classes/persisted-state.test.svelte.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -vi.mock('$app/environment', () => ({ browser: true })); - -const { PersistedState } = await import('./persisted-state.svelte'); - -class TrackingState extends PersistedState { - changes: T[] = []; - protected override onChange(value: T): void { - this.changes.push(value); - } -} - -class StringState extends PersistedState { - protected override serialize(value: string): string { - return value; - } - protected override deserialize(raw: string | null): string { - if (raw === null) return this.defaultValue; - return raw; - } -} - -class NumberState extends PersistedState { - protected override serialize(value: number): string { - return String(value); - } - protected override deserialize(raw: string | null): number { - if (raw === null) return this.defaultValue; - return Number(raw); - } -} - -describe('PersistedState', () => { - beforeEach(() => { - localStorage.clear(); - sessionStorage.clear(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('default value', () => { - it('returns the default when storage key is absent', () => { - const state = new PersistedState('absent', 'default-value'); - expect(state.value).toBe('default-value'); - state.dispose(); - }); - - it('exposes the default value via the defaultValue property', () => { - const state = new PersistedState('absent2', 42); - expect(state.defaultValue).toBe(42); - state.dispose(); - }); - }); - - describe('reading existing storage', () => { - it('reads existing serialized value from localStorage', () => { - localStorage.setItem('existing', JSON.stringify({ a: 1, b: 'two' })); - const state = new PersistedState<{ a: number; b: string }>('existing', { a: 0, b: '' }); - expect(state.value).toEqual({ a: 1, b: 'two' }); - state.dispose(); - }); - - it('reads existing serialized value from sessionStorage', () => { - sessionStorage.setItem('session-existing', JSON.stringify(99)); - const state = new PersistedState('session-existing', 0, { storage: 'session' }); - expect(state.value).toBe(99); - state.dispose(); - }); - }); - - describe('writing values', () => { - it('persists to localStorage and updates the value getter', () => { - const state = new PersistedState('counter', 0); - state.value = 5; - expect(state.value).toBe(5); - expect(localStorage.getItem('counter')).toBe(JSON.stringify(5)); - state.dispose(); - }); - - it('persists to sessionStorage when configured', () => { - const state = new PersistedState('s-counter', 'a', { storage: 'session' }); - state.value = 'b'; - expect(state.value).toBe('b'); - expect(sessionStorage.getItem('s-counter')).toBe(JSON.stringify('b')); - expect(localStorage.getItem('s-counter')).toBeNull(); - state.dispose(); - }); - - it('serializes complex objects via JSON', () => { - const state = new PersistedState<{ items: number[] }>('complex', { items: [] }); - state.value = { items: [1, 2, 3] }; - expect(localStorage.getItem('complex')).toBe(JSON.stringify({ items: [1, 2, 3] })); - state.dispose(); - }); - }); - - describe('reset()', () => { - it('removes the storage entry and restores the default', () => { - const state = new PersistedState('to-reset', 'default'); - state.value = 'changed'; - expect(localStorage.getItem('to-reset')).not.toBeNull(); - - state.reset(); - expect(state.value).toBe('default'); - expect(localStorage.getItem('to-reset')).toBeNull(); - state.dispose(); - }); - }); - - describe('JSON parse error handling', () => { - it('throws when stored value is not valid JSON', () => { - localStorage.setItem('bad-json', 'not-json{'); - expect(() => new PersistedState('bad-json', 'default')).toThrow(); - }); - }); - - describe('dispose()', () => { - it('removes the storage event listener for localStorage', () => { - const removeSpy = vi.spyOn(window, 'removeEventListener'); - const state = new PersistedState('dispose-test', 'x'); - state.dispose(); - expect(removeSpy).toHaveBeenCalledWith('storage', expect.any(Function)); - }); - - it('does not error when called on a sessionStorage instance', () => { - const state = new PersistedState('dispose-session', 'x', { storage: 'session' }); - expect(() => state.dispose()).not.toThrow(); - }); - - it('stops receiving cross-tab updates after dispose()', () => { - const state = new PersistedState('after-dispose', 'init'); - state.dispose(); - - window.dispatchEvent( - new StorageEvent('storage', { - key: 'after-dispose', - newValue: JSON.stringify('updated'), - storageArea: localStorage, - }) - ); - - expect(state.value).toBe('init'); - }); - }); - - describe('cross-tab sync', () => { - it('updates value when a matching storage event fires', () => { - const state = new PersistedState('sync-key', 'initial'); - - window.dispatchEvent( - new StorageEvent('storage', { - key: 'sync-key', - newValue: JSON.stringify('from-other-tab'), - storageArea: localStorage, - }) - ); - - expect(state.value).toBe('from-other-tab'); - state.dispose(); - }); - - it('falls back to default value when storage event has null newValue', () => { - const state = new PersistedState('sync-null', 'fallback'); - state.value = 'something'; - - window.dispatchEvent( - new StorageEvent('storage', { - key: 'sync-null', - newValue: null, - storageArea: localStorage, - }) - ); - - expect(state.value).toBe('fallback'); - state.dispose(); - }); - - it('does not update when key does not match', () => { - const state = new PersistedState('mine', 'initial'); - - window.dispatchEvent( - new StorageEvent('storage', { - key: 'someone-elses', - newValue: JSON.stringify('hijacked'), - storageArea: localStorage, - }) - ); - - expect(state.value).toBe('initial'); - state.dispose(); - }); - - it('does not update when storageArea does not match', () => { - const state = new PersistedState('area-key', 'initial'); - - window.dispatchEvent( - new StorageEvent('storage', { - key: 'area-key', - newValue: JSON.stringify('from-session'), - storageArea: sessionStorage, - }) - ); - - expect(state.value).toBe('initial'); - state.dispose(); - }); - }); - - describe('sessionStorage variant', () => { - it('does not attach a storage event listener', () => { - const addSpy = vi.spyOn(window, 'addEventListener'); - const state = new PersistedState('s-listener', 'x', { storage: 'session' }); - - const storageCalls = addSpy.mock.calls.filter((call) => call[0] === 'storage'); - expect(storageCalls).toHaveLength(0); - state.dispose(); - }); - - it('ignores cross-tab storage events entirely', () => { - const state = new PersistedState('s-ignore', 'initial', { storage: 'session' }); - - window.dispatchEvent( - new StorageEvent('storage', { - key: 's-ignore', - newValue: JSON.stringify('changed'), - storageArea: sessionStorage, - }) - ); - - expect(state.value).toBe('initial'); - state.dispose(); - }); - }); - - describe('onChange hook', () => { - it('fires when the setter is called', () => { - const state = new TrackingState('on-change-set', 0); - state.value = 1; - state.value = 2; - expect(state.changes).toEqual([1, 2]); - state.dispose(); - }); - - it('fires when reset() is called', () => { - const state = new TrackingState('on-change-reset', 'default'); - state.value = 'changed'; - state.changes = []; - state.reset(); - expect(state.changes).toEqual(['default']); - state.dispose(); - }); - - it('fires when a cross-tab storage event updates the value', () => { - const state = new TrackingState('on-change-storage', 'init'); - - window.dispatchEvent( - new StorageEvent('storage', { - key: 'on-change-storage', - newValue: JSON.stringify('remote'), - storageArea: localStorage, - }) - ); - - expect(state.changes).toEqual(['remote']); - state.dispose(); - }); - }); - - describe('custom serialize/deserialize', () => { - it('uses custom string serialization (no JSON quotes)', () => { - const state = new StringState('plain-string', 'default'); - state.value = 'hello world'; - expect(localStorage.getItem('plain-string')).toBe('hello world'); - state.dispose(); - }); - - it('reads pre-existing raw values via custom deserialize', () => { - localStorage.setItem('plain-pre', 'raw-text'); - const state = new StringState('plain-pre', 'default'); - expect(state.value).toBe('raw-text'); - state.dispose(); - }); - - it('round-trips a numeric value via String/Number serialization', () => { - const state = new NumberState('plain-num', 0); - state.value = 42; - expect(localStorage.getItem('plain-num')).toBe('42'); - - const reread = new NumberState('plain-num', 0); - expect(reread.value).toBe(42); - - state.dispose(); - reread.dispose(); - }); - }); - - describe('multiple instances', () => { - it('different keys do not interfere', () => { - const a = new PersistedState('multi-a', 0); - const b = new PersistedState('multi-b', 0); - - a.value = 1; - b.value = 2; - - expect(a.value).toBe(1); - expect(b.value).toBe(2); - expect(localStorage.getItem('multi-a')).toBe(JSON.stringify(1)); - expect(localStorage.getItem('multi-b')).toBe(JSON.stringify(2)); - - a.dispose(); - b.dispose(); - }); - - it('a storage event for one key does not affect another', () => { - const a = new PersistedState('iso-a', 'a-init'); - const b = new PersistedState('iso-b', 'b-init'); - - window.dispatchEvent( - new StorageEvent('storage', { - key: 'iso-a', - newValue: JSON.stringify('a-changed'), - storageArea: localStorage, - }) - ); - - expect(a.value).toBe('a-changed'); - expect(b.value).toBe('b-init'); - - a.dispose(); - b.dispose(); - }); - }); -}); diff --git a/src/lib/state/color-scheme-state.svelte.ts b/src/lib/state/color-scheme-state.svelte.ts deleted file mode 100644 index 73e67f33..00000000 --- a/src/lib/state/color-scheme-state.svelte.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { isString } from '$lib/typeguards'; -import { PersistedState } from './classes/persisted-state.svelte'; - -export enum ColorScheme { - Dark = 'dark', - Light = 'light', - System = 'system', -} - -function isColorSchemeEnum(value: unknown): value is ColorScheme { - if (!isString(value)) return false; - return Object.values(ColorScheme).includes(value as ColorScheme); -} - -export function getDarkReaderState() { - const rootHtml = document.documentElement; - - const proxyInjected = rootHtml.getAttribute('data-darkreader-proxy-injected'); - const metaElement = rootHtml.querySelector('head meta[name="darkreader"]'); - const scheme = rootHtml.getAttribute('data-darkreader-scheme'); - - return { - isInjected: proxyInjected === 'true', - isActive: metaElement !== null, - scheme, - }; -} - -function resolveDarkMode(userPreference: ColorScheme): boolean { - if (userPreference !== ColorScheme.System) { - return userPreference !== ColorScheme.Light; - } - - // If a user has Dark Reader extension installed, assume they prefer dark mode - const darkReaderState = getDarkReaderState(); - if (darkReaderState.isInjected) { - return true; - } - - // Check if the user has a system theme preference for light mode - if (window.matchMedia('(prefers-color-scheme: light)').matches) { - return false; - } - - // Default to dark mode - return true; -} - -function setDarkMode(preference: ColorScheme) { - document.documentElement.classList.toggle('dark', resolveDarkMode(preference)); -} - -class ColorSchemeState extends PersistedState { - constructor() { - super('theme', ColorScheme.System); - } - - protected override onChange(v: ColorScheme) { - setDarkMode(isColorSchemeEnum(v) ? v : this.defaultValue); - } - - protected override deserialize(raw: string | null): ColorScheme { - return isColorSchemeEnum(raw) ? raw : this.defaultValue; - } - - protected override serialize(value: ColorScheme): string { - return value; - } -} - -export const colorScheme = new ColorSchemeState(); - -function handleMediaQueryChange() { - setDarkMode(colorScheme.value); -} - -export function initializeColorScheme() { - handleMediaQueryChange(); - - window - .matchMedia('(prefers-color-scheme: light)') - .addEventListener('change', handleMediaQueryChange); - window - .matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', handleMediaQueryChange); -} diff --git a/src/lib/state/telemetry-consent-state.svelte.ts b/src/lib/state/telemetry-consent-state.svelte.ts index ef2ab809..41bc52fd 100644 --- a/src/lib/state/telemetry-consent-state.svelte.ts +++ b/src/lib/state/telemetry-consent-state.svelte.ts @@ -1,4 +1,4 @@ -import { PersistedState } from './classes/persisted-state.svelte'; +import { PersistedState } from '@openshock/svelte-core/state/classes/persisted-state.svelte'; /** * How much diagnostic data the user agrees to send to SigNoz: diff --git a/src/lib/telemetry/common.ts b/src/lib/telemetry/common.ts index 2f08cf4b..626680ad 100644 --- a/src/lib/telemetry/common.ts +++ b/src/lib/telemetry/common.ts @@ -8,7 +8,7 @@ import { PUBLIC_SIGNOZ_TRACES_URL, } from '$env/static/public'; import { telemetryConsent, type TelemetryLevel } from '$lib/state/telemetry-consent-state.svelte'; -import { isTruthy } from '$lib/utils/parse'; +import { isTruthy } from '@openshock/svelte-core/utils/parse'; import { resourceFromAttributes } from '@opentelemetry/resources'; export const SERVICE_NAME = 'openshock-frontend'; diff --git a/src/lib/telemetry/tracer.ts b/src/lib/telemetry/tracer.ts index 50cb1cc3..c1424669 100644 --- a/src/lib/telemetry/tracer.ts +++ b/src/lib/telemetry/tracer.ts @@ -1,5 +1,5 @@ import { PUBLIC_SIGNOZ_TRACE_PROPAGATION } from '$env/static/public'; -import { isTruthy } from '$lib/utils/parse'; +import { isTruthy } from '@openshock/svelte-core/utils/parse'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; diff --git a/src/lib/tour/onboarding-state.ts b/src/lib/tour/onboarding-state.ts index 0c56510a..5aa4b639 100644 --- a/src/lib/tour/onboarding-state.ts +++ b/src/lib/tour/onboarding-state.ts @@ -1,5 +1,5 @@ import { PUBLIC_DISABLE_ONBOARDING } from '$env/static/public'; -import { isTruthy } from '$lib/utils/parse'; +import { isTruthy } from '@openshock/svelte-core/utils/parse'; export const WELCOME_COOKIE_NAME = 'os.welcomed'; // Pre-cookie builds stored "welcome seen" under this localStorage key. Kept only diff --git a/src/lib/typeguards/basicGuards.ts b/src/lib/typeguards/basicGuards.ts deleted file mode 100644 index 6baa9c21..00000000 --- a/src/lib/typeguards/basicGuards.ts +++ /dev/null @@ -1,18 +0,0 @@ -export function isObject(value: unknown): value is Record { - return typeof value === 'object' && !Array.isArray(value) && value !== null; -} -export function isString(value: unknown): value is string { - return typeof value === 'string' || value instanceof String; -} -export function isNumber(value: unknown): value is number { - return typeof value === 'number' && isFinite(value); -} -export function isBoolean(value: unknown): value is boolean { - return typeof value === 'boolean'; -} -export function isArrayBuffer(value: unknown): value is ArrayBuffer { - return value instanceof ArrayBuffer; -} -export function isStringOrNull(value: unknown): value is string | null { - return isString(value) || value === null; -} diff --git a/src/lib/typeguards/index.ts b/src/lib/typeguards/index.ts deleted file mode 100644 index e5d287ec..00000000 --- a/src/lib/typeguards/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './basicGuards'; -export * from './twitterHandleGuard'; diff --git a/src/lib/typeguards/propGuards.ts b/src/lib/typeguards/propGuards.ts deleted file mode 100644 index f317c506..00000000 --- a/src/lib/typeguards/propGuards.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { isBoolean, isNumber, isObject, isString } from '$lib/typeguards'; - -export function HasString( - obj: Record, - key: K -): obj is object & Record { - return Object.hasOwn(obj, key) && isString(obj[key]); -} - -export function HasStringOrNull( - obj: Record, - key: K -): obj is object & Record { - return Object.hasOwn(obj, key) && (isString(obj[key]) || obj[key] === null); -} - -export function HasNumber( - obj: Record, - key: K -): obj is object & Record { - return Object.hasOwn(obj, key) && isNumber(obj[key]); -} - -export function HasBoolean( - obj: Record, - key: K -): obj is object & Record { - return Object.hasOwn(obj, key) && isBoolean(obj[key]); -} - -export function HasObject( - obj: Record, - key: K -): obj is object & Record> { - return Object.hasOwn(obj, key) && isObject(obj[key]); -} - -export function HasStringArray( - obj: Record, - key: K -): obj is object & Record { - return ( - Object.hasOwn(obj, key) && - Array.isArray(obj[key]) && - obj[key].every((item: unknown) => isString(item)) - ); -} diff --git a/src/lib/typeguards/twitterHandleGuard.ts b/src/lib/typeguards/twitterHandleGuard.ts deleted file mode 100644 index 9dd6c82f..00000000 --- a/src/lib/typeguards/twitterHandleGuard.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { isString } from '$lib/typeguards/basicGuards'; - -export function isTwitterHandle(value: unknown): value is string { - return isString(value) && value.startsWith('@'); -} diff --git a/src/lib/types/AnyComponent.ts b/src/lib/types/AnyComponent.ts deleted file mode 100644 index 56d6e51b..00000000 --- a/src/lib/types/AnyComponent.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any -- type must accept any component props/events/slots signatures */ - -import type { Component, SvelteComponent } from 'svelte'; - -export type AnyComponent = - | Component - | (new (...args: any[]) => SvelteComponent); diff --git a/src/lib/types/ChildrenFunc.ts b/src/lib/types/ChildrenFunc.ts deleted file mode 100644 index 9d9fe5b3..00000000 --- a/src/lib/types/ChildrenFunc.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { Snippet } from 'svelte'; - -export type ChildrenFunc = () => ReturnType; diff --git a/src/lib/types/Tailwind.ts b/src/lib/types/Tailwind.ts deleted file mode 100644 index 83dd200d..00000000 --- a/src/lib/types/Tailwind.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const twTextColor = { - gray: 'text-gray-500', - red: 'text-red-500', - orange: 'text-orange-500', - yellow: 'text-yellow-500', - green: 'text-green-500', - cyan: 'text-cyan-500', - blue: 'text-blue-500', -} as const; - -export type TwTextColor = keyof typeof twTextColor; diff --git a/src/lib/types/ValidationResult.ts b/src/lib/types/ValidationResult.ts deleted file mode 100644 index 7221e2cc..00000000 --- a/src/lib/types/ValidationResult.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { TwTextColor } from '$lib/types/Tailwind'; - -export type ValidationResult = { - valid: boolean; - message?: string; - color?: TwTextColor; - link?: { text: string; href: string }; -}; - -export function GetValResColor(valRes: ValidationResult): TwTextColor { - if (valRes.color !== undefined) return valRes.color; - return valRes.valid ? 'green' : 'red'; -} diff --git a/src/lib/utils/clipboard.svelte.ts b/src/lib/utils/clipboard.svelte.ts deleted file mode 100644 index f12536e4..00000000 --- a/src/lib/utils/clipboard.svelte.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { onDestroy } from 'svelte'; -import { toast } from 'svelte-sonner'; - -const DEFAULT_SUCCESS_MESSAGE = 'Copied to clipboard'; -const DEFAULT_ERROR_MESSAGE = 'Failed to copy to clipboard'; - -/** - * Writes text to the clipboard and toasts the outcome. Fire-and-forget — use - * when no reactive feedback is needed. For a reactive `copied` flag, use the - * `Clipboard` class. - */ -export async function copyToClipboard( - text: string, - successMessage: string = DEFAULT_SUCCESS_MESSAGE -): Promise { - try { - await navigator.clipboard.writeText(text); - toast.success(successMessage); - return true; - } catch { - // Insecure context, permission denied, tab not focused, etc. - toast.error(DEFAULT_ERROR_MESSAGE); - return false; - } -} - -export interface ClipboardOptions { - /** How long `copied` stays true after a successful copy, in ms. Defaults to 2000. */ - resetDelay?: number; -} - -/** - * Clipboard wrapper with a reactive `copied` flag that auto-resets. Call - * `dispose()` when done (or use `useClipboard` to do that automatically). - */ -export class Clipboard { - readonly #resetDelay: number; - #timeout?: ReturnType; - #copied = $state(false); - - constructor(options: ClipboardOptions = {}) { - this.#resetDelay = options.resetDelay ?? 2000; - } - - get copied() { - return this.#copied; - } - - async copy(text: string, successMessage?: string): Promise { - const ok = await copyToClipboard(text, successMessage); - if (!ok) return false; - - this.#copied = true; - clearTimeout(this.#timeout); - this.#timeout = setTimeout(() => (this.#copied = false), this.#resetDelay); - return true; - } - - /** Clears any pending reset timeout. */ - dispose() { - clearTimeout(this.#timeout); - this.#timeout = undefined; - } -} - -/** Component-aware `Clipboard` that clears pending reset timeouts on unmount. */ -export function useClipboard(options?: ClipboardOptions): Clipboard { - const clip = new Clipboard(options); - onDestroy(() => clip.dispose()); - return clip; -} diff --git a/src/lib/utils/convert.test.ts b/src/lib/utils/convert.test.ts deleted file mode 100644 index 3916e486..00000000 Binary files a/src/lib/utils/convert.test.ts and /dev/null differ diff --git a/src/lib/utils/convert.ts b/src/lib/utils/convert.ts deleted file mode 100644 index 7ab798be..00000000 --- a/src/lib/utils/convert.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const NumberToHexPadded = (num: number, pad: number) => - (num >>> 0).toString(16).padStart(pad, '0').toUpperCase(); - -// Convert a Uint8Array to a binary string (1 byte → 1 char) -export const u8arrToBinstr = (array: Uint8Array): string => { - let result = ''; - const chunkSize = 0x8000; // avoid "call stack too large" for huge arrays - for (let i = 0; i < array.length; i += chunkSize) { - const chunk = array.subarray(i, i + chunkSize); - result += String.fromCharCode(...chunk); - } - return result; -}; - -// Convert a binary string back to a Uint8Array -export const binstrToU8arr = (bstr: string): Uint8Array => { - const u8Array = new Uint8Array(bstr.length); - for (let i = 0; i < bstr.length; i++) { - u8Array[i] = bstr.charCodeAt(i); - } - return u8Array; -}; diff --git a/src/lib/utils/crypto.test.ts b/src/lib/utils/crypto.test.ts deleted file mode 100644 index e47cc098..00000000 --- a/src/lib/utils/crypto.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { ArrayBufferToHex, HashBuffer, HashString } from './crypto'; -import { EncodeString } from './encoding'; - -describe('ArrayBufferToHex', () => { - it('converts an ArrayBuffer to a lowercase hex string', () => { - const bytes = new Uint8Array([0x00, 0x0f, 0xff]).buffer; - expect(ArrayBufferToHex(bytes)).toBe('000fff'); - }); - - it('handles an empty ArrayBuffer', () => { - const empty = new Uint8Array([]).buffer; - expect(ArrayBufferToHex(empty)).toBe(''); - }); - - it('pads single-digit hex values with a leading zero', () => { - const bytes = new Uint8Array([0x5, 0xa, 0x10]).buffer; - // 0x05 → "05", 0x0a → "0a", 0x10 → "10" - expect(ArrayBufferToHex(bytes)).toBe('050a10'); - }); -}); - -describe('HashBuffer', () => { - it('computes SHA-1 of a known UTF-8 byte sequence', async () => { - const data = new TextEncoder().encode('abc'); - const hash = await HashBuffer(data, 'SHA-1'); - // SHA-1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d - expect(hash).toBe('a9993e364706816aba3e25717850c26c9cd0d89d'); - }); - - it('computes SHA-256 of the same UTF-8 byte sequence', async () => { - const data = new TextEncoder().encode('abc'); - const hash = await HashBuffer(data, 'SHA-256'); - // SHA-256("abc") = - // ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad - expect(hash).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); - }); - - it('can hash arbitrary byte sequences, not only text', async () => { - const raw = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); - const sha1Hash = await HashBuffer(raw, 'SHA-1'); - // Precomputed SHA-1 of [0xde,0xad,0xbe,0xef]: - // "d78f8bb992a56a597f6c7a1fb918bb78271367eb" - expect(sha1Hash).toBe('d78f8bb992a56a597f6c7a1fb918bb78271367eb'); - - const sha256Hash = await HashBuffer(raw, 'SHA-256'); - // Precomputed SHA-256 of [0xde,0xad,0xbe,0xef]: - // "5f78c33274e43fa9de5659265c1d917e25c03722dcb0b8d27db8d5feaa813953" - expect(sha256Hash).toBe('5f78c33274e43fa9de5659265c1d917e25c03722dcb0b8d27db8d5feaa813953'); - }); -}); - -describe('HashString', () => { - it('delegates to HashBuffer via EncodeString and computes SHA-1', async () => { - // Ensure EncodeString is actually converting "hello" to the correct Uint8Array - const manualBuffer = new TextEncoder().encode('hello'); - const expectedHex = await HashBuffer(manualBuffer, 'SHA-1'); - const hashFromString = await HashString('hello', 'SHA-1'); - expect(hashFromString).toBe(expectedHex); - }); - - it('computes SHA-256 for a longer phrase', async () => { - const phrase = 'The quick brown fox jumps over the lazy dog'; - // Known SHA-256 of that phrase: - // d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592 - const hash = await HashString(phrase, 'SHA-256'); - expect(hash).toBe('d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592'); - }); - - it('returns the same result as HashBuffer when using EncodeString internally', async () => { - const input = 'Vitest'; - const bufferFromEncode = EncodeString(input); - const buffHash = await HashBuffer(bufferFromEncode, 'SHA-256'); - const strHash = await HashString(input, 'SHA-256'); - expect(strHash).toBe(buffHash); - }); - - it('produces SHA-1 of the empty string', async () => { - expect(await HashString('', 'SHA-1')).toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709'); - }); - - it('produces SHA-256 of the empty string', async () => { - expect(await HashString('', 'SHA-256')).toBe( - 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' - ); - }); -}); diff --git a/src/lib/utils/crypto.ts b/src/lib/utils/crypto.ts deleted file mode 100644 index 400f2961..00000000 --- a/src/lib/utils/crypto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { EncodeString } from '$lib/utils/encoding'; - -export const ArrayBufferToHex = (buffer: ArrayBuffer) => - Array.from(new Uint8Array(buffer)) - .map((num) => num.toString(16).padStart(2, '0')) - .join(''); - -export async function HashBuffer( - input: BufferSource, - hashtype: 'SHA-1' | 'SHA-256' -): Promise { - const hashBuffer = await crypto.subtle.digest(hashtype, input); - return ArrayBufferToHex(hashBuffer); -} -export async function HashString(input: string, hashtype: 'SHA-1' | 'SHA-256') { - return HashBuffer(EncodeString(input), hashtype); -} diff --git a/src/lib/utils/debounce.test.ts b/src/lib/utils/debounce.test.ts deleted file mode 100644 index 23f092f1..00000000 --- a/src/lib/utils/debounce.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { debounce } from './debounce'; - -describe('debounce', () => { - beforeEach(() => vi.useFakeTimers()); - afterEach(() => vi.useRealTimers()); - - it('delays invocation until the timer elapses', () => { - const fn = vi.fn(); - const d = debounce(fn, 100); - - d('a'); - expect(fn).not.toHaveBeenCalled(); - vi.advanceTimersByTime(99); - expect(fn).not.toHaveBeenCalled(); - vi.advanceTimersByTime(1); - expect(fn).toHaveBeenCalledExactlyOnceWith('a'); - }); - - it('collapses rapid calls into one invocation with the latest args', () => { - const fn = vi.fn(); - const d = debounce(fn, 100); - - d('a'); - vi.advanceTimersByTime(50); - d('b'); - vi.advanceTimersByTime(50); - d('c'); - vi.advanceTimersByTime(100); - - expect(fn).toHaveBeenCalledExactlyOnceWith('c'); - }); - - it('cancel() drops the pending invocation', () => { - const fn = vi.fn(); - const d = debounce(fn, 100); - - d('a'); - d.cancel(); - vi.advanceTimersByTime(200); - - expect(fn).not.toHaveBeenCalled(); - }); - - it('flush() runs the pending invocation immediately', () => { - const fn = vi.fn(); - const d = debounce(fn, 100); - - d('a'); - d.flush(); - expect(fn).toHaveBeenCalledExactlyOnceWith('a'); - - vi.advanceTimersByTime(200); - expect(fn).toHaveBeenCalledOnce(); - }); - - it('flush() is a no-op when no call is pending', () => { - const fn = vi.fn(); - const d = debounce(fn, 100); - - d.flush(); - expect(fn).not.toHaveBeenCalled(); - }); -}); - -describe('debounce — edge cases', () => { - beforeEach(() => vi.useFakeTimers()); - afterEach(() => vi.useRealTimers()); - - it('supports zero delay (still defers to next tick)', () => { - const fn = vi.fn(); - const d = debounce(fn, 0); - - d('a'); - expect(fn).not.toHaveBeenCalled(); - vi.advanceTimersByTime(0); - expect(fn).toHaveBeenCalledExactlyOnceWith('a'); - }); - - it('cancel() before any call is a no-op', () => { - const fn = vi.fn(); - const d = debounce(fn, 50); - expect(() => d.cancel()).not.toThrow(); - expect(fn).not.toHaveBeenCalled(); - }); - - it('cancel() after fire is a no-op and does not double-fire', () => { - const fn = vi.fn(); - const d = debounce(fn, 50); - - d('a'); - vi.advanceTimersByTime(50); - expect(fn).toHaveBeenCalledExactlyOnceWith('a'); - - d.cancel(); - vi.advanceTimersByTime(500); - expect(fn).toHaveBeenCalledOnce(); - }); - - it('allows another call after a fire and re-debounces it', () => { - const fn = vi.fn(); - const d = debounce(fn, 100); - - d('first'); - vi.advanceTimersByTime(100); - expect(fn).toHaveBeenCalledExactlyOnceWith('first'); - - d('second'); - vi.advanceTimersByTime(99); - expect(fn).toHaveBeenCalledOnce(); - vi.advanceTimersByTime(1); - expect(fn).toHaveBeenCalledTimes(2); - expect(fn).toHaveBeenLastCalledWith('second'); - }); - - it('flush() after fire does not invoke again', () => { - const fn = vi.fn(); - const d = debounce(fn, 50); - - d('a'); - vi.advanceTimersByTime(50); - expect(fn).toHaveBeenCalledOnce(); - - d.flush(); - expect(fn).toHaveBeenCalledOnce(); - }); - - it('passes through multiple arguments correctly', () => { - const fn = vi.fn<(a: number, b: string, c: object) => void>(); - const d = debounce(fn, 10); - - const obj = { x: 1 }; - d(1, 'two', obj); - vi.advanceTimersByTime(10); - - expect(fn).toHaveBeenCalledExactlyOnceWith(1, 'two', obj); - }); - - it('handles many rapid invocations with only the final one firing', () => { - const fn = vi.fn(); - const d = debounce(fn, 25); - - for (let i = 0; i < 1000; i++) { - d(i); - vi.advanceTimersByTime(1); - } - vi.advanceTimersByTime(25); - - expect(fn).toHaveBeenCalledOnce(); - expect(fn).toHaveBeenCalledWith(999); - }); - - it('allows scheduling immediately after cancel()', () => { - const fn = vi.fn(); - const d = debounce(fn, 30); - - d('a'); - vi.advanceTimersByTime(20); - d.cancel(); - expect(fn).not.toHaveBeenCalled(); - - d('b'); - vi.advanceTimersByTime(30); - expect(fn).toHaveBeenCalledExactlyOnceWith('b'); - }); - - it('flush() invokes synchronously without waiting for timer', () => { - const fn = vi.fn(); - const d = debounce(fn, 10_000); - - d('immediate'); - d.flush(); - // No timer advance needed - expect(fn).toHaveBeenCalledExactlyOnceWith('immediate'); - }); -}); diff --git a/src/lib/utils/debounce.ts b/src/lib/utils/debounce.ts deleted file mode 100644 index da24ab19..00000000 --- a/src/lib/utils/debounce.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { onDestroy } from 'svelte'; - -export interface Debounced { - (...args: Args): void; - cancel(): void; - flush(): void; -} - -export function debounce( - fn: (...args: Args) => void, - delay: number -): Debounced { - let handle: ReturnType | undefined; - let pending: Args | undefined; - - const debounced = ((...args: Args) => { - clearTimeout(handle); - pending = args; - handle = setTimeout(() => { - handle = undefined; - const p = pending; - pending = undefined; - if (p) fn(...p); - }, delay); - }) as Debounced; - - debounced.cancel = () => { - clearTimeout(handle); - handle = undefined; - pending = undefined; - }; - - debounced.flush = () => { - if (handle === undefined) return; - clearTimeout(handle); - handle = undefined; - const p = pending; - pending = undefined; - if (p) fn(...p); - }; - - return debounced; -} - -/** Component-aware `debounce` that cancels any pending invocation on unmount. */ -export function useDebounce( - fn: (...args: Args) => void, - delay: number -): Debounced { - const d = debounce(fn, delay); - onDestroy(d.cancel); - return d; -} diff --git a/src/lib/utils/encoding.test.ts b/src/lib/utils/encoding.test.ts deleted file mode 100644 index dd7b1c8b..00000000 --- a/src/lib/utils/encoding.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { DecodeString, EncodeString, escapeHtml } from './encoding'; - -describe('escapeHtml', () => { - it('returns "null" when input is null', () => { - expect(escapeHtml(null)).toBe('null'); - }); - - it('leaves a string without special characters unchanged', () => { - expect(escapeHtml('abc123')).toBe('abc123'); - expect(escapeHtml('Hello World')).toBe('Hello World'); - }); - - it('escapes &, <, >, ", and \' correctly', () => { - const unsafe = `Dolce & Gabbana < "Fashion" > 'Style'`; - const expected = 'Dolce & Gabbana < "Fashion" > 'Style''; - expect(escapeHtml(unsafe)).toBe(expected); - }); - - it('handles strings that are already partially escaped without double-escaping', () => { - const partially = '&<>"''; - // Only raw &, <, >, ", ' get replaced; existing entities contain & so they become & - expect(escapeHtml(partially)).toBe('&amp;&lt;&gt;&quot;&#039;'); - }); - - it('escapes a string containing only special characters', () => { - expect(escapeHtml('&<>"\'')).toBe('&<>"''); - }); - - it('returns an empty string when given an empty string', () => { - expect(escapeHtml('')).toBe(''); - }); -}); - -describe('EncodeString & DecodeString', () => { - it('EncodeString returns a Uint8Array representing UTF-8 bytes', () => { - const ascii = 'hello'; - const asciiEncoded = EncodeString(ascii); - // UTF-8 for "hello" is [104, 101, 108, 108, 111] - expect(asciiEncoded).toBeInstanceOf(Uint8Array); - expect(Array.from(asciiEncoded)).toEqual([104, 101, 108, 108, 111]); - - const unicode = '你好'; - const unicodeEncoded = EncodeString(unicode); - // Verify against TextEncoder directly - const expected = new TextEncoder().encode(unicode); - expect(Array.from(unicodeEncoded)).toEqual(Array.from(expected)); - }); - - it('DecodeString returns the original string from a Uint8Array', () => { - const original = 'Vitest 🚀'; - const encoded = new TextEncoder().encode(original); - const decoded = DecodeString(encoded); - expect(decoded).toBe(original); - }); - - it('round-trips arbitrary strings through EncodeString and DecodeString', () => { - const samples = [ - '', - 'simple text', - 'Line 1\nLine 2\tTabbed', - 'áéíóú üñ ¿¡', - 'Emoji: 😀🎉', - '中文字符测试', - ]; - - for (const s of samples) { - const encoded = EncodeString(s); - const decoded = DecodeString(encoded); - expect(decoded).toBe(s); - } - }); - - it(' EncodeString and DecodeString correctly handle byte values >= 0x80', () => { - // Create a Uint8Array manually with high-byte values and decode then re-encode - const bytes = new Uint8Array([0xc3, 0xa9]); // UTF-8 for "é" - const decoded = DecodeString(bytes); - expect(decoded).toBe('é'); - const reEncoded = EncodeString(decoded); - expect(Array.from(reEncoded)).toEqual([0xc3, 0xa9]); - }); -}); diff --git a/src/lib/utils/encoding.ts b/src/lib/utils/encoding.ts deleted file mode 100644 index 3d8e3fed..00000000 --- a/src/lib/utils/encoding.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function escapeHtml(unsafe: string | null): string { - if (unsafe === null) return 'null'; - return unsafe - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", '''); -} - -export const EncodeString = (input: string) => new TextEncoder().encode(input); -export const DecodeString = (input: Uint8Array) => new TextDecoder().decode(input); diff --git a/src/lib/utils/entropy.test.ts b/src/lib/utils/entropy.test.ts deleted file mode 100644 index 53f3fd29..00000000 --- a/src/lib/utils/entropy.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { calculateStringEntropy } from './entropy'; - -describe('calculateStringEntropy', () => { - it('returns 0 for an empty string', () => { - // uniqueLength = 0, length = 0 => Math.pow(0, 0) = 1 => log2(1) = 0 - expect(calculateStringEntropy('')).toBe(0); - }); - - it('returns 0 when all characters are the same', () => { - // e.g., "aaaa", uniqueLength = 1, length = 4 => 1^4 = 1 => log2(1) = 0 - expect(calculateStringEntropy('aaaa')).toBe(0); - expect(calculateStringEntropy('bbbbbbbb')).toBe(0); - }); - - it('computes correctly for two unique characters', () => { - // "01", uniqueLength = 2, length = 2 => 2^2 = 4 => log2(4) = 2 - expect(calculateStringEntropy('01')).toBe(2); - - // "abab", uniqueLength = 2, length = 4 => 2^4 = 16 => log2(16) = 4 - expect(calculateStringEntropy('abab')).toBe(4); - }); - - it('computes correctly when all characters are unique', () => { - // "abcd", uniqueLength = 4, length = 4 => 4^4 = 256 => log2(256) = 8 - expect(calculateStringEntropy('abcd')).toBe(8); - - // "xyz", uniqueLength = 3, length = 3 => 3^3 = 27 => log2(27) = 3 * log2(3) - const expected = 3 * Math.log2(3); - expect(calculateStringEntropy('xyz')).toBeCloseTo(expected); - }); - - it('is order‐invariant: same set of characters yields same entropy regardless of ordering', () => { - const s1 = 'abcabc'; - const s2 = 'cbaacb'; - // Both: uniqueLength = 3, length = 6 => 3^6 => log2(3^6) = 6 * log2(3) - const expected = 6 * Math.log2(3); - expect(calculateStringEntropy(s1)).toBeCloseTo(expected); - expect(calculateStringEntropy(s2)).toBeCloseTo(expected); - }); - - it('handles longer mixed strings', () => { - const value = 'abcabcabc'; // uniqueLength = 3, length = 9 - // 3^9 = 19683, log2(19683) = 9 * log2(3) - const expected = 9 * Math.log2(3); - expect(calculateStringEntropy(value)).toBeCloseTo(expected); - - const mixed = 'AaBbCc123!@#'; - // uniqueLength = number of distinct chars in mixed - const uniqueLength = new Set(mixed).size; - const length = mixed.length; - const total = uniqueLength ** length; - const expectedMixed = Math.log2(total); - expect(calculateStringEntropy(mixed)).toBeCloseTo(expectedMixed); - }); -}); diff --git a/src/lib/utils/entropy.ts b/src/lib/utils/entropy.ts deleted file mode 100644 index 4191a093..00000000 --- a/src/lib/utils/entropy.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function calculateStringEntropy(value: string): number { - const uniqueChars = new Set(value); - const length = value.length; - const uniqueLength = uniqueChars.size; - - return Math.log2(uniqueLength ** length); -} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 3f45ee86..20689d1b 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,11 +1,2 @@ -export * from './debounce'; -export * from './encoding'; -export * from './entropy'; -export * from './math'; -export * from './parse'; -export * from './rand'; -export * from './shadcn'; export * from './shockerPause'; -export * from './temporal'; -export * from './time'; export * from './userAgent'; diff --git a/src/lib/utils/math.test.ts b/src/lib/utils/math.test.ts deleted file mode 100644 index 4dcf328c..00000000 --- a/src/lib/utils/math.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { DegToRad, RadToDeg, clamp, getCircleX, getCircleY, invLerp, lerp } from './math'; - -describe('math constants', () => { - it('DegToRad ≈ π/180', () => { - expect(DegToRad).toBeCloseTo(Math.PI / 180); - }); - - it('RadToDeg ≈ 180/π', () => { - expect(RadToDeg).toBeCloseTo(180 / Math.PI); - }); - - it('DegToRad and RadToDeg are inverses', () => { - expect(DegToRad * RadToDeg).toBeCloseTo(1); - }); -}); - -describe('clamp(value, min, max)', () => { - it('returns min when value is below min', () => { - expect(clamp(-10, 0, 5)).toBe(0); - }); - - it('returns max when value is above max', () => { - expect(clamp(10, 0, 5)).toBe(5); - }); - - it('returns value when it is between min and max', () => { - expect(clamp(3, 0, 5)).toBe(3); - expect(clamp(0, 0, 5)).toBe(0); - expect(clamp(5, 0, 5)).toBe(5); - }); - - it('works when min and max are negative or reversed', () => { - expect(clamp(-3, -5, -1)).toBe(-3); - expect(clamp(-6, -5, -1)).toBe(-5); - expect(clamp(0, -5, -1)).toBe(-1); - }); -}); - -describe('lerp(a, b, t)', () => { - it('returns a when t=0', () => { - expect(lerp(10, 20, 0)).toBe(10); - }); - - it('returns b when t=1', () => { - expect(lerp(10, 20, 1)).toBe(20); - }); - - it('returns midpoint when t=0.5', () => { - expect(lerp(10, 20, 0.5)).toBe(15); - }); - - it('interpolates correctly for t outside [0,1]', () => { - expect(lerp(0, 100, 1.5)).toBe(150); - expect(lerp(0, 100, -0.5)).toBe(-50); - }); -}); - -describe('invLerp(a, b, value)', () => { - it('returns 0 when value equals a', () => { - expect(invLerp(10, 20, 10)).toBeCloseTo(0); - }); - - it('returns 1 when value equals b', () => { - expect(invLerp(10, 20, 20)).toBeCloseTo(1); - }); - - it('returns 0.5 when value is midpoint', () => { - expect(invLerp(10, 20, 15)).toBeCloseTo(0.5); - }); - - it('returns negative t when value < a and >1 when value > b', () => { - expect(invLerp(10, 20, 5)).toBeCloseTo(-0.5); - expect(invLerp(10, 20, 25)).toBeCloseTo(1.5); - }); - - it('is inverse of lerp for linear inputs', () => { - const a = -50; - const b = 150; - for (const t of [0, 0.2, 0.75, 1, 1.3]) { - const interpolated = lerp(a, b, t); - expect(invLerp(a, b, interpolated)).toBeCloseTo(t); - } - }); -}); - -describe('getCircleX(radius, degrees) and getCircleY(radius, degrees)', () => { - const radius = 10; - - it('at 0° returns (radius, 0)', () => { - expect(getCircleX(radius, 0)).toBeCloseTo(radius); - expect(getCircleY(radius, 0)).toBeCloseTo(0); - }); - - it('at 90° returns (0, radius)', () => { - expect(getCircleX(radius, 90)).toBeCloseTo(0); - expect(getCircleY(radius, 90)).toBeCloseTo(radius); - }); - - it('at 180° returns (-radius, 0)', () => { - expect(getCircleX(radius, 180)).toBeCloseTo(-radius); - expect(getCircleY(radius, 180)).toBeCloseTo(0); - }); - - it('at 270° returns (0, -radius)', () => { - expect(getCircleX(radius, 270)).toBeCloseTo(0); - expect(getCircleY(radius, 270)).toBeCloseTo(-radius); - }); - - it('handles non-right angles correctly (e.g., 45°)', () => { - const diag = radius / Math.sqrt(2); - expect(getCircleX(radius, 45)).toBeCloseTo(diag); - expect(getCircleY(radius, 45)).toBeCloseTo(diag); - }); - - it('works with negative and >360° angles by relying on cosine/sine periodicity', () => { - expect(getCircleX(radius, -90)).toBeCloseTo(0); - expect(getCircleY(radius, -90)).toBeCloseTo(-radius); - expect(getCircleX(radius, 450)).toBeCloseTo(0); // 450° = 360° + 90° - expect(getCircleY(radius, 450)).toBeCloseTo(radius); - }); -}); diff --git a/src/lib/utils/math.ts b/src/lib/utils/math.ts deleted file mode 100644 index e3593a8d..00000000 --- a/src/lib/utils/math.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const DegToRad = Math.PI / 180; -export const RadToDeg = 180 / Math.PI; - -export function clamp(value: number, min: number, max: number): number { - return Math.min(max, Math.max(min, value)); -} -export function lerp(a: number, b: number, t: number): number { - return a + (b - a) * t; -} -export function invLerp(a: number, b: number, value: number): number { - return (value - a) / (b - a); -} - -export function getCircleX(radius: number, degrees: number): number { - return radius * Math.cos(degrees * DegToRad); -} -export function getCircleY(radius: number, degrees: number): number { - return radius * Math.sin(degrees * DegToRad); -} diff --git a/src/lib/utils/md5.test.ts b/src/lib/utils/md5.test.ts deleted file mode 100644 index 71e0d1cc..00000000 --- a/src/lib/utils/md5.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { md5 } from './md5'; - -function fromString(s: string): Uint8Array { - return new TextEncoder().encode(s); -} - -describe('md5', () => { - it('hashes an empty input', () => { - expect(md5(new Uint8Array([]))).toBe('d41d8cd98f00b204e9800998ecf8427e'); - }); - - it('hashes "hello"', () => { - expect(md5(fromString('hello'))).toBe('5d41402abc4b2a76b9719d911017c592'); - }); - - it('hashes "abc"', () => { - expect(md5(fromString('abc'))).toBe('900150983cd24fb0d6963f7d28e17f72'); - }); - - it('hashes a string exactly 55 bytes (just fits in one block with padding)', () => { - const input = 'a'.repeat(55); - expect(md5(fromString(input))).toBe('ef1772b6dff9a122358552954ad0df65'); - }); - - it('hashes a string of 56 bytes (forces a second block for padding)', () => { - const input = 'a'.repeat(56); - expect(md5(fromString(input))).toBe('3b0c8ac703f828b04c6c197006d17218'); - }); - - it('hashes a string exactly 64 bytes (one full block)', () => { - const input = 'a'.repeat(64); - expect(md5(fromString(input))).toBe('014842d480b571495a4a0363793f7367'); - }); - - it('hashes arbitrary binary data', () => { - const data = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); - expect(md5(data)).toBe('2f249230a8e7c2bf6005ccd2679259ec'); - }); - - it('hashes a longer message spanning multiple blocks', () => { - const input = 'The quick brown fox jumps over the lazy dog'; - expect(md5(fromString(input))).toBe('9e107d9d372bb6826bd81d3542a419d6'); - }); - - it('hashes 1 million "a" chars (RFC 1321 test vector)', () => { - const data = new Uint8Array(1_000_000).fill(0x61); // 0x61 = 'a' - expect(md5(data)).toBe('7707d6ae4e027c70eea2a935c2296f21'); - }); - - it('hashes a 1024-byte buffer (16 full blocks, no remainder)', () => { - const data = new Uint8Array(1024).fill(0x00); - expect(md5(data)).toBe('0f343b0931126a20f133d67c2b018a3b'); - }); - - it('hashes byte 0xff repeated 65 times (just over a full block)', () => { - const data = new Uint8Array(65).fill(0xff); - // value verified independently; locks current implementation behavior - expect(md5(data).length).toBe(32); - expect(/^[0-9a-f]{32}$/.test(md5(data))).toBe(true); - }); -}); diff --git a/src/lib/utils/md5.ts b/src/lib/utils/md5.ts deleted file mode 100644 index 88f31603..00000000 --- a/src/lib/utils/md5.ts +++ /dev/null @@ -1,155 +0,0 @@ -// MD5 implementation adapted from Joseph Myers' public domain code -// https://www.myersdaily.org/joseph/javascript/md5-text.html - -function add32(a: number, b: number): number { - return (a + b) & 0xffffffff; -} - -function cmn(q: number, a: number, b: number, x: number, s: number, t: number): number { - a = add32(add32(a, q), add32(x, t)); - return add32((a << s) | (a >>> (32 - s)), b); -} - -function ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { - return cmn((b & c) | (~b & d), a, b, x, s, t); -} - -function gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { - return cmn((b & d) | (c & ~d), a, b, x, s, t); -} - -function hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { - return cmn(b ^ c ^ d, a, b, x, s, t); -} - -function ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { - return cmn(c ^ (b | ~d), a, b, x, s, t); -} - -function md5blk(data: Uint8Array, offset: number): number[] { - const blks: number[] = []; - for (let i = 0; i < 64; i += 4) { - blks[i >> 2] = - data[offset + i] | - (data[offset + i + 1] << 8) | - (data[offset + i + 2] << 16) | - (data[offset + i + 3] << 24); - } - return blks; -} - -function md5cycle(state: number[], blk: number[]) { - let a = state[0], - b = state[1], - c = state[2], - d = state[3]; - - a = ff(a, b, c, d, blk[0], 7, -680876936); - d = ff(d, a, b, c, blk[1], 12, -389564586); - c = ff(c, d, a, b, blk[2], 17, 606105819); - b = ff(b, c, d, a, blk[3], 22, -1044525330); - a = ff(a, b, c, d, blk[4], 7, -176418897); - d = ff(d, a, b, c, blk[5], 12, 1200080426); - c = ff(c, d, a, b, blk[6], 17, -1473231341); - b = ff(b, c, d, a, blk[7], 22, -45705983); - a = ff(a, b, c, d, blk[8], 7, 1770035416); - d = ff(d, a, b, c, blk[9], 12, -1958414417); - c = ff(c, d, a, b, blk[10], 17, -42063); - b = ff(b, c, d, a, blk[11], 22, -1990404162); - a = ff(a, b, c, d, blk[12], 7, 1804603682); - d = ff(d, a, b, c, blk[13], 12, -40341101); - c = ff(c, d, a, b, blk[14], 17, -1502002290); - b = ff(b, c, d, a, blk[15], 22, 1236535329); - - a = gg(a, b, c, d, blk[1], 5, -165796510); - d = gg(d, a, b, c, blk[6], 9, -1069501632); - c = gg(c, d, a, b, blk[11], 14, 643717713); - b = gg(b, c, d, a, blk[0], 20, -373897302); - a = gg(a, b, c, d, blk[5], 5, -701558691); - d = gg(d, a, b, c, blk[10], 9, 38016083); - c = gg(c, d, a, b, blk[15], 14, -660478335); - b = gg(b, c, d, a, blk[4], 20, -405537848); - a = gg(a, b, c, d, blk[9], 5, 568446438); - d = gg(d, a, b, c, blk[14], 9, -1019803690); - c = gg(c, d, a, b, blk[3], 14, -187363961); - b = gg(b, c, d, a, blk[8], 20, 1163531501); - a = gg(a, b, c, d, blk[13], 5, -1444681467); - d = gg(d, a, b, c, blk[2], 9, -51403784); - c = gg(c, d, a, b, blk[7], 14, 1735328473); - b = gg(b, c, d, a, blk[12], 20, -1926607734); - - a = hh(a, b, c, d, blk[5], 4, -378558); - d = hh(d, a, b, c, blk[8], 11, -2022574463); - c = hh(c, d, a, b, blk[11], 16, 1839030562); - b = hh(b, c, d, a, blk[14], 23, -35309556); - a = hh(a, b, c, d, blk[1], 4, -1530992060); - d = hh(d, a, b, c, blk[4], 11, 1272893353); - c = hh(c, d, a, b, blk[7], 16, -155497632); - b = hh(b, c, d, a, blk[10], 23, -1094730640); - a = hh(a, b, c, d, blk[13], 4, 681279174); - d = hh(d, a, b, c, blk[0], 11, -358537222); - c = hh(c, d, a, b, blk[3], 16, -722521979); - b = hh(b, c, d, a, blk[6], 23, 76029189); - a = hh(a, b, c, d, blk[9], 4, -640364487); - d = hh(d, a, b, c, blk[12], 11, -421815835); - c = hh(c, d, a, b, blk[15], 16, 530742520); - b = hh(b, c, d, a, blk[2], 23, -995338651); - - a = ii(a, b, c, d, blk[0], 6, -198630844); - d = ii(d, a, b, c, blk[7], 10, 1126891415); - c = ii(c, d, a, b, blk[14], 15, -1416354905); - b = ii(b, c, d, a, blk[5], 21, -57434055); - a = ii(a, b, c, d, blk[12], 6, 1700485571); - d = ii(d, a, b, c, blk[3], 10, -1894986606); - c = ii(c, d, a, b, blk[10], 15, -1051523); - b = ii(b, c, d, a, blk[1], 21, -2054922799); - a = ii(a, b, c, d, blk[8], 6, 1873313359); - d = ii(d, a, b, c, blk[15], 10, -30611744); - c = ii(c, d, a, b, blk[6], 15, -1560198380); - b = ii(b, c, d, a, blk[13], 21, 1309151649); - a = ii(a, b, c, d, blk[4], 6, -145523070); - d = ii(d, a, b, c, blk[11], 10, -1120210379); - c = ii(c, d, a, b, blk[2], 15, 718787259); - b = ii(b, c, d, a, blk[9], 21, -343485551); - - state[0] = add32(a, state[0]); - state[1] = add32(b, state[1]); - state[2] = add32(c, state[2]); - state[3] = add32(d, state[3]); -} - -const HEX_CHARS = '0123456789abcdef'; - -function rhex(n: number): string { - let s = ''; - for (let j = 0; j < 4; j++) { - s += HEX_CHARS[(n >> (j * 8 + 4)) & 0x0f] + HEX_CHARS[(n >> (j * 8)) & 0x0f]; - } - return s; -} - -export function md5(data: Uint8Array): string { - const n = data.length; - const state = [1732584193, -271733879, -1732584194, 271733878]; - - let i: number; - for (i = 64; i <= n; i += 64) { - md5cycle(state, md5blk(data, i - 64)); - } - - const tail = new Array(16).fill(0); - const rem = n % 64; - const offset = n - rem; - for (i = 0; i < rem; i++) { - tail[i >> 2] |= data[offset + i] << ((i % 4) << 3); - } - tail[i >> 2] |= 0x80 << ((i % 4) << 3); - if (i > 55) { - md5cycle(state, tail); - tail.fill(0); - } - tail[14] = n * 8; - md5cycle(state, tail); - - return state.map(rhex).join(''); -} diff --git a/src/lib/utils/parse.test.ts b/src/lib/utils/parse.test.ts deleted file mode 100644 index 0a7eb4a1..00000000 --- a/src/lib/utils/parse.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { isTruthy } from './parse'; - -describe('isTruthy', () => { - it.each([true, 1, '1', 'true', 'TRUE', 'True', 'yes', 'YES', 'y', 'Y', 'on', 'ON'])( - 'returns true for %j', - (value) => { - expect(isTruthy(value)).toBe(true); - } - ); - - it.each([false, 0, 2, -1, '0', 'false', 'no', 'off', 'random', '', null, undefined])( - 'returns false for %j', - (value) => { - expect(isTruthy(value)).toBe(false); - } - ); -}); - -describe('isTruthy edge cases', () => { - it.each([NaN, Infinity, -Infinity, 1.0001, 0.9999, Number.MAX_SAFE_INTEGER, -0])( - 'returns false for non-1 numeric value %j', - (value) => { - expect(isTruthy(value)).toBe(false); - } - ); - - it('treats numeric 1 (and not 1.0 with epsilon) strictly', () => { - expect(isTruthy(1)).toBe(true); - expect(isTruthy(1 + Number.EPSILON)).toBe(false); - }); - - it.each([' true', 'true ', ' yes ', '\ttrue', 'true\n', '1 ', ' on'])( - 'rejects whitespace-padded truthy strings: %j', - (value) => { - expect(isTruthy(value)).toBe(false); - } - ); - - it.each(['tRuE', 'YeS', 'On', 'oN', 'Y', 'y'])('accepts mixed-case truthy: %j', (value) => { - expect(isTruthy(value)).toBe(true); - }); - - it.each(['truee', 'yess', 'yeah', 'enable', 'enabled', '11', '01'])( - 'rejects near-truthy strings: %j', - (value) => { - expect(isTruthy(value)).toBe(false); - } - ); - - it('handles a very long non-truthy string', () => { - expect(isTruthy('a'.repeat(10_000))).toBe(false); - }); - - it.each([{}, [], () => true, Symbol('x')])( - 'returns false for non-string/number/boolean type %#', - (value) => { - expect(isTruthy(value as unknown as string)).toBe(false); - } - ); -}); diff --git a/src/lib/utils/parse.ts b/src/lib/utils/parse.ts deleted file mode 100644 index df333c6a..00000000 --- a/src/lib/utils/parse.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Coerces an env-var / URL-param style value into a boolean. - * - * Truthy: `true`, the number `1`, or a case-insensitive `'1'`, `'true'`, `'yes'`, `'y'`, or `'on'`. - * - * Everything else - including `null`, `undefined`, other numbers, and unrecognized strings - is falsy. - * - * @param value - The value to coerce (typically an environment variable or query parameter) - * @returns `true` when the value matches a recognized truthy form, `false` otherwise - * - * @example - * ```ts - * isTruthy(env.PUBLIC_DISABLE_SITEMAP) // false when unset, true when "1" / "true" - * ``` - */ -export function isTruthy(value: boolean | number | string | null | undefined): boolean { - if (typeof value === 'boolean') return value; - if (typeof value === 'number') return value === 1; - if (typeof value !== 'string') return false; - - switch (value.toLowerCase()) { - case '1': - case 'true': - case 'yes': - case 'y': - case 'on': - return true; - default: - return false; - } -} diff --git a/src/lib/utils/rand.test.ts b/src/lib/utils/rand.test.ts deleted file mode 100644 index ee683592..00000000 --- a/src/lib/utils/rand.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { randStr } from './rand'; - -const ALLOWED_REGEX = /^[A-Za-z0-9]*$/; - -describe('randStr', () => { - it('returns a string of the specified length', () => { - const lengths = [0, 1, 5, 16, 64]; - for (const len of lengths) { - const s = randStr(len); - expect(s).toHaveLength(len); - } - }); - - it('returns only characters from [A–Z][a–z][0–9]', () => { - for (let i = 0; i < 10; i++) { - const s = randStr(200); - expect(s).toMatch(ALLOWED_REGEX); - } - }); - - it('returns different strings on subsequent calls with the same length', () => { - const len = 12; - const a = randStr(len); - const b = randStr(len); - // Very unlikely to collide twice in a row - expect(a).not.toBe(b); - }); - - it('returns empty string for zero and negative lengths', () => { - expect(randStr(0)).toBe(''); - // Negative length will not enter the loop, so it should also be '' - expect(randStr(-5)).toBe(''); - }); - - it('returns empty string for NaN length', () => { - expect(randStr(Number.NaN)).toBe(''); - }); - - it('uses every character class across many samples (statistical sanity)', () => { - const sample = randStr(2000); - expect(sample).toMatch(/[A-Z]/); - expect(sample).toMatch(/[a-z]/); - expect(sample).toMatch(/[0-9]/); - }); -}); diff --git a/src/lib/utils/rand.ts b/src/lib/utils/rand.ts deleted file mode 100644 index e28a9b49..00000000 --- a/src/lib/utils/rand.ts +++ /dev/null @@ -1,14 +0,0 @@ -const randChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' as const; -const randCharsLength = randChars.length; - -export function randStr(length: number) { - let result = ''; - - let counter = 0; - while (counter < length) { - result += randChars.charAt(Math.floor(Math.random() * randCharsLength)); - counter += 1; - } - - return result; -} diff --git a/src/lib/utils/shadcn.ts b/src/lib/utils/shadcn.ts deleted file mode 100644 index c77c249f..00000000 --- a/src/lib/utils/shadcn.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type ClassValue, clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - -export type WithoutChild = T extends { child?: unknown } ? Omit : T; -export type WithoutChildren = T extends { children?: unknown } ? Omit : T; -export type WithoutChildrenOrChild = WithoutChildren>; -export type WithElementRef = T & { ref?: U | null }; diff --git a/src/lib/utils/temporal.test.ts b/src/lib/utils/temporal.test.ts deleted file mode 100644 index 07ee18fb..00000000 --- a/src/lib/utils/temporal.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import 'temporal-polyfill/global'; -import { describe, expect, it } from 'vitest'; -import { durationBetween, formatDuration, formatElapsed } from './temporal'; - -function instant(isoString: string): Temporal.Instant { - return Temporal.Instant.from(isoString); -} - -describe('durationBetween', () => { - it('returns a positive duration when `to` is after `from`', () => { - const from = instant('2024-01-01T00:00:00Z'); - const to = instant('2024-01-02T00:00:00Z'); - const d = durationBetween(from, to); - expect(d.sign).toBe(1); - }); - - it('returns a negative duration when `to` is before `from`', () => { - const from = instant('2024-01-02T00:00:00Z'); - const to = instant('2024-01-01T00:00:00Z'); - const d = durationBetween(from, to); - expect(d.sign).toBe(-1); - }); - - it('returns zero duration for equal instants', () => { - const from = instant('2024-06-15T12:00:00Z'); - const d = durationBetween(from, from); - expect(d.sign).toBe(0); - }); - - it('captures years correctly', () => { - const from = instant('2020-01-01T00:00:00Z'); - const to = instant('2024-01-01T00:00:00Z'); - const d = durationBetween(from, to); - expect(d.years).toBe(4); - }); - - it('captures months correctly within a year', () => { - const from = instant('2024-01-01T00:00:00Z'); - const to = instant('2024-06-01T00:00:00Z'); - const d = durationBetween(from, to); - expect(d.years).toBe(0); - expect(d.months).toBe(5); - }); -}); - -describe('formatDuration', () => { - function dur(fields: Partial): Temporal.Duration { - return Temporal.Duration.from(fields); - } - - it('returns seconds for sub-minute durations', () => { - expect(formatDuration(dur({ seconds: 1 }))).toBe('1 second'); - expect(formatDuration(dur({ seconds: 45 }))).toBe('45 seconds'); - }); - - it('returns minutes when >= 1 minute', () => { - expect(formatDuration(dur({ minutes: 1 }))).toBe('1 minute'); - expect(formatDuration(dur({ minutes: 30 }))).toBe('30 minutes'); - }); - - it('returns hours when >= 1 hour', () => { - expect(formatDuration(dur({ hours: 1 }))).toBe('1 hour'); - expect(formatDuration(dur({ hours: 3 }))).toBe('3 hours'); - }); - - it('returns days when >= 1 day', () => { - expect(formatDuration(dur({ days: 1 }))).toBe('1 day'); - expect(formatDuration(dur({ days: 6 }))).toBe('6 days'); - }); - - it('returns months when >= 1 month', () => { - expect(formatDuration(dur({ months: 1 }))).toBe('1 month'); - expect(formatDuration(dur({ months: 11 }))).toBe('11 months'); - }); - - it('returns years when >= 1 year', () => { - expect(formatDuration(dur({ years: 1 }))).toBe('1 year'); - expect(formatDuration(dur({ years: 5 }))).toBe('5 years'); - }); - - it('uses the largest non-zero unit', () => { - expect(formatDuration(dur({ years: 2, months: 3, days: 10 }))).toBe('2 years'); - expect(formatDuration(dur({ months: 0, days: 5, hours: 12 }))).toBe('5 days'); - }); - - it('handles negative durations by using absolute values', () => { - expect(formatDuration(dur({ years: -3 }))).toBe('3 years'); - expect(formatDuration(dur({ minutes: -15 }))).toBe('15 minutes'); - }); - - it('uses singular for value of 1', () => { - expect(formatDuration(dur({ seconds: 1 }))).toBe('1 second'); - expect(formatDuration(dur({ minutes: 1 }))).toBe('1 minute'); - expect(formatDuration(dur({ hours: 1 }))).toBe('1 hour'); - expect(formatDuration(dur({ days: 1 }))).toBe('1 day'); - expect(formatDuration(dur({ months: 1 }))).toBe('1 month'); - expect(formatDuration(dur({ years: 1 }))).toBe('1 year'); - }); - - it('uses plural for values other than 1', () => { - expect(formatDuration(dur({ seconds: 2 }))).toBe('2 seconds'); - expect(formatDuration(dur({ years: 10 }))).toBe('10 years'); - }); -}); - -describe('formatElapsed', () => { - function dur(fields: Partial): Temporal.Duration { - return Temporal.Duration.from(fields); - } - - it('prefixes "in " for future durations (positive sign)', () => { - expect(formatElapsed(dur({ hours: 2 }))).toBe('in 2 hours'); - expect(formatElapsed(dur({ days: 1 }))).toBe('in 1 day'); - }); - - it('suffixes " ago" for past durations (negative sign)', () => { - expect(formatElapsed(dur({ hours: -2 }))).toBe('2 hours ago'); - expect(formatElapsed(dur({ days: -1 }))).toBe('1 day ago'); - }); - - it('handles zero-length duration (sign === 0) as future', () => { - expect(formatElapsed(Temporal.Duration.from({ seconds: 0 }))).toBe('in 0 seconds'); - }); - - it('round-trips correctly with durationBetween for a 30-day gap', () => { - const now = instant('2024-03-15T12:00:00Z'); - const future = instant('2024-04-14T12:00:00Z'); - const d = durationBetween(now, future); - expect(formatElapsed(d)).toBe('in 30 days'); - }); - - it('round-trips correctly with durationBetween for a past 2-year gap', () => { - const now = instant('2024-06-01T00:00:00Z'); - const past = instant('2022-06-01T00:00:00Z'); - const d = durationBetween(now, past); - expect(formatElapsed(d)).toBe('2 years ago'); - }); -}); diff --git a/src/lib/utils/temporal.ts b/src/lib/utils/temporal.ts deleted file mode 100644 index 65a36070..00000000 --- a/src/lib/utils/temporal.ts +++ /dev/null @@ -1,25 +0,0 @@ -export function durationBetween(from: Temporal.Instant, to: Temporal.Instant): Temporal.Duration { - const tz = Temporal.Now.timeZoneId(); - return from.toZonedDateTimeISO(tz).until(to.toZonedDateTimeISO(tz), { largestUnit: 'year' }); -} - -export function formatDuration(duration: Temporal.Duration): string { - const abs = duration.abs(); - const { years, months, days, hours, minutes, seconds } = abs; - - if (years >= 1) return formatInterval('year', years); - if (months >= 1) return formatInterval('month', months); - if (days >= 1) return formatInterval('day', days); - if (hours >= 1) return formatInterval('hour', hours); - if (minutes >= 1) return formatInterval('minute', minutes); - return formatInterval('second', seconds); -} - -export function formatElapsed(duration: Temporal.Duration): string { - const text = formatDuration(duration); - return duration.sign < 0 ? text + ' ago' : 'in ' + text; -} - -function formatInterval(unit: string, interval: number): string { - return `${interval} ${unit}${interval !== 1 ? 's' : ''}`; -} diff --git a/src/lib/utils/time.test.ts b/src/lib/utils/time.test.ts deleted file mode 100644 index aa5004d2..00000000 --- a/src/lib/utils/time.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { durationToString, elapsedToString } from './time'; - -const MS_IN_SECOND = 1000; -const MS_IN_MINUTE = 60 * MS_IN_SECOND; -const MS_IN_HOUR = 60 * MS_IN_MINUTE; -const MS_IN_DAY = 24 * MS_IN_HOUR; -const MS_IN_MONTH = 30 * MS_IN_DAY; -const MS_IN_YEAR = 365 * MS_IN_DAY; - -describe('durationToString', () => { - it('returns seconds when duration is less than one minute, with correct singular/plural', () => { - expect(durationToString(0)).toBe('0 seconds'); - expect(durationToString(MS_IN_SECOND - 1)).toBe('0 seconds'); // 999 ms → 0 s - expect(durationToString(MS_IN_SECOND)).toBe('1 second'); - expect(durationToString(2.5 * MS_IN_SECOND)).toBe('2 seconds'); // 2 500 ms → 2 s - expect(durationToString(59 * MS_IN_SECOND)).toBe('59 seconds'); - }); - - it('returns minutes when duration is between one minute and one hour', () => { - expect(durationToString(MS_IN_MINUTE)).toBe('1 minute'); - expect(durationToString(2 * MS_IN_MINUTE - 1)).toBe('1 minute'); // 119 999 ms → 1.999 min → floor → 1 - expect(durationToString(2 * MS_IN_MINUTE)).toBe('2 minutes'); - expect(durationToString(MS_IN_HOUR - 1)).toBe('59 minutes'); // 3 599 999 ms → 59.9999 min → floor → 59 - }); - - it('returns hours when duration is between one hour and one day', () => { - expect(durationToString(MS_IN_HOUR)).toBe('1 hour'); - expect(durationToString(1.5 * MS_IN_HOUR)).toBe('1 hour'); // 5 400 000 ms → 1.5 h → floor → 1 - expect(durationToString(2 * MS_IN_HOUR)).toBe('2 hours'); - expect(durationToString(MS_IN_DAY - 1)).toBe('23 hours'); // 86 399 999 ms → 23.999 h → floor → 23 - }); - - it('returns days when duration is between one day and one month', () => { - expect(durationToString(MS_IN_DAY)).toBe('1 day'); - expect(durationToString(2 * MS_IN_DAY)).toBe('2 days'); - expect(durationToString(2.5 * MS_IN_DAY)).toBe('2 days'); // 216 000 000 ms → 2.5 d → floor → 2 - expect(durationToString(MS_IN_MONTH - 1)).toBe('29 days'); // 2 591 999 999 ms → 29.999 d → floor → 29 - }); - - it('returns months when duration is between one month and one year', () => { - expect(durationToString(MS_IN_MONTH)).toBe('1 month'); - expect(durationToString(2 * MS_IN_MONTH)).toBe('2 months'); - expect(durationToString(45 * MS_IN_DAY)).toBe('1 month'); // 45 d → 1.5 mo → floor → 1 - expect(durationToString(MS_IN_YEAR - 1)).toBe('12 months'); // ~12.16 mo → floor → 12 - }); - - it('returns years when duration is one year or more', () => { - expect(durationToString(MS_IN_YEAR)).toBe('1 year'); - expect(durationToString(2 * MS_IN_YEAR)).toBe('2 years'); - expect(durationToString(1.5 * MS_IN_YEAR)).toBe('1 year'); // 1.5 y → floor → 1 - expect(durationToString(100_000_000_000)).toBe('3 years'); // ~3.17 y → floor → 3 - }); - - it('treats negative inputs as positive when computing unit', () => { - expect(durationToString(-MS_IN_SECOND)).toBe('1 second'); - expect(durationToString(-MS_IN_MINUTE)).toBe('1 minute'); - expect(durationToString(-MS_IN_HOUR)).toBe('1 hour'); - }); -}); - -describe('elapsedToString', () => { - it('prefixes "in " when duration is non-negative', () => { - expect(elapsedToString(0)).toBe('in 0 seconds'); - expect(elapsedToString(2 * MS_IN_SECOND)).toBe('in 2 seconds'); - expect(elapsedToString(MS_IN_MINUTE)).toBe('in 1 minute'); - expect(elapsedToString(2 * MS_IN_HOUR)).toBe('in 2 hours'); - }); - - it('suffixes " ago" when duration is negative', () => { - expect(elapsedToString(-MS_IN_SECOND)).toBe('1 second ago'); - expect(elapsedToString(-MS_IN_MINUTE)).toBe('1 minute ago'); - expect(elapsedToString(-MS_IN_HOUR)).toBe('1 hour ago'); - expect(elapsedToString(-2 * MS_IN_HOUR)).toBe('2 hours ago'); - }); -}); - -describe('durationToString — boundary and edge cases', () => { - it('returns "0 seconds" for sub-second durations regardless of sign', () => { - expect(durationToString(0)).toBe('0 seconds'); - expect(durationToString(1)).toBe('0 seconds'); - expect(durationToString(-1)).toBe('0 seconds'); - expect(durationToString(-999)).toBe('0 seconds'); - }); - - it('handles year/month/day/hour/minute/second exact boundaries', () => { - expect(durationToString(MS_IN_SECOND)).toBe('1 second'); - expect(durationToString(MS_IN_MINUTE)).toBe('1 minute'); - expect(durationToString(MS_IN_HOUR)).toBe('1 hour'); - expect(durationToString(MS_IN_DAY)).toBe('1 day'); - expect(durationToString(MS_IN_MONTH)).toBe('1 month'); - expect(durationToString(MS_IN_YEAR)).toBe('1 year'); - }); - - it('is consistent just below each boundary', () => { - // 60_000ms is exactly 1 minute. 59_999ms → seconds branch → 59 seconds - expect(durationToString(MS_IN_MINUTE - 1)).toBe('59 seconds'); - // hour - 1 → minutes branch - expect(durationToString(MS_IN_HOUR - 1)).toBe('59 minutes'); - }); - - it('handles very large values (centuries)', () => { - const result = durationToString(1000 * MS_IN_YEAR); - expect(result).toBe('1000 years'); - }); - - it('handles Number.MAX_SAFE_INTEGER without throwing', () => { - const result = durationToString(Number.MAX_SAFE_INTEGER); - expect(result).toMatch(/^\d+ years$/); - }); - - it('handles Infinity by returning a years string', () => { - // Math.abs(Infinity) / 1000 = Infinity; Math.floor(Infinity) is Infinity → "Infinity year(s)" - const result = durationToString(Infinity); - expect(result).toBe('Infinity years'); - }); - - it('handles -Infinity by treating absolute value', () => { - const result = durationToString(-Infinity); - expect(result).toBe('Infinity years'); - }); - - it('handles NaN gracefully (NaN seconds → "NaN seconds")', () => { - // Math.floor(NaN) is NaN; Math.abs(NaN) is NaN; NaN / 1000 → NaN; comparisons all false - expect(durationToString(NaN)).toBe('NaN seconds'); - }); -}); - -describe('elapsedToString — boundary and edge cases', () => { - it('treats 0 as non-negative ("in 0 seconds")', () => { - expect(elapsedToString(0)).toBe('in 0 seconds'); - }); - - it('handles -0 as non-negative (since -0 < 0 is false)', () => { - expect(elapsedToString(-0)).toBe('in 0 seconds'); - }); - - it('handles a negative sub-second value (still non-zero negative)', () => { - // -500ms → isNegative true → "0 seconds ago" - expect(elapsedToString(-500)).toBe('0 seconds ago'); - }); - - it('formats large positive year value', () => { - expect(elapsedToString(2 * MS_IN_YEAR)).toBe('in 2 years'); - }); - - it('formats large negative year value', () => { - expect(elapsedToString(-2 * MS_IN_YEAR)).toBe('2 years ago'); - }); - - it('handles NaN (NaN < 0 is false → "in" prefix)', () => { - expect(elapsedToString(NaN)).toBe('in NaN seconds'); - }); -}); diff --git a/src/lib/utils/time.ts b/src/lib/utils/time.ts deleted file mode 100644 index a9b5cd59..00000000 --- a/src/lib/utils/time.ts +++ /dev/null @@ -1,41 +0,0 @@ -function formatInterval(unit: string, interval: number) { - interval = Math.floor(interval); - return interval + ' ' + unit + (interval != 1 ? 's' : ''); -} - -export function durationToString(durationMs: number) { - const seconds = Math.floor(Math.abs(durationMs) / 1000); - - let interval = seconds / 31536000; - if (interval >= 1) return formatInterval('year', interval); - - interval = seconds / 2592000; - if (interval >= 1) return formatInterval('month', interval); - - interval = seconds / 86400; - if (interval >= 1) return formatInterval('day', interval); - - interval = seconds / 3600; - if (interval >= 1) return formatInterval('hour', interval); - - interval = seconds / 60; - if (interval >= 1) return formatInterval('minute', interval); - - return formatInterval('second', seconds); -} - -export function elapsedToString(elapsedMs: number) { - const isNegative = elapsedMs < 0; - const duration = durationToString(elapsedMs); - - return isNegative ? duration + ' ago' : 'in ' + duration; -} - -/** - * Formats a shocker control/limit duration (given in seconds) for display: always - * in seconds with an `s` unit, rounded to one decimal (durations step in 0.1s), - * dropping a trailing `.0` for whole numbers. e.g. 15 -> "15s", 1.5 -> "1.5s". - */ -export function formatDurationSeconds(seconds: number) { - return `${Math.round(seconds * 10) / 10}s`; -} diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index a8775d51..fb1a9840 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -2,8 +2,8 @@ import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import { page } from '$app/state'; - import Container from '$lib/components/Container.svelte'; - import { Spinner } from '$lib/components/ui/spinner'; + import { Container } from '@openshock/svelte-core/components'; + import { Spinner } from '@openshock/svelte-core/ui/spinner'; import { AuthStatus, authState } from '$lib/state/auth-state.svelte'; import type { Snippet } from 'svelte'; diff --git a/src/routes/(app)/admin/+layout.svelte b/src/routes/(app)/admin/+layout.svelte index a46a21a5..4ef8ff6a 100644 --- a/src/routes/(app)/admin/+layout.svelte +++ b/src/routes/(app)/admin/+layout.svelte @@ -5,7 +5,7 @@ diff --git a/src/routes/Footer.svelte b/src/routes/Footer.svelte index 4073f509..fa1db6ac 100644 --- a/src/routes/Footer.svelte +++ b/src/routes/Footer.svelte @@ -5,8 +5,8 @@ import { PUBLIC_GITHUB_PROJECT_URL } from '$env/static/public'; import { getConnectionState } from '$lib/signalr/user.svelte'; import { Wifi, WifiOff } from '@lucide/svelte'; - import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js'; - import { Button } from '$lib/components/ui/button'; + import * as DropdownMenu from '@openshock/svelte-core/ui/dropdown-menu'; + import { Button } from '@openshock/svelte-core/ui/button'; import { backendMetadata } from '$lib/state/backend-metadata-state.svelte'; diff --git a/src/routes/Header.svelte b/src/routes/Header.svelte index 28b57c79..47d390a9 100644 --- a/src/routes/Header.svelte +++ b/src/routes/Header.svelte @@ -5,20 +5,20 @@ import { goto } from '$app/navigation'; import type { Pathname } from '$app/types'; import { PUBLIC_DISCORD_INVITE_URL, PUBLIC_GITHUB_PROJECT_URL } from '$env/static/public'; - import LightSwitch from '$lib/components/LightSwitch.svelte'; - import DiscordLogo from '$lib/components/svg/DiscordLogo.svelte'; - import GithubIcon from '$lib/components/svg/GithubIcon.svelte'; - import { Button } from '$lib/components/ui/button'; - import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; - import { Separator } from '$lib/components/ui/separator'; - import { useSidebar } from '$lib/components/ui/sidebar'; + import { LightSwitch } from '@openshock/svelte-core/components'; + import { DiscordLogo } from '@openshock/svelte-core/components/svg'; + import { GithubIcon } from '@openshock/svelte-core/components/svg'; + import { Button } from '@openshock/svelte-core/ui/button'; + import * as DropdownMenu from '@openshock/svelte-core/ui/dropdown-menu'; + import { Separator } from '@openshock/svelte-core/ui/separator'; + import { useSidebar } from '@openshock/svelte-core/ui/sidebar'; import { userState } from '$lib/state/user-state.svelte'; - import { cn } from '$lib/utils'; + import { cn } from '@openshock/svelte-core/utils/shadcn'; import Breadcrumb from './Breadcrumb.svelte'; import { prefixBase } from '$lib/utils/url'; import { resolve } from '$app/paths'; import { LogIn, UserPlus } from '@lucide/svelte'; - import { Spinner } from '$lib/components/ui/spinner'; + import { Spinner } from '@openshock/svelte-core/ui/spinner'; let sidebar = useSidebar(); diff --git a/src/routes/Sidebar.svelte b/src/routes/Sidebar.svelte index 19cd189b..ca869317 100644 --- a/src/routes/Sidebar.svelte +++ b/src/routes/Sidebar.svelte @@ -34,9 +34,9 @@ MenuSubButton, Root, useSidebar, - } from '$lib/components/ui/sidebar'; + } from '@openshock/svelte-core/ui/sidebar'; import { userState } from '$lib/state/user-state.svelte'; - import type { AnyComponent } from '$lib/types/AnyComponent'; + import type { AnyComponent } from '@openshock/svelte-core/types/AnyComponent'; import { isSerialSupported } from '$lib/utils/compatibility'; import { Collapsible } from 'bits-ui'; import { prefixBase } from '$lib/utils/url'; diff --git a/src/routes/WelcomeScreen.svelte b/src/routes/WelcomeScreen.svelte index f9273c6f..012ca94d 100644 --- a/src/routes/WelcomeScreen.svelte +++ b/src/routes/WelcomeScreen.svelte @@ -6,10 +6,10 @@ /* eslint-disable svelte/no-navigation-without-resolve -- only contains external URLs */ import { asset } from '$app/paths'; import { PUBLIC_DISCORD_INVITE_URL, PUBLIC_GITHUB_PROJECT_URL } from '$env/static/public'; - import DiscordLogo from '$lib/components/svg/DiscordLogo.svelte'; - import GithubIcon from '$lib/components/svg/GithubIcon.svelte'; - import DotGrid from '$lib/components/DotGrid.svelte'; - import { Button } from '$lib/components/ui/button'; + import { DiscordLogo } from '@openshock/svelte-core/components/svg'; + import { GithubIcon } from '@openshock/svelte-core/components/svg'; + import { DotGrid } from '@openshock/svelte-core/components'; + import { Button } from '@openshock/svelte-core/ui/button'; import { markWelcomed, markTourCompleted, diff --git a/src/routes/report/api-tokens/+page.svelte b/src/routes/report/api-tokens/+page.svelte index 01a27422..b50c4c5b 100644 --- a/src/routes/report/api-tokens/+page.svelte +++ b/src/routes/report/api-tokens/+page.svelte @@ -9,13 +9,13 @@ import OctagonAlert from '@lucide/svelte/icons/octagon-alert'; import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; - import Container from '$lib/components/Container.svelte'; + import { Container } from '@openshock/svelte-core/components'; import Turnstile from '$lib/components/Turnstile.svelte'; - import { Button } from '$lib/components/ui/button'; - import * as Card from '$lib/components/ui/card'; - import { Checkbox } from '$lib/components/ui/checkbox'; - import { Label } from '$lib/components/ui/label'; - import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte'; + import { Button } from '@openshock/svelte-core/ui/button'; + import * as Card from '@openshock/svelte-core/ui/card'; + import { Checkbox } from '@openshock/svelte-core/ui/checkbox'; + import { Label } from '@openshock/svelte-core/ui/label'; + import { ScrollArea } from '@openshock/svelte-core/ui/scroll-area'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; import { registerBreadcrumbs } from '$lib/state/breadcrumbs-state.svelte'; import { toast } from 'svelte-sonner'; diff --git a/src/routes/shares/public/[shareId=guid]/+page.svelte b/src/routes/shares/public/[shareId=guid]/+page.svelte index 7b12abfb..c243d620 100644 --- a/src/routes/shares/public/[shareId=guid]/+page.svelte +++ b/src/routes/shares/public/[shareId=guid]/+page.svelte @@ -4,9 +4,9 @@ import { CircleUser, LogIn, Undo2 } from '@lucide/svelte'; import { resolve } from '$app/paths'; import { page } from '$app/state'; - import { Button } from '$lib/components/ui/button'; - import * as Card from '$lib/components/ui/card/index.js'; - import Input from '$lib/components/ui/input/input.svelte'; + import { Button } from '@openshock/svelte-core/ui/button'; + import * as Card from '@openshock/svelte-core/ui/card'; + import { Input } from '@openshock/svelte-core/ui/input'; import { handleApiError } from '$lib/errorhandling/apiErrorHandling'; import { registerBreadcrumbs } from '$lib/state/breadcrumbs-state.svelte'; import { userState } from '$lib/state/user-state.svelte'; diff --git a/src/routes/shares/public/[shareId=guid]/ControlView.svelte b/src/routes/shares/public/[shareId=guid]/ControlView.svelte index 77e05ca0..2a5aacd5 100644 --- a/src/routes/shares/public/[shareId=guid]/ControlView.svelte +++ b/src/routes/shares/public/[shareId=guid]/ControlView.svelte @@ -6,8 +6,8 @@ import LiveControlModule from '$lib/components/ControlModules/LiveControlModule.svelte'; import ShockerCard from '$lib/components/ControlModules/ShockerCard.svelte'; import { getPauseReason } from '$lib/utils'; - import * as Avatar from '$lib/components/ui/avatar'; - import * as Tooltip from '$lib/components/ui/tooltip/index.js'; + import * as Avatar from '@openshock/svelte-core/ui/avatar'; + import * as Tooltip from '@openshock/svelte-core/ui/tooltip'; import { ShareLinkSignalr } from '$lib/signalr/sharelink.svelte'; import type { Control } from '$lib/signalr/models/Control'; import { ControlType } from '$lib/signalr/models/ControlType'; @@ -19,9 +19,9 @@ } from '$lib/state/live-control-state.svelte'; import { onMount, untrack } from 'svelte'; import { page } from '$app/state'; - import CopyInput from '$lib/components/CopyInput.svelte'; + import { CopyInput } from '@openshock/svelte-core/components'; import { getSiteShortURL } from '$lib/utils/url'; - import { Button } from '$lib/components/ui/button'; + import { Button } from '@openshock/svelte-core/ui/button'; import { Pencil } from '@lucide/svelte'; import { userState } from '$lib/state/user-state.svelte'; import { resolve } from '$app/paths'; diff --git a/src/routes/terminal/+page.svelte b/src/routes/terminal/+page.svelte index 182b7df7..0ba06d64 100644 --- a/src/routes/terminal/+page.svelte +++ b/src/routes/terminal/+page.svelte @@ -9,17 +9,17 @@ } from '@lucide/svelte'; import { browser } from '$app/environment'; import type { FirmwareChannel } from '$lib/api/firmwareCDN'; - import Container from '$lib/components/Container.svelte'; + import { Container } from '@openshock/svelte-core/components'; import FirmwareChannelSelector from '$lib/components/FirmwareChannelSelector.svelte'; - import ChromeLogo from '$lib/components/svg/ChromeLogo.svelte'; - import EdgeLogo from '$lib/components/svg/EdgeLogo.svelte'; - import OperaLogo from '$lib/components/svg/OperaLogo.svelte'; - import { Button } from '$lib/components/ui/button'; - import * as Card from '$lib/components/ui/card'; - import { Checkbox } from '$lib/components/ui/checkbox'; - import * as Dialog from '$lib/components/ui/dialog'; - import { Label } from '$lib/components/ui/label'; - import { Progress } from '$lib/components/ui/progress'; + import { ChromeLogo } from '@openshock/svelte-core/components/svg'; + import { EdgeLogo } from '@openshock/svelte-core/components/svg'; + import { OperaLogo } from '@openshock/svelte-core/components/svg'; + import { Button } from '@openshock/svelte-core/ui/button'; + import * as Card from '@openshock/svelte-core/ui/card'; + import { Checkbox } from '@openshock/svelte-core/ui/checkbox'; + import * as Dialog from '@openshock/svelte-core/ui/dialog'; + import { Label } from '@openshock/svelte-core/ui/label'; + import { Progress } from '@openshock/svelte-core/ui/progress'; import { useSerial } from '$lib/utils/serial-context.svelte'; import { getBrowserName } from '$lib/utils/compatibility'; import DeviceConfigurator from './DeviceConfigurator.svelte'; diff --git a/src/routes/terminal/DeviceConfigurator.svelte b/src/routes/terminal/DeviceConfigurator.svelte index 81e231d1..1c616267 100644 --- a/src/routes/terminal/DeviceConfigurator.svelte +++ b/src/routes/terminal/DeviceConfigurator.svelte @@ -1,13 +1,13 @@