@lyeve-labs/ui-kit 0.8.2
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 +199 -0
- package/dist/components/Accordion.svelte +48 -0
- package/dist/components/Accordion.svelte.d.ts +10 -0
- package/dist/components/AccordionItem.svelte +55 -0
- package/dist/components/AccordionItem.svelte.d.ts +10 -0
- package/dist/components/Alert.svelte +60 -0
- package/dist/components/Alert.svelte.d.ts +13 -0
- package/dist/components/Autocomplete.svelte +198 -0
- package/dist/components/Autocomplete.svelte.d.ts +23 -0
- package/dist/components/Avatar.svelte +65 -0
- package/dist/components/Avatar.svelte.d.ts +9 -0
- package/dist/components/AvatarGroup.svelte +45 -0
- package/dist/components/AvatarGroup.svelte.d.ts +14 -0
- package/dist/components/Badge.svelte +48 -0
- package/dist/components/Badge.svelte.d.ts +13 -0
- package/dist/components/Banner.svelte +73 -0
- package/dist/components/Banner.svelte.d.ts +13 -0
- package/dist/components/Breadcrumb.svelte +47 -0
- package/dist/components/Breadcrumb.svelte.d.ts +10 -0
- package/dist/components/Button.svelte +71 -0
- package/dist/components/Button.svelte.d.ts +18 -0
- package/dist/components/ButtonGroup.svelte +33 -0
- package/dist/components/ButtonGroup.svelte.d.ts +9 -0
- package/dist/components/Card.svelte +72 -0
- package/dist/components/Card.svelte.d.ts +17 -0
- package/dist/components/Checkbox.svelte +80 -0
- package/dist/components/Checkbox.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +274 -0
- package/dist/components/DatePicker.svelte.d.ts +17 -0
- package/dist/components/Divider.svelte +23 -0
- package/dist/components/Divider.svelte.d.ts +8 -0
- package/dist/components/Drawer.svelte +120 -0
- package/dist/components/Drawer.svelte.d.ts +16 -0
- package/dist/components/Dropdown.svelte +88 -0
- package/dist/components/Dropdown.svelte.d.ts +23 -0
- package/dist/components/EmptyState.svelte +51 -0
- package/dist/components/EmptyState.svelte.d.ts +13 -0
- package/dist/components/FileInput.svelte +102 -0
- package/dist/components/FileInput.svelte.d.ts +15 -0
- package/dist/components/Indicator.svelte +38 -0
- package/dist/components/Indicator.svelte.d.ts +11 -0
- package/dist/components/Input.svelte +60 -0
- package/dist/components/Input.svelte.d.ts +17 -0
- package/dist/components/Kbd.svelte +18 -0
- package/dist/components/Kbd.svelte.d.ts +8 -0
- package/dist/components/Label.svelte +30 -0
- package/dist/components/Label.svelte.d.ts +11 -0
- package/dist/components/Modal.svelte +105 -0
- package/dist/components/Modal.svelte.d.ts +14 -0
- package/dist/components/MultiSelect.svelte +212 -0
- package/dist/components/MultiSelect.svelte.d.ts +22 -0
- package/dist/components/NumberInput.svelte +98 -0
- package/dist/components/NumberInput.svelte.d.ts +15 -0
- package/dist/components/PageHeader.svelte +22 -0
- package/dist/components/PageHeader.svelte.d.ts +10 -0
- package/dist/components/Pagination.svelte +112 -0
- package/dist/components/Pagination.svelte.d.ts +10 -0
- package/dist/components/Progress.svelte +70 -0
- package/dist/components/Progress.svelte.d.ts +15 -0
- package/dist/components/Radio.svelte +65 -0
- package/dist/components/Radio.svelte.d.ts +14 -0
- package/dist/components/RadioGroup.svelte +96 -0
- package/dist/components/RadioGroup.svelte.d.ts +22 -0
- package/dist/components/SearchInput.svelte +81 -0
- package/dist/components/SearchInput.svelte.d.ts +10 -0
- package/dist/components/Select.svelte +69 -0
- package/dist/components/Select.svelte.d.ts +18 -0
- package/dist/components/Skeleton.svelte +20 -0
- package/dist/components/Skeleton.svelte.d.ts +9 -0
- package/dist/components/Spinner.svelte +19 -0
- package/dist/components/Spinner.svelte.d.ts +7 -0
- package/dist/components/Stat.svelte +45 -0
- package/dist/components/Stat.svelte.d.ts +14 -0
- package/dist/components/StepIndicator.svelte +142 -0
- package/dist/components/StepIndicator.svelte.d.ts +13 -0
- package/dist/components/Table.svelte +36 -0
- package/dist/components/Table.svelte.d.ts +11 -0
- package/dist/components/Tabs.svelte +39 -0
- package/dist/components/Tabs.svelte.d.ts +13 -0
- package/dist/components/Tag.svelte +56 -0
- package/dist/components/Tag.svelte.d.ts +11 -0
- package/dist/components/Textarea.svelte +75 -0
- package/dist/components/Textarea.svelte.d.ts +26 -0
- package/dist/components/ThemeToggle.svelte +33 -0
- package/dist/components/ThemeToggle.svelte.d.ts +6 -0
- package/dist/components/Toaster.svelte +54 -0
- package/dist/components/Toaster.svelte.d.ts +18 -0
- package/dist/components/Toggle.svelte +74 -0
- package/dist/components/Toggle.svelte.d.ts +11 -0
- package/dist/components/Tooltip.svelte +44 -0
- package/dist/components/Tooltip.svelte.d.ts +11 -0
- package/dist/components/dialog/ConfirmDialog.svelte +95 -0
- package/dist/components/dialog/ConfirmDialog.svelte.d.ts +8 -0
- package/dist/components/dialog/Dialog.svelte +225 -0
- package/dist/components/dialog/Dialog.svelte.d.ts +9 -0
- package/dist/components/dialog/DialogContainer.svelte +17 -0
- package/dist/components/dialog/DialogContainer.svelte.d.ts +18 -0
- package/dist/components/dialog/dialog-manager.svelte.d.ts +54 -0
- package/dist/components/dialog/dialog-manager.svelte.js +164 -0
- package/dist/components/dialog/types.d.ts +33 -0
- package/dist/components/dialog/types.js +10 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +72 -0
- package/dist/stores/toast.svelte.d.ts +18 -0
- package/dist/stores/toast.svelte.js +28 -0
- package/dist/styles/theme.css +148 -0
- package/dist/utils/cn.d.ts +13 -0
- package/dist/utils/cn.js +24 -0
- package/dist/utils/theme.d.ts +26 -0
- package/dist/utils/theme.js +55 -0
- package/package.json +81 -0
- package/src/lib/styles/theme.css +148 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LyEve Labs
|
|
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,199 @@
|
|
|
1
|
+
# @lyeve/ui-kit
|
|
2
|
+
|
|
3
|
+
A clean, accessible Svelte 5 component library. The design system behind [LyEve](https://lyeve.com).
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://svelte.dev)
|
|
7
|
+
[](https://tailwindcss.com)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @lyeve/ui-kit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```svelte
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { Button, Card, toast } from '@lyeve/ui-kit';
|
|
16
|
+
import '@lyeve/ui-kit/styles.css';
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<Card title="Welcome">
|
|
20
|
+
<p>Three lines from a clean dark dashboard.</p>
|
|
21
|
+
<Button onclick={() => toast.success('Hello, world!')}>Try it</Button>
|
|
22
|
+
</Card>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
No config file, no theme provider, no setup ceremony.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What's in the box
|
|
30
|
+
|
|
31
|
+
- **48 components:** buttons, inputs, modals, drawers, tabs, tables, toasts, the works.
|
|
32
|
+
- **Two themes:** Soft Dark (default) and Soft Light, switched by a single `data-theme` attribute on `<html>`.
|
|
33
|
+
- **One CSS file:** `@lyeve/ui-kit/styles.css` declares every token; the rest is just Tailwind.
|
|
34
|
+
- **Svelte 5 native:** built on runes and snippets, fully typed end-to-end.
|
|
35
|
+
- **No surprises:** a `<Button />` is a `<button>`, an `<Input />` is an `<input>`. Markup matches the preview.
|
|
36
|
+
|
|
37
|
+
## Component list
|
|
38
|
+
|
|
39
|
+
<details>
|
|
40
|
+
<summary>48 components, organized by purpose</summary>
|
|
41
|
+
|
|
42
|
+
**Layout & display**
|
|
43
|
+
Card · PageHeader · Divider · Accordion · AccordionItem · Table
|
|
44
|
+
|
|
45
|
+
**Forms**
|
|
46
|
+
Button · ButtonGroup · Input · Textarea · NumberInput · SearchInput · FileInput · Label · Select · MultiSelect · Autocomplete · DatePicker · Checkbox · Radio · RadioGroup · Toggle
|
|
47
|
+
|
|
48
|
+
**Navigation**
|
|
49
|
+
Breadcrumb · Tabs · Pagination · StepIndicator · Dropdown
|
|
50
|
+
|
|
51
|
+
**Overlays**
|
|
52
|
+
Modal · Drawer · Tooltip · Dialog · DialogContainer · ConfirmDialog
|
|
53
|
+
|
|
54
|
+
**Feedback**
|
|
55
|
+
Alert · Banner · Badge · Tag · Indicator · Progress · Spinner · Skeleton · EmptyState · Stat · Kbd
|
|
56
|
+
|
|
57
|
+
**Media**
|
|
58
|
+
Avatar · AvatarGroup
|
|
59
|
+
|
|
60
|
+
**Theming**
|
|
61
|
+
ThemeToggle · Toaster (+ `toast` service)
|
|
62
|
+
|
|
63
|
+
</details>
|
|
64
|
+
|
|
65
|
+
## Requirements
|
|
66
|
+
|
|
67
|
+
- **Svelte 5.0** or newer
|
|
68
|
+
- **Tailwind CSS v4** with the `@tailwindcss/vite` plugin
|
|
69
|
+
- **Node 20** or newer
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pnpm add @lyeve/ui-kit
|
|
75
|
+
# or npm install @lyeve/ui-kit
|
|
76
|
+
# or yarn add @lyeve/ui-kit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If you don't have Tailwind v4 yet:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pnpm add -D tailwindcss @tailwindcss/vite
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// vite.config.ts
|
|
87
|
+
import { sveltekit } from '@sveltejs/vite-plugin-svelte';
|
|
88
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
89
|
+
import { defineConfig } from 'vite';
|
|
90
|
+
|
|
91
|
+
export default defineConfig({
|
|
92
|
+
plugins: [tailwindcss(), sveltekit()],
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Use
|
|
97
|
+
|
|
98
|
+
Import the stylesheet once at the top of your app's CSS entry:
|
|
99
|
+
|
|
100
|
+
```css
|
|
101
|
+
/* src/app.css */
|
|
102
|
+
@import '@lyeve/ui-kit/styles.css';
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then drop components into your routes:
|
|
106
|
+
|
|
107
|
+
```svelte
|
|
108
|
+
<script lang="ts">
|
|
109
|
+
import { Button, Card, Toaster, toast } from '@lyeve/ui-kit';
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<Card title="Profile" description="Update your details.">
|
|
113
|
+
<Button onclick={() => toast.success('Saved.')}>Save</Button>
|
|
114
|
+
</Card>
|
|
115
|
+
|
|
116
|
+
<Toaster />
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Mount the `<Toaster />` component in your root layout to surface toast notifications.
|
|
120
|
+
See `src/lib/components/Toaster.svelte` for the full prop list.
|
|
121
|
+
|
|
122
|
+
## Theming
|
|
123
|
+
|
|
124
|
+
Two themes ship out of the box. Switch with one attribute:
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<html data-theme="light">
|
|
128
|
+
<!-- light mode -->
|
|
129
|
+
</html>
|
|
130
|
+
<html>
|
|
131
|
+
<!-- dark mode (default) -->
|
|
132
|
+
</html>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Or use the helpers:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { setTheme, toggleTheme, getTheme } from '@lyeve/ui-kit';
|
|
139
|
+
|
|
140
|
+
setTheme('light');
|
|
141
|
+
toggleTheme(); // returns the new theme
|
|
142
|
+
getTheme(); // 'dark' | 'light'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Avoid the flash-of-wrong-theme by adding the boot script to your `app.html`.
|
|
146
|
+
Import `themeBootScript` from `@lyeve/ui-kit` and inline it in `<head>` before any
|
|
147
|
+
stylesheets.
|
|
148
|
+
|
|
149
|
+
Want to bend the palette to your own brand? Override individual tokens after
|
|
150
|
+
the import:
|
|
151
|
+
|
|
152
|
+
```css
|
|
153
|
+
@import '@lyeve/ui-kit/styles.css';
|
|
154
|
+
|
|
155
|
+
@theme {
|
|
156
|
+
--color-brand: #ff5e9c;
|
|
157
|
+
--color-brand-light: #ff85b3;
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Local development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pnpm install # install dependencies
|
|
165
|
+
pnpm test # run unit tests
|
|
166
|
+
pnpm check # type-check + svelte-check
|
|
167
|
+
pnpm build # svelte-package + publint → dist/
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The documentation site lives in its own repo (`lyeve-labs/ui-kit-docs`).
|
|
171
|
+
This repo is a single-purpose component library. Nothing but `src/lib/`.
|
|
172
|
+
|
|
173
|
+
## Project layout
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
src/
|
|
177
|
+
└── lib/ # → published as @lyeve/ui-kit
|
|
178
|
+
├── components/ # 48 .svelte files
|
|
179
|
+
├── stores/ # toast.svelte.ts
|
|
180
|
+
├── styles/ # theme.css (the one stylesheet)
|
|
181
|
+
├── utils/ # cn.ts, theme.ts
|
|
182
|
+
└── index.ts # public API
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Versioning
|
|
186
|
+
|
|
187
|
+
`@lyeve/ui-kit` follows [SemVer](https://semver.org). While under `1.0`,
|
|
188
|
+
breaking changes bump the **minor** version; additive changes bump the **patch**.
|
|
189
|
+
Every release is logged in [`CHANGELOG.md`](CHANGELOG.md) and on the docs site.
|
|
190
|
+
|
|
191
|
+
## Contributing
|
|
192
|
+
|
|
193
|
+
Bug reports, prop suggestions, and component proposals are welcome. See
|
|
194
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) for the development setup, coding
|
|
195
|
+
conventions, and how the kit stays cohesive.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setContext } from 'svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
multiple?: boolean;
|
|
7
|
+
flush?: boolean;
|
|
8
|
+
class?: string;
|
|
9
|
+
children: Snippet;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { multiple = false, flush = false, class: cls = '', children }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let openSet = $state<Set<string>>(new Set());
|
|
15
|
+
|
|
16
|
+
function toggle(id: string) {
|
|
17
|
+
if (openSet.has(id)) {
|
|
18
|
+
openSet.delete(id);
|
|
19
|
+
} else {
|
|
20
|
+
if (!multiple) openSet.clear();
|
|
21
|
+
openSet.add(id);
|
|
22
|
+
}
|
|
23
|
+
openSet = new Set(openSet);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isOpen(id: string): boolean {
|
|
27
|
+
return openSet.has(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setContext<{ isOpen: (id: string) => boolean; toggle: (id: string) => void; flush: boolean }>(
|
|
31
|
+
'accordion',
|
|
32
|
+
{
|
|
33
|
+
isOpen,
|
|
34
|
+
toggle,
|
|
35
|
+
get flush() {
|
|
36
|
+
return flush;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div
|
|
43
|
+
class="{flush
|
|
44
|
+
? 'divide-y divide-line'
|
|
45
|
+
: 'divide-y divide-line rounded-xl border border-line overflow-hidden'} {cls}"
|
|
46
|
+
>
|
|
47
|
+
{@render children()}
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
multiple?: boolean;
|
|
4
|
+
flush?: boolean;
|
|
5
|
+
class?: string;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const Accordion: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Accordion = ReturnType<typeof Accordion>;
|
|
10
|
+
export default Accordion;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
children: Snippet;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { id, title, class: cls = '', children }: Props = $props();
|
|
13
|
+
|
|
14
|
+
const ctx = getContext<
|
|
15
|
+
| {
|
|
16
|
+
isOpen: (id: string) => boolean;
|
|
17
|
+
toggle: (id: string) => void;
|
|
18
|
+
flush: boolean;
|
|
19
|
+
}
|
|
20
|
+
| undefined
|
|
21
|
+
>('accordion');
|
|
22
|
+
|
|
23
|
+
let open = $derived(ctx?.isOpen(id) ?? false);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="bg-surface {cls}">
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
onclick={() => ctx?.toggle(id)}
|
|
30
|
+
aria-expanded={open}
|
|
31
|
+
class="flex w-full items-center justify-between px-5 py-4 text-left text-sm font-medium
|
|
32
|
+
text-fg transition-colors hover:bg-surface-2/50"
|
|
33
|
+
>
|
|
34
|
+
<span>{title}</span>
|
|
35
|
+
<svg
|
|
36
|
+
width="16"
|
|
37
|
+
height="16"
|
|
38
|
+
viewBox="0 0 24 24"
|
|
39
|
+
fill="none"
|
|
40
|
+
stroke="currentColor"
|
|
41
|
+
stroke-width="2"
|
|
42
|
+
stroke-linecap="round"
|
|
43
|
+
stroke-linejoin="round"
|
|
44
|
+
class="shrink-0 text-faint transition-transform {open ? 'rotate-180' : ''}"
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
>
|
|
47
|
+
<path d="M6 9l6 6 6-6" />
|
|
48
|
+
</svg>
|
|
49
|
+
</button>
|
|
50
|
+
{#if open}
|
|
51
|
+
<div class="px-5 pb-4 text-sm text-muted">
|
|
52
|
+
{@render children()}
|
|
53
|
+
</div>
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const AccordionItem: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type AccordionItem = ReturnType<typeof AccordionItem>;
|
|
10
|
+
export default AccordionItem;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Tone = 'info' | 'success' | 'warn' | 'danger';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
tone?: Tone;
|
|
8
|
+
title?: string;
|
|
9
|
+
dismissible?: boolean;
|
|
10
|
+
ondismiss?: () => void;
|
|
11
|
+
class?: string;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
tone = 'info',
|
|
17
|
+
title = undefined,
|
|
18
|
+
dismissible = false,
|
|
19
|
+
ondismiss = undefined,
|
|
20
|
+
class: klass = '',
|
|
21
|
+
children,
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
const tones: Record<Tone, { wrap: string; icon: string; mark: string }> = {
|
|
25
|
+
info: { wrap: 'bg-brand/8 border-brand/25', icon: 'text-brand', mark: 'ℹ' },
|
|
26
|
+
success: { wrap: 'bg-success/8 border-success/25', icon: 'text-success', mark: '✓' },
|
|
27
|
+
warn: { wrap: 'bg-warn/8 border-warn/25', icon: 'text-warn', mark: '!' },
|
|
28
|
+
danger: { wrap: 'bg-danger/8 border-danger/25', icon: 'text-danger', mark: '×' },
|
|
29
|
+
};
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div
|
|
33
|
+
class="flex items-start gap-3 rounded-lg border px-4 py-3 {tones[tone].wrap} {klass}"
|
|
34
|
+
role="alert"
|
|
35
|
+
>
|
|
36
|
+
<span
|
|
37
|
+
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-current text-xs font-bold {tones[
|
|
38
|
+
tone
|
|
39
|
+
].icon}"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
>
|
|
42
|
+
{tones[tone].mark}
|
|
43
|
+
</span>
|
|
44
|
+
<div class="flex-1 min-w-0">
|
|
45
|
+
{#if title}<p class="text-sm font-semibold text-fg">{title}</p>{/if}
|
|
46
|
+
{#if children}<div class="text-sm text-muted {title ? 'mt-0.5' : ''}">
|
|
47
|
+
{@render children()}
|
|
48
|
+
</div>{/if}
|
|
49
|
+
</div>
|
|
50
|
+
{#if dismissible}
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onclick={ondismiss}
|
|
54
|
+
class="text-faint hover:text-fg transition-colors text-lg leading-none -mt-0.5"
|
|
55
|
+
aria-label="Dismiss"
|
|
56
|
+
>
|
|
57
|
+
×
|
|
58
|
+
</button>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Tone = 'info' | 'success' | 'warn' | 'danger';
|
|
3
|
+
interface Props {
|
|
4
|
+
tone?: Tone;
|
|
5
|
+
title?: string;
|
|
6
|
+
dismissible?: boolean;
|
|
7
|
+
ondismiss?: () => void;
|
|
8
|
+
class?: string;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
}
|
|
11
|
+
declare const Alert: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type Alert = ReturnType<typeof Alert>;
|
|
13
|
+
export default Alert;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Autocomplete (combobox): type to filter a list, navigate with ↑/↓, select with
|
|
3
|
+
// Enter. Binds `value` to the chosen option's value; shows its label in the input.
|
|
4
|
+
interface Option {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
value = $bindable(''),
|
|
12
|
+
options,
|
|
13
|
+
label = undefined,
|
|
14
|
+
hint = undefined,
|
|
15
|
+
error = undefined,
|
|
16
|
+
placeholder = 'Type to search…',
|
|
17
|
+
disabled = false,
|
|
18
|
+
required = false,
|
|
19
|
+
id = undefined,
|
|
20
|
+
name = undefined,
|
|
21
|
+
allowClear = true,
|
|
22
|
+
class: cls = '',
|
|
23
|
+
onchange = undefined,
|
|
24
|
+
}: {
|
|
25
|
+
value?: string;
|
|
26
|
+
options: Option[];
|
|
27
|
+
label?: string;
|
|
28
|
+
hint?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
required?: boolean;
|
|
33
|
+
id?: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
allowClear?: boolean;
|
|
36
|
+
class?: string;
|
|
37
|
+
onchange?: (value: string) => void;
|
|
38
|
+
} = $props();
|
|
39
|
+
|
|
40
|
+
const uid = `ac-${Math.random().toString(36).slice(2, 9)}`;
|
|
41
|
+
const listId = $derived(id ? `${id}-list` : uid);
|
|
42
|
+
|
|
43
|
+
let open = $state(false);
|
|
44
|
+
let query = $state('');
|
|
45
|
+
let active = $state(0); // highlighted index
|
|
46
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
47
|
+
let dirty = $state(false); // user is editing the query vs. showing the selected label
|
|
48
|
+
|
|
49
|
+
const selectedLabel = $derived(options.find((o) => o.value === value)?.label ?? '');
|
|
50
|
+
const display = $derived(dirty ? query : selectedLabel);
|
|
51
|
+
const filtered = $derived(
|
|
52
|
+
dirty && query.trim()
|
|
53
|
+
? options.filter((o) => o.label.toLowerCase().includes(query.trim().toLowerCase()))
|
|
54
|
+
: options,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
function choose(opt: Option) {
|
|
58
|
+
if (opt.disabled) return;
|
|
59
|
+
value = opt.value;
|
|
60
|
+
onchange?.(opt.value);
|
|
61
|
+
query = '';
|
|
62
|
+
dirty = false;
|
|
63
|
+
open = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clear() {
|
|
67
|
+
value = '';
|
|
68
|
+
query = '';
|
|
69
|
+
dirty = false;
|
|
70
|
+
onchange?.('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onInput(e: Event & { currentTarget: HTMLInputElement }) {
|
|
74
|
+
query = e.currentTarget.value;
|
|
75
|
+
dirty = true;
|
|
76
|
+
open = true;
|
|
77
|
+
active = 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onKeydown(e: KeyboardEvent) {
|
|
81
|
+
if (e.key === 'ArrowDown') {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
open = true;
|
|
84
|
+
active = Math.min(active + 1, filtered.length - 1);
|
|
85
|
+
} else if (e.key === 'ArrowUp') {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
active = Math.max(active - 1, 0);
|
|
88
|
+
} else if (e.key === 'Enter') {
|
|
89
|
+
if (open && filtered[active]) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
choose(filtered[active]);
|
|
92
|
+
}
|
|
93
|
+
} else if (e.key === 'Escape') {
|
|
94
|
+
open = false;
|
|
95
|
+
dirty = false;
|
|
96
|
+
query = '';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleOutside(e: MouseEvent) {
|
|
101
|
+
if (containerEl && !containerEl.contains(e.target as Node)) {
|
|
102
|
+
open = false;
|
|
103
|
+
dirty = false;
|
|
104
|
+
query = '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
$effect(() => {
|
|
109
|
+
if (open) document.addEventListener('click', handleOutside, { capture: true });
|
|
110
|
+
return () => document.removeEventListener('click', handleOutside, { capture: true });
|
|
111
|
+
});
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<div class="flex flex-col gap-1.5 {cls}" bind:this={containerEl}>
|
|
115
|
+
{#if label}
|
|
116
|
+
<label for={id} class="text-sm font-medium text-fg">
|
|
117
|
+
{label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
|
|
118
|
+
</label>
|
|
119
|
+
{/if}
|
|
120
|
+
|
|
121
|
+
<div class="relative">
|
|
122
|
+
<input
|
|
123
|
+
{id}
|
|
124
|
+
{name}
|
|
125
|
+
{placeholder}
|
|
126
|
+
{disabled}
|
|
127
|
+
{required}
|
|
128
|
+
role="combobox"
|
|
129
|
+
aria-expanded={open}
|
|
130
|
+
aria-controls={listId}
|
|
131
|
+
aria-autocomplete="list"
|
|
132
|
+
autocomplete="off"
|
|
133
|
+
value={display}
|
|
134
|
+
oninput={onInput}
|
|
135
|
+
onkeydown={onKeydown}
|
|
136
|
+
onfocus={() => (open = true)}
|
|
137
|
+
onblur={() => setTimeout(() => (open = false), 150)}
|
|
138
|
+
class="w-full rounded-lg bg-surface-2 border px-3 py-2 text-sm text-fg placeholder:text-faint
|
|
139
|
+
transition-colors outline-none disabled:opacity-50 disabled:cursor-not-allowed
|
|
140
|
+
{allowClear && value ? 'pr-8' : 'pr-3'}
|
|
141
|
+
{error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand'}"
|
|
142
|
+
/>
|
|
143
|
+
{#if allowClear && value && !disabled}
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
aria-label="Clear"
|
|
147
|
+
onclick={clear}
|
|
148
|
+
class="absolute right-2.5 top-1/2 -translate-y-1/2 text-faint hover:text-fg leading-none"
|
|
149
|
+
>×</button
|
|
150
|
+
>
|
|
151
|
+
{/if}
|
|
152
|
+
|
|
153
|
+
{#if open}
|
|
154
|
+
<div
|
|
155
|
+
id={listId}
|
|
156
|
+
role="listbox"
|
|
157
|
+
class="absolute z-50 mt-1 w-full max-h-60 overflow-y-auto rounded-xl border border-line
|
|
158
|
+
bg-surface shadow-2xl py-1"
|
|
159
|
+
>
|
|
160
|
+
{#each filtered as opt, i (opt.value)}
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
role="option"
|
|
164
|
+
aria-selected={opt.value === value}
|
|
165
|
+
disabled={opt.disabled}
|
|
166
|
+
onmouseenter={() => (active = i)}
|
|
167
|
+
onclick={() => choose(opt)}
|
|
168
|
+
class="w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors
|
|
169
|
+
disabled:opacity-40 disabled:cursor-not-allowed
|
|
170
|
+
{i === active ? 'bg-surface-2' : ''}
|
|
171
|
+
{opt.value === value ? 'text-brand' : 'text-fg'}"
|
|
172
|
+
>
|
|
173
|
+
<span>{opt.label}</span>
|
|
174
|
+
{#if opt.value === value}
|
|
175
|
+
<svg width="12" height="10" viewBox="0 0 12 10" fill="none" aria-hidden="true">
|
|
176
|
+
<path
|
|
177
|
+
d="M1 5l3.5 3.5L11 1.5"
|
|
178
|
+
stroke="currentColor"
|
|
179
|
+
stroke-width="1.5"
|
|
180
|
+
stroke-linecap="round"
|
|
181
|
+
stroke-linejoin="round"
|
|
182
|
+
/>
|
|
183
|
+
</svg>
|
|
184
|
+
{/if}
|
|
185
|
+
</button>
|
|
186
|
+
{:else}
|
|
187
|
+
<p class="px-3 py-2 text-sm text-faint">No matches</p>
|
|
188
|
+
{/each}
|
|
189
|
+
</div>
|
|
190
|
+
{/if}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{#if error}
|
|
194
|
+
<p class="text-xs text-danger">{error}</p>
|
|
195
|
+
{:else if hint}
|
|
196
|
+
<p class="text-xs text-faint">{hint}</p>
|
|
197
|
+
{/if}
|
|
198
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface Option {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
type $$ComponentProps = {
|
|
7
|
+
value?: string;
|
|
8
|
+
options: Option[];
|
|
9
|
+
label?: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
id?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
allowClear?: boolean;
|
|
18
|
+
class?: string;
|
|
19
|
+
onchange?: (value: string) => void;
|
|
20
|
+
};
|
|
21
|
+
declare const Autocomplete: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
22
|
+
type Autocomplete = ReturnType<typeof Autocomplete>;
|
|
23
|
+
export default Autocomplete;
|