@elvora/svelte 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elvora UI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @elvora/svelte
2
+
3
+ Elvora UI components for Svelte 4+ and Svelte 5.
4
+
5
+ This adapter ships components as **raw `.svelte` source files** — the
6
+ recommended distribution shape for Svelte component libraries. SvelteKit
7
+ (or any consumer using the official Svelte preprocessor) will compile them
8
+ on demand, ensuring users always get the right runtime for their Svelte
9
+ version.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @elvora/svelte svelte
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```svelte
20
+ <script lang="ts">
21
+ import { ElvoraProvider, ElvoraButton, ElvoraCard, ElvoraStack } from '@elvora/svelte';
22
+ import { defaultTheme } from '@elvora/themes';
23
+ </script>
24
+
25
+ <ElvoraProvider theme={defaultTheme}>
26
+ <ElvoraCard>
27
+ <ElvoraStack gap={12}>
28
+ <h2>Hello Elvora</h2>
29
+ <ElvoraButton variant="primary" on:click={() => alert('hi')}>Click me</ElvoraButton>
30
+ </ElvoraStack>
31
+ </ElvoraCard>
32
+ </ElvoraProvider>
33
+ ```
34
+
35
+ ## Available components (alpha)
36
+
37
+ `ElvoraProvider`, `ElvoraButton`, `ElvoraIconButton`, `ElvoraBox`, `ElvoraStack`,
38
+ `ElvoraCard`, `ElvoraAlert`, `ElvoraSpinner`, `ElvoraDivider`, `ElvoraTag`,
39
+ `ElvoraBadge`, `ElvoraAvatar`, `ElvoraIcon`, `ElvoraLabel`, `ElvoraInput`,
40
+ `ElvoraTextarea`, `ElvoraCheckbox`, `ElvoraSwitch`, `ElvoraProgress`,
41
+ `ElvoraSkeleton`, `ElvoraEmpty`, `ElvoraModal`.
42
+
43
+ The Svelte adapter consumes the same `@elvora/core` headless logic and
44
+ `@elvora/tokens` design tokens as the React, React Native, Angular, and
45
+ Vue adapters. Full Phase 2–7 parity is on the v1.1 roadmap; today the
46
+ Svelte surface focuses on the primitives most apps need on day one.
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@elvora/svelte",
3
+ "version": "1.0.0-rc.1",
4
+ "description": "Elvora UI components for Svelte 4 / Svelte 5 — same headless API as the React adapter, idiomatic Svelte stores, WCAG 2.1 AA.",
5
+ "license": "MIT",
6
+ "author": "Elvora UI Contributors",
7
+ "homepage": "https://github.com/elvora-ui/elvora/tree/main/packages/svelte#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/elvora-ui/elvora.git",
11
+ "directory": "packages/svelte"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/elvora-ui/elvora/issues"
15
+ },
16
+ "keywords": [
17
+ "svelte",
18
+ "sveltekit",
19
+ "ui",
20
+ "components",
21
+ "design-system",
22
+ "headless",
23
+ "accessibility",
24
+ "elvora"
25
+ ],
26
+ "type": "module",
27
+ "svelte": "./src/index.ts",
28
+ "types": "./src/index.ts",
29
+ "main": "./src/index.ts",
30
+ "module": "./src/index.ts",
31
+ "exports": {
32
+ ".": {
33
+ "svelte": "./src/index.ts",
34
+ "types": "./src/index.ts",
35
+ "import": "./src/index.ts"
36
+ },
37
+ "./*.svelte": {
38
+ "svelte": "./src/components/*.svelte",
39
+ "types": "./src/components/*.svelte.d.ts"
40
+ }
41
+ },
42
+ "files": [
43
+ "src",
44
+ "README.md"
45
+ ],
46
+ "sideEffects": false,
47
+ "peerDependencies": {
48
+ "svelte": ">=4.0.0"
49
+ },
50
+ "dependencies": {
51
+ "@elvora/themes": "1.0.0-rc.1",
52
+ "@elvora/tokens": "1.0.0-rc.1",
53
+ "@elvora/icons": "1.0.0-rc.1",
54
+ "@elvora/core": "1.0.0-rc.1"
55
+ },
56
+ "devDependencies": {
57
+ "svelte": "^5.0.0",
58
+ "typescript": "^5.7.2"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "scripts": {
64
+ "build": "echo 'Svelte components are shipped as source — no precompile step (consumed by SvelteKit / svelte preprocessor)'",
65
+ "dev": "echo 'no dev step'",
66
+ "typecheck": "echo 'svelte-check not configured yet'",
67
+ "lint": "echo 'no lint configured'",
68
+ "test": "echo 'no tests yet (requires svelte-testing-library)'",
69
+ "clean": "echo 'no dist to clean'"
70
+ }
71
+ }
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { ElvoraStatus } from '@elvora/core';
3
+ import { useTheme } from '../context';
4
+
5
+ export let status: ElvoraStatus = 'info';
6
+ export let title: string | undefined = undefined;
7
+ export let dismissible = false;
8
+
9
+ const themeStore = useTheme();
10
+ let dismissed = false;
11
+
12
+ $: intent =
13
+ status === 'success'
14
+ ? $themeStore.colors.intent.success
15
+ : status === 'warning'
16
+ ? $themeStore.colors.intent.warning
17
+ : status === 'error'
18
+ ? $themeStore.colors.intent.danger
19
+ : status === 'neutral'
20
+ ? $themeStore.colors.intent.neutral
21
+ : $themeStore.colors.intent.info;
22
+ </script>
23
+
24
+ {#if !dismissed}
25
+ <div
26
+ role="alert"
27
+ style={`display:flex;gap:8px;padding:12px 14px;border-radius:${$themeStore.radii.md};background:${intent.subtle};color:${intent.fg};border:1px solid ${intent.border};`}
28
+ >
29
+ <div style="flex:1">
30
+ {#if title}
31
+ <div style="font-weight:600;margin-bottom:2px">{title}</div>
32
+ {/if}
33
+ <slot />
34
+ </div>
35
+ {#if dismissible}
36
+ <button
37
+ type="button"
38
+ aria-label="Dismiss"
39
+ style="background:transparent;border:none;cursor:pointer;color:inherit;font-size:16px;line-height:1;padding:0;margin-left:4px"
40
+ on:click={() => (dismissed = true)}
41
+ >
42
+ ×
43
+ </button>
44
+ {/if}
45
+ </div>
46
+ {/if}
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let src: string | undefined = undefined;
5
+ export let alt = '';
6
+ export let initials: string | undefined = undefined;
7
+ export let size = 36;
8
+
9
+ const themeStore = useTheme();
10
+ </script>
11
+
12
+ <span
13
+ style={`display:inline-flex;align-items:center;justify-content:center;width:${size}px;height:${size}px;border-radius:50%;background:${$themeStore.colors.intent.neutral.subtle};color:${$themeStore.colors.fg};font-weight:600;font-size:${Math.max(10, size * 0.4)}px;overflow:hidden`}
14
+ >
15
+ {#if src}
16
+ <img {src} {alt} style="width:100%;height:100%;object-fit:cover" />
17
+ {:else if initials}
18
+ {initials}
19
+ {:else}
20
+ <slot />
21
+ {/if}
22
+ </span>
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import type { ElvoraStatus } from '@elvora/core';
3
+ import { useTheme } from '../context';
4
+
5
+ export let status: ElvoraStatus = 'info';
6
+ export let dot = false;
7
+
8
+ const themeStore = useTheme();
9
+
10
+ $: intent =
11
+ status === 'success'
12
+ ? $themeStore.colors.intent.success
13
+ : status === 'warning'
14
+ ? $themeStore.colors.intent.warning
15
+ : status === 'error'
16
+ ? $themeStore.colors.intent.danger
17
+ : status === 'neutral'
18
+ ? $themeStore.colors.intent.neutral
19
+ : $themeStore.colors.intent.info;
20
+ </script>
21
+
22
+ {#if dot}
23
+ <span
24
+ aria-hidden="true"
25
+ style={`display:inline-block;width:8px;height:8px;border-radius:50%;background:${intent.solid};`}
26
+ ></span>
27
+ {:else}
28
+ <span
29
+ style={`display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;border-radius:10px;background:${intent.solid};color:${intent.solidFg};font-size:11px;font-weight:600;`}
30
+ >
31
+ <slot />
32
+ </span>
33
+ {/if}
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ export let as: keyof HTMLElementTagNameMap = 'div';
3
+ export let padding: string | number | undefined = undefined;
4
+ export let margin: string | number | undefined = undefined;
5
+ export let background: string | undefined = undefined;
6
+ export let color: string | undefined = undefined;
7
+ export let borderRadius: string | number | undefined = undefined;
8
+
9
+ $: style = [
10
+ padding !== undefined ? `padding: ${typeof padding === 'number' ? `${padding}px` : padding};` : '',
11
+ margin !== undefined ? `margin: ${typeof margin === 'number' ? `${margin}px` : margin};` : '',
12
+ background ? `background: ${background};` : '',
13
+ color ? `color: ${color};` : '',
14
+ borderRadius !== undefined
15
+ ? `border-radius: ${typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius};`
16
+ : '',
17
+ ]
18
+ .filter(Boolean)
19
+ .join(' ');
20
+ </script>
21
+
22
+ <svelte:element this={as} {style}>
23
+ <slot />
24
+ </svelte:element>
@@ -0,0 +1,97 @@
1
+ <script lang="ts">
2
+ import {
3
+ defaultButtonProps,
4
+ getButtonStyle,
5
+ type ElvoraSize,
6
+ type ElvoraVariant,
7
+ type ElvoraTheme,
8
+ } from '@elvora/core';
9
+ import { useTheme } from '../context';
10
+
11
+ export let variant: ElvoraVariant = defaultButtonProps.variant;
12
+ export let size: ElvoraSize = defaultButtonProps.size;
13
+ export let intent: keyof ElvoraTheme['colors']['intent'] | undefined = undefined;
14
+ export let fullWidth = defaultButtonProps.fullWidth;
15
+ export let isLoading = defaultButtonProps.isLoading;
16
+ export let isDisabled = defaultButtonProps.isDisabled;
17
+ export let loadingText: string | undefined = defaultButtonProps.loadingText;
18
+ export let type: 'button' | 'submit' | 'reset' = defaultButtonProps.type;
19
+
20
+ const themeStore = useTheme();
21
+
22
+ let hovered = false;
23
+ let pressed = false;
24
+ let focusVisible = false;
25
+
26
+ $: inert = isDisabled || isLoading;
27
+ $: style = getButtonStyle({
28
+ theme: $themeStore,
29
+ variant,
30
+ size,
31
+ fullWidth,
32
+ isLoading,
33
+ isDisabled,
34
+ isPressed: pressed,
35
+ isHovered: hovered,
36
+ isFocusVisible: focusVisible,
37
+ intent,
38
+ });
39
+
40
+ function cssText(record: Record<string, unknown>): string {
41
+ return Object.entries(record)
42
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
43
+ .map(([k, v]) => {
44
+ const kebab = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
45
+ return `${kebab}: ${String(v)};`;
46
+ })
47
+ .join(' ');
48
+ }
49
+ </script>
50
+
51
+ <button
52
+ {type}
53
+ disabled={inert || undefined}
54
+ aria-disabled={inert || undefined}
55
+ aria-busy={isLoading || undefined}
56
+ data-variant={variant}
57
+ data-size={size}
58
+ data-loading={isLoading || undefined}
59
+ style={cssText(style.root as Record<string, unknown>)}
60
+ on:click
61
+ on:keydown
62
+ on:focus={() => (focusVisible = true)}
63
+ on:blur={() => (focusVisible = false)}
64
+ on:mouseenter={() => (hovered = true)}
65
+ on:mouseleave={() => {
66
+ hovered = false;
67
+ pressed = false;
68
+ }}
69
+ on:mousedown={() => (pressed = true)}
70
+ on:mouseup={() => (pressed = false)}
71
+ >
72
+ {#if isLoading}
73
+ <span aria-hidden="true" style="display:inline-flex;align-items:center;justify-content:center;margin-right:6px">
74
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style="animation: elvora-spin 0.75s linear infinite">
75
+ <circle cx="12" cy="12" r="10" stroke={style.spinnerColor} stroke-opacity="0.25" stroke-width="3" />
76
+ <path d="M22 12a10 10 0 0 1-10 10" stroke={style.spinnerColor} stroke-width="3" stroke-linecap="round" />
77
+ </svg>
78
+ </span>
79
+ {#if loadingText}<span class="sr-only">{loadingText}</span>{/if}
80
+ {/if}
81
+ <span style={cssText(style.label as Record<string, unknown>)}>
82
+ <slot />
83
+ </span>
84
+ </button>
85
+
86
+ <style>
87
+ @keyframes elvora-spin {
88
+ to { transform: rotate(360deg); }
89
+ }
90
+ .sr-only {
91
+ position: absolute;
92
+ width: 1px;
93
+ height: 1px;
94
+ overflow: hidden;
95
+ clip: rect(0 0 0 0);
96
+ }
97
+ </style>
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let bordered = true;
5
+ export let padding: number = 16;
6
+
7
+ const themeStore = useTheme();
8
+ </script>
9
+
10
+ <div
11
+ style={`background:${$themeStore.colors.surfaceElevated};color:${$themeStore.colors.fg};border-radius:${$themeStore.radii.md};padding:${padding}px;${bordered ? `border:1px solid ${$themeStore.colors.border};` : ''}box-shadow:${$themeStore.shadows.sm};`}
12
+ >
13
+ <slot />
14
+ </div>
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let checked = false;
5
+ export let isDisabled = false;
6
+ export let id: string | undefined = undefined;
7
+ export let name: string | undefined = undefined;
8
+
9
+ const themeStore = useTheme();
10
+ </script>
11
+
12
+ <label
13
+ style={`display:inline-flex;align-items:center;gap:8px;cursor:${isDisabled ? 'not-allowed' : 'pointer'};color:${$themeStore.colors.fg};font-size:14px;line-height:1.4;opacity:${isDisabled ? 0.6 : 1};`}
14
+ >
15
+ <input
16
+ {id}
17
+ {name}
18
+ type="checkbox"
19
+ bind:checked
20
+ disabled={isDisabled || undefined}
21
+ data-elvora="checkbox"
22
+ style="width:16px;height:16px;cursor:inherit;"
23
+ on:change
24
+ on:focus
25
+ on:blur
26
+ />
27
+ <span><slot /></span>
28
+ </label>
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let orientation: 'horizontal' | 'vertical' = 'horizontal';
5
+
6
+ const themeStore = useTheme();
7
+
8
+ $: style =
9
+ orientation === 'horizontal'
10
+ ? `height:1px;width:100%;background:${$themeStore.colors.border};`
11
+ : `width:1px;height:100%;align-self:stretch;background:${$themeStore.colors.border};`;
12
+ </script>
13
+
14
+ <div role="separator" aria-orientation={orientation} {style}></div>
@@ -0,0 +1,11 @@
1
+ <script lang="ts">
2
+ import type { ElvoraTheme } from '@elvora/core';
3
+ import { defaultTheme } from '@elvora/themes';
4
+ import { provideElvora } from '../context';
5
+
6
+ export let theme: ElvoraTheme = defaultTheme;
7
+
8
+ provideElvora(theme);
9
+ </script>
10
+
11
+ <slot />
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let title = 'No data';
5
+ export let description: string | undefined = undefined;
6
+
7
+ const themeStore = useTheme();
8
+ </script>
9
+
10
+ <div
11
+ data-elvora="empty"
12
+ style={`display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:24px;gap:8px;color:${$themeStore.colors.fgMuted};`}
13
+ >
14
+ <slot name="icon">
15
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
16
+ <circle cx="12" cy="12" r="9" />
17
+ <path d="M12 7v5" />
18
+ <circle cx="12" cy="16" r="0.5" fill="currentColor" />
19
+ </svg>
20
+ </slot>
21
+ <div style={`font-size:14px;font-weight:600;color:${$themeStore.colors.fg};`}>{title}</div>
22
+ {#if description}
23
+ <div style="font-size:13px;max-width:360px">{description}</div>
24
+ {/if}
25
+ <slot name="action" />
26
+ </div>
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ import { getIcon, type IconName } from '@elvora/icons';
3
+
4
+ export let name: IconName;
5
+ export let size = 16;
6
+ export let color = 'currentColor';
7
+
8
+ $: icon = getIcon(name);
9
+ </script>
10
+
11
+ <svg
12
+ width={size}
13
+ height={size}
14
+ viewBox="0 0 24 24"
15
+ fill={icon.stroke ? 'none' : color}
16
+ stroke={icon.stroke ? color : 'none'}
17
+ stroke-width={icon.strokeWidth ?? 1.75}
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ aria-hidden="true"
21
+ focusable="false"
22
+ >
23
+ <path d={icon.d} />
24
+ </svg>
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+ import type { ElvoraVariant } from '@elvora/core';
4
+
5
+ export let variant: ElvoraVariant = 'tertiary';
6
+ export let size: 'sm' | 'md' | 'lg' = 'md';
7
+ export let isDisabled = false;
8
+ export let isLoading = false;
9
+ /** Required: icon-only controls must announce a name. */
10
+ export let label: string;
11
+ export let type: 'button' | 'submit' | 'reset' = 'button';
12
+
13
+ const themeStore = useTheme();
14
+
15
+ const sizing: Record<'sm' | 'md' | 'lg', number> = { sm: 32, md: 40, lg: 48 };
16
+
17
+ $: dim = sizing[size];
18
+ $: bg =
19
+ variant === 'primary'
20
+ ? $themeStore.colors.intent.primary.solid
21
+ : variant === 'destructive'
22
+ ? $themeStore.colors.intent.danger.solid
23
+ : 'transparent';
24
+ $: fg =
25
+ variant === 'primary' || variant === 'destructive'
26
+ ? $themeStore.colors.intent.primary.solidFg
27
+ : $themeStore.colors.fg;
28
+ $: border =
29
+ variant === 'outline' ? `1px solid ${$themeStore.colors.border}` : 'none';
30
+ </script>
31
+
32
+ <button
33
+ {type}
34
+ aria-label={label}
35
+ disabled={isDisabled || isLoading || undefined}
36
+ aria-busy={isLoading || undefined}
37
+ data-elvora="icon-button"
38
+ data-variant={variant}
39
+ data-size={size}
40
+ style={`width:${dim}px;height:${dim}px;display:inline-flex;align-items:center;justify-content:center;background:${bg};color:${fg};border:${border};border-radius:${$themeStore.radii.md};cursor:${isDisabled ? 'not-allowed' : 'pointer'};opacity:${isDisabled ? 0.6 : 1};transition:background 120ms ease;`}
41
+ on:click
42
+ on:keydown
43
+ on:focus
44
+ on:blur
45
+ >
46
+ <slot />
47
+ </button>
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let value = '';
5
+ export let placeholder: string | undefined = undefined;
6
+ export let type: 'text' | 'email' | 'password' | 'tel' | 'url' | 'search' = 'text';
7
+ export let id: string | undefined = undefined;
8
+ export let isDisabled = false;
9
+ export let isInvalid = false;
10
+ export let isReadOnly = false;
11
+ export let size: 'sm' | 'md' | 'lg' = 'md';
12
+
13
+ const themeStore = useTheme();
14
+
15
+ const pad: Record<'sm' | 'md' | 'lg', { padY: number; font: number; minH: number }> = {
16
+ sm: { padY: 6, font: 13, minH: 32 },
17
+ md: { padY: 8, font: 14, minH: 40 },
18
+ lg: { padY: 10, font: 15, minH: 48 },
19
+ };
20
+
21
+ $: dims = pad[size];
22
+ $: borderColor = isInvalid
23
+ ? $themeStore.colors.intent.danger.border
24
+ : $themeStore.colors.border;
25
+ </script>
26
+
27
+ <input
28
+ {id}
29
+ {type}
30
+ {placeholder}
31
+ bind:value
32
+ disabled={isDisabled || undefined}
33
+ readonly={isReadOnly || undefined}
34
+ aria-invalid={isInvalid || undefined}
35
+ data-elvora="input"
36
+ style={`box-sizing:border-box;width:100%;padding:${dims.padY}px 12px;min-height:${dims.minH}px;font-size:${dims.font}px;line-height:1.4;color:${$themeStore.colors.fg};background:${$themeStore.colors.surface};border:1px solid ${borderColor};border-radius:${$themeStore.radii.md};outline:none;transition:border-color 120ms ease;`}
37
+ on:input
38
+ on:change
39
+ on:focus
40
+ on:blur
41
+ on:keydown
42
+ />
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let htmlFor: string | undefined = undefined;
5
+ export let isRequired = false;
6
+
7
+ const themeStore = useTheme();
8
+ </script>
9
+
10
+ <label
11
+ for={htmlFor}
12
+ style={`display:inline-flex;align-items:center;gap:4px;font-size:13px;font-weight:500;color:${$themeStore.colors.fg};`}
13
+ >
14
+ <slot />
15
+ {#if isRequired}
16
+ <span aria-hidden="true" style={`color:${$themeStore.colors.intent.danger.fg}`}>*</span>
17
+ {/if}
18
+ </label>
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy, createEventDispatcher } from 'svelte';
3
+ import { useTheme } from '../context';
4
+
5
+ export let isOpen = false;
6
+ export let title: string | undefined = undefined;
7
+ export let size: 'sm' | 'md' | 'lg' = 'md';
8
+ export let closeOnEscape = true;
9
+ export let closeOnOverlay = true;
10
+ export let closeLabel = 'Close';
11
+
12
+ const themeStore = useTheme();
13
+ const dispatch = createEventDispatcher<{ close: void }>();
14
+
15
+ const widths: Record<'sm' | 'md' | 'lg', number> = { sm: 360, md: 520, lg: 720 };
16
+ $: width = widths[size];
17
+
18
+ function close() {
19
+ dispatch('close');
20
+ }
21
+
22
+ function onKey(e: KeyboardEvent) {
23
+ if (closeOnEscape && e.key === 'Escape' && isOpen) close();
24
+ }
25
+
26
+ onMount(() => {
27
+ if (typeof window !== 'undefined') window.addEventListener('keydown', onKey);
28
+ });
29
+ onDestroy(() => {
30
+ if (typeof window !== 'undefined') window.removeEventListener('keydown', onKey);
31
+ });
32
+ </script>
33
+
34
+ {#if isOpen}
35
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
36
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
37
+ <div
38
+ role="presentation"
39
+ on:click={() => closeOnOverlay && close()}
40
+ style="position:fixed;inset:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:1000;padding:24px;"
41
+ >
42
+ <div
43
+ role="dialog"
44
+ aria-modal="true"
45
+ aria-label={title}
46
+ data-elvora="modal"
47
+ on:click|stopPropagation
48
+ style={`background:${$themeStore.colors.background};color:${$themeStore.colors.fg};border-radius:${$themeStore.radii.lg};box-shadow:0 20px 50px rgba(0,0,0,0.25);width:100%;max-width:${width}px;max-height:calc(100vh - 48px);overflow:auto;`}
49
+ >
50
+ {#if title || $$slots.header}
51
+ <div style={`display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid ${$themeStore.colors.border};`}>
52
+ <div style="font-weight:600;font-size:15px;">
53
+ <slot name="header">{title}</slot>
54
+ </div>
55
+ <button
56
+ type="button"
57
+ aria-label={closeLabel}
58
+ on:click={close}
59
+ style={`border:none;background:transparent;color:${$themeStore.colors.fgMuted};cursor:pointer;font-size:18px;line-height:1;padding:4px 8px;border-radius:${$themeStore.radii.md};`}
60
+ >
61
+ ×
62
+ </button>
63
+ </div>
64
+ {/if}
65
+ <div style="padding:20px"><slot /></div>
66
+ {#if $$slots.footer}
67
+ <div style={`padding:12px 20px;border-top:1px solid ${$themeStore.colors.border};display:flex;justify-content:flex-end;gap:8px;`}>
68
+ <slot name="footer" />
69
+ </div>
70
+ {/if}
71
+ </div>
72
+ </div>
73
+ {/if}
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let value: number | null = null;
5
+ export let max = 100;
6
+ export let label: string | undefined = undefined;
7
+
8
+ const themeStore = useTheme();
9
+
10
+ $: percent = value === null ? null : Math.max(0, Math.min(100, (value / max) * 100));
11
+ </script>
12
+
13
+ <div
14
+ role="progressbar"
15
+ aria-valuemin="0"
16
+ aria-valuemax={max}
17
+ aria-valuenow={value ?? undefined}
18
+ aria-label={label}
19
+ data-elvora="progress"
20
+ style={`width:100%;height:6px;background:${$themeStore.colors.surface};border-radius:999px;overflow:hidden;`}
21
+ >
22
+ {#if percent !== null}
23
+ <div
24
+ aria-hidden="true"
25
+ style={`height:100%;background:${$themeStore.colors.intent.primary.solid};border-radius:inherit;transition:width 200ms ease;width:${percent}%;`}
26
+ ></div>
27
+ {:else}
28
+ <div
29
+ aria-hidden="true"
30
+ style={`height:100%;width:40%;background:${$themeStore.colors.intent.primary.solid};border-radius:inherit;animation:elvora-indeterminate 1.4s infinite ease-in-out;`}
31
+ ></div>
32
+ {/if}
33
+ </div>
34
+
35
+ <style>
36
+ @keyframes elvora-indeterminate {
37
+ 0% { transform: translateX(-100%); }
38
+ 100% { transform: translateX(250%); }
39
+ }
40
+ </style>
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let width: number | string = '100%';
5
+ export let height: number | string = 16;
6
+ export let radius: number | string = 4;
7
+ export let shimmer = true;
8
+
9
+ const themeStore = useTheme();
10
+
11
+ function dim(v: number | string): string {
12
+ return typeof v === 'number' ? `${v}px` : v;
13
+ }
14
+ </script>
15
+
16
+ <div
17
+ aria-hidden="true"
18
+ data-elvora="skeleton"
19
+ style={`display:block;width:${dim(width)};height:${dim(height)};border-radius:${dim(radius)};background:${$themeStore.colors.surface};${shimmer ? 'background-image:linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);background-size:200px 100%;background-repeat:no-repeat;animation:elvora-shimmer 1.4s infinite linear;' : ''}`}
20
+ ></div>
21
+
22
+ <style>
23
+ @keyframes elvora-shimmer {
24
+ 0% { background-position: -200px 0; }
25
+ 100% { background-position: calc(100% + 200px) 0; }
26
+ }
27
+ </style>
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ export let size = 16;
3
+ export let color: string = 'currentColor';
4
+ export let label = 'Loading';
5
+ </script>
6
+
7
+ <span role="status" aria-label={label} style={`display:inline-flex;width:${size}px;height:${size}px`}>
8
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" style="animation: elvora-spin 0.75s linear infinite">
9
+ <circle cx="12" cy="12" r="10" stroke={color} stroke-opacity="0.25" stroke-width="3" />
10
+ <path d="M22 12a10 10 0 0 1-10 10" stroke={color} stroke-width="3" stroke-linecap="round" />
11
+ </svg>
12
+ </span>
13
+
14
+ <style>
15
+ @keyframes elvora-spin {
16
+ to { transform: rotate(360deg); }
17
+ }
18
+ </style>
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ export let direction: 'row' | 'column' = 'column';
3
+ export let gap: number | string = 8;
4
+ export let align: 'start' | 'center' | 'end' | 'stretch' | 'baseline' = 'stretch';
5
+ export let justify: 'start' | 'center' | 'end' | 'between' | 'around' = 'start';
6
+ export let wrap = false;
7
+
8
+ const alignMap: Record<string, string> = {
9
+ start: 'flex-start',
10
+ center: 'center',
11
+ end: 'flex-end',
12
+ stretch: 'stretch',
13
+ baseline: 'baseline',
14
+ };
15
+ const justifyMap: Record<string, string> = {
16
+ start: 'flex-start',
17
+ center: 'center',
18
+ end: 'flex-end',
19
+ between: 'space-between',
20
+ around: 'space-around',
21
+ };
22
+
23
+ $: style = `display:flex;flex-direction:${direction};gap:${typeof gap === 'number' ? `${gap}px` : gap};align-items:${alignMap[align]};justify-content:${justifyMap[justify]};flex-wrap:${wrap ? 'wrap' : 'nowrap'};`;
24
+ </script>
25
+
26
+ <div {style}>
27
+ <slot />
28
+ </div>
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let checked = false;
5
+ export let isDisabled = false;
6
+ export let id: string | undefined = undefined;
7
+ export let name: string | undefined = undefined;
8
+ export let label: string | undefined = undefined;
9
+
10
+ const themeStore = useTheme();
11
+
12
+ function toggle() {
13
+ if (isDisabled) return;
14
+ checked = !checked;
15
+ }
16
+
17
+ function onKey(e: KeyboardEvent) {
18
+ if (e.key === ' ' || e.key === 'Enter') {
19
+ e.preventDefault();
20
+ toggle();
21
+ }
22
+ }
23
+
24
+ $: trackBg = checked
25
+ ? $themeStore.colors.intent.primary.solid
26
+ : $themeStore.colors.borderStrong;
27
+ </script>
28
+
29
+ <button
30
+ {id}
31
+ type="button"
32
+ role="switch"
33
+ aria-checked={checked}
34
+ aria-label={label}
35
+ disabled={isDisabled || undefined}
36
+ data-elvora="switch"
37
+ style={`appearance:none;border:0;background:${trackBg};width:36px;height:20px;border-radius:999px;padding:2px;cursor:${isDisabled ? 'not-allowed' : 'pointer'};display:inline-flex;align-items:center;justify-content:${checked ? 'flex-end' : 'flex-start'};transition:background 150ms ease;opacity:${isDisabled ? 0.6 : 1};`}
38
+ on:click={toggle}
39
+ on:keydown={onKey}
40
+ on:focus
41
+ on:blur
42
+ >
43
+ <input type="hidden" {name} value={checked ? 'on' : ''} />
44
+ <span
45
+ aria-hidden="true"
46
+ style={`display:block;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.15);transition:transform 150ms ease;`}
47
+ ></span>
48
+ </button>
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import type { ElvoraStatus } from '@elvora/core';
3
+ import { createEventDispatcher } from 'svelte';
4
+ import { useTheme } from '../context';
5
+
6
+ export let status: ElvoraStatus = 'neutral';
7
+ export let closable = false;
8
+ export let closeLabel = 'Remove';
9
+
10
+ const themeStore = useTheme();
11
+ const dispatch = createEventDispatcher<{ close: void }>();
12
+
13
+ $: intent =
14
+ status === 'success'
15
+ ? $themeStore.colors.intent.success
16
+ : status === 'warning'
17
+ ? $themeStore.colors.intent.warning
18
+ : status === 'error'
19
+ ? $themeStore.colors.intent.danger
20
+ : status === 'info'
21
+ ? $themeStore.colors.intent.info
22
+ : $themeStore.colors.intent.neutral;
23
+ </script>
24
+
25
+ <span
26
+ style={`display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:${$themeStore.radii.full};background:${intent.subtle};color:${intent.fg};font-size:12px;line-height:1.4;`}
27
+ >
28
+ <slot />
29
+ {#if closable}
30
+ <button
31
+ type="button"
32
+ aria-label={closeLabel}
33
+ style="border:none;background:transparent;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:0"
34
+ on:click={() => dispatch('close')}
35
+ >
36
+ ×
37
+ </button>
38
+ {/if}
39
+ </span>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { useTheme } from '../context';
3
+
4
+ export let value = '';
5
+ export let placeholder: string | undefined = undefined;
6
+ export let id: string | undefined = undefined;
7
+ export let rows = 4;
8
+ export let isDisabled = false;
9
+ export let isInvalid = false;
10
+ export let isReadOnly = false;
11
+
12
+ const themeStore = useTheme();
13
+
14
+ $: borderColor = isInvalid
15
+ ? $themeStore.colors.intent.danger.border
16
+ : $themeStore.colors.border;
17
+ </script>
18
+
19
+ <textarea
20
+ {id}
21
+ {placeholder}
22
+ {rows}
23
+ bind:value
24
+ disabled={isDisabled || undefined}
25
+ readonly={isReadOnly || undefined}
26
+ aria-invalid={isInvalid || undefined}
27
+ data-elvora="textarea"
28
+ style={`box-sizing:border-box;width:100%;padding:10px 12px;font-size:14px;line-height:1.5;color:${$themeStore.colors.fg};background:${$themeStore.colors.surface};border:1px solid ${borderColor};border-radius:${$themeStore.radii.md};outline:none;resize:vertical;transition:border-color 120ms ease;`}
29
+ on:input
30
+ on:change
31
+ on:focus
32
+ on:blur
33
+ on:keydown
34
+ />
package/src/context.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { writable, type Writable } from 'svelte/store';
3
+ import { defaultTheme } from '@elvora/themes';
4
+ import type { ElvoraTheme } from '@elvora/core';
5
+
6
+ const THEME_KEY = Symbol('elvora-theme');
7
+
8
+ export interface ElvoraContextValue {
9
+ theme: Writable<ElvoraTheme>;
10
+ }
11
+
12
+ export function provideElvora(theme: ElvoraTheme = defaultTheme): ElvoraContextValue {
13
+ const store = writable(theme);
14
+ const ctx: ElvoraContextValue = { theme: store };
15
+ setContext(THEME_KEY, ctx);
16
+ return ctx;
17
+ }
18
+
19
+ export function useElvoraContext(): ElvoraContextValue {
20
+ return getContext<ElvoraContextValue>(THEME_KEY) ?? { theme: writable(defaultTheme) };
21
+ }
22
+
23
+ export function useTheme(): Writable<ElvoraTheme> {
24
+ return useElvoraContext().theme;
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ export { default as ElvoraProvider } from './components/ElvoraProvider.svelte';
2
+ export { default as ElvoraButton } from './components/Button.svelte';
3
+ export { default as ElvoraIconButton } from './components/IconButton.svelte';
4
+ export { default as ElvoraBox } from './components/Box.svelte';
5
+ export { default as ElvoraStack } from './components/Stack.svelte';
6
+ export { default as ElvoraCard } from './components/Card.svelte';
7
+ export { default as ElvoraAlert } from './components/Alert.svelte';
8
+ export { default as ElvoraSpinner } from './components/Spinner.svelte';
9
+ export { default as ElvoraDivider } from './components/Divider.svelte';
10
+ export { default as ElvoraTag } from './components/Tag.svelte';
11
+ export { default as ElvoraBadge } from './components/Badge.svelte';
12
+ export { default as ElvoraAvatar } from './components/Avatar.svelte';
13
+ export { default as ElvoraIcon } from './components/Icon.svelte';
14
+ export { default as ElvoraLabel } from './components/Label.svelte';
15
+ export { default as ElvoraInput } from './components/Input.svelte';
16
+ export { default as ElvoraTextarea } from './components/Textarea.svelte';
17
+ export { default as ElvoraCheckbox } from './components/Checkbox.svelte';
18
+ export { default as ElvoraSwitch } from './components/Switch.svelte';
19
+ export { default as ElvoraProgress } from './components/Progress.svelte';
20
+ export { default as ElvoraSkeleton } from './components/Skeleton.svelte';
21
+ export { default as ElvoraEmpty } from './components/Empty.svelte';
22
+ export { default as ElvoraModal } from './components/Modal.svelte';
23
+
24
+ export { provideElvora, useElvoraContext, useTheme } from './context';
25
+ export type { ElvoraContextValue } from './context';
26
+
27
+ export type {
28
+ ElvoraTheme,
29
+ ThemeColors,
30
+ IntentScale,
31
+ ElvoraSize,
32
+ ElvoraVariant,
33
+ ElvoraStatus,
34
+ ElvoraTone,
35
+ Direction,
36
+ Placement,
37
+ Orientation,
38
+ } from '@elvora/core';