@human-synthesis/norns-ui 0.0.5 → 0.0.7
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 +32 -20
- package/package.json +7 -3
- package/src/auto-import.js +1 -4
- package/src/components/Accordion.n +24 -23
- package/src/components/Autocomplete.n +108 -33
- package/src/components/Collapsible.n +8 -11
- package/src/components/ContextMenu.n +55 -36
- package/src/components/DatePicker.n +19 -31
- package/src/components/DateRangePicker.n +17 -29
- package/src/components/Dialog.n +35 -22
- package/src/components/Dropdown.n +54 -23
- package/src/components/MultiSelect.n +138 -36
- package/src/components/Popover.n +30 -15
- package/src/components/ScrollArea.n +9 -20
- package/src/components/Sheet.n +36 -22
- package/src/components/Tabs.n +39 -13
- package/src/components/TimePicker.n +52 -65
- package/src/components/ToggleButton.n +18 -12
- package/src/components/ToggleButtonGroup.n +31 -11
- package/src/components/Tooltip.n +52 -19
- package/src/index.js +1 -4
- package/src/lib/behaviors/clickOutside.svelte.js +54 -0
- package/src/lib/behaviors/escape.svelte.js +36 -0
- package/src/lib/behaviors/floating.svelte.js +137 -0
- package/src/lib/behaviors/focusTrap.svelte.js +80 -0
- package/src/lib/behaviors/index.js +14 -0
- package/src/lib/behaviors/portal.svelte.js +32 -0
- package/src/lib/behaviors/rovingTabindex.svelte.js +76 -0
- package/src/lib/behaviors/scrollLock.svelte.js +56 -0
- package/src/styles/atoms.css +628 -920
- package/src/styles/tokens.css +33 -0
- package/src/types/Collapsible.d.ts +1 -0
- package/src/types/ContextMenu.d.ts +6 -1
- package/src/types/DatePicker.d.ts +7 -2
- package/src/types/DateRangePicker.d.ts +3 -3
- package/src/types/Dialog.d.ts +4 -0
- package/src/types/Dropdown.d.ts +2 -1
- package/src/types/MultiSelect.d.ts +3 -0
- package/src/types/ScrollArea.d.ts +1 -1
- package/src/types/Sheet.d.ts +3 -0
- package/src/types/Tabs.d.ts +2 -0
- package/src/types/ToggleButton.d.ts +2 -1
- package/src/types/ToggleButtonGroup.d.ts +2 -1
- package/src/types/Tooltip.d.ts +3 -0
- package/src/components/Drawer.n +0 -49
- package/src/components/PreviewCard.n +0 -30
- package/src/components/RichTooltip.n +0 -33
- package/src/types/Drawer.d.ts +0 -18
- package/src/types/PreviewCard.d.ts +0 -16
- package/src/types/RichTooltip.d.ts +0 -16
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@human-synthesis/norns-ui`
|
|
2
2
|
|
|
3
|
-
**UI library for the Norns ecosystem.** Pug + Civet components on Tailwind v4, with
|
|
3
|
+
**UI library for the Norns ecosystem.** Pug + Civet components on Tailwind v4, with custom headless behaviors built on `@floating-ui/dom` and Iconify behind the icon system.
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
- [Pug](https://pugjs.org) — templates (in `.n` files)
|
|
9
9
|
- [Civet](https://civet.dev) — `<script>` language
|
|
10
10
|
- [Tailwind CSS v4](https://tailwindcss.com) — styling, **hard peer dep**
|
|
11
|
-
- [
|
|
11
|
+
- [@floating-ui/dom](https://floating-ui.com) — overlay positioning (Popover, Dropdown, Tooltip, ContextMenu, pickers)
|
|
12
|
+
- Custom in-tree headless behaviors (focus-trap, scroll-lock, click-outside, escape, portal, roving-tabindex) — exported at `@human-synthesis/norns-ui/behaviors`
|
|
12
13
|
- [Iconify](https://iconify.design) — icons; ships with [Lucide](https://lucide.dev) preset
|
|
13
14
|
- [tailwind-merge](https://github.com/dcastil/tailwind-merge) — class deduplication
|
|
14
15
|
- [`@human-synthesis/norns`](https://github.com/human-synthesis/norns) — peer (`nornsAutoImport`)
|
|
@@ -49,22 +50,6 @@ export default {
|
|
|
49
50
|
@import '@human-synthesis/norns-ui/styles';
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
`svelte.config.js` — wrap our preprocessors so they no-op on `node_modules`. Without this, svelte-preprocess corrupts Bits UI's `<script lang="ts">` source:
|
|
53
|
-
|
|
54
|
-
```js
|
|
55
|
-
const scopeToProject = (p) => ({
|
|
56
|
-
name: p.name,
|
|
57
|
-
markup: p.markup ? (a) => (a.filename?.includes('/node_modules/') ? null : p.markup(a)) : undefined,
|
|
58
|
-
script: p.script ? (a) => (a.filename?.includes('/node_modules/') ? null : p.script(a)) : undefined,
|
|
59
|
-
style: p.style ? (a) => (a.filename?.includes('/node_modules/') ? null : p.style(a)) : undefined
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
preprocess: [
|
|
63
|
-
...nornsPreprocess().map(scopeToProject),
|
|
64
|
-
scopeToProject(nornsAutoImport({ /* … */ }))
|
|
65
|
-
]
|
|
66
|
-
```
|
|
67
|
-
|
|
68
53
|
## Dark mode
|
|
69
54
|
|
|
70
55
|
Toggle via `<html data-theme="dark">` — every component swaps. Persist the choice with a small inline script in `app.html` to avoid FOUC:
|
|
@@ -115,15 +100,38 @@ Form(action="?/save" form!="{form}")
|
|
|
115
100
|
|
|
116
101
|
## What's in here
|
|
117
102
|
|
|
118
|
-
- **Atoms** (CSS-only `@layer components`): `.btn`, `.input`, `.field`, `.form`, `.checkbox`, `.radio`, `.switch`, `.card`, `.banner`, `.badge`, `.chip`, `.avatar`, `.skeleton`, `.progress`, `.norns-header`, `.hero`, `.stepper`, `.breadcrumbs`, `.pagination`, `.accordion`, `.carousel`, plus variants/sizes.
|
|
103
|
+
- **Atoms** (CSS-only `@layer components`): `.btn`, `.input`, `.field`, `.form`, `.checkbox`, `.radio`, `.switch`, `.card`, `.surface-elevated`, `.btn-icon`, `.banner`, `.badge`, `.chip`, `.avatar`, `.skeleton`, `.progress`, `.norns-header`, `.hero`, `.stepper`, `.breadcrumbs`, `.pagination`, `.accordion`, `.carousel`, plus variants/sizes.
|
|
119
104
|
- **Forms**: `Btn`, `Form`, `Field`, `FieldGroup`, `Input`, `Textarea`, `Select`, `Checkbox`, `Radio`, `Switch`.
|
|
120
|
-
- **Behavior** (
|
|
105
|
+
- **Behavior** (custom on `@floating-ui/dom`): `Dialog`, `Sheet`, `Popover`, `Dropdown`, `Tooltip`, `Tabs`, `Accordion`, `ContextMenu`, `Collapsible`.
|
|
121
106
|
- **Display**: `Card`, `Surface`, `Banner`, `Badge`, `Chip`, `Avatar`, `Skeleton`, `Progress`, `ProgressCircular`, `Icon`.
|
|
122
107
|
- **Composite**: `Header`, `HeroBanner`, `Stepper`, `Breadcrumbs`, `Pagination`, `Carousel`.
|
|
123
108
|
- **Toast**: `ToastProvider` + `toast()` / `notify()` / `dismiss()` from `@human-synthesis/norns-ui/toast`.
|
|
124
109
|
|
|
125
110
|
Hand-rolled `.d.ts` shims live under `src/types/`. Override any component by dropping `src/lib/components/<Name>.n` in your project — `nornsAutoImport`'s first-match-wins shadows the library silently.
|
|
126
111
|
|
|
112
|
+
## Behaviors
|
|
113
|
+
|
|
114
|
+
A small set of headless behaviors is exposed at `@human-synthesis/norns-ui/behaviors` for use in your own components:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import {
|
|
118
|
+
portal, clickOutside, escape, focusTrap,
|
|
119
|
+
scrollLock, useFloating, rovingTabindex
|
|
120
|
+
} from '@human-synthesis/norns-ui/behaviors';
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
These are Svelte 5 actions (`use:portal`, `use:clickOutside={handler}`, etc.) and a small `useFloating()` factory that wraps `@floating-ui/dom`. They're what every overlay component in this library is built on, so reaching for them in your own code keeps interactions consistent.
|
|
124
|
+
|
|
125
|
+
## Migrating from 0.0.5 → 0.0.6
|
|
126
|
+
|
|
127
|
+
bits-ui has been removed; overlays are now built on `@floating-ui/dom` plus the in-tree behaviors module. Most APIs are unchanged. The breaking changes:
|
|
128
|
+
|
|
129
|
+
- `<Drawer>` is gone. Use `<Sheet side="left">` (it now defaults to `'right'`; pass the side you want).
|
|
130
|
+
- `<RichTooltip>` is gone. Use `<Tooltip rich>`.
|
|
131
|
+
- `<PreviewCard>` is gone. Use `<Tooltip>` with the trigger as a link, e.g. `<Tooltip rich content="...">{#snippet trigger()}<a href=...>{/snippet}</Tooltip>`.
|
|
132
|
+
- `<ContextMenu>` no longer renders nested submenus. The `children` field on items is accepted but ignored.
|
|
133
|
+
- The `svelte.config.js` `scopeToProject` workaround is no longer needed (it existed only to keep svelte-preprocess from corrupting bits-ui's TypeScript source). Remove it from your config.
|
|
134
|
+
|
|
127
135
|
## Theming
|
|
128
136
|
|
|
129
137
|
Tokens are in [`src/styles/tokens.css`](src/styles/tokens.css) as a Tailwind v4 `@theme` block. The library exposes:
|
|
@@ -132,6 +140,10 @@ Tokens are in [`src/styles/tokens.css`](src/styles/tokens.css) as a Tailwind v4
|
|
|
132
140
|
- **Role tokens** that atoms reference (and that you should reach for in your own components): `--color-fg`, `--color-fg-muted`, `--color-fg-subtle`, `--color-bg`, `--color-bg-muted`, `--color-bg-subtle`, `--color-bg-elevated`, `--color-border`, `--color-border-strong`, `--color-ring`. These swap automatically on dark mode.
|
|
133
141
|
- **Feedback tokens**: `--color-{success,warning,danger,info}-{bg,fg,border}` for tinted surfaces.
|
|
134
142
|
- **Typography tokens**: `--font-sans`, `--font-mono`.
|
|
143
|
+
- **Sizing**: `--ui-radius-{sm,,lg,full}`, `--ui-h-{sm,md,lg}` (full button heights), `--ui-icon-btn-{sm,md,lg}` (square icon-button sizes).
|
|
144
|
+
- **Motion**: `--ui-motion-{fast,med,slow}` durations + `--ui-motion-ease`. All component transitions reference these.
|
|
145
|
+
- **Elevation**: `--ui-elev-{1,2,3}` (subtle / floating / modal). Dark mode swaps to deeper shadows.
|
|
146
|
+
- **Focus**: `--ui-focus-ring` — the single canonical focus indicator. Apply as `box-shadow: var(--ui-focus-ring)` on `:focus-visible`.
|
|
135
147
|
|
|
136
148
|
Override by re-declaring `@theme { … }` in your `app.css` after the library import.
|
|
137
149
|
|
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.7",
|
|
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)",
|
|
@@ -14,12 +14,16 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "bun test"
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"check:shims": "bun scripts/check-shims.mjs",
|
|
19
|
+
"check:shims:strict": "bun scripts/check-shims.mjs --strict"
|
|
18
20
|
},
|
|
19
21
|
"exports": {
|
|
20
22
|
".": "./src/index.js",
|
|
21
23
|
"./auto-import": "./src/auto-import.js",
|
|
22
24
|
"./cn": "./src/lib/cn.js",
|
|
25
|
+
"./behaviors": "./src/lib/behaviors/index.js",
|
|
26
|
+
"./behaviors/*": "./src/lib/behaviors/*",
|
|
23
27
|
"./toast": "./src/lib/toast.svelte.js",
|
|
24
28
|
"./styles": "./src/styles/index.css",
|
|
25
29
|
"./styles/tokens": "./src/styles/tokens.css",
|
|
@@ -44,10 +48,10 @@
|
|
|
44
48
|
}
|
|
45
49
|
},
|
|
46
50
|
"dependencies": {
|
|
51
|
+
"@floating-ui/dom": "^1.7.6",
|
|
47
52
|
"@fontsource-variable/outfit": "^5.2.8",
|
|
48
53
|
"@iconify-json/lucide": "^1.2.105",
|
|
49
54
|
"@iconify/svelte": "^5.2.1",
|
|
50
|
-
"bits-ui": "^2.18.1",
|
|
51
55
|
"motion": "^12.38.0",
|
|
52
56
|
"tailwind-merge": "^2.5.0"
|
|
53
57
|
},
|
package/src/auto-import.js
CHANGED
|
@@ -66,7 +66,6 @@ export function presetUI() {
|
|
|
66
66
|
|
|
67
67
|
// 0.0.4 — composite layer
|
|
68
68
|
Header: c('Header'),
|
|
69
|
-
Drawer: c('Drawer'),
|
|
70
69
|
HeroBanner: c('HeroBanner'),
|
|
71
70
|
Stepper: c('Stepper'),
|
|
72
71
|
Breadcrumbs: c('Breadcrumbs'),
|
|
@@ -90,12 +89,10 @@ export function presetUI() {
|
|
|
90
89
|
Video: c('Video'),
|
|
91
90
|
Timeline: c('Timeline'),
|
|
92
91
|
|
|
93
|
-
// 0.0.4 — wave B: medium
|
|
92
|
+
// 0.0.4 — wave B: medium-complexity (custom; ex Bits-UI-backed)
|
|
94
93
|
Autocomplete: c('Autocomplete'),
|
|
95
94
|
MultiSelect: c('MultiSelect'),
|
|
96
95
|
ContextMenu: c('ContextMenu'),
|
|
97
|
-
RichTooltip: c('RichTooltip'),
|
|
98
|
-
PreviewCard: c('PreviewCard'),
|
|
99
96
|
ScrollArea: c('ScrollArea'),
|
|
100
97
|
ColorPicker: c('ColorPicker'),
|
|
101
98
|
Uploader: c('Uploader'),
|
|
@@ -1,32 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.accordion-chevron
|
|
13
|
-
Icon(name="lucide:chevron-down" size="size-4")
|
|
14
|
-
AccordionContent.accordion-content
|
|
1
|
+
.accordion(class!="{classes}")
|
|
2
|
+
+each('items as item (item.value)')
|
|
3
|
+
details.accordion-item(
|
|
4
|
+
open!="{isOpen(item.value)}"
|
|
5
|
+
ontoggle!="{(e) => onToggle(item.value, e.target.open)}"
|
|
6
|
+
)
|
|
7
|
+
summary.accordion-trigger
|
|
8
|
+
span {item.title}
|
|
9
|
+
.accordion-chevron
|
|
10
|
+
Icon(name="lucide:chevron-down" size="size-4")
|
|
11
|
+
.accordion-content
|
|
15
12
|
| {@render item.content?.()}
|
|
16
13
|
|
|
17
14
|
<script>
|
|
18
|
-
import { Accordion } from 'bits-ui'
|
|
19
15
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
20
16
|
import Icon from './Icon.n'
|
|
21
17
|
|
|
22
|
-
{
|
|
23
|
-
Root: AccordionRoot
|
|
24
|
-
Item: AccordionItem
|
|
25
|
-
Header: AccordionHeader
|
|
26
|
-
Trigger: AccordionTrigger
|
|
27
|
-
Content: AccordionContent
|
|
28
|
-
} := Accordion
|
|
29
|
-
|
|
30
18
|
{
|
|
31
19
|
items = []
|
|
32
20
|
multiple = false
|
|
@@ -35,4 +23,17 @@ AccordionRoot(
|
|
|
35
23
|
} .= $props()
|
|
36
24
|
|
|
37
25
|
classes := cn 'accordion', extra
|
|
26
|
+
|
|
27
|
+
isOpen := (v) =>
|
|
28
|
+
if multiple
|
|
29
|
+
Array.isArray(value) && value.includes v
|
|
30
|
+
else
|
|
31
|
+
value === v
|
|
32
|
+
|
|
33
|
+
onToggle := (v, isNowOpen) =>
|
|
34
|
+
if multiple
|
|
35
|
+
cur := Array.isArray(value) ? value : []
|
|
36
|
+
value = isNowOpen ? [...new Set([...cur, v])] : cur.filter((x) => x !== v)
|
|
37
|
+
else
|
|
38
|
+
value = isNowOpen ? v : (value === v ? undefined : value)
|
|
38
39
|
</script>
|
|
@@ -1,39 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
.combobox-input
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
.combobox-input-wrap(use:refAction bind:this!="{wrapEl}")
|
|
2
|
+
input.input.combobox-input(
|
|
3
|
+
bind:this!="{inputEl}"
|
|
4
|
+
type="text"
|
|
5
|
+
bind:value!="{term}"
|
|
6
|
+
placeholder!="{placeholder}"
|
|
7
|
+
disabled!="{disabled}"
|
|
8
|
+
role="combobox"
|
|
9
|
+
aria-expanded!="{open}"
|
|
10
|
+
aria-controls!="{listboxId}"
|
|
11
|
+
aria-autocomplete="list"
|
|
12
|
+
aria-activedescendant!="{open && filtered[active] ? optionId(active) : undefined}"
|
|
13
|
+
id!="{resolvedId}"
|
|
14
|
+
aria-invalid!="{hasError ? 'true' : undefined}"
|
|
15
|
+
oninput!="{onInput}"
|
|
16
|
+
onkeydown!="{onKeyDown}"
|
|
17
|
+
onfocus!="{() => open = true}"
|
|
18
|
+
onblur!="{onBlur}"
|
|
19
|
+
)
|
|
20
|
+
button.combobox-trigger(type="button" tabindex="-1" aria-label="Toggle options" onclick!="{toggle}")
|
|
21
|
+
Icon(name="lucide:chevrons-up-down" size="size-4")
|
|
22
|
+
|
|
23
|
+
+if('open && filtered.length > 0')
|
|
24
|
+
ul(
|
|
25
|
+
use:portal
|
|
26
|
+
use:floatAction
|
|
27
|
+
use:clickOutside!="{[onClose, open, wrapEl]}"
|
|
28
|
+
class="dropdown-content combobox-listbox"
|
|
29
|
+
data-state="open"
|
|
30
|
+
id!="{listboxId}"
|
|
31
|
+
role="listbox"
|
|
32
|
+
)
|
|
33
|
+
+each('filtered as item, i (item.value)')
|
|
34
|
+
li.dropdown-item(
|
|
35
|
+
role="option"
|
|
36
|
+
id!="{optionId(i)}"
|
|
37
|
+
aria-selected!="{value === item.value}"
|
|
38
|
+
data-active!="{i === active ? 'true' : undefined}"
|
|
39
|
+
onpointerdown!="{(e) => { e.preventDefault(); pick(item) }}"
|
|
40
|
+
)
|
|
41
|
+
Icon(name="lucide:check" size="size-3.5" class!="{value === item.value ? '' : 'opacity-0'}")
|
|
42
|
+
span {item.label}
|
|
43
|
+
+if('open && filtered.length === 0')
|
|
44
|
+
div(use:portal use:floatAction use:clickOutside!="{[onClose, open, wrapEl]}" class="dropdown-content combobox-listbox" data-state="open")
|
|
45
|
+
.combobox-empty No matches
|
|
22
46
|
|
|
23
47
|
<script>
|
|
24
|
-
import { Combobox } from 'bits-ui'
|
|
25
|
-
import Icon from './Icon.n'
|
|
26
48
|
import { getContext } from 'svelte'
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Trigger: ComboboxTrigger
|
|
32
|
-
Portal: ComboboxPortal
|
|
33
|
-
Content: ComboboxContent
|
|
34
|
-
Viewport: ComboboxViewport
|
|
35
|
-
Item: ComboboxItem
|
|
36
|
-
} := Combobox
|
|
49
|
+
import Icon from './Icon.n'
|
|
50
|
+
import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
|
|
51
|
+
import { clickOutside } from '@human-synthesis/norns-ui/behaviors/clickOutside.svelte.js'
|
|
52
|
+
import { useFloating } from '@human-synthesis/norns-ui/behaviors/floating.svelte.js'
|
|
37
53
|
|
|
38
54
|
{
|
|
39
55
|
items = []
|
|
@@ -50,10 +66,69 @@ ComboboxRoot(bind:value bind:open type="single")
|
|
|
50
66
|
resolvedId := providedId ?? field?.id
|
|
51
67
|
hasError := explicitError || field?.hasError || false
|
|
52
68
|
|
|
69
|
+
uid := Math.random().toString(36).slice(2, 8)
|
|
70
|
+
listboxId := `listbox-${uid}`
|
|
71
|
+
optionId := (i) => `option-${uid}-${i}`
|
|
72
|
+
|
|
73
|
+
{ reference: refAction, floating: floatAction } := useFloating
|
|
74
|
+
placement: 'bottom-start'
|
|
75
|
+
offset: 4
|
|
76
|
+
matchWidth: true
|
|
77
|
+
|
|
53
78
|
term .= $state ''
|
|
79
|
+
active .= $state 0
|
|
80
|
+
inputEl .= $state null
|
|
81
|
+
wrapEl .= $state null
|
|
82
|
+
|
|
83
|
+
// Reset term to selected label when value changes externally.
|
|
84
|
+
$effect =>
|
|
85
|
+
if value !== undefined
|
|
86
|
+
match := items.find (it) => it.value === value
|
|
87
|
+
term = match?.label ?? '' if match && term !== match.label
|
|
54
88
|
|
|
55
89
|
filtered := $derived.by =>
|
|
56
90
|
return items unless term
|
|
57
91
|
t := term.toLowerCase()
|
|
58
92
|
items.filter (it) => it.label.toLowerCase().includes(t)
|
|
93
|
+
|
|
94
|
+
$effect =>
|
|
95
|
+
active = 0 if active >= filtered.length
|
|
96
|
+
|
|
97
|
+
onInput := =>
|
|
98
|
+
open = true
|
|
99
|
+
active = 0
|
|
100
|
+
|
|
101
|
+
toggle := =>
|
|
102
|
+
open = !open
|
|
103
|
+
inputEl?.focus() if open
|
|
104
|
+
|
|
105
|
+
onClose := => open = false
|
|
106
|
+
|
|
107
|
+
onBlur := =>
|
|
108
|
+
// Close after click handlers complete; pointerdown picks before this fires.
|
|
109
|
+
setTimeout (=> open = false), 100
|
|
110
|
+
|
|
111
|
+
pick := (item) =>
|
|
112
|
+
value = item.value
|
|
113
|
+
term = item.label
|
|
114
|
+
open = false
|
|
115
|
+
|
|
116
|
+
onKeyDown := (e) =>
|
|
117
|
+
switch e.key
|
|
118
|
+
when 'ArrowDown'
|
|
119
|
+
e.preventDefault()
|
|
120
|
+
open = true
|
|
121
|
+
active = Math.min(filtered.length - 1, active + 1)
|
|
122
|
+
when 'ArrowUp'
|
|
123
|
+
e.preventDefault()
|
|
124
|
+
open = true
|
|
125
|
+
active = Math.max(0, active - 1)
|
|
126
|
+
when 'Enter'
|
|
127
|
+
if open && filtered[active]
|
|
128
|
+
e.preventDefault()
|
|
129
|
+
pick filtered[active]
|
|
130
|
+
when 'Escape'
|
|
131
|
+
open = false
|
|
132
|
+
when 'Tab'
|
|
133
|
+
open = false
|
|
59
134
|
</script>
|
|
@@ -1,32 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
.collapsible-chevron
|
|
1
|
+
details.collapsible(class!="{classes}" bind:open ontoggle!="{onToggle}")
|
|
2
|
+
summary.collapsible-trigger
|
|
3
|
+
.collapsible-chevron
|
|
4
4
|
Icon(name="lucide:chevron-right" size="size-4")
|
|
5
5
|
+if('title')
|
|
6
6
|
span.collapsible-title {title}
|
|
7
7
|
+if('!title && trigger')
|
|
8
8
|
| {@render trigger()}
|
|
9
|
-
|
|
9
|
+
.collapsible-content
|
|
10
10
|
| {@render children?.()}
|
|
11
11
|
|
|
12
12
|
<script>
|
|
13
|
-
import { Collapsible } from 'bits-ui'
|
|
14
13
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
15
14
|
import Icon from './Icon.n'
|
|
16
15
|
|
|
17
|
-
{
|
|
18
|
-
Root: CollapsibleRoot
|
|
19
|
-
Trigger: CollapsibleTrigger
|
|
20
|
-
Content: CollapsibleContent
|
|
21
|
-
} := Collapsible
|
|
22
|
-
|
|
23
16
|
{
|
|
24
17
|
open = $bindable(false)
|
|
25
18
|
title
|
|
26
19
|
trigger
|
|
27
20
|
children
|
|
21
|
+
onopenchange
|
|
28
22
|
class: extra = ''
|
|
29
23
|
} .= $props()
|
|
30
24
|
|
|
31
25
|
classes := cn 'collapsible', extra
|
|
26
|
+
|
|
27
|
+
onToggle := (e) =>
|
|
28
|
+
onopenchange?.(e.target.open)
|
|
32
29
|
</script>
|
|
@@ -1,48 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
span(class!="{triggerClass ?? ''}" oncontextmenu!="{onContext}")
|
|
2
|
+
| {@render trigger?.()}
|
|
3
|
+
|
|
4
|
+
+if('open')
|
|
5
|
+
div(
|
|
6
|
+
use:portal
|
|
7
|
+
use:clickOutside!="{[onClose, open]}"
|
|
8
|
+
use:escape!="{[onClose, open]}"
|
|
9
|
+
use:rovingTabindex!="{{ orientation: 'vertical', selector: '[data-roving]' }}"
|
|
10
|
+
bind:this!="{menuEl}"
|
|
11
|
+
class="dropdown-content"
|
|
12
|
+
data-state="open"
|
|
13
|
+
role="menu"
|
|
14
|
+
style="position: absolute"
|
|
15
|
+
)
|
|
16
|
+
+each('items as item')
|
|
17
|
+
+if('item.separator')
|
|
18
|
+
.dropdown-separator(role="separator")
|
|
19
|
+
+if('!item.separator')
|
|
20
|
+
button.dropdown-item(
|
|
21
|
+
type="button"
|
|
22
|
+
role="menuitem"
|
|
23
|
+
data-roving
|
|
24
|
+
data-disabled!="{item.disabled ? '' : undefined}"
|
|
25
|
+
disabled!="{item.disabled}"
|
|
26
|
+
onclick!="{() => onItem(item)}"
|
|
27
|
+
)
|
|
20
28
|
+if('item.icon')
|
|
21
|
-
Icon(name!="{item.icon}" size="size-3.5")
|
|
29
|
+
Icon(name!="{item.icon}" size="size-3.5" class="text-fg-muted shrink-0")
|
|
22
30
|
span.flex-1 {item.label}
|
|
23
|
-
Icon(name="lucide:chevron-right" size="size-3.5")
|
|
24
|
-
ContextMenuSubContent.dropdown-content
|
|
25
|
-
| {@render renderItems(item.children)}
|
|
26
31
|
|
|
27
32
|
<script>
|
|
28
|
-
import {
|
|
33
|
+
import { positionAt } from '@human-synthesis/norns-ui/behaviors/floating.svelte.js'
|
|
34
|
+
import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
|
|
35
|
+
import { clickOutside } from '@human-synthesis/norns-ui/behaviors/clickOutside.svelte.js'
|
|
36
|
+
import { escape } from '@human-synthesis/norns-ui/behaviors/escape.svelte.js'
|
|
37
|
+
import { rovingTabindex } from '@human-synthesis/norns-ui/behaviors/rovingTabindex.svelte.js'
|
|
29
38
|
import Icon from './Icon.n'
|
|
30
39
|
|
|
31
|
-
{
|
|
32
|
-
Root: ContextMenuRoot
|
|
33
|
-
Trigger: ContextMenuTrigger
|
|
34
|
-
Portal: ContextMenuPortal
|
|
35
|
-
Content: ContextMenuContent
|
|
36
|
-
Item: ContextMenuItem
|
|
37
|
-
Separator: ContextMenuSeparator
|
|
38
|
-
Sub: ContextMenuSubRoot
|
|
39
|
-
SubTrigger: ContextMenuSubTrigger
|
|
40
|
-
SubContent: ContextMenuSubContent
|
|
41
|
-
} := ContextMenu
|
|
42
|
-
|
|
43
40
|
{
|
|
44
41
|
items = []
|
|
45
42
|
trigger
|
|
46
43
|
triggerClass
|
|
47
44
|
} := $props()
|
|
45
|
+
|
|
46
|
+
open .= $state false
|
|
47
|
+
menuEl .= $state null
|
|
48
|
+
clickX .= 0
|
|
49
|
+
clickY .= 0
|
|
50
|
+
|
|
51
|
+
onContext := (e) =>
|
|
52
|
+
e.preventDefault()
|
|
53
|
+
clickX = e.clientX
|
|
54
|
+
clickY = e.clientY
|
|
55
|
+
open = true
|
|
56
|
+
|
|
57
|
+
$effect =>
|
|
58
|
+
return unless open && menuEl
|
|
59
|
+
positionAt menuEl, clickX, clickY, { placement: 'right-start' }
|
|
60
|
+
|
|
61
|
+
onClose := => open = false
|
|
62
|
+
|
|
63
|
+
onItem := (item) =>
|
|
64
|
+
return if item.disabled
|
|
65
|
+
item.onSelect?.()
|
|
66
|
+
open = false
|
|
48
67
|
</script>
|
|
@@ -1,41 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Icon(name="lucide:calendar" size="size-4" class="text-fg-muted shrink-0")
|
|
10
|
-
span.flex-1.text-left.truncate
|
|
11
|
-
| {value ? formatted : placeholder}
|
|
12
|
-
Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
|
|
13
|
-
PopoverPortal
|
|
14
|
-
PopoverContent.popover-content(
|
|
15
|
-
class="p-3 w-[20rem]"
|
|
16
|
-
onOpenAutoFocus!="{(e) => e.preventDefault()}"
|
|
17
|
-
onCloseAutoFocus!="{(e) => e.preventDefault()}"
|
|
1
|
+
Popover(bind:open class="p-3 w-[20rem]")
|
|
2
|
+
+snippet('trigger')
|
|
3
|
+
button.input.date-picker-trigger(
|
|
4
|
+
type="button"
|
|
5
|
+
disabled!="{disabled}"
|
|
6
|
+
id!="{resolvedId}"
|
|
7
|
+
aria-label!="{label ?? 'Pick a date'}"
|
|
8
|
+
class!="{hasError ? 'input-err' : ''}"
|
|
18
9
|
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
Icon(name="lucide:calendar" size="size-4" class="text-fg-muted shrink-0")
|
|
11
|
+
span.flex-1.text-left.truncate
|
|
12
|
+
| {value ? formatted : placeholder}
|
|
13
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
|
|
14
|
+
Calendar(
|
|
15
|
+
bind:value
|
|
16
|
+
min!="{min}"
|
|
17
|
+
max!="{max}"
|
|
18
|
+
onchange!="{handleChange}"
|
|
19
|
+
)
|
|
25
20
|
|
|
26
21
|
<script>
|
|
27
|
-
import { Popover } from 'bits-ui'
|
|
28
22
|
import { getContext } from 'svelte'
|
|
23
|
+
import Popover from './Popover.n'
|
|
29
24
|
import Icon from './Icon.n'
|
|
30
25
|
import Calendar from './Calendar.n'
|
|
31
26
|
|
|
32
|
-
{
|
|
33
|
-
Root: PopoverRoot
|
|
34
|
-
Trigger: PopoverTrigger
|
|
35
|
-
Portal: PopoverPortal
|
|
36
|
-
Content: PopoverContent
|
|
37
|
-
} := Popover
|
|
38
|
-
|
|
39
27
|
{
|
|
40
28
|
value = $bindable('')
|
|
41
29
|
open = $bindable(false)
|
|
@@ -1,37 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
max!="{max}"
|
|
18
|
-
)
|
|
19
|
-
.flex.justify-end.gap-2.mt-3
|
|
20
|
-
button.btn.btn-ghost.btn-sm(type="button" onclick!="{clear}") Clear
|
|
21
|
-
button.btn.btn-primary.btn-sm(type="button" onclick!="{() => open = false}") Done
|
|
1
|
+
Popover(bind:open class="p-3 w-[20rem]")
|
|
2
|
+
+snippet('trigger')
|
|
3
|
+
button.input.date-picker-trigger(type="button" disabled!="{disabled}" aria-label="Pick a date range")
|
|
4
|
+
Icon(name="lucide:calendar-range" size="size-4" class="text-fg-muted shrink-0")
|
|
5
|
+
span.flex-1.text-left.truncate
|
|
6
|
+
| {label}
|
|
7
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
|
|
8
|
+
Calendar(
|
|
9
|
+
bind:rangeValue!="{value}"
|
|
10
|
+
range!="{true}"
|
|
11
|
+
min!="{min}"
|
|
12
|
+
max!="{max}"
|
|
13
|
+
)
|
|
14
|
+
.flex.justify-end.gap-2.mt-3
|
|
15
|
+
button.btn.btn-ghost.btn-sm(type="button" onclick!="{clear}") Clear
|
|
16
|
+
button.btn.btn-primary.btn-sm(type="button" onclick!="{() => open = false}") Done
|
|
22
17
|
|
|
23
18
|
<script>
|
|
24
|
-
import
|
|
19
|
+
import Popover from './Popover.n'
|
|
25
20
|
import Icon from './Icon.n'
|
|
26
21
|
import Calendar from './Calendar.n'
|
|
27
22
|
|
|
28
|
-
{
|
|
29
|
-
Root: PopoverRoot
|
|
30
|
-
Trigger: PopoverTrigger
|
|
31
|
-
Portal: PopoverPortal
|
|
32
|
-
Content: PopoverContent
|
|
33
|
-
} := Popover
|
|
34
|
-
|
|
35
23
|
{
|
|
36
24
|
value = $bindable({ start: '', end: '' })
|
|
37
25
|
open = $bindable(false)
|