@human-synthesis/norns-ui 0.0.3 → 0.0.6

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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -225
  3. package/package.json +13 -3
  4. package/src/auto-import.js +81 -19
  5. package/src/components/Accordion.n +39 -0
  6. package/src/components/Audio.n +24 -0
  7. package/src/components/Autocomplete.n +134 -0
  8. package/src/components/Avatar.n +27 -0
  9. package/src/components/AvatarGroup.n +25 -0
  10. package/src/components/Badge.n +20 -0
  11. package/src/components/Banner.n +25 -0
  12. package/src/components/Breadcrumbs.n +22 -0
  13. package/src/components/Btn.n +29 -8
  14. package/src/components/ButtonGroup.n +14 -0
  15. package/src/components/Calendar.n +153 -0
  16. package/src/components/Card.n +34 -0
  17. package/src/components/Carousel.n +66 -0
  18. package/src/components/Chip.n +27 -0
  19. package/src/components/Collapsible.n +29 -0
  20. package/src/components/ColorPicker.n +62 -0
  21. package/src/components/ContextMenu.n +67 -0
  22. package/src/components/CopyButton.n +50 -0
  23. package/src/components/DataTable.n +61 -0
  24. package/src/components/DatePicker.n +56 -0
  25. package/src/components/DateRangePicker.n +47 -0
  26. package/src/components/Dialog.n +35 -22
  27. package/src/components/Dropdown.n +54 -23
  28. package/src/components/GradientText.n +21 -0
  29. package/src/components/Header.n +23 -0
  30. package/src/components/HeroBanner.n +37 -0
  31. package/src/components/HierarchicalMenu.n +38 -0
  32. package/src/components/Icon.n +16 -0
  33. package/src/components/Image.n +31 -0
  34. package/src/components/MegaMenu.n +36 -0
  35. package/src/components/MultiSelect.n +158 -0
  36. package/src/components/NumberInput.n +49 -0
  37. package/src/components/OtpField.n +67 -0
  38. package/src/components/Pagination.n +73 -0
  39. package/src/components/Popover.n +30 -15
  40. package/src/components/Progress.n +26 -0
  41. package/src/components/ProgressCircular.n +52 -0
  42. package/src/components/RippleButton.n +56 -0
  43. package/src/components/ScrollArea.n +20 -0
  44. package/src/components/Separator.n +12 -0
  45. package/src/components/Sheet.n +36 -22
  46. package/src/components/ShinyButton.n +36 -0
  47. package/src/components/Skeleton.n +27 -0
  48. package/src/components/Stepper.n +35 -0
  49. package/src/components/Surface.n +10 -0
  50. package/src/components/Tabs.n +39 -13
  51. package/src/components/TagsInput.n +66 -0
  52. package/src/components/ThemeToggler.n +43 -0
  53. package/src/components/TimePicker.n +153 -0
  54. package/src/components/Timeline.n +27 -0
  55. package/src/components/ToggleButton.n +35 -0
  56. package/src/components/ToggleButtonGroup.n +54 -0
  57. package/src/components/Toolbar.n +15 -0
  58. package/src/components/Tooltip.n +52 -19
  59. package/src/components/Tree.n +57 -0
  60. package/src/components/Uploader.n +88 -0
  61. package/src/components/Video.n +26 -0
  62. package/src/index.js +64 -1
  63. package/src/lib/behaviors/clickOutside.svelte.js +54 -0
  64. package/src/lib/behaviors/escape.svelte.js +36 -0
  65. package/src/lib/behaviors/floating.svelte.js +137 -0
  66. package/src/lib/behaviors/focusTrap.svelte.js +80 -0
  67. package/src/lib/behaviors/index.js +14 -0
  68. package/src/lib/behaviors/portal.svelte.js +32 -0
  69. package/src/lib/behaviors/rovingTabindex.svelte.js +76 -0
  70. package/src/lib/behaviors/scrollLock.svelte.js +56 -0
  71. package/src/motion/AnimatedNumber.n +55 -0
  72. package/src/motion/GradientBackground.n +25 -0
  73. package/src/motion/LiquidButton.n +48 -0
  74. package/src/motion/Reveal.n +37 -0
  75. package/src/motion/Sparkles.n +53 -0
  76. package/src/motion/index.js +18 -0
  77. package/src/styles/atoms.css +1481 -223
  78. package/src/styles/tokens.css +204 -40
  79. package/src/types/Accordion.d.ts +17 -0
  80. package/src/types/AnimatedNumber.d.ts +15 -0
  81. package/src/types/Audio.d.ts +18 -0
  82. package/src/types/Autocomplete.d.ts +17 -0
  83. package/src/types/Avatar.d.ts +14 -0
  84. package/src/types/AvatarGroup.d.ts +15 -0
  85. package/src/types/Badge.d.ts +14 -0
  86. package/src/types/Banner.d.ts +15 -0
  87. package/src/types/Breadcrumbs.d.ts +15 -0
  88. package/src/types/Btn.d.ts +10 -0
  89. package/src/types/ButtonGroup.d.ts +10 -0
  90. package/src/types/Calendar.d.ts +20 -0
  91. package/src/types/Card.d.ts +19 -0
  92. package/src/types/Carousel.d.ts +23 -0
  93. package/src/types/Chip.d.ts +12 -0
  94. package/src/types/Collapsible.d.ts +13 -0
  95. package/src/types/ColorPicker.d.ts +15 -0
  96. package/src/types/ContextMenu.d.ts +24 -0
  97. package/src/types/CopyButton.d.ts +16 -0
  98. package/src/types/DataTable.d.ts +27 -0
  99. package/src/types/DatePicker.d.ts +23 -0
  100. package/src/types/DateRangePicker.d.ts +17 -0
  101. package/src/types/Dialog.d.ts +4 -0
  102. package/src/types/Dropdown.d.ts +2 -1
  103. package/src/types/GradientBackground.d.ts +14 -0
  104. package/src/types/GradientText.d.ts +19 -0
  105. package/src/types/Header.d.ts +12 -0
  106. package/src/types/HeroBanner.d.ts +17 -0
  107. package/src/types/HierarchicalMenu.d.ts +20 -0
  108. package/src/types/Icon.d.ts +21 -0
  109. package/src/types/Image.d.ts +15 -0
  110. package/src/types/LiquidButton.d.ts +15 -0
  111. package/src/types/MegaMenu.d.ts +29 -0
  112. package/src/types/MultiSelect.d.ts +16 -0
  113. package/src/types/NumberInput.d.ts +17 -0
  114. package/src/types/OtpField.d.ts +13 -0
  115. package/src/types/Pagination.d.ts +13 -0
  116. package/src/types/Progress.d.ts +14 -0
  117. package/src/types/ProgressCircular.d.ts +14 -0
  118. package/src/types/Reveal.d.ts +20 -0
  119. package/src/types/RippleButton.d.ts +15 -0
  120. package/src/types/ScrollArea.d.ts +11 -0
  121. package/src/types/Separator.d.ts +9 -0
  122. package/src/types/Sheet.d.ts +3 -0
  123. package/src/types/ShinyButton.d.ts +15 -0
  124. package/src/types/Skeleton.d.ts +13 -0
  125. package/src/types/Sparkles.d.ts +13 -0
  126. package/src/types/Stepper.d.ts +19 -0
  127. package/src/types/Surface.d.ts +10 -0
  128. package/src/types/Tabs.d.ts +2 -0
  129. package/src/types/TagsInput.d.ts +17 -0
  130. package/src/types/ThemeToggler.d.ts +17 -0
  131. package/src/types/TimePicker.d.ts +20 -0
  132. package/src/types/Timeline.d.ts +17 -0
  133. package/src/types/ToggleButton.d.ts +16 -0
  134. package/src/types/ToggleButtonGroup.d.ts +21 -0
  135. package/src/types/Toolbar.d.ts +12 -0
  136. package/src/types/Tooltip.d.ts +3 -0
  137. package/src/types/Tree.d.ts +20 -0
  138. package/src/types/Uploader.d.ts +15 -0
  139. package/src/types/Video.d.ts +20 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Teodoroiu / Human Synthesis
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 CHANGED
@@ -1,10 +1,6 @@
1
1
  # `@human-synthesis/norns-ui`
2
2
 
3
- UI library for the [Norns](https://github.com/human-synthesis/norns) ecosystem Pug + Civet components on Tailwind v4.
4
-
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.
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.
8
4
 
9
5
  ## Stack
10
6
 
@@ -12,9 +8,11 @@ UI library for the [Norns](https://github.com/human-synthesis/norns) ecosystem
12
8
  - [Pug](https://pugjs.org) — templates (in `.n` files)
13
9
  - [Civet](https://civet.dev) — `<script>` language
14
10
  - [Tailwind CSS v4](https://tailwindcss.com) — styling, **hard peer dep**
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`
13
+ - [Iconify](https://iconify.design) — icons; ships with [Lucide](https://lucide.dev) preset
15
14
  - [tailwind-merge](https://github.com/dcastil/tailwind-merge) — class deduplication
16
- - [Bits UI](https://bits-ui.com) — headless behavior backing `Dialog`/`Popover`/`Tabs`/etc.
17
- - `@human-synthesis/norns` `^0.0.7` — peer (`nornsAutoImport` for the registration flow)
15
+ - [`@human-synthesis/norns`](https://github.com/human-synthesis/norns) — peer (`nornsAutoImport`)
18
16
 
19
17
  ## Install
20
18
 
@@ -24,272 +22,130 @@ bun add -D @human-synthesis/norns-ui
24
22
 
25
23
  ## Setup
26
24
 
27
- ### 1. Wire `presetUI()` into `nornsAutoImport`
28
-
29
- So `<Btn>`, `<Field>`, `<Input>`, etc. resolve in markup without explicit imports.
25
+ `vite.config.js`:
30
26
 
31
27
  ```js
32
- // vite.config.js
33
- import { defineConfig } from 'vite';
34
- import { sveltekit } from '@sveltejs/kit/vite';
35
- import tailwindcss from '@tailwindcss/vite';
36
28
  import { nornsAutoImport } from '@human-synthesis/norns/auto-import';
37
- import { nornsCivetPlugin } from '@human-synthesis/norns/vite';
38
29
  import { presetUI } from '@human-synthesis/norns-ui/auto-import';
39
30
 
40
31
  const ui = presetUI();
41
32
 
42
- export default defineConfig({
43
- plugins: [
44
- nornsCivetPlugin(),
45
- nornsAutoImport({
46
- componentDirs: ['src/lib/components'],
47
- components: ui.components
48
- }),
49
- tailwindcss(),
50
- sveltekit()
51
- ]
52
- });
53
- ```
54
-
55
- Same shape goes in `svelte.config.js`'s `preprocess` array.
56
-
57
- `componentDirs` resolves first — your `src/lib/components/Btn.n` silently shadows the library's `Btn` whenever you want to override.
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
- });
33
+ export default {
34
+ plugins: [
35
+ nornsCivetPlugin(),
36
+ nornsAutoImport({
37
+ componentDirs: ['src/lib/components'],
38
+ components: ui.components
39
+ }),
40
+ tailwindcss(),
41
+ sveltekit()
42
+ ]
43
+ };
78
44
  ```
79
45
 
80
- ### 2. Import the styles
46
+ `app.css`:
81
47
 
82
48
  ```css
83
- /* app.css */
84
49
  @import 'tailwindcss';
85
50
  @import '@human-synthesis/norns-ui/styles';
86
-
87
- /* override tokens here if you want a different brand color */
88
- @theme {
89
- --color-primary-500: oklch(60% 0.2 200);
90
- }
91
- ```
92
-
93
- ### 3. Use components in Pug
94
-
95
- ```pug
96
- section.space-y-4
97
- h1.text-3xl New note
98
-
99
- Form(method="POST" action="?/create")
100
- Field(label="Title" required error!="{form?.errors?.title}")
101
- Input(name="title" required)
102
- Field(label="Body" help="Optional — supports plain text only")
103
- Textarea(name="body" rows="4")
104
- Btn(type="submit" variant="primary") Create note
105
51
  ```
106
52
 
107
- ## Component catalog
108
-
109
- ### Phase 1 — atoms
110
-
111
- CSS classes via `@layer components`. Use directly via Pug class shorthand:
112
-
113
- - `.btn` + variants (`primary`, `secondary`, `ghost`, `danger`, `link`) + sizes (`sm`, `lg`)
114
- - `.input` / `.textarea` / `.select` + `-err` modifier
115
- - `.checkbox` / `.radio` / `.switch` + `-err` modifier
116
- - `.field` / `.field-label` / `.field-required` / `.field-help` / `.field-error`
117
- - `.field-group` / `.field-group-legend`
118
- - `.form`
119
- - `.ui-spinner`
53
+ ## Dark mode
120
54
 
121
- ### Phase 1Btn
55
+ Toggle via `<html data-theme="dark">` every component swaps. Persist the choice with a small inline script in `app.html` to avoid FOUC:
122
56
 
123
- Wrapped `<button>` with class merging, variant/size props, loading state, and snippet-prop API for icons.
124
-
125
- ```pug
126
- Btn(variant="primary") Save
127
- Btn(variant="secondary" size="sm") Cancel
128
- Btn(loading!="{saving}" type="submit") Save
129
- Btn(variant="danger" onclick!="{remove}")
130
- +snippet('leading')
131
- // icon goes here
132
- | Delete
57
+ ```html
58
+ <script>
59
+ (function () {
60
+ try {
61
+ var t = localStorage.getItem('theme');
62
+ if (t === 'dark' || (t === null && matchMedia('(prefers-color-scheme: dark)').matches)) {
63
+ document.documentElement.dataset.theme = 'dark';
64
+ }
65
+ } catch (_) {}
66
+ })();
67
+ </script>
133
68
  ```
134
69
 
135
- Props (see [`src/types/Btn.d.ts`](src/types/Btn.d.ts)):
136
- - `variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link'` (default `'primary'`)
137
- - `size?: 'sm' | 'md' | 'lg'` (default `'md'`)
138
- - `loading?: boolean` — replaces leading icon with `.ui-spinner`, sets `disabled` and `aria-busy`
139
- - `disabled?: boolean`, `type?`, `onclick?`, `class?`, `children`, `leading`, `trailing`
140
-
141
- ### Phase 2 — forms tier
70
+ ## Icons
142
71
 
143
- #### `<Field>`
72
+ The library ships [`@iconify/svelte`](https://iconify.design/docs/icon-components/svelte/) with the [Lucide](https://lucide.dev) collection bundled. Register the collection once in your app entry so icons render in SSR and the client:
144
73
 
145
- Wraps a control with label + optional help/error text. Provides `id` and `hasError` to descendant controls via Svelte context — child Inputs auto-pick up the right `id` for `<label for=>` and switch to error styling.
146
-
147
- ```pug
148
- Field(label="Email" required error!="{form?.errors?.email}" help="We'll never share it")
149
- Input(name="email" type="email" required)
74
+ ```js
75
+ // src/routes/+layout.svelte (or wherever fits)
76
+ import { addCollection } from '@iconify/svelte';
77
+ import lucide from '@iconify-json/lucide/icons.json';
78
+ addCollection(lucide);
150
79
  ```
151
80
 
152
- Props: `label?`, `help?`, `error?`, `required?`, `id?`, `class?`, `children`.
153
-
154
- #### `<Form>`, `<FieldGroup>`
155
-
156
- Styled `<form>` (vertical stack via `.form`) and `<fieldset>` (`.field-group` with optional legend).
81
+ Then use `<Icon>` anywhere:
157
82
 
158
83
  ```pug
159
- Form(action="?/save")
160
- FieldGroup(legend="Profile")
161
- Field(label="Name")
162
- Input(name="name" required)
163
- Field(label="Bio")
164
- Textarea(name="bio" rows="3")
165
- Btn(type="submit") Save
84
+ Icon(name="lucide:check" size="size-5")
85
+ Btn(icon="lucide:save") Save
166
86
  ```
167
87
 
168
- #### Form controls
169
-
170
- All accept `class?` (merged via `cn`), pull `id` + `hasError` from a parent `<Field>` automatically, and surface `aria-invalid` + `aria-describedby` for accessibility:
171
-
172
- | Component | Bind | Common props |
173
- |---|---|---|
174
- | `<Input>` | `bind:value` | `type`, `size`, `name`, `placeholder`, `required`, `disabled`, `readonly`, `autocomplete` |
175
- | `<Textarea>` | `bind:value` | `name`, `placeholder`, `rows` (default 4), `required`, `disabled`, `readonly` |
176
- | `<Select>` | `bind:value` | `name`, `required`, `disabled`. Children: `<option>` elements |
177
- | `<Checkbox>` | `bind:checked` | `name`, `value`, `required`, `disabled` |
178
- | `<Radio>` | `bind:group` | `name`, `value`, `required`, `disabled` |
179
- | `<Switch>` | `bind:checked` | `name`, `value`, `required`, `disabled`. Renders as a styled toggle (`role="switch"`) |
88
+ To mix in another icon set, install its JSON package (e.g. `@iconify-json/heroicons`) and `addCollection(...)` it the same way.
180
89
 
181
- Each has a hand-rolled `.d.ts` shim under `src/types/`.
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>`
90
+ ## Usage
188
91
 
189
92
  ```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
93
+ Form(action="?/save" form!="{form}")
94
+ Field(label="Title" name="title" required)
95
+ Input(name="title" placeholder="title…" required)
96
+ Btn(type="submit" variant="primary" icon="lucide:save") Save
196
97
  ```
197
98
 
198
- Props: `open` (bindable), `title`, `description`, `hideClose`, `trigger`, `actions`, `children`, `class`, `overlayClass`, `triggerClass`.
199
-
200
- #### `<Sheet>`
99
+ `<Field name="title">` reads its error from the parent `<Form>`'s context map; no per-page boilerplate.
201
100
 
202
- Same API as `Dialog` plus `side?: 'top' | 'right' | 'bottom' | 'left'` (default `'right'`). Slides in from the chosen edge instead of centering.
101
+ ## What's in here
203
102
 
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
- ```
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.
104
+ - **Forms**: `Btn`, `Form`, `Field`, `FieldGroup`, `Input`, `Textarea`, `Select`, `Checkbox`, `Radio`, `Switch`.
105
+ - **Behavior** (custom on `@floating-ui/dom`): `Dialog`, `Sheet`, `Popover`, `Dropdown`, `Tooltip`, `Tabs`, `Accordion`, `ContextMenu`, `Collapsible`.
106
+ - **Display**: `Card`, `Surface`, `Banner`, `Badge`, `Chip`, `Avatar`, `Skeleton`, `Progress`, `ProgressCircular`, `Icon`.
107
+ - **Composite**: `Header`, `HeroBanner`, `Stepper`, `Breadcrumbs`, `Pagination`, `Carousel`.
108
+ - **Toast**: `ToastProvider` + `toast()` / `notify()` / `dismiss()` from `@human-synthesis/norns-ui/toast`.
220
109
 
221
- #### `<Tooltip>`
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.
222
111
 
223
- ```pug
224
- Tooltip(content="Saved 2 minutes ago")
225
- +snippet('trigger')
226
- Btn(variant="ghost" size="sm") ⓘ
227
- ```
112
+ ## Behaviors
228
113
 
229
- #### `<Tabs>`
114
+ A small set of headless behaviors is exposed at `@human-synthesis/norns-ui/behaviors` for use in your own components:
230
115
 
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
- ]}")
116
+ ```js
117
+ import {
118
+ portal, clickOutside, escape, focusTrap,
119
+ scrollLock, useFloating, rovingTabindex
120
+ } from '@human-synthesis/norns-ui/behaviors';
241
121
  ```
242
122
 
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
- ```
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.
252
124
 
253
- ```civet
254
- import { toast } from '@human-synthesis/norns-ui/toast'
125
+ ## Migrating from 0.0.5 → 0.0.6
255
126
 
256
- handleSave := =>
257
- await save()
258
- toast 'Saved!', { variant: 'success' }
259
- ```
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:
260
128
 
261
- Variants: `info` (default), `success`, `warning`, `error`. Default duration 4000ms; pass `duration: 0` for sticky.
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.
262
134
 
263
135
  ## Theming
264
136
 
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.
266
-
267
- Dark mode: toggle via `<html data-theme="dark">`. The library's tokens.css ships dark-theme overrides; Tailwind's existing dark-aware utilities continue to work.
268
-
269
- ## Class merging
270
-
271
- Every component takes a `class` prop merged via `tailwind-merge` (the `cn` helper):
272
-
273
- ```svelte
274
- <Btn variant="primary" class="w-full" />
275
- <Input class="font-mono" />
276
- ```
277
-
278
- `cn` is exported from `@human-synthesis/norns-ui/cn` if you want to swap in plain `clsx` for smaller bundle:
279
-
280
- ```js
281
- import { cn } from '@human-synthesis/norns-ui/cn';
282
- cn('btn', isActive && 'btn-active', extra);
283
- ```
137
+ Tokens are in [`src/styles/tokens.css`](src/styles/tokens.css) as a Tailwind v4 `@theme` block. The library exposes:
284
138
 
285
- ## Override a component
139
+ - **Color scales** (50–950) for `primary`, `success`, `warning`, `danger`, `info` — all in `oklch` for perceptual uniformity. Use as `bg-primary-500`, `text-danger-700`, etc.
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.
141
+ - **Feedback tokens**: `--color-{success,warning,danger,info}-{bg,fg,border}` for tinted surfaces.
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`.
286
147
 
287
- Drop a same-name file in your `src/lib/components/` and `nornsAutoImport`'s first-match-wins resolves to your version silently. No fork needed.
288
-
289
- ```
290
- src/lib/components/Btn.n ← your override wins
291
- node_modules/@human-synthesis/norns-ui/src/components/Btn.n ← fallback
292
- ```
148
+ Override by re-declaring `@theme { }` in your `app.css` after the library import.
293
149
 
294
150
  ## License
295
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@human-synthesis/norns-ui",
3
- "version": "0.0.3",
3
+ "version": "0.0.6",
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,17 +14,23 @@
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",
26
30
  "./styles/atoms": "./src/styles/atoms.css",
27
31
  "./components/*": "./src/components/*",
32
+ "./motion": "./src/motion/index.js",
33
+ "./motion/*": "./src/motion/*",
28
34
  "./package.json": "./package.json"
29
35
  },
30
36
  "peerDependencies": {
@@ -42,7 +48,11 @@
42
48
  }
43
49
  },
44
50
  "dependencies": {
45
- "bits-ui": "^2.18.1",
51
+ "@floating-ui/dom": "^1.7.6",
52
+ "@fontsource-variable/outfit": "^5.2.8",
53
+ "@iconify-json/lucide": "^1.2.105",
54
+ "@iconify/svelte": "^5.2.1",
55
+ "motion": "^12.38.0",
46
56
  "tailwind-merge": "^2.5.0"
47
57
  },
48
58
  "engines": {
@@ -26,33 +26,95 @@
26
26
  * @returns {UIPreset}
27
27
  */
28
28
  export function presetUI() {
29
+ const c = (name) => `@human-synthesis/norns-ui/components/${name}.n`;
29
30
  return {
30
31
  components: {
31
32
  // Phase 1
32
- Btn: '@human-synthesis/norns-ui/components/Btn.n',
33
+ Btn: c('Btn'),
33
34
 
34
35
  // Phase 2 — forms tier
35
- Field: '@human-synthesis/norns-ui/components/Field.n',
36
- FieldGroup: '@human-synthesis/norns-ui/components/FieldGroup.n',
37
- Form: '@human-synthesis/norns-ui/components/Form.n',
38
- Input: '@human-synthesis/norns-ui/components/Input.n',
39
- Textarea: '@human-synthesis/norns-ui/components/Textarea.n',
40
- Select: '@human-synthesis/norns-ui/components/Select.n',
41
- Checkbox: '@human-synthesis/norns-ui/components/Checkbox.n',
42
- Radio: '@human-synthesis/norns-ui/components/Radio.n',
43
- Switch: '@human-synthesis/norns-ui/components/Switch.n',
36
+ Field: c('Field'),
37
+ FieldGroup: c('FieldGroup'),
38
+ Form: c('Form'),
39
+ Input: c('Input'),
40
+ Textarea: c('Textarea'),
41
+ Select: c('Select'),
42
+ Checkbox: c('Checkbox'),
43
+ Radio: c('Radio'),
44
+ Switch: c('Switch'),
44
45
 
45
46
  // 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'
47
+ Dialog: c('Dialog'),
48
+ Sheet: c('Sheet'),
49
+ Popover: c('Popover'),
50
+ Dropdown: c('Dropdown'),
51
+ Tooltip: c('Tooltip'),
52
+ Tabs: c('Tabs'),
53
+ ToastProvider: c('ToastProvider'),
53
54
 
54
- // Phase 4+ adds: Accordion, Listbox, Combobox, Pagination,
55
- // Avatar, Badge, Spinner, Progress, Skeleton, Icon, Card
55
+ // 0.0.4 display layer
56
+ Icon: c('Icon'),
57
+ Card: c('Card'),
58
+ Surface: c('Surface'),
59
+ Banner: c('Banner'),
60
+ Badge: c('Badge'),
61
+ Chip: c('Chip'),
62
+ Avatar: c('Avatar'),
63
+ Skeleton: c('Skeleton'),
64
+ Progress: c('Progress'),
65
+ ProgressCircular: c('ProgressCircular'),
66
+
67
+ // 0.0.4 — composite layer
68
+ Header: c('Header'),
69
+ HeroBanner: c('HeroBanner'),
70
+ Stepper: c('Stepper'),
71
+ Breadcrumbs: c('Breadcrumbs'),
72
+ Pagination: c('Pagination'),
73
+ Accordion: c('Accordion'),
74
+ Carousel: c('Carousel'),
75
+ Tree: c('Tree'),
76
+
77
+ // 0.0.4 — wave A: small/standalone
78
+ Separator: c('Separator'),
79
+ Toolbar: c('Toolbar'),
80
+ ButtonGroup: c('ButtonGroup'),
81
+ ToggleButton: c('ToggleButton'),
82
+ ToggleButtonGroup: c('ToggleButtonGroup'),
83
+ Collapsible: c('Collapsible'),
84
+ NumberInput: c('NumberInput'),
85
+ OtpField: c('OtpField'),
86
+ TagsInput: c('TagsInput'),
87
+ Image: c('Image'),
88
+ Audio: c('Audio'),
89
+ Video: c('Video'),
90
+ Timeline: c('Timeline'),
91
+
92
+ // 0.0.4 — wave B: medium-complexity (custom; ex Bits-UI-backed)
93
+ Autocomplete: c('Autocomplete'),
94
+ MultiSelect: c('MultiSelect'),
95
+ ContextMenu: c('ContextMenu'),
96
+ ScrollArea: c('ScrollArea'),
97
+ ColorPicker: c('ColorPicker'),
98
+ Uploader: c('Uploader'),
99
+ HierarchicalMenu: c('HierarchicalMenu'),
100
+ MegaMenu: c('MegaMenu'),
101
+
102
+ // 0.0.4 — wave C: pickers + table
103
+ DatePicker: c('DatePicker'),
104
+ DateRangePicker: c('DateRangePicker'),
105
+ TimePicker: c('TimePicker'),
106
+ Calendar: c('Calendar'),
107
+ DataTable: c('DataTable'),
108
+
109
+ // 0.0.5 — CSS-only motion components
110
+ AvatarGroup: c('AvatarGroup'),
111
+ GradientText: c('GradientText'),
112
+ CopyButton: c('CopyButton'),
113
+ ThemeToggler: c('ThemeToggler'),
114
+ ShinyButton: c('ShinyButton'),
115
+ RippleButton: c('RippleButton')
116
+ // Motion-driven components live in `@human-synthesis/norns-ui/motion`
117
+ // and are NOT registered here (opt-in import only).
56
118
  },
57
119
  helpers: [
58
120
  {
@@ -0,0 +1,39 @@
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
12
+ | {@render item.content?.()}
13
+
14
+ <script>
15
+ import { cn } from '@human-synthesis/norns-ui/cn'
16
+ import Icon from './Icon.n'
17
+
18
+ {
19
+ items = []
20
+ multiple = false
21
+ value = $bindable(undefined)
22
+ class: extra = ''
23
+ } .= $props()
24
+
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)
39
+ </script>
@@ -0,0 +1,24 @@
1
+ audio(src!="{src}" controls!="{controls}" autoplay!="{autoplay}" loop!="{looping}" muted!="{muted}" preload!="{preload}" class!="{classes}")
2
+ +each('sources as source')
3
+ source(src!="{source.src}" type!="{source.type}")
4
+ | {fallback}
5
+
6
+ <script>
7
+ import { cn } from '@human-synthesis/norns-ui/cn'
8
+
9
+ // Note: HTML attribute `loop` becomes `looping` here because `loop` is a
10
+ // Civet reserved word.
11
+ {
12
+ src
13
+ sources = []
14
+ controls = true
15
+ autoplay = false
16
+ loop: looping = false
17
+ muted = false
18
+ preload = 'metadata'
19
+ fallback = 'Your browser does not support the audio element.'
20
+ class: extra = ''
21
+ } := $props()
22
+
23
+ classes := cn 'audio-player w-full', extra
24
+ </script>