@human-synthesis/norns-ui 0.0.2 → 0.0.3
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/README.md +105 -1
- package/package.json +17 -15
- package/src/auto-import.js +21 -12
- package/src/components/Dialog.n +54 -0
- package/src/components/Dropdown.n +41 -0
- package/src/components/Field.n +29 -20
- package/src/components/Form.n +22 -0
- package/src/components/Popover.n +32 -0
- package/src/components/Sheet.n +55 -0
- package/src/components/Tabs.n +28 -0
- package/src/components/ToastProvider.n +16 -0
- package/src/components/Tooltip.n +34 -0
- package/src/index.js +10 -0
- package/src/lib/toast.svelte.js +42 -0
- package/src/styles/atoms.css +168 -0
- package/src/types/Dialog.d.ts +17 -0
- package/src/types/Dropdown.d.ts +23 -0
- package/src/types/Field.d.ts +14 -3
- package/src/types/Form.d.ts +17 -0
- package/src/types/Popover.d.ts +18 -0
- package/src/types/Sheet.d.ts +20 -0
- package/src/types/Tabs.d.ts +17 -0
- package/src/types/ToastProvider.d.ts +15 -0
- package/src/types/Tooltip.d.ts +15 -0
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
UI library for the [Norns](https://github.com/human-synthesis/norns) ecosystem — Pug + Civet components on Tailwind v4.
|
|
4
4
|
|
|
5
|
-
**Status: Phase
|
|
5
|
+
**Status: Phase 3 (behavior tier).** Currently ships `Btn`, `Form`, `Field`, `FieldGroup`, `Input`, `Textarea`, `Select`, `Checkbox`, `Radio`, `Switch`, the in-house `ToastProvider` + `toast()` helper, and the Bits-UI-backed `Dialog`, `Sheet`, `Popover`, `Dropdown`, `Tooltip`, `Tabs`. Data tier (Combobox, Listbox, Pagination, Accordion) and polish tier (Avatar, Badge, Spinner, …) land in Phase 4–5.
|
|
6
|
+
|
|
7
|
+
> **Required: scope your preprocessors to project files.** Bits UI ships TypeScript-typed `<script lang="ts">` source, and the default Norns preprocess pipeline (svelte-preprocess + auto-import) corrupts those node_modules files. Wrap each preprocessor with a `node_modules` guard so they no-op on third-party `.svelte` files; vite-plugin-svelte handles them natively from there. See the **Setup** section below for the four-line `scopeToProject` helper.
|
|
6
8
|
|
|
7
9
|
## Stack
|
|
8
10
|
|
|
@@ -11,6 +13,7 @@ UI library for the [Norns](https://github.com/human-synthesis/norns) ecosystem
|
|
|
11
13
|
- [Civet](https://civet.dev) — `<script>` language
|
|
12
14
|
- [Tailwind CSS v4](https://tailwindcss.com) — styling, **hard peer dep**
|
|
13
15
|
- [tailwind-merge](https://github.com/dcastil/tailwind-merge) — class deduplication
|
|
16
|
+
- [Bits UI](https://bits-ui.com) — headless behavior backing `Dialog`/`Popover`/`Tabs`/etc.
|
|
14
17
|
- `@human-synthesis/norns` `^0.0.7` — peer (`nornsAutoImport` for the registration flow)
|
|
15
18
|
|
|
16
19
|
## Install
|
|
@@ -53,6 +56,27 @@ Same shape goes in `svelte.config.js`'s `preprocess` array.
|
|
|
53
56
|
|
|
54
57
|
`componentDirs` resolves first — your `src/lib/components/Btn.n` silently shadows the library's `Btn` whenever you want to override.
|
|
55
58
|
|
|
59
|
+
### 1b. Scope project preprocessors to skip `node_modules`
|
|
60
|
+
|
|
61
|
+
Wrap `nornsPreprocess()` outputs and `nornsAutoImport(...)` so their hooks no-op on third-party `.svelte` files. Bits UI's `lang="ts"` source compiles cleanly under vite-plugin-svelte's native TS handling, but breaks under our project-side svelte-preprocess + auto-import pipeline.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
// svelte.config.js
|
|
65
|
+
const scopeToProject = (p) => ({
|
|
66
|
+
name: p.name,
|
|
67
|
+
markup: p.markup ? (a) => (a.filename?.includes('/node_modules/') ? null : p.markup(a)) : undefined,
|
|
68
|
+
script: p.script ? (a) => (a.filename?.includes('/node_modules/') ? null : p.script(a)) : undefined,
|
|
69
|
+
style: p.style ? (a) => (a.filename?.includes('/node_modules/') ? null : p.style(a)) : undefined
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export default nornsConfig({
|
|
73
|
+
preprocess: [
|
|
74
|
+
...nornsPreprocess().map(scopeToProject),
|
|
75
|
+
scopeToProject(nornsAutoImport({ /* … */ }))
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
56
80
|
### 2. Import the styles
|
|
57
81
|
|
|
58
82
|
```css
|
|
@@ -156,6 +180,86 @@ All accept `class?` (merged via `cn`), pull `id` + `hasError` from a parent `<Fi
|
|
|
156
180
|
|
|
157
181
|
Each has a hand-rolled `.d.ts` shim under `src/types/`.
|
|
158
182
|
|
|
183
|
+
### Phase 3 — behavior tier (Bits UI)
|
|
184
|
+
|
|
185
|
+
Headless behavior + accessibility from [Bits UI](https://bits-ui.com), wrapped with default Tailwind styling and a snippet-prop API designed for Pug.
|
|
186
|
+
|
|
187
|
+
#### `<Dialog>`
|
|
188
|
+
|
|
189
|
+
```pug
|
|
190
|
+
Dialog(bind:open!="{deleteOpen}" title="Delete note?" description="This cannot be undone.")
|
|
191
|
+
+snippet('trigger')
|
|
192
|
+
Btn(variant="danger" size="sm") Delete
|
|
193
|
+
+snippet('actions')
|
|
194
|
+
Btn(variant="ghost" onclick!="{() => deleteOpen = false}") Cancel
|
|
195
|
+
Btn(variant="danger" onclick!="{confirm}") Delete
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Props: `open` (bindable), `title`, `description`, `hideClose`, `trigger`, `actions`, `children`, `class`, `overlayClass`, `triggerClass`.
|
|
199
|
+
|
|
200
|
+
#### `<Sheet>`
|
|
201
|
+
|
|
202
|
+
Same API as `Dialog` plus `side?: 'top' | 'right' | 'bottom' | 'left'` (default `'right'`). Slides in from the chosen edge instead of centering.
|
|
203
|
+
|
|
204
|
+
#### `<Popover>` and `<Dropdown>`
|
|
205
|
+
|
|
206
|
+
```pug
|
|
207
|
+
Popover
|
|
208
|
+
+snippet('trigger')
|
|
209
|
+
Btn(variant="ghost" size="sm") Filters
|
|
210
|
+
.space-y-2
|
|
211
|
+
Field(label="Status")
|
|
212
|
+
Select(name="status")
|
|
213
|
+
option(value="all") All
|
|
214
|
+
option(value="open") Open
|
|
215
|
+
|
|
216
|
+
Dropdown(items!="{[{label:'Edit', onSelect: edit}, {separator: true}, {label:'Delete', onSelect: del}]}")
|
|
217
|
+
+snippet('trigger')
|
|
218
|
+
Btn(variant="ghost") ⋯
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `<Tooltip>`
|
|
222
|
+
|
|
223
|
+
```pug
|
|
224
|
+
Tooltip(content="Saved 2 minutes ago")
|
|
225
|
+
+snippet('trigger')
|
|
226
|
+
Btn(variant="ghost" size="sm") ⓘ
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### `<Tabs>`
|
|
230
|
+
|
|
231
|
+
```pug
|
|
232
|
+
+snippet('panelHuman')
|
|
233
|
+
p Play against another person on this device.
|
|
234
|
+
+snippet('panelCpu')
|
|
235
|
+
p Play against a simple CPU.
|
|
236
|
+
|
|
237
|
+
Tabs(bind:value!="{mode}" items!="{[
|
|
238
|
+
{ value: 'human', label: 'Human', panel: panelHuman },
|
|
239
|
+
{ value: 'cpu', label: 'CPU', panel: panelCpu }
|
|
240
|
+
]}")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
#### `<ToastProvider>` + `toast()`
|
|
244
|
+
|
|
245
|
+
Mount once near the app root, then call `toast()` from anywhere:
|
|
246
|
+
|
|
247
|
+
```pug
|
|
248
|
+
// +layout.n
|
|
249
|
+
ToastProvider
|
|
250
|
+
| {@render children?.()}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```civet
|
|
254
|
+
import { toast } from '@human-synthesis/norns-ui/toast'
|
|
255
|
+
|
|
256
|
+
handleSave := =>
|
|
257
|
+
await save()
|
|
258
|
+
toast 'Saved!', { variant: 'success' }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Variants: `info` (default), `success`, `warning`, `error`. Default duration 4000ms; pass `duration: 0` for sticky.
|
|
262
|
+
|
|
159
263
|
## Theming
|
|
160
264
|
|
|
161
265
|
Tokens live in [`src/styles/tokens.css`](src/styles/tokens.css) as a Tailwind v4 `@theme` block — colors, radii, sizes. Override by re-declaring `@theme { ... }` in your own `app.css` after the library import. Tailwind v4 merges layered theme blocks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@human-synthesis/norns-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "UI library for the Norns ecosystem — Pug + Civet components on Tailwind v4.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Daniel Teodoroiu (https://humansynthesis.ai)",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
".": "./src/index.js",
|
|
21
21
|
"./auto-import": "./src/auto-import.js",
|
|
22
22
|
"./cn": "./src/lib/cn.js",
|
|
23
|
+
"./toast": "./src/lib/toast.svelte.js",
|
|
23
24
|
"./styles": "./src/styles/index.css",
|
|
24
25
|
"./styles/tokens": "./src/styles/tokens.css",
|
|
25
26
|
"./styles/atoms": "./src/styles/atoms.css",
|
|
@@ -34,19 +35,20 @@
|
|
|
34
35
|
},
|
|
35
36
|
"peerDependenciesMeta": {
|
|
36
37
|
"@human-synthesis/norns": {
|
|
37
|
-
|
|
38
|
+
"optional": false
|
|
39
|
+
},
|
|
40
|
+
"@human-synthesis/norns-core": {
|
|
41
|
+
"optional": false
|
|
42
|
+
}
|
|
38
43
|
},
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
"publishConfig": {
|
|
50
|
-
"access": "public"
|
|
51
|
-
}
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"bits-ui": "^2.18.1",
|
|
46
|
+
"tailwind-merge": "^2.5.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
52
54
|
}
|
package/src/auto-import.js
CHANGED
|
@@ -12,18 +12,16 @@
|
|
|
12
12
|
* plugins: [
|
|
13
13
|
* nornsCivetPlugin(),
|
|
14
14
|
* nornsAutoImport({
|
|
15
|
-
* componentDirs: ['src/lib/components'],
|
|
16
|
-
* components: ui.components
|
|
15
|
+
* componentDirs: ['src/lib/components'],
|
|
16
|
+
* components: ui.components,
|
|
17
|
+
* helpers: [...DEFAULT_HELPERS, ...ui.helpers]
|
|
17
18
|
* }),
|
|
18
19
|
* sveltekit()
|
|
19
20
|
* ]
|
|
20
21
|
*
|
|
21
|
-
* Components are imported via bare specifiers so the consumer's
|
|
22
|
-
* `node_modules/@human-synthesis/norns-ui/...` is the resolution target.
|
|
23
|
-
*
|
|
24
22
|
* @typedef {Object} UIPreset
|
|
25
|
-
* @property {Record<string, string>} components
|
|
26
|
-
* @property {Array<{ from: string, imports: string[] }>} helpers
|
|
23
|
+
* @property {Record<string, string>} components
|
|
24
|
+
* @property {Array<{ from: string, imports: string[] }>} helpers
|
|
27
25
|
*
|
|
28
26
|
* @returns {UIPreset}
|
|
29
27
|
*/
|
|
@@ -42,14 +40,25 @@ export function presetUI() {
|
|
|
42
40
|
Select: '@human-synthesis/norns-ui/components/Select.n',
|
|
43
41
|
Checkbox: '@human-synthesis/norns-ui/components/Checkbox.n',
|
|
44
42
|
Radio: '@human-synthesis/norns-ui/components/Radio.n',
|
|
45
|
-
Switch: '@human-synthesis/norns-ui/components/Switch.n'
|
|
43
|
+
Switch: '@human-synthesis/norns-ui/components/Switch.n',
|
|
44
|
+
|
|
45
|
+
// Phase 3 — behavior tier (Bits UI)
|
|
46
|
+
Dialog: '@human-synthesis/norns-ui/components/Dialog.n',
|
|
47
|
+
Sheet: '@human-synthesis/norns-ui/components/Sheet.n',
|
|
48
|
+
Popover: '@human-synthesis/norns-ui/components/Popover.n',
|
|
49
|
+
Dropdown: '@human-synthesis/norns-ui/components/Dropdown.n',
|
|
50
|
+
Tooltip: '@human-synthesis/norns-ui/components/Tooltip.n',
|
|
51
|
+
Tabs: '@human-synthesis/norns-ui/components/Tabs.n',
|
|
52
|
+
ToastProvider: '@human-synthesis/norns-ui/components/ToastProvider.n'
|
|
46
53
|
|
|
47
|
-
// Phase
|
|
48
|
-
//
|
|
49
|
-
// Pagination, Avatar, Badge, Spinner, Progress, Skeleton, Icon
|
|
54
|
+
// Phase 4+ adds: Accordion, Listbox, Combobox, Pagination,
|
|
55
|
+
// Avatar, Badge, Spinner, Progress, Skeleton, Icon, Card
|
|
50
56
|
},
|
|
51
57
|
helpers: [
|
|
52
|
-
|
|
58
|
+
{
|
|
59
|
+
from: '@human-synthesis/norns-ui/toast',
|
|
60
|
+
imports: ['toast', 'notify', 'dismiss']
|
|
61
|
+
}
|
|
53
62
|
]
|
|
54
63
|
};
|
|
55
64
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
DialogRoot(bind:open)
|
|
2
|
+
+if('trigger')
|
|
3
|
+
DialogTrigger(class!="{triggerClass ?? undefined}")
|
|
4
|
+
| {@render trigger()}
|
|
5
|
+
DialogPortal
|
|
6
|
+
DialogOverlay(class!="{overlayClasses}")
|
|
7
|
+
DialogContent(class!="{contentClasses}")
|
|
8
|
+
+if('title')
|
|
9
|
+
DialogTitle.dialog-title
|
|
10
|
+
| {title}
|
|
11
|
+
+if('description')
|
|
12
|
+
DialogDescription.dialog-description
|
|
13
|
+
| {description}
|
|
14
|
+
+if('children')
|
|
15
|
+
.dialog-body
|
|
16
|
+
| {@render children()}
|
|
17
|
+
+if('actions')
|
|
18
|
+
.dialog-actions
|
|
19
|
+
| {@render actions()}
|
|
20
|
+
+if('!hideClose')
|
|
21
|
+
DialogClose.dialog-close(aria-label="Close")
|
|
22
|
+
| ✕
|
|
23
|
+
|
|
24
|
+
<script>
|
|
25
|
+
import { Dialog } from 'bits-ui'
|
|
26
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
Root: DialogRoot
|
|
30
|
+
Trigger: DialogTrigger
|
|
31
|
+
Portal: DialogPortal
|
|
32
|
+
Overlay: DialogOverlay
|
|
33
|
+
Content: DialogContent
|
|
34
|
+
Title: DialogTitle
|
|
35
|
+
Description: DialogDescription
|
|
36
|
+
Close: DialogClose
|
|
37
|
+
} := Dialog
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
open = $bindable(false)
|
|
41
|
+
title
|
|
42
|
+
description
|
|
43
|
+
hideClose = false
|
|
44
|
+
trigger
|
|
45
|
+
actions
|
|
46
|
+
children
|
|
47
|
+
triggerClass
|
|
48
|
+
overlayClass = ''
|
|
49
|
+
class: extra = ''
|
|
50
|
+
} .= $props()
|
|
51
|
+
|
|
52
|
+
overlayClasses := cn 'dialog-overlay', overlayClass
|
|
53
|
+
contentClasses := cn 'dialog-content', extra
|
|
54
|
+
</script>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
MenuRoot(bind:open)
|
|
2
|
+
+if('trigger')
|
|
3
|
+
MenuTrigger(class!="{triggerClass ?? undefined}")
|
|
4
|
+
| {@render trigger()}
|
|
5
|
+
MenuPortal
|
|
6
|
+
MenuContent(class!="{contentClasses}" side!="{side}" align!="{align}" sideOffset!="{sideOffset}")
|
|
7
|
+
+each('items as item, i')
|
|
8
|
+
+if('item.separator')
|
|
9
|
+
MenuSeparator.dropdown-separator
|
|
10
|
+
+if('!item.separator')
|
|
11
|
+
MenuItem.dropdown-item(disabled!="{item.disabled}" onSelect!="{item.onSelect}")
|
|
12
|
+
| {item.label}
|
|
13
|
+
| {@render children?.()}
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
import { DropdownMenu } from 'bits-ui'
|
|
17
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
Root: MenuRoot
|
|
21
|
+
Trigger: MenuTrigger
|
|
22
|
+
Portal: MenuPortal
|
|
23
|
+
Content: MenuContent
|
|
24
|
+
Item: MenuItem
|
|
25
|
+
Separator: MenuSeparator
|
|
26
|
+
} := DropdownMenu
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
open = $bindable(false)
|
|
30
|
+
side = 'bottom'
|
|
31
|
+
align = 'start'
|
|
32
|
+
sideOffset = 4
|
|
33
|
+
items = []
|
|
34
|
+
trigger
|
|
35
|
+
children
|
|
36
|
+
triggerClass
|
|
37
|
+
class: extra = ''
|
|
38
|
+
} .= $props()
|
|
39
|
+
|
|
40
|
+
contentClasses := cn 'dropdown-content', extra
|
|
41
|
+
</script>
|
package/src/components/Field.n
CHANGED
|
@@ -1,46 +1,55 @@
|
|
|
1
1
|
.field(class!="{classes}")
|
|
2
2
|
+if('label')
|
|
3
|
-
label.field-label(for!="{
|
|
3
|
+
label.field-label(for!="{resolvedId}")
|
|
4
4
|
| {label}
|
|
5
5
|
+if('required')
|
|
6
6
|
span.field-required *
|
|
7
7
|
| {@render children?.()}
|
|
8
|
-
+if('
|
|
9
|
-
p.field-error(role="alert" id!="{
|
|
10
|
-
| {
|
|
11
|
-
+if('!
|
|
12
|
-
p.field-help(id!="{
|
|
8
|
+
+if('resolvedError')
|
|
9
|
+
p.field-error(role="alert" id!="{resolvedId ? `${resolvedId}-error` : undefined}")
|
|
10
|
+
| {resolvedError}
|
|
11
|
+
+if('!resolvedError && help')
|
|
12
|
+
p.field-help(id!="{resolvedId ? `${resolvedId}-help` : undefined}")
|
|
13
13
|
| {help}
|
|
14
14
|
|
|
15
15
|
<script>
|
|
16
16
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
17
|
-
import { setContext } from 'svelte'
|
|
17
|
+
import { setContext, getContext } from 'svelte'
|
|
18
18
|
|
|
19
19
|
{
|
|
20
20
|
label
|
|
21
21
|
help
|
|
22
22
|
error
|
|
23
|
+
name
|
|
23
24
|
required = false
|
|
24
25
|
id: providedId
|
|
25
26
|
children
|
|
26
27
|
class: extra = ''
|
|
27
28
|
} := $props()
|
|
28
29
|
|
|
29
|
-
//
|
|
30
|
-
// 1. explicit prop wins
|
|
31
|
-
// 2.
|
|
32
|
-
// 3.
|
|
33
|
-
|
|
30
|
+
// Error resolution order:
|
|
31
|
+
// 1. explicit `error` prop wins (caller has full control)
|
|
32
|
+
// 2. else look up `name` in the parent <Form>'s errors map (auto-wired)
|
|
33
|
+
// 3. else no error
|
|
34
|
+
formCtx := getContext 'norns-ui:form'
|
|
35
|
+
resolvedError := $derived error ?? (name && formCtx?.errors?.[name]) ?? undefined
|
|
36
|
+
|
|
37
|
+
// Deterministic id derivation. SSR-safe (no random fallback):
|
|
38
|
+
// 1. explicit `id` prop wins
|
|
39
|
+
// 2. else `name` (single source of truth for submission + id)
|
|
40
|
+
// 3. else slugify the label
|
|
41
|
+
// 4. else undefined — no label, no <label for=> association needed
|
|
34
42
|
slugify := (s) => s.toLowerCase().trim().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, '')
|
|
35
|
-
resolvedId := providedId ?? (label && `field-${slugify(label)}`) ?? undefined
|
|
43
|
+
resolvedId := $derived providedId ?? name ?? (label && `field-${slugify(label)}`) ?? undefined
|
|
36
44
|
|
|
37
|
-
// Shared with descendants
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// Shared with descendants. Getters wrap $derived so context reads
|
|
46
|
+
// remain reactive in both SSR (synchronous) and client (post-mount)
|
|
47
|
+
// — `$effect` would only fire client-side, leaving SSR with stale
|
|
48
|
+
// initial values.
|
|
49
|
+
ctx := {
|
|
50
|
+
get id() { resolvedId }
|
|
51
|
+
get hasError() { !!resolvedError }
|
|
52
|
+
}
|
|
44
53
|
setContext 'norns-ui:field', ctx
|
|
45
54
|
|
|
46
55
|
classes := cn 'field', extra
|
package/src/components/Form.n
CHANGED
|
@@ -9,15 +9,37 @@ form(
|
|
|
9
9
|
|
|
10
10
|
<script>
|
|
11
11
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
12
|
+
import { setContext } from 'svelte'
|
|
12
13
|
|
|
13
14
|
{
|
|
14
15
|
method = 'POST'
|
|
15
16
|
action
|
|
16
17
|
enctype
|
|
17
18
|
onsubmit
|
|
19
|
+
form
|
|
18
20
|
children
|
|
19
21
|
class: extra = ''
|
|
20
22
|
} := $props()
|
|
21
23
|
|
|
24
|
+
// Errors-by-name map. Built reactively from the valibot issue list so
|
|
25
|
+
// descendant Fields can look up their own error by `name` without a
|
|
26
|
+
// per-page $derived helper.
|
|
27
|
+
//
|
|
28
|
+
// $derived (not $state + $effect) is critical for SSR — $effect runs
|
|
29
|
+
// only client-side, so an effect-based context would produce empty
|
|
30
|
+
// errors during initial server render.
|
|
31
|
+
errorsMap := $derived.by =>
|
|
32
|
+
out := {}
|
|
33
|
+
if form?.errors
|
|
34
|
+
for issue of form.errors
|
|
35
|
+
name := issue.path?.[0]?.key
|
|
36
|
+
if name && !out[name]
|
|
37
|
+
out[name] = issue.message
|
|
38
|
+
out
|
|
39
|
+
|
|
40
|
+
// Getter wraps the $derived so context consumers re-evaluate on read.
|
|
41
|
+
ctx := { get errors() { errorsMap } }
|
|
42
|
+
setContext 'norns-ui:form', ctx
|
|
43
|
+
|
|
22
44
|
classes := cn 'form', extra
|
|
23
45
|
</script>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
PopoverRoot(bind:open)
|
|
2
|
+
+if('trigger')
|
|
3
|
+
PopoverTrigger(class!="{triggerClass ?? undefined}")
|
|
4
|
+
| {@render trigger()}
|
|
5
|
+
PopoverPortal
|
|
6
|
+
PopoverContent(class!="{contentClasses}" side!="{side}" align!="{align}" sideOffset!="{sideOffset}")
|
|
7
|
+
| {@render children?.()}
|
|
8
|
+
|
|
9
|
+
<script>
|
|
10
|
+
import { Popover } from 'bits-ui'
|
|
11
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
Root: PopoverRoot
|
|
15
|
+
Trigger: PopoverTrigger
|
|
16
|
+
Portal: PopoverPortal
|
|
17
|
+
Content: PopoverContent
|
|
18
|
+
} := Popover
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
open = $bindable(false)
|
|
22
|
+
side = 'bottom'
|
|
23
|
+
align = 'center'
|
|
24
|
+
sideOffset = 8
|
|
25
|
+
trigger
|
|
26
|
+
children
|
|
27
|
+
triggerClass
|
|
28
|
+
class: extra = ''
|
|
29
|
+
} .= $props()
|
|
30
|
+
|
|
31
|
+
contentClasses := cn 'popover-content', extra
|
|
32
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
DialogRoot(bind:open)
|
|
2
|
+
+if('trigger')
|
|
3
|
+
DialogTrigger(class!="{triggerClass ?? undefined}")
|
|
4
|
+
| {@render trigger()}
|
|
5
|
+
DialogPortal
|
|
6
|
+
DialogOverlay(class!="{overlayClasses}")
|
|
7
|
+
DialogContent(class!="{contentClasses}" data-side!="{side}")
|
|
8
|
+
+if('title')
|
|
9
|
+
DialogTitle.dialog-title
|
|
10
|
+
| {title}
|
|
11
|
+
+if('description')
|
|
12
|
+
DialogDescription.dialog-description
|
|
13
|
+
| {description}
|
|
14
|
+
+if('children')
|
|
15
|
+
.sheet-body
|
|
16
|
+
| {@render children()}
|
|
17
|
+
+if('actions')
|
|
18
|
+
.dialog-actions
|
|
19
|
+
| {@render actions()}
|
|
20
|
+
+if('!hideClose')
|
|
21
|
+
DialogClose.dialog-close(aria-label="Close")
|
|
22
|
+
| ✕
|
|
23
|
+
|
|
24
|
+
<script>
|
|
25
|
+
import { Dialog } from 'bits-ui'
|
|
26
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
Root: DialogRoot
|
|
30
|
+
Trigger: DialogTrigger
|
|
31
|
+
Portal: DialogPortal
|
|
32
|
+
Overlay: DialogOverlay
|
|
33
|
+
Content: DialogContent
|
|
34
|
+
Title: DialogTitle
|
|
35
|
+
Description: DialogDescription
|
|
36
|
+
Close: DialogClose
|
|
37
|
+
} := Dialog
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
open = $bindable(false)
|
|
41
|
+
side = 'right'
|
|
42
|
+
title
|
|
43
|
+
description
|
|
44
|
+
hideClose = false
|
|
45
|
+
trigger
|
|
46
|
+
actions
|
|
47
|
+
children
|
|
48
|
+
triggerClass
|
|
49
|
+
overlayClass = ''
|
|
50
|
+
class: extra = ''
|
|
51
|
+
} .= $props()
|
|
52
|
+
|
|
53
|
+
overlayClasses := cn 'dialog-overlay', overlayClass
|
|
54
|
+
contentClasses := cn 'sheet-content', `sheet-${side}`, extra
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
TabsRoot(bind:value class!="{rootClasses}")
|
|
2
|
+
TabsList.tabs-list
|
|
3
|
+
+each('items as item (item.value)')
|
|
4
|
+
TabsTrigger.tabs-trigger(value!="{item.value}" disabled!="{item.disabled}")
|
|
5
|
+
| {item.label}
|
|
6
|
+
+each('items as item (item.value)')
|
|
7
|
+
TabsContent.tabs-content(value!="{item.value}")
|
|
8
|
+
| {@render item.panel?.()}
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
import { Tabs } from 'bits-ui'
|
|
12
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
Root: TabsRoot
|
|
16
|
+
List: TabsList
|
|
17
|
+
Trigger: TabsTrigger
|
|
18
|
+
Content: TabsContent
|
|
19
|
+
} := Tabs
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
value = $bindable()
|
|
23
|
+
items = []
|
|
24
|
+
class: extra = ''
|
|
25
|
+
} .= $props()
|
|
26
|
+
|
|
27
|
+
rootClasses := cn 'tabs-root', extra
|
|
28
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
div(role="region" aria-label="Notifications" class!="{regionClasses}")
|
|
2
|
+
+each('toastStore.items as item (item.id)')
|
|
3
|
+
div(class!="{`toast toast-${item.variant}`}" role="status" data-id!="{item.id}")
|
|
4
|
+
.toast-message
|
|
5
|
+
| {item.message}
|
|
6
|
+
button.toast-close(type="button" aria-label="Dismiss" onclick!="{() => dismiss(item.id)}")
|
|
7
|
+
| ✕
|
|
8
|
+
|
|
9
|
+
<script>
|
|
10
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
11
|
+
import { toastStore, dismiss } from '@human-synthesis/norns-ui/toast'
|
|
12
|
+
|
|
13
|
+
{ class: extra = '' } := $props()
|
|
14
|
+
|
|
15
|
+
regionClasses := cn 'toast-region', extra
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
TooltipProvider(delayDuration!="{delay}")
|
|
2
|
+
TooltipRoot
|
|
3
|
+
TooltipTrigger(class!="{triggerClass ?? undefined}")
|
|
4
|
+
| {@render trigger?.()}
|
|
5
|
+
TooltipPortal
|
|
6
|
+
TooltipContent(class!="{contentClasses}" side!="{side}" sideOffset!="{sideOffset}")
|
|
7
|
+
| {content ?? ''}
|
|
8
|
+
| {@render children?.()}
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
import { Tooltip } from 'bits-ui'
|
|
12
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
Provider: TooltipProvider
|
|
16
|
+
Root: TooltipRoot
|
|
17
|
+
Trigger: TooltipTrigger
|
|
18
|
+
Portal: TooltipPortal
|
|
19
|
+
Content: TooltipContent
|
|
20
|
+
} := Tooltip
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
content
|
|
24
|
+
side = 'top'
|
|
25
|
+
sideOffset = 6
|
|
26
|
+
delay = 300
|
|
27
|
+
trigger
|
|
28
|
+
children
|
|
29
|
+
triggerClass
|
|
30
|
+
class: extra = ''
|
|
31
|
+
} .= $props()
|
|
32
|
+
|
|
33
|
+
contentClasses := cn 'tooltip-content', extra
|
|
34
|
+
</script>
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,17 @@ export { default as Select } from './components/Select.n';
|
|
|
21
21
|
export { default as Switch } from './components/Switch.n';
|
|
22
22
|
export { default as Textarea } from './components/Textarea.n';
|
|
23
23
|
|
|
24
|
+
// Phase 3 — behavior tier (Bits UI)
|
|
25
|
+
export { default as Dialog } from './components/Dialog.n';
|
|
26
|
+
export { default as Sheet } from './components/Sheet.n';
|
|
27
|
+
export { default as Popover } from './components/Popover.n';
|
|
28
|
+
export { default as Dropdown } from './components/Dropdown.n';
|
|
29
|
+
export { default as Tooltip } from './components/Tooltip.n';
|
|
30
|
+
export { default as Tabs } from './components/Tabs.n';
|
|
31
|
+
export { default as ToastProvider } from './components/ToastProvider.n';
|
|
32
|
+
|
|
24
33
|
// Helpers
|
|
25
34
|
export { cn } from './lib/cn.js';
|
|
26
35
|
export { variantClasses } from './lib/variants.js';
|
|
27
36
|
export { presetUI } from './auto-import.js';
|
|
37
|
+
export { toast, notify, dismiss, clear } from './lib/toast.svelte.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast store + helpers. Single in-process queue, mounted once via
|
|
3
|
+
* <ToastProvider>. `toast(msg)` pushes from anywhere — server actions,
|
|
4
|
+
* client effects, button handlers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
let nextId = 0;
|
|
8
|
+
|
|
9
|
+
export const toastStore = $state({ items: [] });
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Push a toast. Returns the toast id so the caller can dismiss early.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} message
|
|
15
|
+
* @param {{ variant?: 'info' | 'success' | 'warning' | 'error', duration?: number }} [opts]
|
|
16
|
+
* `duration` in ms, default 4000. Set to 0 for sticky.
|
|
17
|
+
*/
|
|
18
|
+
export function toast(message, opts = {}) {
|
|
19
|
+
const id = ++nextId;
|
|
20
|
+
const item = {
|
|
21
|
+
id,
|
|
22
|
+
message,
|
|
23
|
+
variant: opts.variant ?? 'info',
|
|
24
|
+
duration: opts.duration ?? 4000
|
|
25
|
+
};
|
|
26
|
+
toastStore.items.push(item);
|
|
27
|
+
if (item.duration > 0) {
|
|
28
|
+
setTimeout(() => dismiss(id), item.duration);
|
|
29
|
+
}
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const notify = toast;
|
|
34
|
+
|
|
35
|
+
export function dismiss(id) {
|
|
36
|
+
const i = toastStore.items.findIndex((t) => t.id === id);
|
|
37
|
+
if (i >= 0) toastStore.items.splice(i, 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function clear() {
|
|
41
|
+
toastStore.items.length = 0;
|
|
42
|
+
}
|
package/src/styles/atoms.css
CHANGED
|
@@ -193,4 +193,172 @@
|
|
|
193
193
|
.form {
|
|
194
194
|
@apply space-y-4;
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
/* --- Dialog (modal) ---
|
|
198
|
+
* Bits UI toggles `data-state="open|closed"`. We use plain CSS transitions
|
|
199
|
+
* targeting that attribute for fade + scale; no `tailwindcss-animate`
|
|
200
|
+
* dependency, no v4 plugin.
|
|
201
|
+
*/
|
|
202
|
+
.dialog-overlay {
|
|
203
|
+
@apply fixed inset-0 z-50 bg-black/50 backdrop-blur-sm opacity-0 transition-opacity duration-150;
|
|
204
|
+
}
|
|
205
|
+
.dialog-overlay[data-state='open'] {
|
|
206
|
+
@apply opacity-100;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.dialog-content {
|
|
210
|
+
@apply fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2;
|
|
211
|
+
@apply rounded-lg border border-neutral-200 bg-white p-6 shadow-xl;
|
|
212
|
+
@apply opacity-0 scale-95 transition duration-150;
|
|
213
|
+
}
|
|
214
|
+
.dialog-content[data-state='open'] {
|
|
215
|
+
@apply opacity-100 scale-100;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.dialog-title {
|
|
219
|
+
@apply text-lg font-semibold leading-tight tracking-tight text-neutral-900;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.dialog-description {
|
|
223
|
+
@apply mt-1 text-sm text-neutral-600;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.dialog-body {
|
|
227
|
+
@apply mt-4;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.dialog-actions {
|
|
231
|
+
@apply mt-6 flex items-center justify-end gap-2;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.dialog-close {
|
|
235
|
+
@apply absolute right-3 top-3 inline-flex size-7 items-center justify-center rounded-md text-neutral-500;
|
|
236
|
+
@apply hover:bg-neutral-100 hover:text-neutral-900;
|
|
237
|
+
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* --- Sheet (side panel) --- */
|
|
241
|
+
.sheet-content {
|
|
242
|
+
@apply fixed z-50 border border-neutral-200 bg-white shadow-xl transition-transform duration-200 ease-out;
|
|
243
|
+
}
|
|
244
|
+
.sheet-right {
|
|
245
|
+
@apply right-0 top-0 h-full w-full max-w-md translate-x-full p-6;
|
|
246
|
+
}
|
|
247
|
+
.sheet-right[data-state='open'] {
|
|
248
|
+
@apply translate-x-0;
|
|
249
|
+
}
|
|
250
|
+
.sheet-left {
|
|
251
|
+
@apply left-0 top-0 h-full w-full max-w-md -translate-x-full p-6;
|
|
252
|
+
}
|
|
253
|
+
.sheet-left[data-state='open'] {
|
|
254
|
+
@apply translate-x-0;
|
|
255
|
+
}
|
|
256
|
+
.sheet-top {
|
|
257
|
+
@apply left-0 top-0 w-full -translate-y-full p-6;
|
|
258
|
+
}
|
|
259
|
+
.sheet-top[data-state='open'] {
|
|
260
|
+
@apply translate-y-0;
|
|
261
|
+
}
|
|
262
|
+
.sheet-bottom {
|
|
263
|
+
@apply bottom-0 left-0 w-full translate-y-full p-6;
|
|
264
|
+
}
|
|
265
|
+
.sheet-bottom[data-state='open'] {
|
|
266
|
+
@apply translate-y-0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.sheet-body {
|
|
270
|
+
@apply mt-4;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* --- Popover --- */
|
|
274
|
+
.popover-content {
|
|
275
|
+
@apply z-50 rounded-md border border-neutral-200 bg-white p-3 text-sm shadow-md;
|
|
276
|
+
@apply opacity-0 scale-95 transition duration-150;
|
|
277
|
+
}
|
|
278
|
+
.popover-content[data-state='open'] {
|
|
279
|
+
@apply opacity-100 scale-100;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* --- Dropdown menu --- */
|
|
283
|
+
.dropdown-content {
|
|
284
|
+
@apply z-50 min-w-[8rem] rounded-md border border-neutral-200 bg-white p-1 text-sm shadow-md;
|
|
285
|
+
@apply opacity-0 scale-95 transition duration-150;
|
|
286
|
+
}
|
|
287
|
+
.dropdown-content[data-state='open'] {
|
|
288
|
+
@apply opacity-100 scale-100;
|
|
289
|
+
}
|
|
290
|
+
.dropdown-item {
|
|
291
|
+
@apply flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm text-neutral-800 outline-none;
|
|
292
|
+
@apply data-[highlighted]:bg-neutral-100 data-[highlighted]:text-neutral-900;
|
|
293
|
+
@apply data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50;
|
|
294
|
+
}
|
|
295
|
+
.dropdown-separator {
|
|
296
|
+
@apply -mx-1 my-1 h-px bg-neutral-200;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* --- Tooltip --- */
|
|
300
|
+
.tooltip-content {
|
|
301
|
+
@apply z-50 overflow-hidden rounded-md bg-neutral-900 px-2 py-1 text-xs text-white shadow-md;
|
|
302
|
+
@apply opacity-0 scale-95 transition duration-150;
|
|
303
|
+
}
|
|
304
|
+
.tooltip-content[data-state='delayed-open'],
|
|
305
|
+
.tooltip-content[data-state='instant-open'] {
|
|
306
|
+
@apply opacity-100 scale-100;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* --- Tabs --- */
|
|
310
|
+
.tabs-root {
|
|
311
|
+
@apply space-y-3;
|
|
312
|
+
}
|
|
313
|
+
.tabs-list {
|
|
314
|
+
@apply inline-flex h-9 items-center justify-center rounded-md bg-neutral-100 p-1 text-neutral-600;
|
|
315
|
+
}
|
|
316
|
+
.tabs-trigger {
|
|
317
|
+
@apply inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 text-sm font-medium;
|
|
318
|
+
@apply transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500;
|
|
319
|
+
@apply data-[state=active]:bg-white data-[state=active]:text-neutral-900 data-[state=active]:shadow-sm;
|
|
320
|
+
@apply disabled:cursor-not-allowed disabled:opacity-50;
|
|
321
|
+
}
|
|
322
|
+
.tabs-content {
|
|
323
|
+
@apply focus-visible:outline-none;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* --- Toast --- */
|
|
327
|
+
.toast-region {
|
|
328
|
+
@apply pointer-events-none fixed bottom-4 right-4 z-[100] flex w-full max-w-sm flex-col gap-2;
|
|
329
|
+
}
|
|
330
|
+
.toast {
|
|
331
|
+
@apply pointer-events-auto flex items-start gap-3 rounded-md border border-neutral-200 bg-white p-3 shadow-md;
|
|
332
|
+
animation: norns-toast-in 200ms ease-out;
|
|
333
|
+
}
|
|
334
|
+
@keyframes norns-toast-in {
|
|
335
|
+
from {
|
|
336
|
+
opacity: 0;
|
|
337
|
+
transform: translateX(1rem);
|
|
338
|
+
}
|
|
339
|
+
to {
|
|
340
|
+
opacity: 1;
|
|
341
|
+
transform: translateX(0);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
.toast-info {
|
|
345
|
+
@apply border-neutral-200;
|
|
346
|
+
}
|
|
347
|
+
.toast-success {
|
|
348
|
+
@apply border-green-200 bg-green-50 text-green-900;
|
|
349
|
+
}
|
|
350
|
+
.toast-warning {
|
|
351
|
+
@apply border-amber-200 bg-amber-50 text-amber-900;
|
|
352
|
+
}
|
|
353
|
+
.toast-error {
|
|
354
|
+
@apply border-red-200 bg-red-50 text-red-900;
|
|
355
|
+
}
|
|
356
|
+
.toast-message {
|
|
357
|
+
@apply flex-1 text-sm leading-snug;
|
|
358
|
+
}
|
|
359
|
+
.toast-close {
|
|
360
|
+
@apply -mr-1 -mt-0.5 inline-flex size-6 shrink-0 items-center justify-center rounded text-neutral-500;
|
|
361
|
+
@apply hover:bg-neutral-100 hover:text-neutral-900;
|
|
362
|
+
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500;
|
|
363
|
+
}
|
|
196
364
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
|
|
3
|
+
export type DialogProps = {
|
|
4
|
+
open?: boolean;
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
hideClose?: boolean;
|
|
8
|
+
trigger?: Snippet;
|
|
9
|
+
actions?: Snippet;
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
triggerClass?: string;
|
|
12
|
+
overlayClass?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare const Dialog: Component<DialogProps>;
|
|
17
|
+
export default Dialog;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
|
|
3
|
+
export type DropdownItem = {
|
|
4
|
+
label?: string;
|
|
5
|
+
separator?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onSelect?: (event: Event) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type DropdownProps = {
|
|
11
|
+
open?: boolean;
|
|
12
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
13
|
+
align?: 'start' | 'center' | 'end';
|
|
14
|
+
sideOffset?: number;
|
|
15
|
+
items?: DropdownItem[];
|
|
16
|
+
trigger?: Snippet;
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
triggerClass?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
declare const Dropdown: Component<DropdownProps>;
|
|
23
|
+
export default Dropdown;
|
package/src/types/Field.d.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import type { Component, Snippet } from 'svelte';
|
|
2
2
|
|
|
3
3
|
export type FieldProps = {
|
|
4
|
-
/** Label text rendered above the control. Used as id-derivation fallback if no `id` is given. */
|
|
4
|
+
/** Label text rendered above the control. Used as id-derivation fallback if no `id`/`name` is given. */
|
|
5
5
|
label?: string;
|
|
6
6
|
/** Helper text shown below the control when there's no error. */
|
|
7
7
|
help?: string;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Explicit error message. Overrides any auto-resolved error from the
|
|
10
|
+
* parent `<Form form={...}>` context. Switches the inner control to
|
|
11
|
+
* error styling.
|
|
12
|
+
*/
|
|
9
13
|
error?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Field name — used to look up an auto-error from the parent `<Form>`
|
|
16
|
+
* context (matching `form.errors[*].path[0].key`) and as the default
|
|
17
|
+
* id for the inner control. When set, an inner `<Input>` doesn't need
|
|
18
|
+
* an explicit `name=` either if it inherits from this Field.
|
|
19
|
+
*/
|
|
20
|
+
name?: string;
|
|
10
21
|
/** Adds a red asterisk after the label. Cosmetic; pair with `required` on the actual input. */
|
|
11
22
|
required?: boolean;
|
|
12
|
-
/** Explicit id for the control. Otherwise
|
|
23
|
+
/** Explicit id for the control. Otherwise: `name`, then slugified `label`, then undefined. */
|
|
13
24
|
id?: string;
|
|
14
25
|
/** Optional class merged into the field wrapper. */
|
|
15
26
|
class?: string;
|
package/src/types/Form.d.ts
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import type { Component, Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLFormAttributes } from 'svelte/elements';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The shape returned by `fail(400, { errors, values })` from a SvelteKit
|
|
6
|
+
* action — what `+page.server.c`'s `page.actions` produces on validation
|
|
7
|
+
* failure (or a manually-constructed equivalent). `errors` is a valibot
|
|
8
|
+
* issue list; `values` is the raw form data echoed back for re-rendering.
|
|
9
|
+
*/
|
|
10
|
+
export type FormActionResult = {
|
|
11
|
+
errors?: Array<{ path?: Array<{ key?: string }>; message: string }>;
|
|
12
|
+
values?: Record<string, string>;
|
|
13
|
+
} | null | undefined;
|
|
14
|
+
|
|
4
15
|
export type FormProps = Omit<HTMLFormAttributes, 'class' | 'children'> & {
|
|
5
16
|
method?: 'GET' | 'POST';
|
|
6
17
|
action?: string;
|
|
7
18
|
enctype?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain';
|
|
19
|
+
/**
|
|
20
|
+
* Pass the page's `form` prop here. Form derives a `name → message` errors
|
|
21
|
+
* map and exposes it via context so descendant `<Field name="...">` looks
|
|
22
|
+
* up its own error automatically.
|
|
23
|
+
*/
|
|
24
|
+
form?: FormActionResult;
|
|
8
25
|
class?: string;
|
|
9
26
|
children?: Snippet;
|
|
10
27
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
|
|
3
|
+
export type PopoverSide = 'top' | 'right' | 'bottom' | 'left';
|
|
4
|
+
export type PopoverAlign = 'start' | 'center' | 'end';
|
|
5
|
+
|
|
6
|
+
export type PopoverProps = {
|
|
7
|
+
open?: boolean;
|
|
8
|
+
side?: PopoverSide;
|
|
9
|
+
align?: PopoverAlign;
|
|
10
|
+
sideOffset?: number;
|
|
11
|
+
trigger?: Snippet;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
triggerClass?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
declare const Popover: Component<PopoverProps>;
|
|
18
|
+
export default Popover;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
|
|
3
|
+
export type SheetSide = 'top' | 'right' | 'bottom' | 'left';
|
|
4
|
+
|
|
5
|
+
export type SheetProps = {
|
|
6
|
+
open?: boolean;
|
|
7
|
+
side?: SheetSide;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
hideClose?: boolean;
|
|
11
|
+
trigger?: Snippet;
|
|
12
|
+
actions?: Snippet;
|
|
13
|
+
children?: Snippet;
|
|
14
|
+
triggerClass?: string;
|
|
15
|
+
overlayClass?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
declare const Sheet: Component<SheetProps>;
|
|
20
|
+
export default Sheet;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
|
|
3
|
+
export type TabsItem = {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
panel?: Snippet;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type TabsProps = {
|
|
11
|
+
value?: string;
|
|
12
|
+
items?: TabsItem[];
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare const Tabs: Component<TabsProps>;
|
|
17
|
+
export default Tabs;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
|
|
3
|
+
export type ToastVariant = 'info' | 'success' | 'warning' | 'error';
|
|
4
|
+
|
|
5
|
+
export type ToastProviderProps = {
|
|
6
|
+
class?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type ToastOpts = {
|
|
10
|
+
variant?: ToastVariant;
|
|
11
|
+
duration?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
declare const ToastProvider: Component<ToastProviderProps>;
|
|
15
|
+
export default ToastProvider;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
|
|
3
|
+
export type TooltipProps = {
|
|
4
|
+
content?: string;
|
|
5
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
6
|
+
sideOffset?: number;
|
|
7
|
+
delay?: number;
|
|
8
|
+
trigger?: Snippet;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
triggerClass?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
declare const Tooltip: Component<TooltipProps>;
|
|
15
|
+
export default Tooltip;
|