@askewly/design 0.4.1 → 0.4.2

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 (2) hide show
  1. package/dist/kickstart.js +149 -10
  2. package/package.json +1 -1
package/dist/kickstart.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { builtinModules } from "node:module";
3
3
  import path from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
@@ -198,7 +198,11 @@ export function renderBrandCss(answers) {
198
198
  const a = ACCENTS[answers.color];
199
199
  const c = CANVASES[answers.tone];
200
200
  const font = FONT_STACKS[answers.type];
201
- const vars = (m, primary, onPrimary, ring) => ` --background: ${m.bg};
201
+ // `color-scheme` is what the *browser* reads. Tokens only repaint what we
202
+ // draw; scrollbars, native selects and checkboxes, and the overscroll gutter
203
+ // behind the page are the browser's, and they stay light without this.
204
+ const vars = (m, primary, onPrimary, ring, scheme) => ` color-scheme: ${scheme};
205
+ --background: ${m.bg};
202
206
  --foreground: ${m.fg};
203
207
  --card: ${m.card};
204
208
  --card-foreground: ${m.fg};
@@ -229,13 +233,22 @@ export function renderBrandCss(answers) {
229
233
  return `/* Generated by \`askewly-design init --block\` from DESIGN.md (tone=${answers.tone}, color=${answers.color}).
230
234
  * This file is the project's token definition layer — the block's required
231
235
  * CSS variables (block-contract §4/§6) are defined here, nowhere else. */
236
+
237
+ /* One switch for dark mode. Without this line Tailwind v4 keys \`dark:\`
238
+ * utilities off \`prefers-color-scheme\` while the token values below key off
239
+ * the \`.dark\` class — two independent switches, so a project that toggles the
240
+ * class gets dark tokens with light-mode utilities (and an OS-dark visitor gets
241
+ * the reverse). Transplanted shadcn primitives carry \`dark:\` utilities, so this
242
+ * is load-bearing, not cosmetic. Toggle by putting \`dark\` on <html>. */
243
+ @custom-variant dark (&:where(.dark, .dark *));
244
+
232
245
  :root {
233
246
  font-family: ${font};
234
- ${vars(c.light, a.light, a.onLight, a.ring)}
247
+ ${vars(c.light, a.light, a.onLight, a.ring, "light")}
235
248
  }
236
249
 
237
250
  .dark {
238
- ${vars(c.dark, a.dark, a.onDark, a.darkRing)}
251
+ ${vars(c.dark, a.dark, a.onDark, a.darkRing, "dark")}
239
252
  }
240
253
 
241
254
  @theme inline {
@@ -354,6 +367,124 @@ export function blockExportName(pageSource) {
354
367
  }
355
368
  return null;
356
369
  }
370
+ // ---------------------------------------------------------------------------
371
+ // The `@/` alias the transplanted code needs.
372
+ //
373
+ // Every registry source imports through `@/…`, so a project without the alias
374
+ // configured fails to typecheck the moment the block lands — six `tsc -b`
375
+ // errors on a stock vite react-ts app, patched by hand in both M28 E2E runs.
376
+ // The printed handoff said nothing about it.
377
+ //
378
+ // Two traps this has to survive:
379
+ // 1. tsconfig files are JSONC. Parsing them would mean shipping a parser, so
380
+ // the check is a raw scan with comments removed — a commented-out `"@/*"`
381
+ // must not read as configured.
382
+ // 2. vite's react-ts template makes the root tsconfig.json a *solution* file:
383
+ // references only, no compilerOptions. `paths` written there is ignored by
384
+ // `tsc -b`. The step has to name the file that actually owns the options.
385
+ // ---------------------------------------------------------------------------
386
+ /** Blank out comments without touching string contents (JSONC + TS configs). */
387
+ function stripComments(source) {
388
+ let out = "";
389
+ let quote = null;
390
+ for (let i = 0; i < source.length; i++) {
391
+ const ch = source[i];
392
+ if (quote) {
393
+ out += ch;
394
+ if (ch === "\\") {
395
+ out += source[++i] ?? "";
396
+ }
397
+ else if (ch === quote) {
398
+ quote = null;
399
+ }
400
+ continue;
401
+ }
402
+ if (ch === '"' || ch === "'" || ch === "`") {
403
+ quote = ch;
404
+ out += ch;
405
+ continue;
406
+ }
407
+ if (ch === "/" && source[i + 1] === "/") {
408
+ while (i < source.length && source[i] !== "\n")
409
+ i++;
410
+ out += "\n";
411
+ continue;
412
+ }
413
+ if (ch === "/" && source[i + 1] === "*") {
414
+ i += 2;
415
+ while (i < source.length && !(source[i] === "*" && source[i + 1] === "/"))
416
+ i++;
417
+ i++;
418
+ continue;
419
+ }
420
+ out += ch;
421
+ }
422
+ return out;
423
+ }
424
+ const TSCONFIG_ALIAS = /"@\/\*"\s*:/;
425
+ const VITE_ALIAS = /vite-tsconfig-paths|resolve\s*:[\s\S]{0,400}?alias/;
426
+ /**
427
+ * Which half of the `@/` alias is missing.
428
+ *
429
+ * Uncertainty resolves toward printing: an extra step in an already-configured
430
+ * project costs a glance, a missing one costs the build.
431
+ */
432
+ export function detectPathAlias(targetDir) {
433
+ const names = existsSync(targetDir)
434
+ ? readdirSync(targetDir).filter((n) => /^tsconfig.*\.json$/.test(n))
435
+ : [];
436
+ const read = (name) => {
437
+ try {
438
+ return stripComments(readFileSync(path.join(targetDir, name), "utf8"));
439
+ }
440
+ catch {
441
+ return "";
442
+ }
443
+ };
444
+ const sources = new Map(names.map((n) => [n, read(n)]));
445
+ const declared = names.some((n) => TSCONFIG_ALIAS.test(sources.get(n) ?? ""));
446
+ // Name the file that owns compilerOptions — writing paths anywhere else is a
447
+ // step the reader can follow and still fail.
448
+ const owner = names.find((n) => n !== "tsconfig.json" && /"compilerOptions"\s*:/.test(sources.get(n) ?? "")) ??
449
+ names.find((n) => /"compilerOptions"\s*:/.test(sources.get(n) ?? "")) ??
450
+ names[0] ??
451
+ "tsconfig.json";
452
+ const viteConfig = existsSync(targetDir)
453
+ ? readdirSync(targetDir).find((n) => /^vite\.config\.[cm]?[jt]s$/.test(n))
454
+ : undefined;
455
+ const vite = viteConfig ? VITE_ALIAS.test(stripComments(readFileSync(path.join(targetDir, viteConfig), "utf8"))) : false;
456
+ return { tsconfig: declared ? null : owner, vite };
457
+ }
458
+ /**
459
+ * The copy-paste step, or null when both halves are already in place.
460
+ *
461
+ * Deliberately free of new npm dependencies: the printed `npm i` list is
462
+ * contracted to be what the transplanted files import (M28), and recommending
463
+ * a build-tool plugin there would blur that. `srcRel` keeps the snippet true to
464
+ * where the block was actually written.
465
+ */
466
+ export function aliasStep(status, srcRel) {
467
+ if (!status.tsconfig && status.vite)
468
+ return null;
469
+ const parts = [
470
+ `configure the \`@/\` alias — the transplanted files import through it, and \`tsc\` fails without it:`,
471
+ ];
472
+ if (status.tsconfig) {
473
+ // No `baseUrl`: paths resolve relative to the tsconfig since TS 4.4, and on
474
+ // TS 6 the option is deprecated hard enough to fail the build (TS5101,
475
+ // measured on a stock vite react-ts project — the first snippet shipped it
476
+ // and traded six missing-module errors for one deprecation error).
477
+ parts.push(` in ${status.tsconfig}, inside "compilerOptions":`);
478
+ parts.push(` "paths": { "@/*": ["${srcRel}/*"] }`);
479
+ }
480
+ if (!status.vite) {
481
+ parts.push(` in vite.config.ts:`);
482
+ parts.push(` import { fileURLToPath } from "node:url"`);
483
+ parts.push(` // inside defineConfig({ … })`);
484
+ parts.push(` resolve: { alias: { "@": fileURLToPath(new URL("${srcRel}", import.meta.url)) } }`);
485
+ }
486
+ return parts.join("\n");
487
+ }
357
488
  export async function fetchBlock(blockName, registryBase) {
358
489
  const block = await fetchJson(`${registryBase}/r/${blockName}.json`);
359
490
  if (block.meta?.tier !== "block") {
@@ -452,11 +583,19 @@ export async function runKickstart(dir, opts) {
452
583
  // disk. A printed step the reader cannot follow is worse than no step.
453
584
  const pagePath = path.join(blockDir, "page.tsx");
454
585
  const exportName = existsSync(pagePath) ? blockExportName(readFileSync(pagePath, "utf8")) : null;
586
+ const srcRel = srcRoot === targetDir ? "." : "./src";
587
+ const steps = [];
588
+ const alias = aliasStep(detectPathAlias(targetDir), srcRel);
589
+ if (alias)
590
+ steps.push(alias);
591
+ steps.push(`npm i ${[...result.npmDeps].sort().join(" ")}`);
592
+ steps.push(`import "./askewly-brand.css" after \`@import "tailwindcss";\` in your global stylesheet (order matters — it registers the dark variant)`);
593
+ steps.push(`toggle dark mode by putting the \`dark\` class on <html> — tokens and \`dark:\` utilities both key off it`);
594
+ steps.push(exportName
595
+ ? `render the block: import { ${exportName} } from "@/components/blocks/${opts.block}/page"`
596
+ : `render the block from "@/components/blocks/${opts.block}/page" — open it to see which component it exports`);
597
+ steps.push(`wire routing yourself — the block deliberately ships none (block-contract §3)`);
455
598
  console.log(`\nNext steps:`);
456
- console.log(` 1. npm i ${[...result.npmDeps].sort().join(" ")}`);
457
- console.log(` 2. import "./askewly-brand.css" after \`@import "tailwindcss";\` in your global stylesheet`);
458
- console.log(exportName
459
- ? ` 3. render the block: import { ${exportName} } from "@/components/blocks/${opts.block}/page"`
460
- : ` 3. render the block from "@/components/blocks/${opts.block}/page" — open it to see which component it exports`);
461
- console.log(` 4. wire routing yourself — the block deliberately ships none (block-contract §3)`);
599
+ for (const [index, step] of steps.entries())
600
+ console.log(` ${index + 1}. ${step}`);
462
601
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askewly/design",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Query, inject, and verify the Askewly Design system (tokens, UI vocabulary, recipes) from your terminal or coding agent.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://ui.askewly.com",