@nqmcreative/ui 0.2.2 → 0.3.0

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 (189) hide show
  1. package/README.md +15 -15
  2. package/cli/index.mjs +81 -42
  3. package/dist/core/index.d.ts +1 -1
  4. package/dist/core/index.js +1 -1
  5. package/dist/core/toast.svelte.d.ts +3 -0
  6. package/dist/core/trigger.d.ts +6 -0
  7. package/dist/core/trigger.js +13 -0
  8. package/dist/styles/matte/AccordionItem.svelte +5 -0
  9. package/dist/styles/matte/AccordionItem.svelte.d.ts +2 -0
  10. package/dist/styles/matte/Avatar.svelte +15 -0
  11. package/dist/styles/matte/Avatar.svelte.d.ts +3 -0
  12. package/dist/styles/matte/Badge.svelte +5 -0
  13. package/dist/styles/matte/Badge.svelte.d.ts +2 -0
  14. package/dist/styles/matte/Breadcrumb.svelte +11 -4
  15. package/dist/styles/matte/Breadcrumb.svelte.d.ts +3 -0
  16. package/dist/styles/matte/Button.svelte +62 -11
  17. package/dist/styles/matte/Button.svelte.d.ts +14 -2
  18. package/dist/styles/matte/Card.svelte +9 -4
  19. package/dist/styles/matte/Card.svelte.d.ts +2 -0
  20. package/dist/styles/matte/CheckboxGroup.svelte +6 -0
  21. package/dist/styles/matte/CheckboxGroup.svelte.d.ts +2 -0
  22. package/dist/styles/matte/Combobox.svelte +7 -2
  23. package/dist/styles/matte/Combobox.svelte.d.ts +3 -0
  24. package/dist/styles/matte/CommandPalette.svelte +38 -7
  25. package/dist/styles/matte/CommandPalette.svelte.d.ts +8 -0
  26. package/dist/styles/matte/DatePicker.svelte +2 -2
  27. package/dist/styles/matte/Drawer.svelte +7 -2
  28. package/dist/styles/matte/Drawer.svelte.d.ts +2 -0
  29. package/dist/styles/matte/Dropzone.svelte +1 -1
  30. package/dist/styles/matte/FeatureCard.svelte +1 -1
  31. package/dist/styles/matte/Footer.svelte +6 -2
  32. package/dist/styles/matte/Footer.svelte.d.ts +2 -0
  33. package/dist/styles/matte/Input.svelte +4 -4
  34. package/dist/styles/matte/Link.svelte +9 -0
  35. package/dist/styles/matte/Link.svelte.d.ts +4 -0
  36. package/dist/styles/matte/MenuItem.svelte +3 -2
  37. package/dist/styles/matte/Modal.svelte +8 -3
  38. package/dist/styles/matte/Modal.svelte.d.ts +2 -0
  39. package/dist/styles/matte/MultiSelect.svelte +7 -1
  40. package/dist/styles/matte/MultiSelect.svelte.d.ts +3 -0
  41. package/dist/styles/matte/Navbar.svelte +18 -4
  42. package/dist/styles/matte/Navbar.svelte.d.ts +2 -0
  43. package/dist/styles/matte/PricingCard.svelte +19 -11
  44. package/dist/styles/matte/PricingCard.svelte.d.ts +2 -0
  45. package/dist/styles/matte/RadioGroup.svelte +6 -0
  46. package/dist/styles/matte/RadioGroup.svelte.d.ts +2 -0
  47. package/dist/styles/matte/SegmentedControl.svelte +17 -3
  48. package/dist/styles/matte/SegmentedControl.svelte.d.ts +5 -0
  49. package/dist/styles/matte/Select.svelte +1 -1
  50. package/dist/styles/matte/Sidebar.svelte +76 -26
  51. package/dist/styles/matte/Sidebar.svelte.d.ts +15 -1
  52. package/dist/styles/matte/Stat.svelte +8 -1
  53. package/dist/styles/matte/Stat.svelte.d.ts +3 -0
  54. package/dist/styles/matte/StatsBand.svelte +1 -1
  55. package/dist/styles/matte/Steps.svelte +7 -0
  56. package/dist/styles/matte/Steps.svelte.d.ts +3 -0
  57. package/dist/styles/matte/Table.svelte +154 -98
  58. package/dist/styles/matte/Table.svelte.d.ts +12 -0
  59. package/dist/styles/matte/Tabs.svelte +5 -0
  60. package/dist/styles/matte/Tabs.svelte.d.ts +3 -0
  61. package/dist/styles/matte/Testimonial.svelte +2 -2
  62. package/dist/styles/matte/Textarea.svelte +1 -1
  63. package/dist/styles/matte/Toaster.svelte +3 -0
  64. package/dist/styles/matte/icon.d.ts +21 -0
  65. package/dist/styles/matte/icon.js +21 -0
  66. package/dist/styles/matte/index.d.ts +1 -3
  67. package/dist/styles/matte/index.js +0 -2
  68. package/dist/styles/paper/AccordionItem.svelte +5 -0
  69. package/dist/styles/paper/AccordionItem.svelte.d.ts +2 -0
  70. package/dist/styles/paper/Avatar.svelte +15 -0
  71. package/dist/styles/paper/Avatar.svelte.d.ts +3 -0
  72. package/dist/styles/paper/Badge.svelte +5 -0
  73. package/dist/styles/paper/Badge.svelte.d.ts +2 -0
  74. package/dist/styles/paper/Breadcrumb.svelte +8 -4
  75. package/dist/styles/paper/Breadcrumb.svelte.d.ts +3 -0
  76. package/dist/styles/paper/Button.svelte +58 -7
  77. package/dist/styles/paper/Button.svelte.d.ts +14 -2
  78. package/dist/styles/paper/Card.svelte +6 -1
  79. package/dist/styles/paper/Card.svelte.d.ts +2 -0
  80. package/dist/styles/paper/CheckboxGroup.svelte +6 -0
  81. package/dist/styles/paper/CheckboxGroup.svelte.d.ts +2 -0
  82. package/dist/styles/paper/Combobox.svelte +5 -0
  83. package/dist/styles/paper/Combobox.svelte.d.ts +3 -0
  84. package/dist/styles/paper/CommandPalette.svelte +31 -2
  85. package/dist/styles/paper/CommandPalette.svelte.d.ts +8 -0
  86. package/dist/styles/paper/Drawer.svelte +5 -0
  87. package/dist/styles/paper/Drawer.svelte.d.ts +2 -0
  88. package/dist/styles/paper/Footer.svelte +6 -2
  89. package/dist/styles/paper/Footer.svelte.d.ts +2 -0
  90. package/dist/styles/paper/Link.svelte +9 -0
  91. package/dist/styles/paper/Link.svelte.d.ts +4 -0
  92. package/dist/styles/paper/MenuItem.svelte +2 -1
  93. package/dist/styles/paper/Modal.svelte +14 -1
  94. package/dist/styles/paper/Modal.svelte.d.ts +2 -0
  95. package/dist/styles/paper/MultiSelect.svelte +7 -1
  96. package/dist/styles/paper/MultiSelect.svelte.d.ts +3 -0
  97. package/dist/styles/paper/Navbar.svelte +18 -4
  98. package/dist/styles/paper/Navbar.svelte.d.ts +2 -0
  99. package/dist/styles/paper/PricingCard.svelte +23 -15
  100. package/dist/styles/paper/PricingCard.svelte.d.ts +2 -0
  101. package/dist/styles/paper/RadioGroup.svelte +6 -0
  102. package/dist/styles/paper/RadioGroup.svelte.d.ts +2 -0
  103. package/dist/styles/paper/SegmentedControl.svelte +16 -2
  104. package/dist/styles/paper/SegmentedControl.svelte.d.ts +5 -0
  105. package/dist/styles/paper/Sidebar.svelte +76 -26
  106. package/dist/styles/paper/Sidebar.svelte.d.ts +15 -1
  107. package/dist/styles/paper/Stat.svelte +8 -1
  108. package/dist/styles/paper/Stat.svelte.d.ts +3 -0
  109. package/dist/styles/paper/Steps.svelte +7 -0
  110. package/dist/styles/paper/Steps.svelte.d.ts +3 -0
  111. package/dist/styles/paper/Table.svelte +153 -97
  112. package/dist/styles/paper/Table.svelte.d.ts +12 -0
  113. package/dist/styles/paper/Tabs.svelte +5 -0
  114. package/dist/styles/paper/Tabs.svelte.d.ts +3 -0
  115. package/dist/styles/paper/Toaster.svelte +3 -0
  116. package/dist/styles/paper/fonts.css +34 -7
  117. package/dist/styles/paper/icon.d.ts +21 -0
  118. package/dist/styles/paper/icon.js +21 -0
  119. package/dist/styles/paper/index.d.ts +1 -3
  120. package/dist/styles/paper/index.js +0 -2
  121. package/dist/styles/paper/theme.css +11 -8
  122. package/dist/styles/sprout/AccordionItem.svelte +5 -0
  123. package/dist/styles/sprout/AccordionItem.svelte.d.ts +2 -0
  124. package/dist/styles/sprout/Avatar.svelte +15 -0
  125. package/dist/styles/sprout/Avatar.svelte.d.ts +3 -0
  126. package/dist/styles/sprout/Badge.svelte +5 -0
  127. package/dist/styles/sprout/Badge.svelte.d.ts +2 -0
  128. package/dist/styles/sprout/Breadcrumb.svelte +8 -4
  129. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +3 -0
  130. package/dist/styles/sprout/Button.svelte +58 -7
  131. package/dist/styles/sprout/Button.svelte.d.ts +14 -2
  132. package/dist/styles/sprout/Card.svelte +6 -1
  133. package/dist/styles/sprout/Card.svelte.d.ts +2 -0
  134. package/dist/styles/sprout/CheckboxGroup.svelte +6 -0
  135. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +2 -0
  136. package/dist/styles/sprout/Combobox.svelte +5 -0
  137. package/dist/styles/sprout/Combobox.svelte.d.ts +3 -0
  138. package/dist/styles/sprout/CommandPalette.svelte +31 -2
  139. package/dist/styles/sprout/CommandPalette.svelte.d.ts +8 -0
  140. package/dist/styles/sprout/Drawer.svelte +5 -0
  141. package/dist/styles/sprout/Drawer.svelte.d.ts +2 -0
  142. package/dist/styles/sprout/Footer.svelte +6 -2
  143. package/dist/styles/sprout/Footer.svelte.d.ts +2 -0
  144. package/dist/styles/sprout/Link.svelte +9 -0
  145. package/dist/styles/sprout/Link.svelte.d.ts +4 -0
  146. package/dist/styles/sprout/MenuItem.svelte +2 -1
  147. package/dist/styles/sprout/Modal.svelte +14 -1
  148. package/dist/styles/sprout/Modal.svelte.d.ts +2 -0
  149. package/dist/styles/sprout/MultiSelect.svelte +7 -1
  150. package/dist/styles/sprout/MultiSelect.svelte.d.ts +3 -0
  151. package/dist/styles/sprout/Navbar.svelte +18 -4
  152. package/dist/styles/sprout/Navbar.svelte.d.ts +2 -0
  153. package/dist/styles/sprout/PricingCard.svelte +23 -15
  154. package/dist/styles/sprout/PricingCard.svelte.d.ts +2 -0
  155. package/dist/styles/sprout/RadioGroup.svelte +6 -0
  156. package/dist/styles/sprout/RadioGroup.svelte.d.ts +2 -0
  157. package/dist/styles/sprout/SegmentedControl.svelte +16 -2
  158. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +5 -0
  159. package/dist/styles/sprout/Sidebar.svelte +76 -26
  160. package/dist/styles/sprout/Sidebar.svelte.d.ts +15 -1
  161. package/dist/styles/sprout/Stat.svelte +8 -1
  162. package/dist/styles/sprout/Stat.svelte.d.ts +3 -0
  163. package/dist/styles/sprout/Steps.svelte +7 -0
  164. package/dist/styles/sprout/Steps.svelte.d.ts +3 -0
  165. package/dist/styles/sprout/Table.svelte +153 -97
  166. package/dist/styles/sprout/Table.svelte.d.ts +12 -0
  167. package/dist/styles/sprout/Tabs.svelte +5 -0
  168. package/dist/styles/sprout/Tabs.svelte.d.ts +3 -0
  169. package/dist/styles/sprout/Toaster.svelte +3 -0
  170. package/dist/styles/sprout/fonts.css +7 -8
  171. package/dist/styles/sprout/icon.d.ts +21 -0
  172. package/dist/styles/sprout/icon.js +21 -0
  173. package/dist/styles/sprout/index.d.ts +1 -3
  174. package/dist/styles/sprout/index.js +0 -2
  175. package/dist/styles/sprout/theme.css +6 -6
  176. package/package.json +2 -26
  177. package/registry.json +3 -36
  178. package/dist/styles/matte/NumberInput.svelte +0 -96
  179. package/dist/styles/matte/NumberInput.svelte.d.ts +0 -15
  180. package/dist/styles/matte/PasswordInput.svelte +0 -95
  181. package/dist/styles/matte/PasswordInput.svelte.d.ts +0 -12
  182. package/dist/styles/paper/NumberInput.svelte +0 -96
  183. package/dist/styles/paper/NumberInput.svelte.d.ts +0 -15
  184. package/dist/styles/paper/PasswordInput.svelte +0 -100
  185. package/dist/styles/paper/PasswordInput.svelte.d.ts +0 -12
  186. package/dist/styles/sprout/NumberInput.svelte +0 -97
  187. package/dist/styles/sprout/NumberInput.svelte.d.ts +0 -15
  188. package/dist/styles/sprout/PasswordInput.svelte +0 -101
  189. package/dist/styles/sprout/PasswordInput.svelte.d.ts +0 -12
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @nqmcreative/ui
2
2
 
3
3
  Svelte 5 (runes) + Tailwind CSS v4 component library. One package, **three
4
- styles**, 65 components each — the same props, the same behaviour, a different
4
+ styles**, 63 components each — the same props, the same behaviour, a different
5
5
  look. Eight tones, light and dark, no runtime dependencies.
6
6
 
7
7
  | style | import | character |
@@ -131,9 +131,9 @@ Under SSR use `<LocaleProvider>` instead, so the locale is scoped to a request.
131
131
 
132
132
  ```
133
133
  src/lib/core/ behaviour, a11y, types — no markup, no classes
134
- src/lib/styles/matte/ 65 components
135
- src/lib/styles/paper/ 65 components
136
- src/lib/styles/sprout/ 65 components
134
+ src/lib/styles/matte/ 63 components
135
+ src/lib/styles/paper/ 63 components
136
+ src/lib/styles/sprout/ 63 components
137
137
  scripts/catalogue.mjs the list every style must implement
138
138
  ```
139
139
 
@@ -150,17 +150,17 @@ same.
150
150
 
151
151
  ## Components
152
152
 
153
- | group | components |
154
- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
155
- | Actions | `Button`, `Link` |
156
- | Forms | `Input`, `Textarea`, `Select`, `Combobox`, `MultiSelect`, `DatePicker`, `Calendar`, `Checkbox`, `CheckboxGroup`, `Radio`, `RadioGroup`, `SegmentedControl`, `Switch`, `Slider`, `NumberInput`, `PasswordInput`, `InputGroup`, `Dropzone`, `Field`, `Label` |
157
- | Data display | `Badge`, `Avatar`, `AvatarGroup`, `Card`, `Stat`, `Table`, `Kbd` |
158
- | Feedback | `Alert`, `Toaster` + `toast`, `Progress`, `Spinner`, `Skeleton`, `EmptyState` |
159
- | Layout & nav | `Divider`, `Tabs`, `Accordion`, `AccordionItem`, `Breadcrumb`, `Pagination`, `Steps` |
160
- | App shell | `Navbar`, `Sidebar`, `Footer` |
161
- | Overlay | `Modal`, `ConfirmDialog`, `Drawer`, `Dropdown` + `MenuItem` + `MenuSeparator`, `ContextMenu`, `CommandPalette`, `Popover`, `Tooltip` |
162
- | Marketing | `HeroSection`, `FeatureGrid`, `FeatureCard`, `PricingCard`, `Testimonial`, `LogoCloud`, `CTASection`, `StatsBand` |
163
- | System | `ThemeToggle`, `LocaleProvider` |
153
+ | group | components |
154
+ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
155
+ | Actions | `Button`, `Link` |
156
+ | Forms | `Input`, `Textarea`, `Select`, `Combobox`, `MultiSelect`, `DatePicker`, `Calendar`, `Checkbox`, `CheckboxGroup`, `Radio`, `RadioGroup`, `SegmentedControl`, `Switch`, `Slider`, `InputGroup`, `Dropzone`, `Field`, `Label` |
157
+ | Data display | `Badge`, `Avatar`, `AvatarGroup`, `Card`, `Stat`, `Table`, `Kbd` |
158
+ | Feedback | `Alert`, `Toaster` + `toast`, `Progress`, `Spinner`, `Skeleton`, `EmptyState` |
159
+ | Layout & nav | `Divider`, `Tabs`, `Accordion`, `AccordionItem`, `Breadcrumb`, `Pagination`, `Steps` |
160
+ | App shell | `Navbar`, `Sidebar`, `Footer` |
161
+ | Overlay | `Modal`, `ConfirmDialog`, `Drawer`, `Dropdown` + `MenuItem` + `MenuSeparator`, `ContextMenu`, `CommandPalette`, `Popover`, `Tooltip` |
162
+ | Marketing | `HeroSection`, `FeatureGrid`, `FeatureCard`, `PricingCard`, `Testimonial`, `LogoCloud`, `CTASection`, `StatsBand` |
163
+ | System | `ThemeToggle`, `LocaleProvider` |
164
164
 
165
165
  Overlays are built on the native top layer and three in-house actions
166
166
  (`portal`, `focusTrap`, `anchored`) rather than a positioning dependency.
package/cli/index.mjs CHANGED
@@ -3,16 +3,19 @@
3
3
  * @nqmcreative/ui CLI
4
4
  *
5
5
  * nqm-ui init wire the design system into this project
6
+ * nqm-ui update rewrite that wiring after upgrading the package
6
7
  * nqm-ui add <name...> print (or insert) the import for a component
7
8
  * nqm-ui list [category] every component and its subpath
8
9
  * nqm-ui info <name> what one component pulls in
9
10
  *
10
11
  * Node built-ins only, so `bunx @nqmcreative/ui init` needs nothing installed.
11
- * Every write is idempotent: running init twice changes nothing the second
12
- * time.
12
+ * Every write is idempotent: the lines this CLI owns are rewritten from
13
+ * scratch on every run, so running it twice changes nothing the second time
14
+ * and running it after an upgrade picks up whatever those lines became.
13
15
  */
14
16
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
15
17
  import { existsSync } from 'node:fs';
18
+ import { createInterface } from 'node:readline/promises';
16
19
  import { dirname, join, relative, resolve } from 'node:path';
17
20
  import { fileURLToPath } from 'node:url';
18
21
 
@@ -76,27 +79,56 @@ function findComponent(list, query) {
76
79
  * The style to work in. There is no default on purpose: a component belongs to
77
80
  * a style, and picking one by accident is the mistake this package exists to
78
81
  * prevent. Asking once beats shipping the wrong look.
82
+ *
83
+ * `--style` answers it up front. Without it a terminal gets a numbered prompt;
84
+ * a pipe or a CI job gets the list and a non-zero exit, because there is
85
+ * nobody there to answer.
79
86
  */
80
- function pickStyle(styles) {
87
+ async function pickStyle(styles) {
81
88
  const index = argv.indexOf('--style');
82
89
  const wanted = index !== -1 ? argv[index + 1] : null;
83
90
 
84
- if (!wanted) {
85
- log();
86
- log(' which style?');
87
- log();
88
- for (const style of styles) {
89
- log(` ${c.cyan(style.name.padEnd(8))} ${c.dim(style.description)}`);
90
- }
91
- log();
92
- log(` ${c.dim(`e.g. ${command} --style ${styles[0].name}`)}`);
91
+ if (wanted) {
92
+ const named = styles.find((s) => s.name === wanted);
93
+ if (!named) fail(`unknown style "${wanted}" — try ${styles.map((s) => s.name).join(' or ')}`);
94
+ return named;
95
+ }
96
+
97
+ log();
98
+ log(' which style?');
99
+ log();
100
+ styles.forEach((style, i) => {
101
+ log(` ${c.cyan(`${i + 1})`)} ${c.bold(style.name.padEnd(8))} ${c.dim(style.description)}`);
102
+ });
103
+ log();
104
+
105
+ if (!process.stdin.isTTY) {
106
+ log(` ${c.dim(`not a terminal — pass it: ${command} --style ${styles[0].name}`)}`);
93
107
  log();
94
108
  process.exit(1);
95
109
  }
96
110
 
97
- const style = styles.find((s) => s.name === wanted);
98
- if (!style) fail(`unknown style "${wanted}" — try ${styles.map((s) => s.name).join(' or ')}`);
99
- return style;
111
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
112
+ try {
113
+ for (let attempt = 0; attempt < 3; attempt++) {
114
+ const answer = (await rl.question(` 1-${styles.length}, or a name [1]: `)).trim();
115
+ if (!answer) return styles[0];
116
+
117
+ const number = Number(answer);
118
+ if (Number.isInteger(number) && number >= 1 && number <= styles.length) {
119
+ return styles[number - 1];
120
+ }
121
+
122
+ const named = styles.find((s) => s.name === answer.toLowerCase());
123
+ if (named) return named;
124
+
125
+ warn(`no style called "${answer}"`);
126
+ }
127
+ } finally {
128
+ rl.close();
129
+ }
130
+
131
+ fail(`try ${command} --style ${styles[0].name}`);
100
132
  }
101
133
 
102
134
  /* ------------------------------------------------------------ file edits -- */
@@ -151,7 +183,7 @@ function sourcePath(cssRel) {
151
183
 
152
184
  async function init() {
153
185
  const { styles } = await registry();
154
- const style = pickStyle(styles);
186
+ const style = await pickStyle(styles);
155
187
  const project = await detectProject();
156
188
 
157
189
  log();
@@ -185,28 +217,31 @@ async function init() {
185
217
 
186
218
  const css = await readOr(project.css);
187
219
 
188
- // Matched against *this* style's theme, not any of them. A file left over
189
- // from an older layout or wired to a different style has to be reported,
190
- // not skipped as already done.
191
- if (css.includes(`@nqmcreative/ui/${style.name}/theme.css`)) {
192
- skip(`${project.cssRel} already imports the theme`);
220
+ // The block above belongs to this package, so it is rewritten on every run
221
+ // rather than left alone once it is there. That is what makes an upgrade
222
+ // land: when a release changes which files a style imports, or where
223
+ // `@source` has to point, the old lines would otherwise sit here and the
224
+ // new components would come up unstyled.
225
+ //
226
+ // Only those lines. Everything below is the project's own CSS and is
227
+ // carried over untouched — including an `@import` of another package, which
228
+ // is why the match is anchored to this one.
229
+ const stale =
230
+ /^@import ['"]@nqmcreative\/ui\/[^'"]+\.css['"];$|^@source ['"][^'"]*@nqmcreative\/ui[^'"]*['"];$/;
231
+
232
+ const kept = css
233
+ .split('\n')
234
+ .filter((line) => !stale.test(line.trim()))
235
+ .join('\n')
236
+ .replace(/@import ['"]tailwindcss['"];\n?/, '')
237
+ .replace(/\/\* Tailwind v4 skips node_modules[\s\S]*?\*\/\n?/, '')
238
+ .replace(/\n{3,}/g, '\n\n')
239
+ .trim();
240
+
241
+ const body = kept ? `${lines.join('\n')}\n\n${kept}\n` : `${lines.join('\n')}\n`;
242
+ if (body === css) {
243
+ skip(`${project.cssRel} already up to date`);
193
244
  } else {
194
- // Drop whatever this package left here before — an older layout's
195
- // `@nqmcreative/ui/theme.css`, another style's, and the `@source` that
196
- // went with them. Keeping those would either fight the new tokens or fail
197
- // to resolve, so a rewrite is cleaner than an append.
198
- const stale =
199
- /^@import ['"]@nqmcreative\/ui\/[^'"]+\.css['"];$|^@source ['"][^'"]*@nqmcreative\/ui[^'"]*['"];$/;
200
-
201
- const kept = css
202
- .split('\n')
203
- .filter((line) => !stale.test(line.trim()))
204
- .join('\n')
205
- .replace(/@import ['"]tailwindcss['"];\n?/, '')
206
- .replace(/\n{3,}/g, '\n\n')
207
- .trim();
208
-
209
- const body = kept ? `${lines.join('\n')}\n\n${kept}\n` : `${lines.join('\n')}\n`;
210
245
  await write(project.css, body, project.cssRel);
211
246
  }
212
247
 
@@ -321,7 +356,7 @@ async function init() {
321
356
 
322
357
  async function add() {
323
358
  const { components, styles } = await registry();
324
- const style = pickStyle(styles);
359
+ const style = await pickStyle(styles);
325
360
  const names = args.slice(1);
326
361
  if (names.length === 0) fail('which component? e.g. `nqm-ui add button dialog`');
327
362
 
@@ -435,22 +470,26 @@ function help() {
435
470
  log(`
436
471
  ${c.bold('@nqmcreative/ui')}
437
472
 
438
- ${c.cyan('init --style <s>')} wire one style into this project
473
+ ${c.cyan('init')} wire one style into this project
474
+ ${c.cyan('update')} rewrite that wiring after upgrading the package
439
475
  ${c.cyan('add <name...>')} print the import for a component
440
- ${c.dim('--style <s>')} which style to import from (required)
441
476
  ${c.dim('--to <file>')} …and insert it into that file instead
442
477
  ${c.cyan('list [category]')} every component in the catalogue
443
478
  ${c.cyan('info <name>')} what one component pulls in, in every style
444
479
 
480
+ ${c.dim('--style <s>')} skip the picker: matte, paper or sprout
445
481
  ${c.dim('--dry-run')} show the writes without making them
446
482
 
447
- ${c.dim('Every style implements every component. Run init once per project.')}
483
+ ${c.dim('Every style implements every component. Run init once per project,')}
484
+ ${c.dim('then update after each upgrade — it overwrites the lines it owns.')}
448
485
  `);
449
486
  }
450
487
 
451
488
  /* ------------------------------------------------------------------ main -- */
452
489
 
453
- const commands = { init, add, list, info };
490
+ // `update` is `init` under the name people reach for after an upgrade: both
491
+ // rewrite the lines this CLI owns and leave the rest of the file alone.
492
+ const commands = { init, update: init, add, list, info };
454
493
  if (!command || flags.has('--help') || command === 'help') {
455
494
  help();
456
495
  } else if (commands[command]) {
@@ -15,7 +15,7 @@ export type { Placement, AnchorOptions } from './actions/anchor.js';
15
15
  export { clickOutside, focusTrap, focusable, navigateList, portal } from './actions/dismissable.js';
16
16
  export type { ClickOutsideOptions, FocusTrapOptions } from './actions/dismissable.js';
17
17
  export { syncDialog, isBackdropClick } from './dialog.svelte.js';
18
- export { popupTrigger, describeTrigger, menuItems, HOVER_GRACE } from './trigger.js';
18
+ export { popupTrigger, describeTrigger, menuItems, isTypingTarget, HOVER_GRACE } from './trigger.js';
19
19
  export type { PopupTriggerOptions } from './trigger.js';
20
20
  export { ListCursor, groupItems, matchQuery, revealIndex } from './list.svelte.js';
21
21
  export type { ListItem } from './list.svelte.js';
@@ -15,7 +15,7 @@ export { anchored } from './actions/anchor.js';
15
15
  export { clickOutside, focusTrap, focusable, navigateList, portal } from './actions/dismissable.js';
16
16
  /* --- overlays --- */
17
17
  export { syncDialog, isBackdropClick } from './dialog.svelte.js';
18
- export { popupTrigger, describeTrigger, menuItems, HOVER_GRACE } from './trigger.js';
18
+ export { popupTrigger, describeTrigger, menuItems, isTypingTarget, HOVER_GRACE } from './trigger.js';
19
19
  /* --- lists --- */
20
20
  export { ListCursor, groupItems, matchQuery, revealIndex } from './list.svelte.js';
21
21
  /* --- data --- */
@@ -8,6 +8,7 @@
8
8
  * const id = toast.loading('Rendering…');
9
9
  * toast.dismiss(id);
10
10
  */
11
+ import type { Snippet } from 'svelte';
11
12
  import type { Tone } from './tones.js';
12
13
  export interface ToastAction {
13
14
  label: string;
@@ -19,6 +20,8 @@ export interface ToastOptions {
19
20
  /** Milliseconds on screen. `0` keeps it until dismissed. Default 5000. */
20
21
  duration?: number;
21
22
  action?: ToastAction;
23
+ /** Replaces the tone's own glyph. */
24
+ icon?: Snippet;
22
25
  /** Swaps the glyph for a spinner and defaults `duration` to 0. */
23
26
  loading?: boolean;
24
27
  }
@@ -33,3 +33,9 @@ export declare function popupTrigger(node: HTMLElement, options: PopupTriggerOpt
33
33
  export declare function describeTrigger(wrapper: HTMLElement | null, kind: 'menu' | 'dialog' | 'listbox', open: boolean): void;
34
34
  /** The menu items a roving-focus menu should walk, in DOM order. */
35
35
  export declare function menuItems(menu: HTMLElement | null): HTMLElement[];
36
+ /**
37
+ * Whether a keystroke landed in something the reader is typing into. A
38
+ * bare-key shortcut — `/` for search — has to stand down for those, or it
39
+ * eats the character instead of opening anything.
40
+ */
41
+ export declare function isTypingTarget(target: EventTarget | null): boolean;
@@ -75,3 +75,16 @@ export function describeTrigger(wrapper, kind, open) {
75
75
  export function menuItems(menu) {
76
76
  return menu ? [...menu.querySelectorAll('[role="menuitem"]:not([disabled])')] : [];
77
77
  }
78
+ /**
79
+ * Whether a keystroke landed in something the reader is typing into. A
80
+ * bare-key shortcut — `/` for search — has to stand down for those, or it
81
+ * eats the character instead of opening anything.
82
+ */
83
+ export function isTypingTarget(target) {
84
+ if (!(target instanceof HTMLElement))
85
+ return false;
86
+ return (target.isContentEditable ||
87
+ target.tagName === 'INPUT' ||
88
+ target.tagName === 'TEXTAREA' ||
89
+ target.tagName === 'SELECT');
90
+ }
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { toneText, type Tone } from '../../core/tones.js';
5
+ import { iconMd } from './icon.js';
5
6
 
6
7
  interface Props extends HTMLAttributes<HTMLDetailsElement> {
7
8
  title: string;
@@ -9,6 +10,8 @@
9
10
  tone?: Tone;
10
11
  /** Short text on the right of the summary row. */
11
12
  meta?: string;
13
+ /** Leading icon on the summary row. */
14
+ icon?: Snippet;
12
15
  children: Snippet;
13
16
  }
14
17
 
@@ -17,6 +20,7 @@
17
20
  open = $bindable(false),
18
21
  tone = 'brand',
19
22
  meta,
23
+ icon,
20
24
  class: className = '',
21
25
  children,
22
26
  ...rest
@@ -27,6 +31,7 @@
27
31
  <summary
28
32
  class="flex cursor-pointer list-none items-center gap-4 py-4 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out select-none hover:text-text-secondary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current [&::-webkit-details-marker]:hidden"
29
33
  >
34
+ {#if icon}<span class={iconMd}>{@render icon()}</span>{/if}
30
35
  <span class="flex-1 font-medium">{title}</span>
31
36
  {#if meta}<span class="font-mono text-xs text-text-muted">{meta}</span>{/if}
32
37
  <svg
@@ -7,6 +7,8 @@ interface Props extends HTMLAttributes<HTMLDetailsElement> {
7
7
  tone?: Tone;
8
8
  /** Short text on the right of the summary row. */
9
9
  meta?: string;
10
+ /** Leading icon on the summary row. */
11
+ icon?: Snippet;
10
12
  children: Snippet;
11
13
  }
12
14
  declare const AccordionItem: import("svelte").Component<Props, {}, "open">;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { toneSoft, type Tone } from '../../core/tones.js';
4
5
 
@@ -13,6 +14,8 @@
13
14
  tone?: Tone;
14
15
  /** Square instead of the default circle. */
15
16
  squared?: boolean;
17
+ /** Drawn instead of the initials when there is no `src` — a user mark. */
18
+ fallback?: Snippet;
16
19
  }
17
20
 
18
21
  let {
@@ -22,6 +25,7 @@
22
25
  size = 'md',
23
26
  tone = 'brand',
24
27
  squared = false,
28
+ fallback,
25
29
  class: className = '',
26
30
  ...rest
27
31
  }: Props = $props();
@@ -34,6 +38,15 @@
34
38
  xl: 'size-16 text-lg'
35
39
  };
36
40
 
41
+ // The mark tracks the initials: about half the circle, whatever the size.
42
+ const glyphs: Record<AvatarSize, string> = {
43
+ xs: 'grid place-items-center *:size-3',
44
+ sm: 'grid place-items-center *:size-4',
45
+ md: 'grid place-items-center *:size-5',
46
+ lg: 'grid place-items-center *:size-6',
47
+ xl: 'grid place-items-center *:size-8'
48
+ };
49
+
37
50
  const initials = $derived(
38
51
  name
39
52
  .trim()
@@ -52,6 +65,8 @@
52
65
  >
53
66
  {#if src}
54
67
  <img {src} alt={alt ?? name} class="size-full object-cover" />
68
+ {:else if fallback}
69
+ <span class={glyphs[size]}>{@render fallback()}</span>
55
70
  {:else}
56
71
  {initials}
57
72
  {/if}
@@ -1,3 +1,4 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { HTMLAttributes } from 'svelte/elements';
2
3
  import { type Tone } from '../../core/tones.js';
3
4
  export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
@@ -10,6 +11,8 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
10
11
  tone?: Tone;
11
12
  /** Square instead of the default circle. */
12
13
  squared?: boolean;
14
+ /** Drawn instead of the initials when there is no `src` — a user mark. */
15
+ fallback?: Snippet;
13
16
  }
14
17
  declare const Avatar: import("svelte").Component<Props, {}, "">;
15
18
  type Avatar = ReturnType<typeof Avatar>;
@@ -9,6 +9,7 @@
9
9
  toneText,
10
10
  type Tone
11
11
  } from '../../core/tones.js';
12
+ import { iconSm } from './icon.js';
12
13
 
13
14
  export type BadgeVariant = 'soft' | 'solid' | 'outline';
14
15
  export type BadgeSize = 'sm' | 'md';
@@ -19,6 +20,8 @@
19
20
  size?: BadgeSize;
20
21
  /** Small filled circle before the label. */
21
22
  dot?: boolean;
23
+ /** Leading icon, 14px — sits where the dot would. */
24
+ icon?: Snippet;
22
25
  children: Snippet;
23
26
  }
24
27
 
@@ -27,6 +30,7 @@
27
30
  variant = 'soft',
28
31
  size = 'md',
29
32
  dot = false,
33
+ icon,
30
34
  class: className = '',
31
35
  children,
32
36
  ...rest
@@ -54,5 +58,6 @@
54
58
  class="size-1.5 shrink-0 rounded-full {variant === 'solid' ? 'bg-current' : toneFill[tone]}"
55
59
  ></span>
56
60
  {/if}
61
+ {#if icon}<span class={iconSm}>{@render icon()}</span>{/if}
57
62
  {@render children()}
58
63
  </span>
@@ -9,6 +9,8 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
9
9
  size?: BadgeSize;
10
10
  /** Small filled circle before the label. */
11
11
  dot?: boolean;
12
+ /** Leading icon, 14px — sits where the dot would. */
13
+ icon?: Snippet;
12
14
  children: Snippet;
13
15
  }
14
16
  declare const Badge: import("svelte").Component<Props, {}, "">;
@@ -1,11 +1,15 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { toneHoverText, type Tone } from '../../core/tones.js';
4
5
  import { useLocale } from '../../core/locale.svelte.js';
6
+ import { iconMd } from './icon.js';
5
7
 
6
8
  export interface BreadcrumbItem {
7
9
  label: string;
8
10
  href?: string;
11
+ /** Leading icon — a home mark on the first crumb, say. */
12
+ icon?: Snippet;
9
13
  }
10
14
 
11
15
  interface Props extends HTMLAttributes<HTMLElement> {
@@ -29,15 +33,18 @@
29
33
  {#if item.href && i < items.length - 1}
30
34
  <a
31
35
  href={item.href}
32
- class="text-text-secondary transition-colors duration-150 ease-brand-out {toneHoverText[
36
+ class="inline-flex items-center gap-1.5 text-text-secondary transition-colors duration-150 ease-brand-out {toneHoverText[
33
37
  tone
34
38
  ]}"
35
39
  >
36
- {item.label}
40
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}{item.label}
37
41
  </a>
38
42
  {:else}
39
- <span class="text-text" aria-current={i === items.length - 1 ? 'page' : undefined}>
40
- {item.label}
43
+ <span
44
+ class="inline-flex items-center gap-1.5 text-text"
45
+ aria-current={i === items.length - 1 ? 'page' : undefined}
46
+ >
47
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}{item.label}
41
48
  </span>
42
49
  {/if}
43
50
  </li>
@@ -1,8 +1,11 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { HTMLAttributes } from 'svelte/elements';
2
3
  import { type Tone } from '../../core/tones.js';
3
4
  export interface BreadcrumbItem {
4
5
  label: string;
5
6
  href?: string;
7
+ /** Leading icon — a home mark on the first crumb, say. */
8
+ icon?: Snippet;
6
9
  }
7
10
  interface Props extends HTMLAttributes<HTMLElement> {
8
11
  items: BreadcrumbItem[];
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLButtonAttributes } from 'svelte/elements';
4
4
  import Spinner from './Spinner.svelte';
5
+ import { iconMd, iconSm } from './icon.js';
5
6
  import {
6
7
  focusRing,
7
8
  toneBorder,
@@ -17,7 +18,7 @@
17
18
  export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'primary';
18
19
  export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
19
20
 
20
- interface Props extends HTMLButtonAttributes {
21
+ interface Props extends Omit<HTMLButtonAttributes, 'onclick'> {
21
22
  variant?: ButtonVariant;
22
23
  tone?: Tone;
23
24
  size?: ButtonSize;
@@ -28,7 +29,19 @@
28
29
  loading?: boolean;
29
30
  /** Stretch to the container's full width. */
30
31
  block?: boolean;
31
- children: Snippet;
32
+ /** Leading icon. The spinner takes its place while the button is busy. */
33
+ icon?: Snippet;
34
+ /** Trailing icon. */
35
+ iconEnd?: Snippet;
36
+ /** Square button with no label — pass an `aria-label`. */
37
+ iconOnly?: boolean;
38
+ /**
39
+ * Return a promise — a save, a submit, a fetch — and the button spins and
40
+ * locks itself until it settles, without any state in the parent. It
41
+ * unlocks on a rejection too, so catch failures inside the handler.
42
+ */
43
+ onclick?: (event: MouseEvent) => unknown;
44
+ children?: Snippet;
32
45
  }
33
46
 
34
47
  let {
@@ -38,6 +51,10 @@
38
51
  href,
39
52
  loading = false,
40
53
  block = false,
54
+ icon,
55
+ iconEnd,
56
+ iconOnly = false,
57
+ onclick,
41
58
  disabled = false,
42
59
  type = 'button',
43
60
  class: className = '',
@@ -49,10 +66,17 @@
49
66
  'inline-flex items-center justify-center gap-2 border font-sans font-medium whitespace-nowrap transition-all duration-150 ease-brand-out disabled:opacity-50 disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:pointer-events-none';
50
67
 
51
68
  const sizes: Record<ButtonSize, string> = {
52
- sm: 'h-9 px-4 text-[13px]',
53
- md: 'h-11 px-7 text-[15px]',
54
- lg: 'h-12 px-8 text-base',
55
- xl: 'h-14 px-10 text-base'
69
+ sm: 'h-8 px-3.5 text-[13px]',
70
+ md: 'h-10 px-5 text-[15px]',
71
+ lg: 'h-11 px-6 text-base',
72
+ xl: 'h-12 px-8 text-base'
73
+ };
74
+
75
+ const squares: Record<ButtonSize, string> = {
76
+ sm: 'size-8',
77
+ md: 'size-10',
78
+ lg: 'size-11',
79
+ xl: 'size-12'
56
80
  };
57
81
 
58
82
  const variantClass = $derived.by(() => {
@@ -72,7 +96,24 @@
72
96
  });
73
97
 
74
98
  const isLink = $derived(variant === 'link');
75
- const inert = $derived(disabled || loading);
99
+
100
+ /** Set by an `onclick` that returned a promise, on top of the `loading` prop. */
101
+ let pending = $state(false);
102
+ const busy = $derived(loading || pending);
103
+ const inert = $derived(disabled || busy);
104
+
105
+ const iconClass = $derived(size === 'sm' ? iconSm : iconMd);
106
+
107
+ async function handleClick(event: MouseEvent) {
108
+ const result = onclick?.(event);
109
+ // Anything else is a plain handler: nothing to wait for, nothing to lock.
110
+ if (!(result instanceof Promise)) return;
111
+ pending = true;
112
+ // The handler owns its own errors — catch them there to show a message.
113
+ // All the button needs to know is that the work has settled.
114
+ await result.catch(() => {});
115
+ pending = false;
116
+ }
76
117
  </script>
77
118
 
78
119
  <svelte:element
@@ -81,14 +122,24 @@
81
122
  type={href ? undefined : type}
82
123
  disabled={href ? undefined : inert}
83
124
  aria-disabled={href && inert ? 'true' : undefined}
84
- aria-busy={loading ? 'true' : undefined}
125
+ aria-busy={busy ? 'true' : undefined}
85
126
  role={href ? 'button' : undefined}
86
- class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink ? '' : sizes[size]}
127
+ class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink
128
+ ? ''
129
+ : iconOnly
130
+ ? squares[size]
131
+ : sizes[size]}
87
132
  {block ? 'w-full' : ''} {className}"
88
133
  {...rest}
134
+ onclick={handleClick}
89
135
  >
90
- {#if loading}
136
+ {#if busy}
91
137
  <Spinner size={size === 'sm' ? 'xs' : 'sm'} label="" />
138
+ {:else if icon}
139
+ <span class={iconClass} aria-hidden="true">{@render icon()}</span>
140
+ {/if}
141
+ {@render children?.()}
142
+ {#if iconEnd && !iconOnly}
143
+ <span class={iconClass} aria-hidden="true">{@render iconEnd()}</span>
92
144
  {/if}
93
- {@render children()}
94
145
  </svelte:element>