srcdev-nuxt-components 9.1.56 → 9.1.58

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.
@@ -53,7 +53,8 @@
53
53
  "Bash(rm -rf /Users/simoncornforth/websites/nuxt-components/.claude/projects/)",
54
54
  "Bash(SRCDEV_STANDALONE=true npx vitest run app/components/01.atoms/toast)",
55
55
  "Bash(SRCDEV_STANDALONE=true npx vitest run app/components/02.molecules/alert-content)",
56
- "Bash(SRCDEV_STANDALONE=true npx vitest run app/components/01.atoms/prompt)"
56
+ "Bash(SRCDEV_STANDALONE=true npx vitest run app/components/01.atoms/prompt)",
57
+ "Bash(git fetch *)"
57
58
  ],
58
59
  "additionalDirectories": [
59
60
  "/Users/simoncornforth/websites/instepreflexology",
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- `useColourScheme` provides reactive dark/light/auto mode switching, persisted in `localStorage` and applied via a CSS class on `<html>`. It reads an `enabled` flag from runtime config so consuming apps can disable the feature entirely.
5
+ `useColourScheme` provides reactive dark/light/auto mode switching, persisted in `localStorage` and applied via a `data-color-scheme` attribute on `<html>`. It reads an `enabled` flag from runtime config so consuming apps can disable the feature entirely.
6
6
 
7
7
  **This composable ships inside the `srcdev-nuxt-components` layer** (`app/composables/useColourScheme.ts`). It is auto-imported via the Nuxt layer — **do not create a local copy** in the consuming app.
8
8
 
@@ -45,11 +45,11 @@ To let the user toggle the scheme, bind `currentColourScheme` to a control:
45
45
  </select>
46
46
  ```
47
47
 
48
- Setting `currentColourScheme.value` triggers the watcher, which writes to `localStorage` and calls `applyColourScheme()` to update the `<html>` class immediately.
48
+ Setting `currentColourScheme.value` triggers the watcher, which writes to `localStorage` and calls `applyColourScheme()` to update the `<html>` `data-color-scheme` attribute immediately.
49
49
 
50
50
  ## How scheme application works
51
51
 
52
- The layer ships a head script (`utils/colour-scheme-init.ts`) that runs before paint to read `localStorage` and apply the correct class to `<html>`, preventing flash of wrong theme. `useColourScheme` then syncs its reactive state to match on `onMounted`.
52
+ The layer ships a head script (`utils/colour-scheme-init.ts`) that runs before paint to read `localStorage` and apply the correct `data-color-scheme` attribute to `<html>`, preventing flash of wrong theme. `useColourScheme` then syncs its reactive state to match on `onMounted`.
53
53
 
54
54
  The three valid values are:
55
55
 
@@ -2,11 +2,11 @@ html {
2
2
  color-scheme: light dark;
3
3
  interpolate-size: allow-keywords;
4
4
 
5
- &.light {
5
+ &[data-color-scheme="light"] {
6
6
  color-scheme: light;
7
7
  }
8
8
 
9
- &.dark {
9
+ &[data-color-scheme="dark"] {
10
10
  color-scheme: dark;
11
11
  }
12
12
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.1.56",
4
+ "version": "9.1.58",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
@@ -34,6 +34,8 @@
34
34
  ".claude/",
35
35
  "app/",
36
36
  "modules/",
37
+ "scripts/",
38
+ "ramps.config.mjs",
37
39
  "nuxt.config.ts",
38
40
  "types.d.ts"
39
41
  ],
@@ -0,0 +1,28 @@
1
+ // One source of truth for the oklch colour curve.
2
+ // Edit this file; run `npm run generate:ramps` to rebuild the CSS.
3
+
4
+ /** Lightness % at each step index 00–10 (light → dark) */
5
+ export const LIGHTNESS = [98, 94, 88, 80, 72, 64, 56, 48, 40, 32, 25];
6
+
7
+ /** Chroma multipliers at each step index 00–10.
8
+ * Actual chroma = ramp.chroma × multiplier.
9
+ * Ramps at mid-tone (06) reach full chroma; tapers toward both extremes. */
10
+ export const CHROMA_MULTIPLIERS = [0.045, 0.18, 0.32, 0.5, 0.68, 0.86, 1, 0.95, 0.86, 0.77, 0.64];
11
+
12
+ /**
13
+ * Named ramps.
14
+ * hue – oklch hue angle (degrees)
15
+ * chroma – maximum chroma (at the 06 step)
16
+ * drift – optional: rotates hue linearly across steps
17
+ * hue_i = hue + drift × (i / 10)
18
+ * e.g. drift -25 on hue 50 → step 00 = 50°, step 10 = 25°
19
+ */
20
+ export const ramps = {
21
+ blue: { hue: 255, chroma: 0.22 },
22
+ red: { hue: 30, chroma: 0.24 },
23
+ green: { hue: 157, chroma: 0.19 },
24
+ amber: { hue: 75, chroma: 0.19 },
25
+ orange: { hue: 60, chroma: 0.15 },
26
+ sunset: { hue: 50, chroma: 0.22, drift: -25 },
27
+ slate: { hue: 260, chroma: 0.02 },
28
+ };
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CI check: runs the generator then fails if any generated file changed.
4
+ * Usage: node scripts/check-ramps.mjs
5
+ */
6
+
7
+ import { execSync } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+ import { dirname } from "node:path";
10
+
11
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
12
+
13
+ const GENERATED = [
14
+ "app/assets/styles/setup/02.colours/_blue.css",
15
+ "app/assets/styles/setup/02.colours/_red.css",
16
+ "app/assets/styles/setup/02.colours/_green.css",
17
+ "app/assets/styles/setup/02.colours/_amber.css",
18
+ "app/assets/styles/setup/02.colours/_orange.css",
19
+ "app/assets/styles/setup/02.colours/_sunset.css",
20
+ "app/assets/styles/setup/02.colours/_slate.css",
21
+ "app/assets/styles/setup/02.colours/_theme-params.css",
22
+ "app/assets/styles/setup/03.theming/theme-ramp.css",
23
+ ];
24
+
25
+ console.log("Running generator…");
26
+ execSync("node scripts/generate-ramps.mjs", { cwd: ROOT, stdio: "inherit" });
27
+
28
+ console.log("Checking for diff…");
29
+ const diff = execSync(`git diff --name-only ${GENERATED.join(" ")}`, { cwd: ROOT })
30
+ .toString()
31
+ .trim();
32
+
33
+ if (diff) {
34
+ console.error("\n✗ Generated files are out of date. Run `npm run generate:ramps` and commit:\n");
35
+ console.error(diff);
36
+ process.exit(1);
37
+ }
38
+
39
+ // Also check for untracked generated files
40
+ const untracked = execSync(`git ls-files --others --exclude-standard ${GENERATED.join(" ")}`, { cwd: ROOT })
41
+ .toString()
42
+ .trim();
43
+
44
+ if (untracked) {
45
+ console.error("\n✗ Generated files are new and untracked. Run `npm run generate:ramps` and commit:\n");
46
+ console.error(untracked);
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log("✓ Generated files are up to date.");
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Consumer colour ramp generator.
4
+ *
5
+ * Run from your consuming app's project root:
6
+ * node node_modules/srcdev-nuxt-components/scripts/generate-consumer-ramps.mjs
7
+ *
8
+ * Reads: ramps.config.mjs in your project root
9
+ * Writes: app/assets/styles/setup/02.colours/ in your project root
10
+ *
11
+ * Your ramps.config.mjs can contain any combination of:
12
+ * - New palettes (e.g. gold, copper) — adds --gold-00..10 and --palette-gold-* vars
13
+ * - Built-in names (e.g. blue, red, green) — your generated file overrides the layer's
14
+ * values because consumer CSS loads after layer CSS in the cascade
15
+ *
16
+ * Example ramps.config.mjs:
17
+ * export const ramps = {
18
+ * gold: { hue: 85, chroma: 0.20 },
19
+ * };
20
+ *
21
+ * Produces:
22
+ * _gold.css — --gold-00 … --gold-10 (literal oklch values)
23
+ * _palette-params.css — --palette-gold-hue, --palette-gold-chroma
24
+ *
25
+ * Import both in your app/assets/styles/setup/02.colours/index.css, then
26
+ * reference --gold-09 etc. in your theme override files.
27
+ */
28
+
29
+ import { writeFileSync, mkdirSync } from "node:fs";
30
+ import { fileURLToPath, pathToFileURL } from "node:url";
31
+ import { dirname, join } from "node:path";
32
+
33
+ const __dirname = dirname(fileURLToPath(import.meta.url));
34
+ const LAYER_ROOT = join(__dirname, "..");
35
+ const CONSUMER_ROOT = process.cwd();
36
+
37
+ // Lightness / chroma curves come from the layer so all ramps share the same curve shape
38
+ const { LIGHTNESS, CHROMA_MULTIPLIERS } = await import(pathToFileURL(join(LAYER_ROOT, "ramps.config.mjs")).href);
39
+
40
+ // Consumer's palette definitions
41
+ let consumerRamps;
42
+ try {
43
+ const config = await import(pathToFileURL(join(CONSUMER_ROOT, "ramps.config.mjs")).href);
44
+ consumerRamps = config.ramps;
45
+ if (!consumerRamps || typeof consumerRamps !== "object") throw new Error("ramps export missing");
46
+ } catch {
47
+ console.error("✗ Could not load ramps.config.mjs from your project root.");
48
+ console.error(" Create one exporting a `ramps` object, e.g.:");
49
+ console.error(" export const ramps = {");
50
+ console.error(" gold: { hue: 85, chroma: 0.20 },");
51
+ console.error(" };");
52
+ process.exit(1);
53
+ }
54
+
55
+ const COLOURS_DIR = join(CONSUMER_ROOT, "app/assets/styles/setup/02.colours");
56
+ mkdirSync(COLOURS_DIR, { recursive: true });
57
+
58
+ const HEADER = "/* GENERATED — edit your ramps.config.mjs, not this file */";
59
+
60
+ /** Round to at most `places` decimal places, stripping trailing zeros. */
61
+ function r(n, places = 4) {
62
+ return parseFloat(n.toFixed(places));
63
+ }
64
+
65
+ // ─── Individual colour files ──────────────────────────────────────────────────
66
+
67
+ for (const [name, cfg] of Object.entries(consumerRamps)) {
68
+ const { hue, chroma, drift = 0 } = cfg;
69
+ const lines = [HEADER, ":where(html) {"];
70
+
71
+ for (let i = 0; i <= 10; i++) {
72
+ const step = String(i).padStart(2, "0");
73
+ const L = LIGHTNESS[i];
74
+ const C = r(chroma * CHROMA_MULTIPLIERS[i]);
75
+ const H = drift !== 0 ? r(hue + drift * (i / 10), 1) : hue;
76
+ lines.push(` --${name}-${step}: oklch(${L}% ${C} ${H});`);
77
+ }
78
+
79
+ lines.push("}");
80
+ writeFileSync(join(COLOURS_DIR, `_${name}.css`), lines.join("\n") + "\n");
81
+ console.log(` ✓ _${name}.css`);
82
+ }
83
+
84
+ // ─── _palette-params.css ─────────────────────────────────────────────────────
85
+
86
+ const paramLines = [HEADER, ":where(html) {"];
87
+ for (const [name, { hue, chroma, drift }] of Object.entries(consumerRamps)) {
88
+ paramLines.push(` --palette-${name}-hue: ${hue};`);
89
+ paramLines.push(` --palette-${name}-chroma: ${chroma};`);
90
+ if (drift !== undefined) paramLines.push(` --palette-${name}-drift: ${drift};`);
91
+ }
92
+ paramLines.push("}");
93
+ writeFileSync(join(COLOURS_DIR, "_palette-params.css"), paramLines.join("\n") + "\n");
94
+ console.log(" ✓ _palette-params.css");
95
+
96
+ console.log("");
97
+ console.log("Done. Next steps:");
98
+ console.log(" 1. Import generated files in app/assets/styles/setup/02.colours/index.css");
99
+ console.log(" 2. Reference --palette-*-hue / --palette-*-chroma in your theme override");
100
+ console.log(" 3. Use named steps (e.g. var(--gold-09)) in page-level token overrides");
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generates colour ramp CSS from ramps.config.mjs.
4
+ * Outputs:
5
+ * app/assets/styles/setup/02.colours/_{name}.css — per-ramp literal values
6
+ * app/assets/styles/setup/02.colours/_theme-params.css — --palette-*-hue/chroma vars
7
+ * app/assets/styles/setup/03.theming/theme-ramp.css — parametric --colour-theme-* slot
8
+ */
9
+
10
+ import { writeFileSync } from "node:fs";
11
+ import { fileURLToPath, pathToFileURL } from "node:url";
12
+ import { dirname, join } from "node:path";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const ROOT = join(__dirname, "..");
16
+
17
+ const { LIGHTNESS, CHROMA_MULTIPLIERS, ramps } = await import(pathToFileURL(join(ROOT, "ramps.config.mjs")).href);
18
+
19
+ const COLOURS_DIR = join(ROOT, "app/assets/styles/setup/02.colours");
20
+ const THEMING_DIR = join(ROOT, "app/assets/styles/setup/03.theming");
21
+
22
+ const HEADER = "/* GENERATED — edit ramps.config.mjs, not this file */";
23
+
24
+ /** Round to at most `places` decimal places, stripping trailing zeros. */
25
+ function r(n, places = 4) {
26
+ return parseFloat(n.toFixed(places));
27
+ }
28
+
29
+ // ─── Individual ramp files ───────────────────────────────────────────────────
30
+
31
+ for (const [name, cfg] of Object.entries(ramps)) {
32
+ const { hue, chroma, drift = 0 } = cfg;
33
+ const lines = [HEADER, ":where(html) {"];
34
+
35
+ for (let i = 0; i <= 10; i++) {
36
+ const step = String(i).padStart(2, "0");
37
+ const L = LIGHTNESS[i];
38
+ const C = r(chroma * CHROMA_MULTIPLIERS[i]);
39
+ const H = drift !== 0 ? r(hue + drift * (i / 10), 1) : hue;
40
+ lines.push(` --${name}-${step}: oklch(${L}% ${C} ${H});`);
41
+ }
42
+
43
+ lines.push("}");
44
+ writeFileSync(join(COLOURS_DIR, `_${name}.css`), lines.join("\n") + "\n");
45
+ console.log(` ✓ _${name}.css`);
46
+ }
47
+
48
+ // ─── _theme-params.css ────────────────────────────────────────────────────────
49
+
50
+ const paramLines = [HEADER, ":where(html) {"];
51
+ for (const [name, { hue, chroma, drift }] of Object.entries(ramps)) {
52
+ paramLines.push(` --palette-${name}-hue: ${hue};`);
53
+ paramLines.push(` --palette-${name}-chroma: ${chroma};`);
54
+ if (drift !== undefined) paramLines.push(` --palette-${name}-drift: ${drift};`);
55
+ }
56
+ paramLines.push("}");
57
+ writeFileSync(join(COLOURS_DIR, "_theme-params.css"), paramLines.join("\n") + "\n");
58
+ console.log(" ✓ _theme-params.css");
59
+
60
+ // ─── theme-ramp.css ───────────────────────────────────────────────────────────
61
+ // Declares --colour-theme-0..10 as oklch formulas on every element that can
62
+ // carry --theme-hue / --theme-chroma. The selector list must include every
63
+ // element that can redefine those vars so the formula re-evaluates there.
64
+ // --theme-hue-drift defaults to 0; set it (e.g. var(--palette-sunset-drift))
65
+ // to rotate hue linearly across steps, matching the ramp drift behaviour.
66
+
67
+ const rampLines = [
68
+ HEADER,
69
+ "/* Parametric theme-colour slot. Set --theme-hue and --theme-chroma on a",
70
+ " theme selector; these vars automatically pick up the right palette. */",
71
+ ":where(html, [data-theme], [data-invalid]) {",
72
+ ];
73
+
74
+ for (let i = 0; i <= 10; i++) {
75
+ const L = LIGHTNESS[i];
76
+ const M = CHROMA_MULTIPLIERS[i];
77
+ const fraction = i === 0 ? "0" : i === 10 ? "1" : `${i} / 10`;
78
+ rampLines.push(
79
+ ` --colour-theme-${i}: oklch(${L}% calc(var(--theme-chroma) * ${M}) calc(var(--theme-hue) + var(--theme-hue-drift, 0) * (${fraction})));`
80
+ );
81
+ }
82
+
83
+ // Alias out-of-range indices used by older component code
84
+ rampLines.push(" /* Aliases for legacy --colour-theme-11 / -12 references */");
85
+ rampLines.push(" --colour-theme-11: var(--colour-theme-10);");
86
+ rampLines.push(" --colour-theme-12: var(--colour-theme-10);");
87
+ rampLines.push("}");
88
+
89
+ writeFileSync(join(THEMING_DIR, "theme-ramp.css"), rampLines.join("\n") + "\n");
90
+ console.log(" ✓ theme-ramp.css");
91
+
92
+ console.log("Done.");