@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.
- package/LICENSE +21 -0
- package/README.md +81 -225
- package/package.json +13 -3
- package/src/auto-import.js +81 -19
- package/src/components/Accordion.n +39 -0
- package/src/components/Audio.n +24 -0
- package/src/components/Autocomplete.n +134 -0
- package/src/components/Avatar.n +27 -0
- package/src/components/AvatarGroup.n +25 -0
- package/src/components/Badge.n +20 -0
- package/src/components/Banner.n +25 -0
- package/src/components/Breadcrumbs.n +22 -0
- package/src/components/Btn.n +29 -8
- package/src/components/ButtonGroup.n +14 -0
- package/src/components/Calendar.n +153 -0
- package/src/components/Card.n +34 -0
- package/src/components/Carousel.n +66 -0
- package/src/components/Chip.n +27 -0
- package/src/components/Collapsible.n +29 -0
- package/src/components/ColorPicker.n +62 -0
- package/src/components/ContextMenu.n +67 -0
- package/src/components/CopyButton.n +50 -0
- package/src/components/DataTable.n +61 -0
- package/src/components/DatePicker.n +56 -0
- package/src/components/DateRangePicker.n +47 -0
- package/src/components/Dialog.n +35 -22
- package/src/components/Dropdown.n +54 -23
- package/src/components/GradientText.n +21 -0
- package/src/components/Header.n +23 -0
- package/src/components/HeroBanner.n +37 -0
- package/src/components/HierarchicalMenu.n +38 -0
- package/src/components/Icon.n +16 -0
- package/src/components/Image.n +31 -0
- package/src/components/MegaMenu.n +36 -0
- package/src/components/MultiSelect.n +158 -0
- package/src/components/NumberInput.n +49 -0
- package/src/components/OtpField.n +67 -0
- package/src/components/Pagination.n +73 -0
- package/src/components/Popover.n +30 -15
- package/src/components/Progress.n +26 -0
- package/src/components/ProgressCircular.n +52 -0
- package/src/components/RippleButton.n +56 -0
- package/src/components/ScrollArea.n +20 -0
- package/src/components/Separator.n +12 -0
- package/src/components/Sheet.n +36 -22
- package/src/components/ShinyButton.n +36 -0
- package/src/components/Skeleton.n +27 -0
- package/src/components/Stepper.n +35 -0
- package/src/components/Surface.n +10 -0
- package/src/components/Tabs.n +39 -13
- package/src/components/TagsInput.n +66 -0
- package/src/components/ThemeToggler.n +43 -0
- package/src/components/TimePicker.n +153 -0
- package/src/components/Timeline.n +27 -0
- package/src/components/ToggleButton.n +35 -0
- package/src/components/ToggleButtonGroup.n +54 -0
- package/src/components/Toolbar.n +15 -0
- package/src/components/Tooltip.n +52 -19
- package/src/components/Tree.n +57 -0
- package/src/components/Uploader.n +88 -0
- package/src/components/Video.n +26 -0
- package/src/index.js +64 -1
- 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/motion/AnimatedNumber.n +55 -0
- package/src/motion/GradientBackground.n +25 -0
- package/src/motion/LiquidButton.n +48 -0
- package/src/motion/Reveal.n +37 -0
- package/src/motion/Sparkles.n +53 -0
- package/src/motion/index.js +18 -0
- package/src/styles/atoms.css +1481 -223
- package/src/styles/tokens.css +204 -40
- package/src/types/Accordion.d.ts +17 -0
- package/src/types/AnimatedNumber.d.ts +15 -0
- package/src/types/Audio.d.ts +18 -0
- package/src/types/Autocomplete.d.ts +17 -0
- package/src/types/Avatar.d.ts +14 -0
- package/src/types/AvatarGroup.d.ts +15 -0
- package/src/types/Badge.d.ts +14 -0
- package/src/types/Banner.d.ts +15 -0
- package/src/types/Breadcrumbs.d.ts +15 -0
- package/src/types/Btn.d.ts +10 -0
- package/src/types/ButtonGroup.d.ts +10 -0
- package/src/types/Calendar.d.ts +20 -0
- package/src/types/Card.d.ts +19 -0
- package/src/types/Carousel.d.ts +23 -0
- package/src/types/Chip.d.ts +12 -0
- package/src/types/Collapsible.d.ts +13 -0
- package/src/types/ColorPicker.d.ts +15 -0
- package/src/types/ContextMenu.d.ts +24 -0
- package/src/types/CopyButton.d.ts +16 -0
- package/src/types/DataTable.d.ts +27 -0
- package/src/types/DatePicker.d.ts +23 -0
- package/src/types/DateRangePicker.d.ts +17 -0
- package/src/types/Dialog.d.ts +4 -0
- package/src/types/Dropdown.d.ts +2 -1
- package/src/types/GradientBackground.d.ts +14 -0
- package/src/types/GradientText.d.ts +19 -0
- package/src/types/Header.d.ts +12 -0
- package/src/types/HeroBanner.d.ts +17 -0
- package/src/types/HierarchicalMenu.d.ts +20 -0
- package/src/types/Icon.d.ts +21 -0
- package/src/types/Image.d.ts +15 -0
- package/src/types/LiquidButton.d.ts +15 -0
- package/src/types/MegaMenu.d.ts +29 -0
- package/src/types/MultiSelect.d.ts +16 -0
- package/src/types/NumberInput.d.ts +17 -0
- package/src/types/OtpField.d.ts +13 -0
- package/src/types/Pagination.d.ts +13 -0
- package/src/types/Progress.d.ts +14 -0
- package/src/types/ProgressCircular.d.ts +14 -0
- package/src/types/Reveal.d.ts +20 -0
- package/src/types/RippleButton.d.ts +15 -0
- package/src/types/ScrollArea.d.ts +11 -0
- package/src/types/Separator.d.ts +9 -0
- package/src/types/Sheet.d.ts +3 -0
- package/src/types/ShinyButton.d.ts +15 -0
- package/src/types/Skeleton.d.ts +13 -0
- package/src/types/Sparkles.d.ts +13 -0
- package/src/types/Stepper.d.ts +19 -0
- package/src/types/Surface.d.ts +10 -0
- package/src/types/Tabs.d.ts +2 -0
- package/src/types/TagsInput.d.ts +17 -0
- package/src/types/ThemeToggler.d.ts +17 -0
- package/src/types/TimePicker.d.ts +20 -0
- package/src/types/Timeline.d.ts +17 -0
- package/src/types/ToggleButton.d.ts +16 -0
- package/src/types/ToggleButtonGroup.d.ts +21 -0
- package/src/types/Toolbar.d.ts +12 -0
- package/src/types/Tooltip.d.ts +3 -0
- package/src/types/Tree.d.ts +20 -0
- package/src/types/Uploader.d.ts +15 -0
- 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
|
|
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
|
-
- [
|
|
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
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
#### `<Sheet>`
|
|
99
|
+
`<Field name="title">` reads its error from the parent `<Form>`'s context map; no per-page boilerplate.
|
|
201
100
|
|
|
202
|
-
|
|
101
|
+
## What's in here
|
|
203
102
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
Tooltip(content="Saved 2 minutes ago")
|
|
225
|
-
+snippet('trigger')
|
|
226
|
-
Btn(variant="ghost" size="sm") ⓘ
|
|
227
|
-
```
|
|
112
|
+
## Behaviors
|
|
228
113
|
|
|
229
|
-
|
|
114
|
+
A small set of headless behaviors is exposed at `@human-synthesis/norns-ui/behaviors` for use in your own components:
|
|
230
115
|
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
import { toast } from '@human-synthesis/norns-ui/toast'
|
|
125
|
+
## Migrating from 0.0.5 → 0.0.6
|
|
255
126
|
|
|
256
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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
|
-
"
|
|
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": {
|
package/src/auto-import.js
CHANGED
|
@@ -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: '
|
|
33
|
+
Btn: c('Btn'),
|
|
33
34
|
|
|
34
35
|
// Phase 2 — forms tier
|
|
35
|
-
Field: '
|
|
36
|
-
FieldGroup: '
|
|
37
|
-
Form: '
|
|
38
|
-
Input: '
|
|
39
|
-
Textarea: '
|
|
40
|
-
Select: '
|
|
41
|
-
Checkbox: '
|
|
42
|
-
Radio: '
|
|
43
|
-
Switch: '
|
|
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: '
|
|
47
|
-
Sheet: '
|
|
48
|
-
Popover: '
|
|
49
|
-
Dropdown: '
|
|
50
|
-
Tooltip: '
|
|
51
|
-
Tabs: '
|
|
52
|
-
ToastProvider: '
|
|
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
|
-
//
|
|
55
|
-
|
|
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>
|