@human-synthesis/norns-ui 0.0.3 → 0.0.5

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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -229
  3. package/package.json +7 -1
  4. package/src/auto-import.js +84 -19
  5. package/src/components/Accordion.n +38 -0
  6. package/src/components/Audio.n +24 -0
  7. package/src/components/Autocomplete.n +59 -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 +32 -0
  20. package/src/components/ColorPicker.n +62 -0
  21. package/src/components/ContextMenu.n +48 -0
  22. package/src/components/CopyButton.n +50 -0
  23. package/src/components/DataTable.n +61 -0
  24. package/src/components/DatePicker.n +68 -0
  25. package/src/components/DateRangePicker.n +59 -0
  26. package/src/components/Drawer.n +49 -0
  27. package/src/components/GradientText.n +21 -0
  28. package/src/components/Header.n +23 -0
  29. package/src/components/HeroBanner.n +37 -0
  30. package/src/components/HierarchicalMenu.n +38 -0
  31. package/src/components/Icon.n +16 -0
  32. package/src/components/Image.n +31 -0
  33. package/src/components/MegaMenu.n +36 -0
  34. package/src/components/MultiSelect.n +56 -0
  35. package/src/components/NumberInput.n +49 -0
  36. package/src/components/OtpField.n +67 -0
  37. package/src/components/Pagination.n +73 -0
  38. package/src/components/PreviewCard.n +30 -0
  39. package/src/components/Progress.n +26 -0
  40. package/src/components/ProgressCircular.n +52 -0
  41. package/src/components/RichTooltip.n +33 -0
  42. package/src/components/RippleButton.n +56 -0
  43. package/src/components/ScrollArea.n +31 -0
  44. package/src/components/Separator.n +12 -0
  45. package/src/components/ShinyButton.n +36 -0
  46. package/src/components/Skeleton.n +27 -0
  47. package/src/components/Stepper.n +35 -0
  48. package/src/components/Surface.n +10 -0
  49. package/src/components/TagsInput.n +66 -0
  50. package/src/components/ThemeToggler.n +43 -0
  51. package/src/components/TimePicker.n +166 -0
  52. package/src/components/Timeline.n +27 -0
  53. package/src/components/ToggleButton.n +29 -0
  54. package/src/components/ToggleButtonGroup.n +34 -0
  55. package/src/components/Toolbar.n +15 -0
  56. package/src/components/Tree.n +57 -0
  57. package/src/components/Uploader.n +88 -0
  58. package/src/components/Video.n +26 -0
  59. package/src/index.js +66 -0
  60. package/src/motion/AnimatedNumber.n +55 -0
  61. package/src/motion/GradientBackground.n +25 -0
  62. package/src/motion/LiquidButton.n +48 -0
  63. package/src/motion/Reveal.n +37 -0
  64. package/src/motion/Sparkles.n +53 -0
  65. package/src/motion/index.js +18 -0
  66. package/src/styles/atoms.css +1673 -118
  67. package/src/styles/tokens.css +171 -40
  68. package/src/types/Accordion.d.ts +17 -0
  69. package/src/types/AnimatedNumber.d.ts +15 -0
  70. package/src/types/Audio.d.ts +18 -0
  71. package/src/types/Autocomplete.d.ts +17 -0
  72. package/src/types/Avatar.d.ts +14 -0
  73. package/src/types/AvatarGroup.d.ts +15 -0
  74. package/src/types/Badge.d.ts +14 -0
  75. package/src/types/Banner.d.ts +15 -0
  76. package/src/types/Breadcrumbs.d.ts +15 -0
  77. package/src/types/Btn.d.ts +10 -0
  78. package/src/types/ButtonGroup.d.ts +10 -0
  79. package/src/types/Calendar.d.ts +20 -0
  80. package/src/types/Card.d.ts +19 -0
  81. package/src/types/Carousel.d.ts +23 -0
  82. package/src/types/Chip.d.ts +12 -0
  83. package/src/types/Collapsible.d.ts +12 -0
  84. package/src/types/ColorPicker.d.ts +15 -0
  85. package/src/types/ContextMenu.d.ts +19 -0
  86. package/src/types/CopyButton.d.ts +16 -0
  87. package/src/types/DataTable.d.ts +27 -0
  88. package/src/types/DatePicker.d.ts +18 -0
  89. package/src/types/DateRangePicker.d.ts +17 -0
  90. package/src/types/Drawer.d.ts +18 -0
  91. package/src/types/GradientBackground.d.ts +14 -0
  92. package/src/types/GradientText.d.ts +19 -0
  93. package/src/types/Header.d.ts +12 -0
  94. package/src/types/HeroBanner.d.ts +17 -0
  95. package/src/types/HierarchicalMenu.d.ts +20 -0
  96. package/src/types/Icon.d.ts +21 -0
  97. package/src/types/Image.d.ts +15 -0
  98. package/src/types/LiquidButton.d.ts +15 -0
  99. package/src/types/MegaMenu.d.ts +29 -0
  100. package/src/types/MultiSelect.d.ts +13 -0
  101. package/src/types/NumberInput.d.ts +17 -0
  102. package/src/types/OtpField.d.ts +13 -0
  103. package/src/types/Pagination.d.ts +13 -0
  104. package/src/types/PreviewCard.d.ts +16 -0
  105. package/src/types/Progress.d.ts +14 -0
  106. package/src/types/ProgressCircular.d.ts +14 -0
  107. package/src/types/Reveal.d.ts +20 -0
  108. package/src/types/RichTooltip.d.ts +16 -0
  109. package/src/types/RippleButton.d.ts +15 -0
  110. package/src/types/ScrollArea.d.ts +11 -0
  111. package/src/types/Separator.d.ts +9 -0
  112. package/src/types/ShinyButton.d.ts +15 -0
  113. package/src/types/Skeleton.d.ts +13 -0
  114. package/src/types/Sparkles.d.ts +13 -0
  115. package/src/types/Stepper.d.ts +19 -0
  116. package/src/types/Surface.d.ts +10 -0
  117. package/src/types/TagsInput.d.ts +17 -0
  118. package/src/types/ThemeToggler.d.ts +17 -0
  119. package/src/types/TimePicker.d.ts +20 -0
  120. package/src/types/Timeline.d.ts +17 -0
  121. package/src/types/ToggleButton.d.ts +15 -0
  122. package/src/types/ToggleButtonGroup.d.ts +20 -0
  123. package/src/types/Toolbar.d.ts +12 -0
  124. package/src/types/Tree.d.ts +20 -0
  125. package/src/types/Uploader.d.ts +15 -0
  126. 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 Bits UI behind the behavior tier and Iconify behind the icon system.
8
4
 
9
5
  ## Stack
10
6
 
@@ -12,9 +8,10 @@ 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
+ - [Bits UI](https://bits-ui.com) — headless behavior (Dialog, Popover, Tabs, Accordion, …)
12
+ - [Iconify](https://iconify.design) — icons; ships with [Lucide](https://lucide.dev) preset
15
13
  - [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)
14
+ - [`@human-synthesis/norns`](https://github.com/human-synthesis/norns) — peer (`nornsAutoImport`)
18
15
 
19
16
  ## Install
20
17
 
@@ -24,272 +21,119 @@ bun add -D @human-synthesis/norns-ui
24
21
 
25
22
  ## Setup
26
23
 
27
- ### 1. Wire `presetUI()` into `nornsAutoImport`
28
-
29
- So `<Btn>`, `<Field>`, `<Input>`, etc. resolve in markup without explicit imports.
24
+ `vite.config.js`:
30
25
 
31
26
  ```js
32
- // vite.config.js
33
- import { defineConfig } from 'vite';
34
- import { sveltekit } from '@sveltejs/kit/vite';
35
- import tailwindcss from '@tailwindcss/vite';
36
27
  import { nornsAutoImport } from '@human-synthesis/norns/auto-import';
37
- import { nornsCivetPlugin } from '@human-synthesis/norns/vite';
38
28
  import { presetUI } from '@human-synthesis/norns-ui/auto-import';
39
29
 
40
30
  const ui = presetUI();
41
31
 
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
- });
32
+ export default {
33
+ plugins: [
34
+ nornsCivetPlugin(),
35
+ nornsAutoImport({
36
+ componentDirs: ['src/lib/components'],
37
+ components: ui.components
38
+ }),
39
+ tailwindcss(),
40
+ sveltekit()
41
+ ]
42
+ };
53
43
  ```
54
44
 
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
- });
78
- ```
79
-
80
- ### 2. Import the styles
45
+ `app.css`:
81
46
 
82
47
  ```css
83
- /* app.css */
84
48
  @import 'tailwindcss';
85
49
  @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
50
  ```
106
51
 
107
- ## Component catalog
108
-
109
- ### Phase 1 — atoms
110
-
111
- CSS classes via `@layer components`. Use directly via Pug class shorthand:
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:
112
53
 
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`
120
-
121
- ### Phase 1 — Btn
122
-
123
- Wrapped `<button>` with class merging, variant/size props, loading state, and snippet-prop API for icons.
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
+ });
124
61
 
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
62
+ preprocess: [
63
+ ...nornsPreprocess().map(scopeToProject),
64
+ scopeToProject(nornsAutoImport({ /* … */ }))
65
+ ]
133
66
  ```
134
67
 
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
142
-
143
- #### `<Field>`
68
+ ## Dark mode
144
69
 
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.
70
+ Toggle via `<html data-theme="dark">` every component swaps. Persist the choice with a small inline script in `app.html` to avoid FOUC:
146
71
 
147
- ```pug
148
- Field(label="Email" required error!="{form?.errors?.email}" help="We'll never share it")
149
- Input(name="email" type="email" required)
72
+ ```html
73
+ <script>
74
+ (function () {
75
+ try {
76
+ var t = localStorage.getItem('theme');
77
+ if (t === 'dark' || (t === null && matchMedia('(prefers-color-scheme: dark)').matches)) {
78
+ document.documentElement.dataset.theme = 'dark';
79
+ }
80
+ } catch (_) {}
81
+ })();
82
+ </script>
150
83
  ```
151
84
 
152
- Props: `label?`, `help?`, `error?`, `required?`, `id?`, `class?`, `children`.
153
-
154
- #### `<Form>`, `<FieldGroup>`
85
+ ## Icons
155
86
 
156
- Styled `<form>` (vertical stack via `.form`) and `<fieldset>` (`.field-group` with optional legend).
87
+ 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:
157
88
 
158
- ```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
166
- ```
167
-
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"`) |
180
-
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>`
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
89
+ ```js
90
+ // src/routes/+layout.svelte (or wherever fits)
91
+ import { addCollection } from '@iconify/svelte';
92
+ import lucide from '@iconify-json/lucide/icons.json';
93
+ addCollection(lucide);
196
94
  ```
197
95
 
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>`
96
+ Then use `<Icon>` anywhere:
205
97
 
206
98
  ```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") ⋯
99
+ Icon(name="lucide:check" size="size-5")
100
+ Btn(icon="lucide:save") Save
219
101
  ```
220
102
 
221
- #### `<Tooltip>`
103
+ To mix in another icon set, install its JSON package (e.g. `@iconify-json/heroicons`) and `addCollection(...)` it the same way.
222
104
 
223
- ```pug
224
- Tooltip(content="Saved 2 minutes ago")
225
- +snippet('trigger')
226
- Btn(variant="ghost" size="sm") ⓘ
227
- ```
228
-
229
- #### `<Tabs>`
105
+ ## Usage
230
106
 
231
107
  ```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
- ]}")
108
+ Form(action="?/save" form!="{form}")
109
+ Field(label="Title" name="title" required)
110
+ Input(name="title" placeholder="title…" required)
111
+ Btn(type="submit" variant="primary" icon="lucide:save") Save
241
112
  ```
242
113
 
243
- #### `<ToastProvider>` + `toast()`
114
+ `<Field name="title">` reads its error from the parent `<Form>`'s context map; no per-page boilerplate.
244
115
 
245
- Mount once near the app root, then call `toast()` from anywhere:
116
+ ## What's in here
246
117
 
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
- ```
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.
119
+ - **Forms**: `Btn`, `Form`, `Field`, `FieldGroup`, `Input`, `Textarea`, `Select`, `Checkbox`, `Radio`, `Switch`.
120
+ - **Behavior** (Bits UI): `Dialog`, `Sheet`, `Popover`, `Dropdown`, `Tooltip`, `Tabs`, `Accordion`, `Drawer`.
121
+ - **Display**: `Card`, `Surface`, `Banner`, `Badge`, `Chip`, `Avatar`, `Skeleton`, `Progress`, `ProgressCircular`, `Icon`.
122
+ - **Composite**: `Header`, `HeroBanner`, `Stepper`, `Breadcrumbs`, `Pagination`, `Carousel`.
123
+ - **Toast**: `ToastProvider` + `toast()` / `notify()` / `dismiss()` from `@human-synthesis/norns-ui/toast`.
260
124
 
261
- Variants: `info` (default), `success`, `warning`, `error`. Default duration 4000ms; pass `duration: 0` for sticky.
125
+ 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.
262
126
 
263
127
  ## Theming
264
128
 
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
- ```
284
-
285
- ## Override a component
129
+ Tokens are in [`src/styles/tokens.css`](src/styles/tokens.css) as a Tailwind v4 `@theme` block. The library exposes:
286
130
 
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.
131
+ - **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.
132
+ - **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
+ - **Feedback tokens**: `--color-{success,warning,danger,info}-{bg,fg,border}` for tinted surfaces.
134
+ - **Typography tokens**: `--font-sans`, `--font-mono`.
288
135
 
289
- ```
290
- src/lib/components/Btn.n ← your override wins
291
- node_modules/@human-synthesis/norns-ui/src/components/Btn.n ← fallback
292
- ```
136
+ Override by re-declaring `@theme { … }` in your `app.css` after the library import.
293
137
 
294
138
  ## License
295
139
 
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.5",
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)",
@@ -25,6 +25,8 @@
25
25
  "./styles/tokens": "./src/styles/tokens.css",
26
26
  "./styles/atoms": "./src/styles/atoms.css",
27
27
  "./components/*": "./src/components/*",
28
+ "./motion": "./src/motion/index.js",
29
+ "./motion/*": "./src/motion/*",
28
30
  "./package.json": "./package.json"
29
31
  },
30
32
  "peerDependencies": {
@@ -42,7 +44,11 @@
42
44
  }
43
45
  },
44
46
  "dependencies": {
47
+ "@fontsource-variable/outfit": "^5.2.8",
48
+ "@iconify-json/lucide": "^1.2.105",
49
+ "@iconify/svelte": "^5.2.1",
45
50
  "bits-ui": "^2.18.1",
51
+ "motion": "^12.38.0",
46
52
  "tailwind-merge": "^2.5.0"
47
53
  },
48
54
  "engines": {
@@ -26,33 +26,98 @@
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
+ Drawer: c('Drawer'),
70
+ HeroBanner: c('HeroBanner'),
71
+ Stepper: c('Stepper'),
72
+ Breadcrumbs: c('Breadcrumbs'),
73
+ Pagination: c('Pagination'),
74
+ Accordion: c('Accordion'),
75
+ Carousel: c('Carousel'),
76
+ Tree: c('Tree'),
77
+
78
+ // 0.0.4 — wave A: small/standalone
79
+ Separator: c('Separator'),
80
+ Toolbar: c('Toolbar'),
81
+ ButtonGroup: c('ButtonGroup'),
82
+ ToggleButton: c('ToggleButton'),
83
+ ToggleButtonGroup: c('ToggleButtonGroup'),
84
+ Collapsible: c('Collapsible'),
85
+ NumberInput: c('NumberInput'),
86
+ OtpField: c('OtpField'),
87
+ TagsInput: c('TagsInput'),
88
+ Image: c('Image'),
89
+ Audio: c('Audio'),
90
+ Video: c('Video'),
91
+ Timeline: c('Timeline'),
92
+
93
+ // 0.0.4 — wave B: medium / Bits-UI-backed
94
+ Autocomplete: c('Autocomplete'),
95
+ MultiSelect: c('MultiSelect'),
96
+ ContextMenu: c('ContextMenu'),
97
+ RichTooltip: c('RichTooltip'),
98
+ PreviewCard: c('PreviewCard'),
99
+ ScrollArea: c('ScrollArea'),
100
+ ColorPicker: c('ColorPicker'),
101
+ Uploader: c('Uploader'),
102
+ HierarchicalMenu: c('HierarchicalMenu'),
103
+ MegaMenu: c('MegaMenu'),
104
+
105
+ // 0.0.4 — wave C: pickers + table
106
+ DatePicker: c('DatePicker'),
107
+ DateRangePicker: c('DateRangePicker'),
108
+ TimePicker: c('TimePicker'),
109
+ Calendar: c('Calendar'),
110
+ DataTable: c('DataTable'),
111
+
112
+ // 0.0.5 — CSS-only motion components
113
+ AvatarGroup: c('AvatarGroup'),
114
+ GradientText: c('GradientText'),
115
+ CopyButton: c('CopyButton'),
116
+ ThemeToggler: c('ThemeToggler'),
117
+ ShinyButton: c('ShinyButton'),
118
+ RippleButton: c('RippleButton')
119
+ // Motion-driven components live in `@human-synthesis/norns-ui/motion`
120
+ // and are NOT registered here (opt-in import only).
56
121
  },
57
122
  helpers: [
58
123
  {
@@ -0,0 +1,38 @@
1
+ AccordionRoot(
2
+ type!="{multiple ? 'multiple' : 'single'}"
3
+ bind:value
4
+ class!="{classes}"
5
+ )
6
+ +each('items as item')
7
+ AccordionItem.accordion-item(value!="{item.value}")
8
+ AccordionHeader
9
+ AccordionTrigger.accordion-trigger
10
+ span
11
+ | {item.title}
12
+ .accordion-chevron
13
+ Icon(name="lucide:chevron-down" size="size-4")
14
+ AccordionContent.accordion-content
15
+ | {@render item.content?.()}
16
+
17
+ <script>
18
+ import { Accordion } from 'bits-ui'
19
+ import { cn } from '@human-synthesis/norns-ui/cn'
20
+ import Icon from './Icon.n'
21
+
22
+ {
23
+ Root: AccordionRoot
24
+ Item: AccordionItem
25
+ Header: AccordionHeader
26
+ Trigger: AccordionTrigger
27
+ Content: AccordionContent
28
+ } := Accordion
29
+
30
+ {
31
+ items = []
32
+ multiple = false
33
+ value = $bindable(undefined)
34
+ class: extra = ''
35
+ } .= $props()
36
+
37
+ classes := cn 'accordion', extra
38
+ </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>
@@ -0,0 +1,59 @@
1
+ ComboboxRoot(bind:value bind:open type="single")
2
+ .combobox-input-wrap
3
+ ComboboxInput.input.combobox-input(
4
+ placeholder!="{placeholder}"
5
+ disabled!="{disabled}"
6
+ oninput!="{(e) => term = e.currentTarget.value}"
7
+ id!="{resolvedId}"
8
+ aria-invalid!="{hasError ? 'true' : undefined}"
9
+ )
10
+ ComboboxTrigger.combobox-trigger(aria-label="Toggle options")
11
+ Icon(name="lucide:chevrons-up-down" size="size-4")
12
+ ComboboxPortal
13
+ ComboboxContent.dropdown-content
14
+ ComboboxViewport
15
+ +each('filtered as item')
16
+ ComboboxItem.dropdown-item(value!="{item.value}" label!="{item.label}")
17
+ Icon(name="lucide:check" size="size-3.5" class="opacity-0 data-[selected]:opacity-100")
18
+ span {item.label}
19
+ +if('filtered.length === 0')
20
+ .dropdown-item(class="text-fg-muted")
21
+ | No matches
22
+
23
+ <script>
24
+ import { Combobox } from 'bits-ui'
25
+ import Icon from './Icon.n'
26
+ import { getContext } from 'svelte'
27
+
28
+ {
29
+ Root: ComboboxRoot
30
+ Input: ComboboxInput
31
+ Trigger: ComboboxTrigger
32
+ Portal: ComboboxPortal
33
+ Content: ComboboxContent
34
+ Viewport: ComboboxViewport
35
+ Item: ComboboxItem
36
+ } := Combobox
37
+
38
+ {
39
+ items = []
40
+ value = $bindable(undefined)
41
+ open = $bindable(false)
42
+ placeholder = 'Search…'
43
+ disabled = false
44
+ name
45
+ id: providedId
46
+ error: explicitError = false
47
+ } .= $props()
48
+
49
+ field := getContext 'norns-ui:field'
50
+ resolvedId := providedId ?? field?.id
51
+ hasError := explicitError || field?.hasError || false
52
+
53
+ term .= $state ''
54
+
55
+ filtered := $derived.by =>
56
+ return items unless term
57
+ t := term.toLowerCase()
58
+ items.filter (it) => it.label.toLowerCase().includes(t)
59
+ </script>