@nqmcreative/ui 0.2.3 → 0.4.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 (232) hide show
  1. package/README.md +209 -15
  2. package/cli/index.mjs +328 -112
  3. package/dist/core/color.d.ts +24 -0
  4. package/dist/core/color.js +39 -0
  5. package/dist/core/index.d.ts +8 -1
  6. package/dist/core/index.js +5 -1
  7. package/dist/core/locale.svelte.d.ts +8 -0
  8. package/dist/core/locale.svelte.js +14 -0
  9. package/dist/core/number.d.ts +22 -0
  10. package/dist/core/number.js +33 -0
  11. package/dist/core/pin.d.ts +23 -0
  12. package/dist/core/pin.js +38 -0
  13. package/dist/core/tags.d.ts +37 -0
  14. package/dist/core/tags.js +51 -0
  15. package/dist/core/time.d.ts +30 -0
  16. package/dist/core/time.js +78 -0
  17. package/dist/core/toast.svelte.d.ts +3 -0
  18. package/dist/styles/matte/AccordionItem.svelte +5 -0
  19. package/dist/styles/matte/AccordionItem.svelte.d.ts +2 -0
  20. package/dist/styles/matte/Avatar.svelte +15 -0
  21. package/dist/styles/matte/Avatar.svelte.d.ts +3 -0
  22. package/dist/styles/matte/Badge.svelte +5 -0
  23. package/dist/styles/matte/Badge.svelte.d.ts +2 -0
  24. package/dist/styles/matte/Breadcrumb.svelte +11 -4
  25. package/dist/styles/matte/Breadcrumb.svelte.d.ts +3 -0
  26. package/dist/styles/matte/Button.svelte +58 -7
  27. package/dist/styles/matte/Button.svelte.d.ts +14 -2
  28. package/dist/styles/matte/Card.svelte +6 -1
  29. package/dist/styles/matte/Card.svelte.d.ts +2 -0
  30. package/dist/styles/matte/CheckboxGroup.svelte +6 -0
  31. package/dist/styles/matte/CheckboxGroup.svelte.d.ts +2 -0
  32. package/dist/styles/matte/ColorInput.svelte +110 -0
  33. package/dist/styles/matte/ColorInput.svelte.d.ts +16 -0
  34. package/dist/styles/matte/Combobox.svelte +5 -0
  35. package/dist/styles/matte/Combobox.svelte.d.ts +3 -0
  36. package/dist/styles/matte/CommandPalette.svelte +6 -0
  37. package/dist/styles/matte/CommandPalette.svelte.d.ts +3 -0
  38. package/dist/styles/matte/CurrencyInput.svelte +108 -0
  39. package/dist/styles/matte/CurrencyInput.svelte.d.ts +26 -0
  40. package/dist/styles/matte/Drawer.svelte +5 -0
  41. package/dist/styles/matte/Drawer.svelte.d.ts +2 -0
  42. package/dist/styles/matte/FileInput.svelte +194 -0
  43. package/dist/styles/matte/FileInput.svelte.d.ts +32 -0
  44. package/dist/styles/matte/Footer.svelte +6 -2
  45. package/dist/styles/matte/Footer.svelte.d.ts +2 -0
  46. package/dist/styles/matte/InputAddon.svelte +36 -0
  47. package/dist/styles/matte/InputAddon.svelte.d.ts +13 -0
  48. package/dist/styles/matte/Link.svelte +9 -0
  49. package/dist/styles/matte/Link.svelte.d.ts +4 -0
  50. package/dist/styles/matte/MenuItem.svelte +2 -1
  51. package/dist/styles/matte/Modal.svelte +5 -0
  52. package/dist/styles/matte/Modal.svelte.d.ts +2 -0
  53. package/dist/styles/matte/MultiSelect.svelte +7 -1
  54. package/dist/styles/matte/MultiSelect.svelte.d.ts +3 -0
  55. package/dist/styles/matte/Navbar.svelte +18 -4
  56. package/dist/styles/matte/Navbar.svelte.d.ts +2 -0
  57. package/dist/styles/matte/PinInput.svelte +137 -0
  58. package/dist/styles/matte/PinInput.svelte.d.ts +25 -0
  59. package/dist/styles/matte/PricingCard.svelte +16 -8
  60. package/dist/styles/matte/PricingCard.svelte.d.ts +2 -0
  61. package/dist/styles/matte/RadioGroup.svelte +6 -0
  62. package/dist/styles/matte/RadioGroup.svelte.d.ts +2 -0
  63. package/dist/styles/matte/SearchInput.svelte +87 -0
  64. package/dist/styles/matte/SearchInput.svelte.d.ts +24 -0
  65. package/dist/styles/matte/SegmentedControl.svelte +16 -2
  66. package/dist/styles/matte/SegmentedControl.svelte.d.ts +5 -0
  67. package/dist/styles/matte/Sidebar.svelte +2 -1
  68. package/dist/styles/matte/Stat.svelte +8 -1
  69. package/dist/styles/matte/Stat.svelte.d.ts +3 -0
  70. package/dist/styles/matte/Steps.svelte +7 -0
  71. package/dist/styles/matte/Steps.svelte.d.ts +3 -0
  72. package/dist/styles/matte/Table.svelte +153 -97
  73. package/dist/styles/matte/Table.svelte.d.ts +12 -0
  74. package/dist/styles/matte/Tabs.svelte +5 -0
  75. package/dist/styles/matte/Tabs.svelte.d.ts +3 -0
  76. package/dist/styles/matte/TagsInput.svelte +173 -0
  77. package/dist/styles/matte/TagsInput.svelte.d.ts +29 -0
  78. package/dist/styles/matte/TimeInput.svelte +129 -0
  79. package/dist/styles/matte/TimeInput.svelte.d.ts +21 -0
  80. package/dist/styles/matte/Toaster.svelte +3 -0
  81. package/dist/styles/matte/icon.d.ts +21 -0
  82. package/dist/styles/matte/icon.js +21 -0
  83. package/dist/styles/matte/index.d.ts +8 -2
  84. package/dist/styles/matte/index.js +8 -2
  85. package/dist/styles/paper/AccordionItem.svelte +5 -0
  86. package/dist/styles/paper/AccordionItem.svelte.d.ts +2 -0
  87. package/dist/styles/paper/Avatar.svelte +15 -0
  88. package/dist/styles/paper/Avatar.svelte.d.ts +3 -0
  89. package/dist/styles/paper/Badge.svelte +5 -0
  90. package/dist/styles/paper/Badge.svelte.d.ts +2 -0
  91. package/dist/styles/paper/Breadcrumb.svelte +8 -4
  92. package/dist/styles/paper/Breadcrumb.svelte.d.ts +3 -0
  93. package/dist/styles/paper/Button.svelte +58 -7
  94. package/dist/styles/paper/Button.svelte.d.ts +14 -2
  95. package/dist/styles/paper/Card.svelte +6 -1
  96. package/dist/styles/paper/Card.svelte.d.ts +2 -0
  97. package/dist/styles/paper/CheckboxGroup.svelte +6 -0
  98. package/dist/styles/paper/CheckboxGroup.svelte.d.ts +2 -0
  99. package/dist/styles/paper/ColorInput.svelte +112 -0
  100. package/dist/styles/paper/ColorInput.svelte.d.ts +16 -0
  101. package/dist/styles/paper/Combobox.svelte +5 -0
  102. package/dist/styles/paper/Combobox.svelte.d.ts +3 -0
  103. package/dist/styles/paper/CommandPalette.svelte +6 -0
  104. package/dist/styles/paper/CommandPalette.svelte.d.ts +3 -0
  105. package/dist/styles/paper/CurrencyInput.svelte +108 -0
  106. package/dist/styles/paper/CurrencyInput.svelte.d.ts +26 -0
  107. package/dist/styles/paper/Drawer.svelte +5 -0
  108. package/dist/styles/paper/Drawer.svelte.d.ts +2 -0
  109. package/dist/styles/paper/FileInput.svelte +204 -0
  110. package/dist/styles/paper/FileInput.svelte.d.ts +32 -0
  111. package/dist/styles/paper/Footer.svelte +6 -2
  112. package/dist/styles/paper/Footer.svelte.d.ts +2 -0
  113. package/dist/styles/paper/InputAddon.svelte +36 -0
  114. package/dist/styles/paper/InputAddon.svelte.d.ts +13 -0
  115. package/dist/styles/paper/Link.svelte +9 -0
  116. package/dist/styles/paper/Link.svelte.d.ts +4 -0
  117. package/dist/styles/paper/MenuItem.svelte +2 -1
  118. package/dist/styles/paper/Modal.svelte +5 -0
  119. package/dist/styles/paper/Modal.svelte.d.ts +2 -0
  120. package/dist/styles/paper/MultiSelect.svelte +7 -1
  121. package/dist/styles/paper/MultiSelect.svelte.d.ts +3 -0
  122. package/dist/styles/paper/Navbar.svelte +18 -4
  123. package/dist/styles/paper/Navbar.svelte.d.ts +2 -0
  124. package/dist/styles/paper/PinInput.svelte +137 -0
  125. package/dist/styles/paper/PinInput.svelte.d.ts +25 -0
  126. package/dist/styles/paper/PricingCard.svelte +23 -15
  127. package/dist/styles/paper/PricingCard.svelte.d.ts +2 -0
  128. package/dist/styles/paper/RadioGroup.svelte +6 -0
  129. package/dist/styles/paper/RadioGroup.svelte.d.ts +2 -0
  130. package/dist/styles/paper/SearchInput.svelte +92 -0
  131. package/dist/styles/paper/SearchInput.svelte.d.ts +24 -0
  132. package/dist/styles/paper/SegmentedControl.svelte +16 -2
  133. package/dist/styles/paper/SegmentedControl.svelte.d.ts +5 -0
  134. package/dist/styles/paper/Sidebar.svelte +2 -1
  135. package/dist/styles/paper/Stat.svelte +8 -1
  136. package/dist/styles/paper/Stat.svelte.d.ts +3 -0
  137. package/dist/styles/paper/Steps.svelte +7 -0
  138. package/dist/styles/paper/Steps.svelte.d.ts +3 -0
  139. package/dist/styles/paper/Table.svelte +153 -97
  140. package/dist/styles/paper/Table.svelte.d.ts +12 -0
  141. package/dist/styles/paper/Tabs.svelte +5 -0
  142. package/dist/styles/paper/Tabs.svelte.d.ts +3 -0
  143. package/dist/styles/paper/TagsInput.svelte +178 -0
  144. package/dist/styles/paper/TagsInput.svelte.d.ts +29 -0
  145. package/dist/styles/paper/TimeInput.svelte +155 -0
  146. package/dist/styles/paper/TimeInput.svelte.d.ts +21 -0
  147. package/dist/styles/paper/Toaster.svelte +3 -0
  148. package/dist/styles/paper/icon.d.ts +21 -0
  149. package/dist/styles/paper/icon.js +21 -0
  150. package/dist/styles/paper/index.d.ts +8 -2
  151. package/dist/styles/paper/index.js +8 -2
  152. package/dist/styles/sprout/AccordionItem.svelte +5 -0
  153. package/dist/styles/sprout/AccordionItem.svelte.d.ts +2 -0
  154. package/dist/styles/sprout/Avatar.svelte +15 -0
  155. package/dist/styles/sprout/Avatar.svelte.d.ts +3 -0
  156. package/dist/styles/sprout/Badge.svelte +5 -0
  157. package/dist/styles/sprout/Badge.svelte.d.ts +2 -0
  158. package/dist/styles/sprout/Breadcrumb.svelte +8 -4
  159. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +3 -0
  160. package/dist/styles/sprout/Button.svelte +58 -7
  161. package/dist/styles/sprout/Button.svelte.d.ts +14 -2
  162. package/dist/styles/sprout/Card.svelte +6 -1
  163. package/dist/styles/sprout/Card.svelte.d.ts +2 -0
  164. package/dist/styles/sprout/CheckboxGroup.svelte +6 -0
  165. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +2 -0
  166. package/dist/styles/sprout/ColorInput.svelte +112 -0
  167. package/dist/styles/sprout/ColorInput.svelte.d.ts +16 -0
  168. package/dist/styles/sprout/Combobox.svelte +5 -0
  169. package/dist/styles/sprout/Combobox.svelte.d.ts +3 -0
  170. package/dist/styles/sprout/CommandPalette.svelte +6 -0
  171. package/dist/styles/sprout/CommandPalette.svelte.d.ts +3 -0
  172. package/dist/styles/sprout/CurrencyInput.svelte +108 -0
  173. package/dist/styles/sprout/CurrencyInput.svelte.d.ts +26 -0
  174. package/dist/styles/sprout/Drawer.svelte +5 -0
  175. package/dist/styles/sprout/Drawer.svelte.d.ts +2 -0
  176. package/dist/styles/sprout/FileInput.svelte +205 -0
  177. package/dist/styles/sprout/FileInput.svelte.d.ts +32 -0
  178. package/dist/styles/sprout/Footer.svelte +6 -2
  179. package/dist/styles/sprout/Footer.svelte.d.ts +2 -0
  180. package/dist/styles/sprout/InputAddon.svelte +37 -0
  181. package/dist/styles/sprout/InputAddon.svelte.d.ts +13 -0
  182. package/dist/styles/sprout/Link.svelte +9 -0
  183. package/dist/styles/sprout/Link.svelte.d.ts +4 -0
  184. package/dist/styles/sprout/MenuItem.svelte +2 -1
  185. package/dist/styles/sprout/Modal.svelte +5 -0
  186. package/dist/styles/sprout/Modal.svelte.d.ts +2 -0
  187. package/dist/styles/sprout/MultiSelect.svelte +7 -1
  188. package/dist/styles/sprout/MultiSelect.svelte.d.ts +3 -0
  189. package/dist/styles/sprout/Navbar.svelte +18 -4
  190. package/dist/styles/sprout/Navbar.svelte.d.ts +2 -0
  191. package/dist/styles/sprout/PinInput.svelte +138 -0
  192. package/dist/styles/sprout/PinInput.svelte.d.ts +25 -0
  193. package/dist/styles/sprout/PricingCard.svelte +23 -15
  194. package/dist/styles/sprout/PricingCard.svelte.d.ts +2 -0
  195. package/dist/styles/sprout/RadioGroup.svelte +6 -0
  196. package/dist/styles/sprout/RadioGroup.svelte.d.ts +2 -0
  197. package/dist/styles/sprout/SearchInput.svelte +92 -0
  198. package/dist/styles/sprout/SearchInput.svelte.d.ts +24 -0
  199. package/dist/styles/sprout/SegmentedControl.svelte +16 -2
  200. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +5 -0
  201. package/dist/styles/sprout/Sidebar.svelte +2 -1
  202. package/dist/styles/sprout/Stat.svelte +8 -1
  203. package/dist/styles/sprout/Stat.svelte.d.ts +3 -0
  204. package/dist/styles/sprout/Steps.svelte +7 -0
  205. package/dist/styles/sprout/Steps.svelte.d.ts +3 -0
  206. package/dist/styles/sprout/Table.svelte +153 -97
  207. package/dist/styles/sprout/Table.svelte.d.ts +12 -0
  208. package/dist/styles/sprout/Tabs.svelte +5 -0
  209. package/dist/styles/sprout/Tabs.svelte.d.ts +3 -0
  210. package/dist/styles/sprout/TagsInput.svelte +179 -0
  211. package/dist/styles/sprout/TagsInput.svelte.d.ts +29 -0
  212. package/dist/styles/sprout/TimeInput.svelte +155 -0
  213. package/dist/styles/sprout/TimeInput.svelte.d.ts +21 -0
  214. package/dist/styles/sprout/Toaster.svelte +3 -0
  215. package/dist/styles/sprout/icon.d.ts +21 -0
  216. package/dist/styles/sprout/icon.js +21 -0
  217. package/dist/styles/sprout/index.d.ts +8 -2
  218. package/dist/styles/sprout/index.js +8 -2
  219. package/package.json +98 -26
  220. package/registry.json +143 -35
  221. package/dist/styles/matte/NumberInput.svelte +0 -96
  222. package/dist/styles/matte/NumberInput.svelte.d.ts +0 -15
  223. package/dist/styles/matte/PasswordInput.svelte +0 -95
  224. package/dist/styles/matte/PasswordInput.svelte.d.ts +0 -12
  225. package/dist/styles/paper/NumberInput.svelte +0 -96
  226. package/dist/styles/paper/NumberInput.svelte.d.ts +0 -15
  227. package/dist/styles/paper/PasswordInput.svelte +0 -100
  228. package/dist/styles/paper/PasswordInput.svelte.d.ts +0 -12
  229. package/dist/styles/sprout/NumberInput.svelte +0 -97
  230. package/dist/styles/sprout/NumberInput.svelte.d.ts +0 -15
  231. package/dist/styles/sprout/PasswordInput.svelte +0 -101
  232. package/dist/styles/sprout/PasswordInput.svelte.d.ts +0 -12
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 -- */
@@ -121,6 +153,39 @@ async function write(path, content, label) {
121
153
 
122
154
  /* -------------------------------------------------------------- detection -- */
123
155
 
156
+ /**
157
+ * Which kind of project this is.
158
+ *
159
+ * The library itself is plain Svelte 5 — no `$app/*`, no `$env/*`, no
160
+ * `@sveltejs/kit` anywhere in it — so a framework only decides where four
161
+ * things live:
162
+ *
163
+ * kit src/app.css src/app.html src/routes/+layout.svelte
164
+ * vite src/app.css index.html src/main.ts
165
+ * laravel resources/css/… resources/views/… the @vite directive
166
+ */
167
+ function detectFramework(deps) {
168
+ if ('@sveltejs/kit' in deps) return 'kit';
169
+ if (existsSync(join(cwd, 'artisan'))) return 'laravel';
170
+ return 'vite';
171
+ }
172
+
173
+ const FRAMEWORK_LABEL = {
174
+ kit: 'SvelteKit',
175
+ laravel: 'Laravel + Svelte',
176
+ vite: 'Svelte + Vite'
177
+ };
178
+
179
+ /** The first candidate that exists, or the first one as the place to create. */
180
+ function pickPath(candidates) {
181
+ return candidates.find((path) => existsSync(join(cwd, path))) ?? candidates[0];
182
+ }
183
+
184
+ /** The one that exists, or null — for files we patch but never invent. */
185
+ function findPath(candidates) {
186
+ return candidates.find((path) => existsSync(join(cwd, path))) ?? null;
187
+ }
188
+
124
189
  async function detectProject() {
125
190
  const pkgPath = join(cwd, 'package.json');
126
191
  if (!existsSync(pkgPath)) fail('no package.json here — run this inside your project');
@@ -128,17 +193,156 @@ async function detectProject() {
128
193
  const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
129
194
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
130
195
 
131
- const isKit = '@sveltejs/kit' in deps;
196
+ const framework = detectFramework(deps);
197
+ const isKit = framework === 'kit';
132
198
  const hasTailwind = '@tailwindcss/vite' in deps || 'tailwindcss' in deps;
133
199
  const hasUi = '@nqmcreative/ui' in deps;
200
+ // Kit brings its own; everywhere else the components are .svelte files in
201
+ // node_modules and something has to compile them.
202
+ const hasCompiler = isKit || '@sveltejs/vite-plugin-svelte' in deps;
134
203
 
135
204
  // Where the Tailwind entry CSS lives, or should.
136
- const candidates = isKit
137
- ? ['src/app.css', 'src/routes/+layout.css', 'src/app.postcss']
138
- : ['src/app.css', 'src/style.css', 'src/main.css'];
139
- const css = candidates.find((path) => existsSync(join(cwd, path))) ?? candidates[0];
205
+ const css = pickPath(
206
+ {
207
+ kit: ['src/app.css', 'src/routes/+layout.css', 'src/app.postcss'],
208
+ laravel: ['resources/css/app.css'],
209
+ vite: ['src/app.css', 'src/style.css', 'src/main.css']
210
+ }[framework]
211
+ );
212
+
213
+ // The document head, for the font preconnect and the no-flash theme script.
214
+ // Kit gets one written if it is missing; the other two are only patched,
215
+ // because guessing at someone's Blade layout would do more harm than a
216
+ // printed reminder.
217
+ const head = {
218
+ kit: 'src/app.html',
219
+ laravel: findPath([
220
+ 'resources/views/app.blade.php',
221
+ 'resources/views/layouts/app.blade.php',
222
+ 'resources/views/components/layouts/app.blade.php',
223
+ 'resources/views/welcome.blade.php'
224
+ ]),
225
+ vite: findPath(['index.html'])
226
+ }[framework];
227
+
228
+ // Where the entry CSS gets imported. Laravel has nowhere: the `@vite`
229
+ // directive in the Blade layout links it directly.
230
+ const entry = {
231
+ kit: 'src/routes/+layout.svelte',
232
+ laravel: null,
233
+ vite: findPath(['src/main.ts', 'src/main.js'])
234
+ }[framework];
235
+
236
+ return {
237
+ pkg,
238
+ deps,
239
+ framework,
240
+ isKit,
241
+ hasTailwind,
242
+ hasUi,
243
+ hasCompiler,
244
+ css: join(cwd, css),
245
+ cssRel: css,
246
+ head,
247
+ entry
248
+ };
249
+ }
250
+
251
+ /* ------------------------------------------------------------------ head -- */
140
252
 
141
- return { pkg, deps, isKit, hasTailwind, hasUi, css: join(cwd, css), cssRel: css };
253
+ /** Escapes a literal for use inside a RegExp the markers contain `%` and `/`. */
254
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
255
+
256
+ /** The two things that have to run before first paint, in document order. */
257
+ function headAdditions(style, html) {
258
+ const additions = [];
259
+ if (style.fonts && !html.includes('fonts.gstatic.com')) {
260
+ additions.push(`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />`);
261
+ }
262
+ if (!html.includes('nqm-theme')) {
263
+ additions.push(
264
+ `<script>`,
265
+ `\tconst saved = localStorage.getItem('nqm-theme');`,
266
+ `\tif (saved === 'dark' || saved === 'light') document.documentElement.classList.add(saved);`,
267
+ `</script>`
268
+ );
269
+ }
270
+ return additions;
271
+ }
272
+
273
+ /**
274
+ * Puts them in the document head, wherever that is for this framework.
275
+ *
276
+ * Kit renders into a `%sveltekit.head%` placeholder; a Vite index.html and a
277
+ * Blade layout both have a real `</head>` to sit above. When there is no file
278
+ * to patch — or no marker in it — the lines are printed instead of guessed at.
279
+ */
280
+ async function wireHead(project, style) {
281
+ const { framework, head } = project;
282
+
283
+ const printByHand = (additions, reason) => {
284
+ log();
285
+ warn(reason);
286
+ log(additions.map((line) => c.cyan(` ${line}`)).join('\n'));
287
+ };
288
+
289
+ if (!head) {
290
+ printByHand(
291
+ headAdditions(style, ''),
292
+ framework === 'laravel'
293
+ ? 'no Blade layout found — put these inside <head> in yours:'
294
+ : 'no index.html found — put these inside its <head>:'
295
+ );
296
+ return;
297
+ }
298
+
299
+ const htmlPath = join(cwd, head);
300
+ let html = await readOr(htmlPath);
301
+
302
+ if (!html && framework === 'kit') {
303
+ // Some `sv create` templates omit it and let SvelteKit fall back to a
304
+ // built-in. Write the standard one so there is somewhere to put the
305
+ // preconnect and the no-flash theme script.
306
+ html = [
307
+ '<!doctype html>',
308
+ '<html lang="en">',
309
+ ' <head>',
310
+ ' <meta charset="utf-8" />',
311
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
312
+ ' %sveltekit.head%',
313
+ ' </head>',
314
+ ' <body data-sveltekit-preload-data="hover">',
315
+ ' <div style="display: contents">%sveltekit.body%</div>',
316
+ ' </body>',
317
+ '</html>',
318
+ ''
319
+ ].join('\n');
320
+ skip('src/app.html was missing — creating it');
321
+ }
322
+
323
+ const additions = headAdditions(style, html);
324
+ if (additions.length === 0) {
325
+ skip(`${head} already wired`);
326
+ return;
327
+ }
328
+
329
+ const marker = framework === 'kit' ? '%sveltekit.head%' : '</head>';
330
+ if (!html.includes(marker)) {
331
+ printByHand(additions, `${head} has no ${marker} — add these by hand, inside <head>:`);
332
+ return;
333
+ }
334
+
335
+ // Matched with its own indentation so the block lands at the right depth,
336
+ // and replaced through a function so a `$` in the lines stays literal.
337
+ html = html.replace(new RegExp(`([ \\t]*)${escapeRe(marker)}`), (_match, indent) => {
338
+ // The placeholder is itself a child of <head>; a closing tag is not, so
339
+ // its children sit one level deeper — in whichever unit the file uses.
340
+ const unit = indent.startsWith('\t') ? '\t' : ' ';
341
+ const inner = framework === 'kit' ? indent : `${indent}${unit}`;
342
+ const body = additions.map((line) => line.replace(/^\t/, unit));
343
+ return `${inner}${body.join(`\n${inner}`)}\n${indent}${marker}`;
344
+ });
345
+ await write(htmlPath, html, head);
142
346
  }
143
347
 
144
348
  /** How many `../` from the CSS file up to node_modules. */
@@ -151,12 +355,12 @@ function sourcePath(cssRel) {
151
355
 
152
356
  async function init() {
153
357
  const { styles } = await registry();
154
- const style = pickStyle(styles);
358
+ const style = await pickStyle(styles);
155
359
  const project = await detectProject();
156
360
 
157
361
  log();
158
362
  log(c.bold(` @nqmcreative/ui — ${style.title}`));
159
- log(c.dim(` ${project.isKit ? 'SvelteKit' : 'Svelte + Vite'} project detected`));
363
+ log(c.dim(` ${FRAMEWORK_LABEL[project.framework]} project detected`));
160
364
  log();
161
365
 
162
366
  if (!project.hasUi) {
@@ -171,6 +375,13 @@ async function init() {
171
375
  log();
172
376
  }
173
377
 
378
+ if (!project.hasCompiler) {
379
+ // The package ships .svelte files, so the app compiles them itself.
380
+ warn('nothing here compiles Svelte components:');
381
+ log(c.cyan(' bun add -d svelte @sveltejs/vite-plugin-svelte'));
382
+ log();
383
+ }
384
+
174
385
  /* --- 1. the entry CSS --- */
175
386
  const source = sourcePath(project.cssRel);
176
387
  const lines = [
@@ -185,110 +396,90 @@ async function init() {
185
396
 
186
397
  const css = await readOr(project.css);
187
398
 
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`);
399
+ // The block above belongs to this package, so it is rewritten on every run
400
+ // rather than left alone once it is there. That is what makes an upgrade
401
+ // land: when a release changes which files a style imports, or where
402
+ // `@source` has to point, the old lines would otherwise sit here and the
403
+ // new components would come up unstyled.
404
+ //
405
+ // Only those lines. Everything below is the project's own CSS and is
406
+ // carried over untouched — including an `@import` of another package, which
407
+ // is why the match is anchored to this one.
408
+ const stale =
409
+ /^@import ['"]@nqmcreative\/ui\/[^'"]+\.css['"];$|^@source ['"][^'"]*@nqmcreative\/ui[^'"]*['"];$/;
410
+
411
+ const kept = css
412
+ .split('\n')
413
+ .filter((line) => !stale.test(line.trim()))
414
+ .join('\n')
415
+ .replace(/@import ['"]tailwindcss['"];\n?/, '')
416
+ .replace(/\/\* Tailwind v4 skips node_modules[\s\S]*?\*\/\n?/, '')
417
+ .replace(/\n{3,}/g, '\n\n')
418
+ .trim();
419
+
420
+ const body = kept ? `${lines.join('\n')}\n\n${kept}\n` : `${lines.join('\n')}\n`;
421
+ if (body === css) {
422
+ skip(`${project.cssRel} already up to date`);
193
423
  } 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
424
  await write(project.css, body, project.cssRel);
211
425
  }
212
426
 
213
- /* --- 2. app.html: font preconnect + no-flash theme --- */
214
- if (project.isKit) {
215
- const htmlPath = join(cwd, 'src/app.html');
216
- let html = await readOr(htmlPath);
217
- if (!html) {
218
- // Some `sv create` templates omit it and let SvelteKit fall back to a
219
- // built-in. Write the standard one so there is somewhere to put the
220
- // preconnect and the no-flash theme script.
221
- html = [
222
- '<!doctype html>',
223
- '<html lang="en">',
224
- ' <head>',
225
- ' <meta charset="utf-8" />',
226
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
227
- ' %sveltekit.head%',
228
- ' </head>',
229
- ' <body data-sveltekit-preload-data="hover">',
230
- ' <div style="display: contents">%sveltekit.body%</div>',
231
- ' </body>',
232
- '</html>',
233
- ''
234
- ].join('\n');
235
- skip('src/app.html was missing — creating it');
236
- }
237
- {
238
- // Unindented — `%sveltekit.head%` already sits at its own indentation,
239
- // so the join below supplies it and the first line does not double up.
240
- const additions = [];
241
- if (style.fonts && !html.includes('fonts.gstatic.com')) {
242
- additions.push(`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />`);
243
- }
244
- if (!html.includes('nqm-theme')) {
245
- additions.push(
246
- `<script>`,
247
- `\tconst saved = localStorage.getItem('nqm-theme');`,
248
- `\tif (saved === 'dark' || saved === 'light') document.documentElement.classList.add(saved);`,
249
- `</script>`
250
- );
251
- }
252
- if (additions.length === 0) {
253
- skip('src/app.html already wired');
254
- } else if (!html.includes('%sveltekit.head%')) {
255
- warn('src/app.html has no %sveltekit.head% — add these by hand:');
256
- log(additions.map((line) => `\t\t${line}`).join('\n'));
257
- } else {
258
- const indent = /([ \t]*)%sveltekit\.head%/.exec(html)?.[1] ?? '\t\t';
259
- html = html.replace(
260
- '%sveltekit.head%',
261
- `${additions.join(`\n${indent}`)}\n${indent}%sveltekit.head%`
262
- );
263
- await write(htmlPath, html, 'src/app.html');
264
- }
265
- }
427
+ // Laravel keeps its markup outside resources/css, so automatic detection
428
+ // never reaches it. These are the project's own lines, not ours — printed
429
+ // rather than written, so removing them stays possible.
430
+ if (project.framework === 'laravel' && !/@source ['"]\.\.\/(js|views)/.test(body)) {
431
+ log();
432
+ warn(`Tailwind also has to see your own markup add to ${project.cssRel}:`);
433
+ log(c.cyan(` @source '../js';`));
434
+ log(c.cyan(` @source '../views';`));
266
435
  }
267
436
 
437
+ /* --- 2. the document head: font preconnect + no-flash theme --- */
438
+ await wireHead(project, style);
439
+
268
440
  /* --- 3. remind about the vite plugin, which we will not rewrite --- */
269
441
  const viteConfig =
270
- ['vite.config.ts', 'vite.config.js'].map((f) => join(cwd, f)).find((f) => existsSync(f)) ??
271
- null;
442
+ ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']
443
+ .map((f) => join(cwd, f))
444
+ .find((f) => existsSync(f)) ?? null;
272
445
  const viteSource = viteConfig ? await readOr(viteConfig) : '';
273
446
  if (!viteSource.includes('@tailwindcss/vite')) {
447
+ const plugins = {
448
+ kit: `plugins: [tailwindcss(), sveltekit()]`,
449
+ laravel: `plugins: [laravel({ input: ['${project.cssRel}', 'resources/js/app.js'] }), tailwindcss(), svelte()]`,
450
+ vite: `plugins: [tailwindcss(), svelte()]`
451
+ }[project.framework];
452
+ const after = project.framework === 'kit' ? 'sveltekit()' : 'svelte()';
453
+
274
454
  log();
275
455
  warn(
276
- `add the Tailwind plugin to ${viteConfig ? relative(cwd, viteConfig) : 'your vite config'} — before sveltekit():`
456
+ `add the Tailwind plugin to ${viteConfig ? relative(cwd, viteConfig) : 'your vite config'} — before ${after}:`
277
457
  );
278
458
  log(c.cyan(` import tailwindcss from '@tailwindcss/vite';`));
279
- log(c.cyan(` plugins: [tailwindcss(), sveltekit()]`));
459
+ log(c.cyan(` ${plugins}`));
280
460
  } else {
281
461
  skip('vite config already has the Tailwind plugin');
282
462
  }
283
463
 
284
464
  /* --- 4. import the CSS once --- */
285
- if (project.isKit) {
286
- const layoutPath = join(cwd, 'src/routes/+layout.svelte');
465
+ if (project.framework === 'laravel') {
466
+ // Nothing imports it: the Blade layout hands the path to Vite, which is
467
+ // also what makes `php artisan serve` pick up the built file.
468
+ const blade = project.head ? await readOr(join(cwd, project.head)) : '';
469
+ if (blade.includes(project.cssRel)) {
470
+ skip(`${project.head} already loads ${project.cssRel}`);
471
+ } else {
472
+ log();
473
+ warn(`load the CSS from your Blade layout, inside <head>:`);
474
+ log(c.cyan(` @vite(['${project.cssRel}', 'resources/js/app.js'])`));
475
+ }
476
+ } else if (project.framework === 'kit') {
477
+ const layoutPath = join(cwd, project.entry);
287
478
  let layout = await readOr(layoutPath);
288
479
  const line = `import '../app.css';`;
289
480
 
290
481
  if (layout.includes('app.css')) {
291
- skip('src/routes/+layout.svelte already imports the CSS');
482
+ skip(`${project.entry} already imports the CSS`);
292
483
  } else if (!layout.trim()) {
293
484
  await write(
294
485
  layoutPath,
@@ -300,16 +491,37 @@ async function init() {
300
491
 
301
492
  {@render children()}
302
493
  `,
303
- 'src/routes/+layout.svelte'
494
+ project.entry
304
495
  );
305
496
  } else if (/<script[^>]*>/.test(layout)) {
306
497
  layout = layout.replace(/(<script[^>]*>\n)/, `$1\t${line}\n`);
307
- await write(layoutPath, layout, 'src/routes/+layout.svelte');
498
+ await write(layoutPath, layout, project.entry);
308
499
  } else {
309
500
  log();
310
- warn('import the CSS once, at the top of src/routes/+layout.svelte:');
501
+ warn(`import the CSS once, at the top of ${project.entry}:`);
311
502
  log(c.cyan(` ${line}`));
312
503
  }
504
+ } else {
505
+ // Plain Vite: the entry module is the one thing every page loads.
506
+ const rel = project.entry
507
+ ? relative(dirname(join(cwd, project.entry)), project.css)
508
+ .split(/[\\/]/)
509
+ .join('/')
510
+ : null;
511
+ const line = rel ? `import '${rel.startsWith('.') ? rel : `./${rel}`}';` : null;
512
+
513
+ if (!project.entry) {
514
+ log();
515
+ warn(`import ${project.cssRel} once, at the top of your entry module:`);
516
+ log(c.cyan(` import './${project.cssRel.split('/').pop()}';`));
517
+ } else {
518
+ const source = await readOr(join(cwd, project.entry));
519
+ if (source.includes(line) || /import ['"][^'"]*app\.css['"]/.test(source)) {
520
+ skip(`${project.entry} already imports the CSS`);
521
+ } else {
522
+ await write(join(cwd, project.entry), `${line}\n${source}`, project.entry);
523
+ }
524
+ }
313
525
  }
314
526
 
315
527
  log();
@@ -321,7 +533,7 @@ async function init() {
321
533
 
322
534
  async function add() {
323
535
  const { components, styles } = await registry();
324
- const style = pickStyle(styles);
536
+ const style = await pickStyle(styles);
325
537
  const names = args.slice(1);
326
538
  if (names.length === 0) fail('which component? e.g. `nqm-ui add button dialog`');
327
539
 
@@ -435,22 +647,26 @@ function help() {
435
647
  log(`
436
648
  ${c.bold('@nqmcreative/ui')}
437
649
 
438
- ${c.cyan('init --style <s>')} wire one style into this project
650
+ ${c.cyan('init')} wire one style into this project
651
+ ${c.cyan('update')} rewrite that wiring after upgrading the package
439
652
  ${c.cyan('add <name...>')} print the import for a component
440
- ${c.dim('--style <s>')} which style to import from (required)
441
653
  ${c.dim('--to <file>')} …and insert it into that file instead
442
654
  ${c.cyan('list [category]')} every component in the catalogue
443
655
  ${c.cyan('info <name>')} what one component pulls in, in every style
444
656
 
657
+ ${c.dim('--style <s>')} skip the picker: matte, paper or sprout
445
658
  ${c.dim('--dry-run')} show the writes without making them
446
659
 
447
- ${c.dim('Every style implements every component. Run init once per project.')}
660
+ ${c.dim('Every style implements every component. Run init once per project,')}
661
+ ${c.dim('then update after each upgrade — it overwrites the lines it owns.')}
448
662
  `);
449
663
  }
450
664
 
451
665
  /* ------------------------------------------------------------------ main -- */
452
666
 
453
- const commands = { init, add, list, info };
667
+ // `update` is `init` under the name people reach for after an upgrade: both
668
+ // rewrite the lines this CLI owns and leave the rest of the file alone.
669
+ const commands = { init, update: init, add, list, info };
454
670
  if (!command || flags.has('--help') || command === 'help') {
455
671
  help();
456
672
  } else if (commands[command]) {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Hex colours for the colour field.
3
+ *
4
+ * Only hex: it is the one notation `<input type="color">` accepts and the one
5
+ * every design tool copies to the clipboard. Anything else the app can convert
6
+ * before it binds.
7
+ */
8
+ /**
9
+ * `abc`, `#ABC`, `#aabbcc` → `#aabbcc`. Returns `null` when it is not a hex
10
+ * colour, so a half-typed value can be left alone rather than corrected under
11
+ * the caret.
12
+ *
13
+ * Shorthand is expanded and alpha is dropped: the native swatch understands
14
+ * neither, and a value the swatch cannot show is worse than a rounded one.
15
+ */
16
+ export declare function normaliseHex(text: string): string | null;
17
+ /**
18
+ * True when a colour is dark enough to want light text on top of it.
19
+ *
20
+ * Rec. 601 luma rather than WCAG relative luminance — the threshold is for
21
+ * picking one of two label colours on a swatch, not for proving a contrast
22
+ * ratio, and this is the version everyone's design tokens are tuned against.
23
+ */
24
+ export declare function isDark(hex: string): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Hex colours for the colour field.
3
+ *
4
+ * Only hex: it is the one notation `<input type="color">` accepts and the one
5
+ * every design tool copies to the clipboard. Anything else the app can convert
6
+ * before it binds.
7
+ */
8
+ const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
9
+ /**
10
+ * `abc`, `#ABC`, `#aabbcc` → `#aabbcc`. Returns `null` when it is not a hex
11
+ * colour, so a half-typed value can be left alone rather than corrected under
12
+ * the caret.
13
+ *
14
+ * Shorthand is expanded and alpha is dropped: the native swatch understands
15
+ * neither, and a value the swatch cannot show is worse than a rounded one.
16
+ */
17
+ export function normaliseHex(text) {
18
+ const match = HEX.exec(text.trim());
19
+ if (!match)
20
+ return null;
21
+ let digits = match[1].toLowerCase();
22
+ if (digits.length <= 4)
23
+ digits = [...digits].map((digit) => digit + digit).join('');
24
+ return `#${digits.slice(0, 6)}`;
25
+ }
26
+ /**
27
+ * True when a colour is dark enough to want light text on top of it.
28
+ *
29
+ * Rec. 601 luma rather than WCAG relative luminance — the threshold is for
30
+ * picking one of two label colours on a swatch, not for proving a contrast
31
+ * ratio, and this is the version everyone's design tokens are tuned against.
32
+ */
33
+ export function isDark(hex) {
34
+ const value = normaliseHex(hex);
35
+ if (!value)
36
+ return false;
37
+ const [r, g, b] = [1, 3, 5].map((at) => parseInt(value.slice(at, at + 2), 16));
38
+ return (r * 299 + g * 587 + b * 114) / 1000 < 140;
39
+ }
@@ -25,7 +25,14 @@ export { pageRange } from './pagination.js';
25
25
  export { sortFiles, matchesAccept, formatSize } from './files.js';
26
26
  export type { RejectedFile, FileRules } from './files.js';
27
27
  export { passwordScore } from './password.js';
28
- export { clampToStep, percentOf, stepDecimals } from './number.js';
28
+ export { clampToStep, formatGrouped, parseGrouped, percentOf, stepDecimals } from './number.js';
29
+ export type { GroupOptions } from './number.js';
30
+ export { caretIndex, clearFrom, fillFrom, sanitisePin } from './pin.js';
31
+ export type { PinMode } from './pin.js';
32
+ export { addTag, splitTags } from './tags.js';
33
+ export type { TagRules, TagRejection } from './tags.js';
34
+ export { normaliseHex, isDark } from './color.js';
35
+ export { MINUTES_IN_DAY, clampTime, formatClock, formatTime, parseTime, stepTime } from './time.js';
29
36
  export { toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from './date.js';
30
37
  export type { DateFormat } from './date.js';
31
38
  export { calendarKeyMove, dateHint } from './calendar.js';
@@ -23,7 +23,11 @@ export { nextSort, sortRows, toggleKey } from './table.js';
23
23
  export { pageRange } from './pagination.js';
24
24
  export { sortFiles, matchesAccept, formatSize } from './files.js';
25
25
  export { passwordScore } from './password.js';
26
- export { clampToStep, percentOf, stepDecimals } from './number.js';
26
+ export { clampToStep, formatGrouped, parseGrouped, percentOf, stepDecimals } from './number.js';
27
+ export { caretIndex, clearFrom, fillFrom, sanitisePin } from './pin.js';
28
+ export { addTag, splitTags } from './tags.js';
29
+ export { normaliseHex, isDark } from './color.js';
30
+ export { MINUTES_IN_DAY, clampTime, formatClock, formatTime, parseTime, stepTime } from './time.js';
27
31
  /* --- dates --- */
28
32
  export { toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from './date.js';
29
33
  export { calendarKeyMove, dateHint } from './calendar.js';