@nqmcreative/ui 0.3.0 → 0.4.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 +220 -28
- package/cli/index.mjs +247 -70
- package/dist/core/calendar.d.ts +1 -1
- package/dist/core/calendar.js +1 -1
- package/dist/core/color.d.ts +24 -0
- package/dist/core/color.js +39 -0
- package/dist/core/date.d.ts +3 -3
- package/dist/core/date.js +1 -1
- package/dist/core/files.d.ts +2 -3
- package/dist/core/files.js +2 -3
- package/dist/core/index.d.ts +9 -4
- package/dist/core/index.js +6 -4
- package/dist/core/number.d.ts +22 -0
- package/dist/core/number.js +33 -0
- package/dist/core/password.d.ts +1 -1
- package/dist/core/password.js +1 -1
- package/dist/core/pin.d.ts +23 -0
- package/dist/core/pin.js +38 -0
- package/dist/core/tags.d.ts +37 -0
- package/dist/core/tags.js +51 -0
- package/dist/core/time.d.ts +30 -0
- package/dist/core/time.js +78 -0
- package/dist/styles/matte/Alert.svelte +1 -3
- package/dist/styles/matte/Breadcrumb.svelte +1 -4
- package/dist/styles/matte/Calendar.svelte +6 -9
- package/dist/styles/matte/ColorInput.svelte +107 -0
- package/dist/styles/matte/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/matte/Combobox.svelte +3 -6
- package/dist/styles/matte/CommandPalette.svelte +3 -6
- package/dist/styles/matte/ConfirmDialog.svelte +2 -5
- package/dist/styles/matte/ContextMenu.svelte +1 -4
- package/dist/styles/matte/CurrencyInput.svelte +108 -0
- package/dist/styles/matte/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/matte/DatePicker.svelte +9 -9
- package/dist/styles/matte/DatePicker.svelte.d.ts +3 -0
- package/dist/styles/matte/Drawer.svelte +1 -3
- package/dist/styles/matte/Dropdown.svelte +2 -5
- package/dist/styles/matte/Dropdown.svelte.d.ts +1 -1
- package/dist/styles/matte/Dropzone.svelte +9 -12
- package/dist/styles/matte/Dropzone.svelte.d.ts +1 -1
- package/dist/styles/matte/FileInput.svelte +191 -0
- package/dist/styles/matte/FileInput.svelte.d.ts +32 -0
- package/dist/styles/matte/Footer.svelte +1 -1
- package/dist/styles/matte/Footer.svelte.d.ts +1 -1
- package/dist/styles/matte/InputAddon.svelte +36 -0
- package/dist/styles/matte/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/matte/Modal.svelte +1 -3
- package/dist/styles/matte/MultiSelect.svelte +5 -8
- package/dist/styles/matte/Navbar.svelte +3 -9
- package/dist/styles/matte/Pagination.svelte +3 -6
- package/dist/styles/matte/PinInput.svelte +134 -0
- package/dist/styles/matte/PinInput.svelte.d.ts +25 -0
- package/dist/styles/matte/SearchInput.svelte +84 -0
- package/dist/styles/matte/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/matte/Sidebar.svelte +3 -6
- package/dist/styles/matte/Spinner.svelte +2 -5
- package/dist/styles/matte/Spinner.svelte.d.ts +1 -1
- package/dist/styles/matte/Table.svelte +5 -8
- package/dist/styles/matte/TagsInput.svelte +170 -0
- package/dist/styles/matte/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/matte/ThemeToggle.svelte +5 -8
- package/dist/styles/matte/TimeInput.svelte +126 -0
- package/dist/styles/matte/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/matte/Toaster.svelte +2 -5
- package/dist/styles/matte/index.d.ts +10 -3
- package/dist/styles/matte/index.js +9 -3
- package/dist/styles/paper/Alert.svelte +1 -3
- package/dist/styles/paper/Breadcrumb.svelte +1 -4
- package/dist/styles/paper/Calendar.svelte +6 -9
- package/dist/styles/paper/ColorInput.svelte +109 -0
- package/dist/styles/paper/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/paper/Combobox.svelte +3 -6
- package/dist/styles/paper/CommandPalette.svelte +3 -6
- package/dist/styles/paper/ConfirmDialog.svelte +2 -5
- package/dist/styles/paper/ContextMenu.svelte +1 -4
- package/dist/styles/paper/CurrencyInput.svelte +108 -0
- package/dist/styles/paper/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/paper/DatePicker.svelte +9 -9
- package/dist/styles/paper/DatePicker.svelte.d.ts +3 -0
- package/dist/styles/paper/Drawer.svelte +1 -3
- package/dist/styles/paper/Dropdown.svelte +2 -5
- package/dist/styles/paper/Dropdown.svelte.d.ts +1 -1
- package/dist/styles/paper/Dropzone.svelte +9 -12
- package/dist/styles/paper/Dropzone.svelte.d.ts +1 -1
- package/dist/styles/paper/FileInput.svelte +201 -0
- package/dist/styles/paper/FileInput.svelte.d.ts +32 -0
- package/dist/styles/paper/Footer.svelte +1 -1
- package/dist/styles/paper/Footer.svelte.d.ts +1 -1
- package/dist/styles/paper/InputAddon.svelte +36 -0
- package/dist/styles/paper/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/paper/Modal.svelte +1 -3
- package/dist/styles/paper/MultiSelect.svelte +5 -8
- package/dist/styles/paper/Navbar.svelte +3 -9
- package/dist/styles/paper/Pagination.svelte +3 -6
- package/dist/styles/paper/PinInput.svelte +134 -0
- package/dist/styles/paper/PinInput.svelte.d.ts +25 -0
- package/dist/styles/paper/SearchInput.svelte +89 -0
- package/dist/styles/paper/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/paper/Sidebar.svelte +3 -6
- package/dist/styles/paper/Spinner.svelte +2 -5
- package/dist/styles/paper/Spinner.svelte.d.ts +1 -1
- package/dist/styles/paper/Table.svelte +5 -8
- package/dist/styles/paper/TagsInput.svelte +175 -0
- package/dist/styles/paper/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/paper/ThemeToggle.svelte +5 -8
- package/dist/styles/paper/TimeInput.svelte +152 -0
- package/dist/styles/paper/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/paper/Toaster.svelte +3 -6
- package/dist/styles/paper/index.d.ts +10 -3
- package/dist/styles/paper/index.js +9 -3
- package/dist/styles/sprout/Alert.svelte +1 -3
- package/dist/styles/sprout/Breadcrumb.svelte +1 -4
- package/dist/styles/sprout/Calendar.svelte +6 -9
- package/dist/styles/sprout/ColorInput.svelte +109 -0
- package/dist/styles/sprout/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/sprout/Combobox.svelte +3 -6
- package/dist/styles/sprout/CommandPalette.svelte +3 -6
- package/dist/styles/sprout/ConfirmDialog.svelte +2 -5
- package/dist/styles/sprout/ContextMenu.svelte +1 -4
- package/dist/styles/sprout/CurrencyInput.svelte +108 -0
- package/dist/styles/sprout/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/sprout/DatePicker.svelte +9 -9
- package/dist/styles/sprout/DatePicker.svelte.d.ts +3 -0
- package/dist/styles/sprout/Drawer.svelte +1 -3
- package/dist/styles/sprout/Dropdown.svelte +2 -5
- package/dist/styles/sprout/Dropdown.svelte.d.ts +1 -1
- package/dist/styles/sprout/Dropzone.svelte +9 -12
- package/dist/styles/sprout/Dropzone.svelte.d.ts +1 -1
- package/dist/styles/sprout/FileInput.svelte +202 -0
- package/dist/styles/sprout/FileInput.svelte.d.ts +32 -0
- package/dist/styles/sprout/Footer.svelte +1 -1
- package/dist/styles/sprout/Footer.svelte.d.ts +1 -1
- package/dist/styles/sprout/InputAddon.svelte +37 -0
- package/dist/styles/sprout/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/sprout/Modal.svelte +1 -3
- package/dist/styles/sprout/MultiSelect.svelte +5 -8
- package/dist/styles/sprout/Navbar.svelte +3 -9
- package/dist/styles/sprout/Pagination.svelte +3 -6
- package/dist/styles/sprout/PinInput.svelte +135 -0
- package/dist/styles/sprout/PinInput.svelte.d.ts +25 -0
- package/dist/styles/sprout/SearchInput.svelte +89 -0
- package/dist/styles/sprout/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/sprout/Sidebar.svelte +3 -6
- package/dist/styles/sprout/Spinner.svelte +2 -5
- package/dist/styles/sprout/Spinner.svelte.d.ts +1 -1
- package/dist/styles/sprout/Table.svelte +5 -8
- package/dist/styles/sprout/TagsInput.svelte +176 -0
- package/dist/styles/sprout/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/sprout/ThemeToggle.svelte +5 -8
- package/dist/styles/sprout/TimeInput.svelte +152 -0
- package/dist/styles/sprout/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/sprout/Toaster.svelte +3 -6
- package/dist/styles/sprout/index.d.ts +10 -3
- package/dist/styles/sprout/index.js +9 -3
- package/package.json +98 -19
- package/registry.json +139 -38
- package/dist/core/locale.svelte.d.ts +0 -93
- package/dist/core/locale.svelte.js +0 -153
- package/dist/styles/matte/LocaleProvider.svelte +0 -24
- package/dist/styles/matte/LocaleProvider.svelte.d.ts +0 -10
- package/dist/styles/paper/LocaleProvider.svelte +0 -24
- package/dist/styles/paper/LocaleProvider.svelte.d.ts +0 -10
- package/dist/styles/sprout/LocaleProvider.svelte +0 -24
- package/dist/styles/sprout/LocaleProvider.svelte.d.ts +0 -10
package/cli/index.mjs
CHANGED
|
@@ -153,6 +153,39 @@ async function write(path, content, label) {
|
|
|
153
153
|
|
|
154
154
|
/* -------------------------------------------------------------- detection -- */
|
|
155
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
|
+
|
|
156
189
|
async function detectProject() {
|
|
157
190
|
const pkgPath = join(cwd, 'package.json');
|
|
158
191
|
if (!existsSync(pkgPath)) fail('no package.json here — run this inside your project');
|
|
@@ -160,17 +193,156 @@ async function detectProject() {
|
|
|
160
193
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
161
194
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
162
195
|
|
|
163
|
-
const
|
|
196
|
+
const framework = detectFramework(deps);
|
|
197
|
+
const isKit = framework === 'kit';
|
|
164
198
|
const hasTailwind = '@tailwindcss/vite' in deps || 'tailwindcss' in deps;
|
|
165
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;
|
|
166
203
|
|
|
167
204
|
// Where the Tailwind entry CSS lives, or should.
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
}
|
|
172
250
|
|
|
173
|
-
|
|
251
|
+
/* ------------------------------------------------------------------ head -- */
|
|
252
|
+
|
|
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);
|
|
174
346
|
}
|
|
175
347
|
|
|
176
348
|
/** How many `../` from the CSS file up to node_modules. */
|
|
@@ -188,7 +360,7 @@ async function init() {
|
|
|
188
360
|
|
|
189
361
|
log();
|
|
190
362
|
log(c.bold(` @nqmcreative/ui — ${style.title}`));
|
|
191
|
-
log(c.dim(` ${project.
|
|
363
|
+
log(c.dim(` ${FRAMEWORK_LABEL[project.framework]} project detected`));
|
|
192
364
|
log();
|
|
193
365
|
|
|
194
366
|
if (!project.hasUi) {
|
|
@@ -203,6 +375,13 @@ async function init() {
|
|
|
203
375
|
log();
|
|
204
376
|
}
|
|
205
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
|
+
|
|
206
385
|
/* --- 1. the entry CSS --- */
|
|
207
386
|
const source = sourcePath(project.cssRel);
|
|
208
387
|
const lines = [
|
|
@@ -245,85 +424,62 @@ async function init() {
|
|
|
245
424
|
await write(project.css, body, project.cssRel);
|
|
246
425
|
}
|
|
247
426
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
html = [
|
|
257
|
-
'<!doctype html>',
|
|
258
|
-
'<html lang="en">',
|
|
259
|
-
' <head>',
|
|
260
|
-
' <meta charset="utf-8" />',
|
|
261
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
262
|
-
' %sveltekit.head%',
|
|
263
|
-
' </head>',
|
|
264
|
-
' <body data-sveltekit-preload-data="hover">',
|
|
265
|
-
' <div style="display: contents">%sveltekit.body%</div>',
|
|
266
|
-
' </body>',
|
|
267
|
-
'</html>',
|
|
268
|
-
''
|
|
269
|
-
].join('\n');
|
|
270
|
-
skip('src/app.html was missing — creating it');
|
|
271
|
-
}
|
|
272
|
-
{
|
|
273
|
-
// Unindented — `%sveltekit.head%` already sits at its own indentation,
|
|
274
|
-
// so the join below supplies it and the first line does not double up.
|
|
275
|
-
const additions = [];
|
|
276
|
-
if (style.fonts && !html.includes('fonts.gstatic.com')) {
|
|
277
|
-
additions.push(`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />`);
|
|
278
|
-
}
|
|
279
|
-
if (!html.includes('nqm-theme')) {
|
|
280
|
-
additions.push(
|
|
281
|
-
`<script>`,
|
|
282
|
-
`\tconst saved = localStorage.getItem('nqm-theme');`,
|
|
283
|
-
`\tif (saved === 'dark' || saved === 'light') document.documentElement.classList.add(saved);`,
|
|
284
|
-
`</script>`
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
if (additions.length === 0) {
|
|
288
|
-
skip('src/app.html already wired');
|
|
289
|
-
} else if (!html.includes('%sveltekit.head%')) {
|
|
290
|
-
warn('src/app.html has no %sveltekit.head% — add these by hand:');
|
|
291
|
-
log(additions.map((line) => `\t\t${line}`).join('\n'));
|
|
292
|
-
} else {
|
|
293
|
-
const indent = /([ \t]*)%sveltekit\.head%/.exec(html)?.[1] ?? '\t\t';
|
|
294
|
-
html = html.replace(
|
|
295
|
-
'%sveltekit.head%',
|
|
296
|
-
`${additions.join(`\n${indent}`)}\n${indent}%sveltekit.head%`
|
|
297
|
-
);
|
|
298
|
-
await write(htmlPath, html, 'src/app.html');
|
|
299
|
-
}
|
|
300
|
-
}
|
|
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';`));
|
|
301
435
|
}
|
|
302
436
|
|
|
437
|
+
/* --- 2. the document head: font preconnect + no-flash theme --- */
|
|
438
|
+
await wireHead(project, style);
|
|
439
|
+
|
|
303
440
|
/* --- 3. remind about the vite plugin, which we will not rewrite --- */
|
|
304
441
|
const viteConfig =
|
|
305
|
-
['vite.config.ts', 'vite.config.js'
|
|
306
|
-
|
|
442
|
+
['vite.config.ts', 'vite.config.js', 'vite.config.mjs']
|
|
443
|
+
.map((f) => join(cwd, f))
|
|
444
|
+
.find((f) => existsSync(f)) ?? null;
|
|
307
445
|
const viteSource = viteConfig ? await readOr(viteConfig) : '';
|
|
308
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
|
+
|
|
309
454
|
log();
|
|
310
455
|
warn(
|
|
311
|
-
`add the Tailwind plugin to ${viteConfig ? relative(cwd, viteConfig) : 'your vite config'} — before
|
|
456
|
+
`add the Tailwind plugin to ${viteConfig ? relative(cwd, viteConfig) : 'your vite config'} — before ${after}:`
|
|
312
457
|
);
|
|
313
458
|
log(c.cyan(` import tailwindcss from '@tailwindcss/vite';`));
|
|
314
|
-
log(c.cyan(` plugins
|
|
459
|
+
log(c.cyan(` ${plugins}`));
|
|
315
460
|
} else {
|
|
316
461
|
skip('vite config already has the Tailwind plugin');
|
|
317
462
|
}
|
|
318
463
|
|
|
319
464
|
/* --- 4. import the CSS once --- */
|
|
320
|
-
if (project.
|
|
321
|
-
|
|
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);
|
|
322
478
|
let layout = await readOr(layoutPath);
|
|
323
479
|
const line = `import '../app.css';`;
|
|
324
480
|
|
|
325
481
|
if (layout.includes('app.css')) {
|
|
326
|
-
skip(
|
|
482
|
+
skip(`${project.entry} already imports the CSS`);
|
|
327
483
|
} else if (!layout.trim()) {
|
|
328
484
|
await write(
|
|
329
485
|
layoutPath,
|
|
@@ -335,16 +491,37 @@ async function init() {
|
|
|
335
491
|
|
|
336
492
|
{@render children()}
|
|
337
493
|
`,
|
|
338
|
-
|
|
494
|
+
project.entry
|
|
339
495
|
);
|
|
340
496
|
} else if (/<script[^>]*>/.test(layout)) {
|
|
341
497
|
layout = layout.replace(/(<script[^>]*>\n)/, `$1\t${line}\n`);
|
|
342
|
-
await write(layoutPath, layout,
|
|
498
|
+
await write(layoutPath, layout, project.entry);
|
|
343
499
|
} else {
|
|
344
500
|
log();
|
|
345
|
-
warn(
|
|
501
|
+
warn(`import the CSS once, at the top of ${project.entry}:`);
|
|
346
502
|
log(c.cyan(` ${line}`));
|
|
347
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
|
+
}
|
|
348
525
|
}
|
|
349
526
|
|
|
350
527
|
log();
|
package/dist/core/calendar.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export declare function calendarKeyMove(key: string, focused: Date, weekStart: 0 | 1, shiftKey?: boolean): Date | null;
|
|
2
|
-
/** The placeholder a text date field shows for a
|
|
2
|
+
/** The placeholder a text date field shows for a given part order. */
|
|
3
3
|
export declare function dateHint(format: 'dmy' | 'mdy' | 'ymd'): string;
|
package/dist/core/calendar.js
CHANGED
|
@@ -30,7 +30,7 @@ export function calendarKeyMove(key, focused, weekStart, shiftKey = false) {
|
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
/** The placeholder a text date field shows for a
|
|
33
|
+
/** The placeholder a text date field shows for a given part order. */
|
|
34
34
|
export function dateHint(format) {
|
|
35
35
|
return { dmy: 'DD/MM/YYYY', mdy: 'MM/DD/YYYY', ymd: 'YYYY/MM/DD' }[format];
|
|
36
36
|
}
|
|
@@ -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
|
+
}
|
package/dist/core/date.d.ts
CHANGED
|
@@ -21,12 +21,12 @@ export declare function addMonths(date: Date, months: number): Date;
|
|
|
21
21
|
*/
|
|
22
22
|
export declare function monthGrid(month: Date, weekStart?: 0 | 1): Date[];
|
|
23
23
|
/** Localised weekday initials, starting on `weekStart`. */
|
|
24
|
-
export declare function weekdayNames(locale
|
|
25
|
-
export declare function monthLabel(month: Date, locale
|
|
24
|
+
export declare function weekdayNames(locale?: string, weekStart?: 0 | 1): string[];
|
|
25
|
+
export declare function monthLabel(month: Date, locale?: string): string;
|
|
26
26
|
/** `true` when `date` falls outside `min`/`max`, both inclusive and optional. */
|
|
27
27
|
export declare function isOutOfRange(date: Date, min?: string, max?: string): boolean;
|
|
28
28
|
export type DateFormat = 'dmy' | 'mdy' | 'ymd';
|
|
29
|
-
/** Prints `YYYY-MM-DD` in the
|
|
29
|
+
/** Prints `YYYY-MM-DD` in the chosen part order, e.g. `31/12/2026`. */
|
|
30
30
|
export declare function formatISO(value: string, format: DateFormat, separator?: string): string;
|
|
31
31
|
/**
|
|
32
32
|
* The inverse of `formatISO`, forgiving about separators and single digits.
|
package/dist/core/date.js
CHANGED
|
@@ -69,7 +69,7 @@ export function isOutOfRange(date, min, max) {
|
|
|
69
69
|
return true;
|
|
70
70
|
return false;
|
|
71
71
|
}
|
|
72
|
-
/** Prints `YYYY-MM-DD` in the
|
|
72
|
+
/** Prints `YYYY-MM-DD` in the chosen part order, e.g. `31/12/2026`. */
|
|
73
73
|
export function formatISO(value, format, separator = '/') {
|
|
74
74
|
const date = fromISO(value);
|
|
75
75
|
if (!date)
|
package/dist/core/files.d.ts
CHANGED
|
@@ -29,7 +29,6 @@ export declare function sortFiles(incoming: FileList | File[] | null, rules?: Fi
|
|
|
29
29
|
rejected: RejectedFile[];
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
|
-
* `900 B`, `2 MB`, `1.5 GB`.
|
|
33
|
-
* suffix can be translated together.
|
|
32
|
+
* `900 B`, `2 MB`, `1.5 GB`. Pass `units` to relabel the suffixes.
|
|
34
33
|
*/
|
|
35
|
-
export declare function formatSize(bytes: number, units
|
|
34
|
+
export declare function formatSize(bytes: number, units?: readonly string[]): string;
|
package/dist/core/files.js
CHANGED
|
@@ -46,10 +46,9 @@ export function sortFiles(incoming, rules = {}) {
|
|
|
46
46
|
return { accepted, rejected };
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* `900 B`, `2 MB`, `1.5 GB`.
|
|
50
|
-
* suffix can be translated together.
|
|
49
|
+
* `900 B`, `2 MB`, `1.5 GB`. Pass `units` to relabel the suffixes.
|
|
51
50
|
*/
|
|
52
|
-
export function formatSize(bytes, units) {
|
|
51
|
+
export function formatSize(bytes, units = ['B', 'KB', 'MB', 'GB']) {
|
|
53
52
|
if (bytes < 1024)
|
|
54
53
|
return `${bytes} ${units[0]}`;
|
|
55
54
|
let value = bytes / 1024;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @nqmcreative/ui/core — everything the styles share.
|
|
3
3
|
*
|
|
4
4
|
* Behaviour, not looks: focus containment, keyboard navigation, date maths,
|
|
5
|
-
* file validation, the toast queue
|
|
5
|
+
* file validation, the toast queue. Nothing in here
|
|
6
6
|
* renders anything, and nothing in here knows which style is being used.
|
|
7
7
|
*
|
|
8
8
|
* Styles are free to ignore any of it — but there is no `matte` version of a
|
|
@@ -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';
|
|
@@ -33,5 +40,3 @@ export { toast } from './toast.svelte.js';
|
|
|
33
40
|
export type { Toast, ToastOptions, ToastAction } from './toast.svelte.js';
|
|
34
41
|
export { applyTheme, storedTheme, persistTheme, nextTheme } from './theme.svelte.js';
|
|
35
42
|
export type { Theme } from './theme.svelte.js';
|
|
36
|
-
export { locale, setLocale, provideLocale, useLocale, enUS, idID, LocaleStore } from './locale.svelte.js';
|
|
37
|
-
export type { Locale } from './locale.svelte.js';
|
package/dist/core/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @nqmcreative/ui/core — everything the styles share.
|
|
3
3
|
*
|
|
4
4
|
* Behaviour, not looks: focus containment, keyboard navigation, date maths,
|
|
5
|
-
* file validation, the toast queue
|
|
5
|
+
* file validation, the toast queue. Nothing in here
|
|
6
6
|
* renders anything, and nothing in here knows which style is being used.
|
|
7
7
|
*
|
|
8
8
|
* Styles are free to ignore any of it — but there is no `matte` version of a
|
|
@@ -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';
|
|
@@ -31,5 +35,3 @@ export { calendarKeyMove, dateHint } from './calendar.js';
|
|
|
31
35
|
export { toast } from './toast.svelte.js';
|
|
32
36
|
/* --- theme --- */
|
|
33
37
|
export { applyTheme, storedTheme, persistTheme, nextTheme } from './theme.svelte.js';
|
|
34
|
-
/* --- locale --- */
|
|
35
|
-
export { locale, setLocale, provideLocale, useLocale, enUS, idID, LocaleStore } from './locale.svelte.js';
|
package/dist/core/number.d.ts
CHANGED
|
@@ -5,3 +5,25 @@ export declare function stepDecimals(step: number): number;
|
|
|
5
5
|
export declare function clampToStep(value: number, min: number, max: number, step: number): number;
|
|
6
6
|
/** Position of `value` within `min…max`, as 0–100. */
|
|
7
7
|
export declare function percentOf(value: number, min: number, max: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* How a grouped number is written. Separators are the app's to choose rather
|
|
10
|
+
* than `Intl`'s, because the field has to *parse* what it prints — and a
|
|
11
|
+
* locale that groups with a non-breaking space produces a character nobody can
|
|
12
|
+
* type back in.
|
|
13
|
+
*/
|
|
14
|
+
export interface GroupOptions {
|
|
15
|
+
/** Thousands separator. */
|
|
16
|
+
group?: string;
|
|
17
|
+
/** Decimal separator. */
|
|
18
|
+
decimal?: string;
|
|
19
|
+
/** Fixed decimal places. `undefined` keeps whatever was typed. */
|
|
20
|
+
precision?: number;
|
|
21
|
+
}
|
|
22
|
+
/** `1234567.5` → `1,234,567.50`, or `1.234.567,50` in a locale that swaps them. */
|
|
23
|
+
export declare function formatGrouped(value: number, options?: GroupOptions): string;
|
|
24
|
+
/**
|
|
25
|
+
* The inverse, and deliberately forgiving: anything that is not a digit, a
|
|
26
|
+
* minus or the decimal separator is dropped, so a pasted `Rp 1.250.000,-`
|
|
27
|
+
* still reads as a number. `null` means there were no digits at all.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseGrouped(text: string, options?: GroupOptions): number | null;
|
package/dist/core/number.js
CHANGED
|
@@ -15,3 +15,36 @@ export function percentOf(value, min, max) {
|
|
|
15
15
|
return 0;
|
|
16
16
|
return Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100));
|
|
17
17
|
}
|
|
18
|
+
/** `1234567.5` → `1,234,567.50`, or `1.234.567,50` in a locale that swaps them. */
|
|
19
|
+
export function formatGrouped(value, options = {}) {
|
|
20
|
+
const { group = ',', decimal = '.', precision } = options;
|
|
21
|
+
if (!Number.isFinite(value))
|
|
22
|
+
return '';
|
|
23
|
+
const fixed = precision === undefined ? String(value) : value.toFixed(precision);
|
|
24
|
+
const negative = fixed.startsWith('-');
|
|
25
|
+
const [whole, fraction] = (negative ? fixed.slice(1) : fixed).split('.');
|
|
26
|
+
// Replaced through a function so a separator that looks like `$&` stays put.
|
|
27
|
+
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, () => group);
|
|
28
|
+
return `${negative ? '-' : ''}${grouped}${fraction ? decimal + fraction : ''}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The inverse, and deliberately forgiving: anything that is not a digit, a
|
|
32
|
+
* minus or the decimal separator is dropped, so a pasted `Rp 1.250.000,-`
|
|
33
|
+
* still reads as a number. `null` means there were no digits at all.
|
|
34
|
+
*/
|
|
35
|
+
export function parseGrouped(text, options = {}) {
|
|
36
|
+
const { decimal = '.' } = options;
|
|
37
|
+
let cleaned = '';
|
|
38
|
+
for (const char of text) {
|
|
39
|
+
if (char === decimal)
|
|
40
|
+
cleaned += '.';
|
|
41
|
+
else if (char >= '0' && char <= '9')
|
|
42
|
+
cleaned += char;
|
|
43
|
+
else if (char === '-' && cleaned === '')
|
|
44
|
+
cleaned += char;
|
|
45
|
+
}
|
|
46
|
+
if (!cleaned || cleaned === '-' || cleaned === '.')
|
|
47
|
+
return null;
|
|
48
|
+
const value = Number(cleaned);
|
|
49
|
+
return Number.isFinite(value) ? value : null;
|
|
50
|
+
}
|
package/dist/core/password.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Password strength, 0–4,
|
|
2
|
+
* Password strength, 0–4, for labelling as you see fit.
|
|
3
3
|
*
|
|
4
4
|
* Deliberately simple — length plus character variety. It is a nudge in the
|
|
5
5
|
* form, not a security control, and anything cleverer would need a dictionary
|
package/dist/core/password.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Password strength, 0–4,
|
|
2
|
+
* Password strength, 0–4, for labelling as you see fit.
|
|
3
3
|
*
|
|
4
4
|
* Deliberately simple — length plus character variety. It is a nudge in the
|
|
5
5
|
* form, not a security control, and anything cleverer would need a dictionary
|