@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 +21 -0
- package/README.md +46 -0
- package/package.json +71 -0
- package/src/components/Alert.svelte +46 -0
- package/src/components/Avatar.svelte +22 -0
- package/src/components/Badge.svelte +33 -0
- package/src/components/Box.svelte +24 -0
- package/src/components/Button.svelte +97 -0
- package/src/components/Card.svelte +14 -0
- package/src/components/Checkbox.svelte +28 -0
- package/src/components/Divider.svelte +14 -0
- package/src/components/ElvoraProvider.svelte +11 -0
- package/src/components/Empty.svelte +26 -0
- package/src/components/Icon.svelte +24 -0
- package/src/components/IconButton.svelte +47 -0
- package/src/components/Input.svelte +42 -0
- package/src/components/Label.svelte +18 -0
- package/src/components/Modal.svelte +73 -0
- package/src/components/Progress.svelte +40 -0
- package/src/components/Skeleton.svelte +27 -0
- package/src/components/Spinner.svelte +18 -0
- package/src/components/Stack.svelte +28 -0
- package/src/components/Switch.svelte +48 -0
- package/src/components/Tag.svelte +39 -0
- package/src/components/Textarea.svelte +34 -0
- package/src/context.ts +25 -0
- package/src/index.ts +38 -0
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';
|