@human-synthesis/norns-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # `@human-synthesis/norns-ui`
2
+
3
+ UI library for the [Norns](https://github.com/human-synthesis/norns) ecosystem — Pug + Civet components on Tailwind v4.
4
+
5
+ **Status: Phase 1 (foundation).** Currently ships `Btn` and the atom CSS layer. Forms tier (Input, Field, Form, …) lands in Phase 2; Bits-UI-backed behavior tier (Dialog, Popover, Tabs, Toast, …) in Phase 3.
6
+
7
+ ## Stack
8
+
9
+ - [Svelte 5](https://svelte.dev) — components and runes
10
+ - [Pug](https://pugjs.org) — templates (in `.n` files)
11
+ - [Civet](https://civet.dev) — `<script>` language
12
+ - [Tailwind CSS v4](https://tailwindcss.com) — styling, **hard peer dep**
13
+ - [tailwind-merge](https://github.com/dcastil/tailwind-merge) — class deduplication
14
+ - `@human-synthesis/norns` `^0.0.7` — peer (`nornsAutoImport` for the registration flow)
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ bun add -D @human-synthesis/norns-ui
20
+ ```
21
+
22
+ ## Setup
23
+
24
+ ### 1. Wire `presetUI()` into `nornsAutoImport`
25
+
26
+ So `<Btn>`, `<Card>`, `<Field>` resolve in markup without explicit imports.
27
+
28
+ ```js
29
+ // vite.config.js
30
+ import { defineConfig } from 'vite';
31
+ import { sveltekit } from '@sveltejs/kit/vite';
32
+ import tailwindcss from '@tailwindcss/vite';
33
+ import { nornsAutoImport } from '@human-synthesis/norns/auto-import';
34
+ import { nornsCivetPlugin } from '@human-synthesis/norns/vite';
35
+ import { presetUI } from '@human-synthesis/norns-ui/auto-import';
36
+
37
+ const ui = presetUI();
38
+
39
+ export default defineConfig({
40
+ plugins: [
41
+ nornsCivetPlugin(),
42
+ nornsAutoImport({
43
+ componentDirs: ['src/lib/components'],
44
+ components: ui.components
45
+ }),
46
+ tailwindcss(),
47
+ sveltekit()
48
+ ]
49
+ });
50
+ ```
51
+
52
+ Same shape goes in `svelte.config.js`'s `preprocess` array.
53
+
54
+ `componentDirs` resolves first — your `src/lib/components/Btn.n` silently shadows the library's `Btn` whenever you want to override.
55
+
56
+ ### 2. Import the styles
57
+
58
+ ```css
59
+ /* app.css */
60
+ @import 'tailwindcss';
61
+ @import '@human-synthesis/norns-ui/styles';
62
+
63
+ /* override tokens here if you want a different brand color */
64
+ @theme {
65
+ --color-primary-500: oklch(60% 0.2 200);
66
+ }
67
+ ```
68
+
69
+ ### 3. Use components in Pug
70
+
71
+ ```pug
72
+ section.space-y-3
73
+ h1.text-3xl Notes
74
+ form(method="POST" action="?/create")
75
+ input.input(name="title" required)
76
+ Btn(type="submit" variant="primary") Create note
77
+ ```
78
+
79
+ ## What ships
80
+
81
+ ### Atoms (CSS-only — Tailwind `@layer components`)
82
+
83
+ Use directly via Pug class shorthand:
84
+
85
+ - `.btn` + variants: `.btn-primary`, `.btn-secondary`, `.btn-ghost`, `.btn-danger`, `.btn-link`
86
+ - `.btn` + sizes: `.btn-sm`, `.btn-lg` (default md is built into `.btn`)
87
+ - `.ui-spinner` — small inline spinner
88
+
89
+ More atoms (`.input`, `.card`, `.badge`, `.field-*`) land in Phase 2.
90
+
91
+ ### Components
92
+
93
+ #### `<Btn>` ([source](src/components/Btn.n))
94
+
95
+ Wrapped `<button>` with class merging, variant/size props, loading state, and snippet-prop API for icons.
96
+
97
+ ```pug
98
+ Btn(variant="primary") Save
99
+ Btn(variant="secondary" size="sm") Cancel
100
+ Btn(loading!="{saving}" type="submit") Save
101
+ Btn(variant="danger" onclick!="{remove}")
102
+ +snippet('leading')
103
+ // icon goes here
104
+ | Delete
105
+ ```
106
+
107
+ Props (see [`src/types/Btn.d.ts`](src/types/Btn.d.ts)):
108
+ - `variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link'` (default `'primary'`)
109
+ - `size?: 'sm' | 'md' | 'lg'` (default `'md'`)
110
+ - `loading?: boolean` — replaces leading icon with `.ui-spinner`, sets `disabled` and `aria-busy`
111
+ - `disabled?: boolean`
112
+ - `type?: 'button' | 'submit' | 'reset'` (default `'button'`)
113
+ - `onclick?: (event) => void`
114
+ - `class?: string` — merged via [`tailwind-merge`](https://github.com/dcastil/tailwind-merge)
115
+ - `children` — default snippet (the button label)
116
+ - `leading`, `trailing` — snippet slots for icons
117
+
118
+ ## Theming
119
+
120
+ 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.
121
+
122
+ 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.
123
+
124
+ ## Class merging
125
+
126
+ Every component takes a `class` prop merged via `tailwind-merge` (the `cn` helper):
127
+
128
+ ```svelte
129
+ <Btn variant="primary" class="w-full" /> <!-- both classes survive; later wins on conflicts -->
130
+ ```
131
+
132
+ `cn` is exported from `@human-synthesis/norns-ui/cn` if you want to swap in plain `clsx` for smaller bundle:
133
+
134
+ ```js
135
+ import { cn } from '@human-synthesis/norns-ui/cn';
136
+ cn('btn', isActive && 'btn-active', extra)
137
+ ```
138
+
139
+ ## Override a component
140
+
141
+ Drop a same-name file in your `src/lib/components/` and `nornsAutoImport`'s first-match-wins resolves to your version silently. No fork needed.
142
+
143
+ ```
144
+ src/lib/components/Btn.n ← your override wins
145
+ node_modules/@human-synthesis/norns-ui/src/components/Btn.n ← fallback
146
+ ```
147
+
148
+ ## License
149
+
150
+ MIT © Daniel Teodoroiu / [Human Synthesis](https://humansynthesis.ai). Built on top of [Svelte](https://github.com/sveltejs/svelte) © Svelte Contributors, MIT licensed.
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@human-synthesis/norns-ui",
3
+ "version": "0.0.1",
4
+ "description": "UI library for the Norns ecosystem — Pug + Civet components on Tailwind v4.",
5
+ "license": "MIT",
6
+ "author": "Daniel Teodoroiu (https://humansynthesis.ai)",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/human-synthesis/norns-ui.git"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "test": "bun test"
18
+ },
19
+ "exports": {
20
+ ".": "./src/index.js",
21
+ "./auto-import": "./src/auto-import.js",
22
+ "./cn": "./src/lib/cn.js",
23
+ "./styles": "./src/styles/index.css",
24
+ "./styles/tokens": "./src/styles/tokens.css",
25
+ "./styles/atoms": "./src/styles/atoms.css",
26
+ "./components/*": "./src/components/*",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "peerDependencies": {
30
+ "@human-synthesis/norns": "^0.0.7",
31
+ "@human-synthesis/norns-core": "^0.0.7",
32
+ "svelte": "^5.0.0",
33
+ "tailwindcss": "^4.0.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@human-synthesis/norns": {
37
+ "optional": false
38
+ },
39
+ "@human-synthesis/norns-core": {
40
+ "optional": false
41
+ }
42
+ },
43
+ "dependencies": {
44
+ "tailwind-merge": "^2.5.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Norns UI auto-import preset.
3
+ *
4
+ * Returns `{ components, helpers }` for `nornsAutoImport`'s options. Wire it
5
+ * into your app's vite.config (or svelte.config) like:
6
+ *
7
+ * import { nornsAutoImport } from '@human-synthesis/norns/auto-import';
8
+ * import { presetUI } from '@human-synthesis/norns-ui/auto-import';
9
+ *
10
+ * const ui = presetUI();
11
+ *
12
+ * // vite.config.js
13
+ * plugins: [
14
+ * nornsCivetPlugin(),
15
+ * nornsAutoImport({
16
+ * componentDirs: ['src/lib/components'], // user folder wins
17
+ * components: ui.components, // norns-ui as fallback
18
+ * helpers: ui.helpers
19
+ * }),
20
+ * sveltekit()
21
+ * ]
22
+ *
23
+ * Components are imported via bare specifiers so the consumer's
24
+ * `node_modules/@human-synthesis/norns-ui/...` is the resolution target.
25
+ * Helpers (functional APIs like `toast()`) are added incrementally as the
26
+ * library grows.
27
+ *
28
+ * @typedef {Object} UIPreset
29
+ * @property {Record<string, string>} components name → bare-specifier import path
30
+ * @property {Array<{ from: string, imports: string[] }>} helpers name groups for nornsAutoImport
31
+ *
32
+ * @returns {UIPreset}
33
+ */
34
+ export function presetUI() {
35
+ return {
36
+ components: {
37
+ Btn: '@human-synthesis/norns-ui/components/Btn.n'
38
+ // Phase 2+ adds: Input, Textarea, Select, Checkbox, Radio, Switch,
39
+ // Field, Form, FieldGroup, Card, Dialog, Sheet, Popover, Dropdown,
40
+ // Tooltip, Tabs, Accordion, Toast, Listbox, Combobox, Pagination,
41
+ // Avatar, Badge, Spinner, Progress, Skeleton, Icon
42
+ },
43
+ helpers: [
44
+ // Phase 3+: { from: '@human-synthesis/norns-ui', imports: ['toast'] }
45
+ ]
46
+ };
47
+ }
@@ -0,0 +1,34 @@
1
+ button(
2
+ type!="{type}"
3
+ class!="{classes}"
4
+ disabled!="{disabled || loading}"
5
+ onclick!="{onclick}"
6
+ aria-busy!="{loading ? 'true' : undefined}"
7
+ )
8
+ +if('loading')
9
+ span.ui-spinner(aria-hidden="true")
10
+ +if('!loading && leading')
11
+ | {@render leading()}
12
+ +if('children')
13
+ | {@render children()}
14
+ +if('!loading && trailing')
15
+ | {@render trailing()}
16
+
17
+ <script>
18
+ import { cn } from '@human-synthesis/norns-ui/cn'
19
+
20
+ {
21
+ variant = 'primary'
22
+ size = 'md'
23
+ loading = false
24
+ disabled = false
25
+ type = 'button'
26
+ onclick = undefined
27
+ children
28
+ leading
29
+ trailing
30
+ class: extra = ''
31
+ } := $props()
32
+
33
+ classes := cn 'btn', `btn-${variant}`, size !== 'md' && `btn-${size}`, extra
34
+ </script>
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Main barrel — re-exports user-facing components and helpers.
3
+ *
4
+ * Most consumption goes through auto-import (via the `presetUI()` factory in
5
+ * `@human-synthesis/norns-ui/auto-import`). This barrel exists for users
6
+ * who prefer explicit imports or who need to reach a component from JS
7
+ * code that auto-import doesn't process (e.g. dynamic mount).
8
+ */
9
+
10
+ export { default as Btn } from './components/Btn.n';
11
+ export { cn } from './lib/cn.js';
12
+ export { variantClasses } from './lib/variants.js';
13
+ export { presetUI } from './auto-import.js';
package/src/lib/cn.js ADDED
@@ -0,0 +1,19 @@
1
+ import { twMerge } from 'tailwind-merge';
2
+
3
+ /**
4
+ * Merge a list of class strings, deduplicating Tailwind utilities so the
5
+ * later one wins (e.g. `cn('p-4', 'p-2')` → `'p-2'`).
6
+ *
7
+ * Falsy inputs (null / undefined / '' / false) are filtered, so consumers
8
+ * can pass conditionals inline:
9
+ * cn('btn', variant === 'primary' && 'btn-primary', extra)
10
+ *
11
+ * Exported from a subpath so users can swap to plain `clsx` if they don't
12
+ * want tailwind-merge's bundle cost.
13
+ *
14
+ * @param {...(string | false | null | undefined)} parts
15
+ * @returns {string}
16
+ */
17
+ export function cn(...parts) {
18
+ return twMerge(parts.filter(Boolean).join(' '));
19
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Map a `{ variant, size, ... }` prop bag to a class string following the
3
+ * `<atom>-<variant>` naming convention. Skips falsy values (so an undefined
4
+ * variant doesn't emit `btn-undefined`).
5
+ *
6
+ * variantClasses('btn', { variant: 'primary', size: 'sm' })
7
+ * → 'btn-primary btn-sm'
8
+ *
9
+ * Used by component wrappers (Btn, Badge, etc.) so the variant convention
10
+ * stays uniform across the library.
11
+ *
12
+ * @param {string} atom
13
+ * @param {Record<string, string | undefined | false>} props
14
+ * @returns {string}
15
+ */
16
+ export function variantClasses(atom, props) {
17
+ const out = [];
18
+ for (const value of Object.values(props)) {
19
+ if (value) out.push(`${atom}-${value}`);
20
+ }
21
+ return out.join(' ');
22
+ }
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Norns UI — component atom classes.
3
+ *
4
+ * Pure Tailwind utility-composition via `@layer components`. No JS, no Svelte
5
+ * — just classes consumers can use directly via Pug shorthand (`button.btn.btn-primary`)
6
+ * or via the wrapper components (`<Btn variant="primary">`).
7
+ *
8
+ * Naming convention: `<atom>-<variant>` and `<atom>-<size>`. AI-predictable.
9
+ */
10
+
11
+ @reference 'tailwindcss';
12
+
13
+ @layer components {
14
+ /* --- Button --- */
15
+ .btn {
16
+ @apply inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium leading-none whitespace-nowrap;
17
+ @apply transition-[background-color,color,box-shadow,opacity] duration-150 ease-out;
18
+ @apply select-none cursor-pointer;
19
+ @apply disabled:cursor-not-allowed disabled:opacity-60;
20
+ @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary-500;
21
+ height: var(--ui-h-md);
22
+ padding-inline: 0.875rem;
23
+ }
24
+
25
+ .btn-sm {
26
+ @apply text-xs;
27
+ height: var(--ui-h-sm);
28
+ padding-inline: 0.625rem;
29
+ }
30
+
31
+ .btn-lg {
32
+ @apply text-base;
33
+ height: var(--ui-h-lg);
34
+ padding-inline: 1.125rem;
35
+ }
36
+
37
+ .btn-primary {
38
+ @apply bg-primary-600 text-white shadow-sm hover:bg-primary-700 active:bg-primary-800;
39
+ }
40
+
41
+ .btn-secondary {
42
+ @apply bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-200 active:bg-neutral-300;
43
+ }
44
+
45
+ .btn-ghost {
46
+ @apply bg-transparent text-neutral-700 hover:bg-neutral-100 active:bg-neutral-200;
47
+ }
48
+
49
+ .btn-danger {
50
+ @apply bg-red-600 text-white shadow-sm hover:bg-red-700 active:bg-red-800 focus-visible:ring-red-500;
51
+ }
52
+
53
+ .btn-link {
54
+ @apply bg-transparent text-primary-600 underline-offset-4 hover:underline px-0;
55
+ height: auto;
56
+ }
57
+
58
+ /* --- Spinner (used inside Btn loading state) --- */
59
+ .ui-spinner {
60
+ @apply inline-block size-4 animate-spin rounded-full border-2 border-current border-t-transparent;
61
+ }
62
+ }
@@ -0,0 +1,8 @@
1
+ /*
2
+ * Single entry point — consumers do `@import '@human-synthesis/norns-ui/styles'`
3
+ * in their app.css after the Tailwind import. Tokens come first so atoms can
4
+ * `@apply` against them.
5
+ */
6
+
7
+ @import './tokens.css';
8
+ @import './atoms.css';
@@ -0,0 +1,58 @@
1
+ /*
2
+ * Norns UI — design tokens.
3
+ *
4
+ * Tailwind v4 `@theme` block. Users override by re-declaring `@theme { ... }`
5
+ * after `@import '@human-synthesis/norns-ui/styles'` in their own app.css —
6
+ * Tailwind merges layered theme blocks. Same goes for dark mode via the
7
+ * `[data-theme="dark"]` selector at the bottom.
8
+ */
9
+
10
+ @theme {
11
+ /* Brand color (violet) — keeps Norns's existing violet-500 accent. */
12
+ --color-primary-50: oklch(97.7% 0.014 308.299);
13
+ --color-primary-100: oklch(94.6% 0.033 307.174);
14
+ --color-primary-200: oklch(90.2% 0.063 306.703);
15
+ --color-primary-300: oklch(81.1% 0.111 293.571);
16
+ --color-primary-400: oklch(70.2% 0.183 293.541);
17
+ --color-primary-500: oklch(60.6% 0.25 292.717);
18
+ --color-primary-600: oklch(54.1% 0.281 293.009);
19
+ --color-primary-700: oklch(49.1% 0.27 292.581);
20
+ --color-primary-800: oklch(43.2% 0.232 292.759);
21
+ --color-primary-900: oklch(38% 0.189 293.745);
22
+ --color-primary-950: oklch(28.3% 0.141 291.089);
23
+
24
+ /* Semantic surface colors derived from neutral + primary. Components
25
+ * reference `--color-fg`, `--color-muted`, etc. so consumer themes can
26
+ * remap surfaces without overriding every utility. */
27
+ --color-fg: var(--color-neutral-900);
28
+ --color-fg-muted: var(--color-neutral-600);
29
+ --color-fg-subtle: var(--color-neutral-400);
30
+ --color-bg: white;
31
+ --color-bg-muted: var(--color-neutral-100);
32
+ --color-bg-subtle: var(--color-neutral-50);
33
+ --color-border: var(--color-neutral-200);
34
+ --color-border-strong: var(--color-neutral-300);
35
+
36
+ /* Component sizing — kept minimal; most sizing is via Tailwind utilities. */
37
+ --ui-radius-sm: 0.25rem;
38
+ --ui-radius: 0.375rem;
39
+ --ui-radius-lg: 0.5rem;
40
+ --ui-radius-full: 9999px;
41
+
42
+ --ui-h-sm: 1.75rem; /* 28px */
43
+ --ui-h-md: 2.25rem; /* 36px */
44
+ --ui-h-lg: 2.75rem; /* 44px */
45
+ }
46
+
47
+ /* Dark theme — user toggles via <html data-theme="dark">. Override surfaces;
48
+ * primary scale stays the same (Tailwind primaries already work on dark). */
49
+ [data-theme='dark'] {
50
+ --color-fg: var(--color-neutral-100);
51
+ --color-fg-muted: var(--color-neutral-400);
52
+ --color-fg-subtle: var(--color-neutral-500);
53
+ --color-bg: var(--color-neutral-900);
54
+ --color-bg-muted: var(--color-neutral-800);
55
+ --color-bg-subtle: var(--color-neutral-850, oklch(25% 0 0));
56
+ --color-border: var(--color-neutral-800);
57
+ --color-border-strong: var(--color-neutral-700);
58
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Type shim for `Btn.n`. Hand-rolled until we have a proper
3
+ * `svelte-package` build over the Civet+Pug source.
4
+ */
5
+
6
+ import type { Component, Snippet } from 'svelte';
7
+ import type { HTMLButtonAttributes } from 'svelte/elements';
8
+
9
+ export type BtnVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'link';
10
+ export type BtnSize = 'sm' | 'md' | 'lg';
11
+
12
+ export type BtnProps = Omit<HTMLButtonAttributes, 'class' | 'children'> & {
13
+ variant?: BtnVariant;
14
+ size?: BtnSize;
15
+ loading?: boolean;
16
+ disabled?: boolean;
17
+ type?: 'button' | 'submit' | 'reset';
18
+ class?: string;
19
+ children?: Snippet;
20
+ leading?: Snippet;
21
+ trailing?: Snippet;
22
+ };
23
+
24
+ declare const Btn: Component<BtnProps>;
25
+ export default Btn;