@loworbitstudio/visor-theme-engine 0.4.0 → 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.
- package/dist/adapters/index.d.ts +73 -8
- package/dist/adapters/index.js +719 -21
- package/dist/{chunk-NZF2MS4L.js → chunk-SXT2KY6D.js} +30 -6
- package/dist/index.d.ts +122 -6
- package/dist/index.js +266 -66
- package/dist/{types-DgAumoCX.d.ts → types-ljcTtODU.d.ts} +57 -0
- package/package.json +1 -1
- package/src/visor-theme.schema.json +68 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
MATERIAL_TEXT_SLOTS,
|
|
2
3
|
TAILWIND_GRAY,
|
|
3
4
|
VISOR_FONTS_CDN,
|
|
4
5
|
buildVisorFontUrl,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
rgbToHex,
|
|
32
33
|
rgbToOklch,
|
|
33
34
|
serializeColor
|
|
34
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-SXT2KY6D.js";
|
|
35
36
|
|
|
36
37
|
// src/pipeline.ts
|
|
37
38
|
import { parse as parseYaml } from "yaml";
|
|
@@ -223,6 +224,29 @@ var visor_theme_schema_default = {
|
|
|
223
224
|
normal: { type: "string" },
|
|
224
225
|
wide: { type: "string" }
|
|
225
226
|
}
|
|
227
|
+
},
|
|
228
|
+
slots: {
|
|
229
|
+
type: "object",
|
|
230
|
+
description: "Per-slot overrides for the generated Flutter Material TextTheme. Any subset of the 16 slots may be specified; omitted slots fall through to the Material 3 2024 defaults shipped in visor_core.",
|
|
231
|
+
additionalProperties: false,
|
|
232
|
+
properties: {
|
|
233
|
+
displayLarge: { $ref: "#/$defs/textSlotOverride" },
|
|
234
|
+
displayMedium: { $ref: "#/$defs/textSlotOverride" },
|
|
235
|
+
displaySmall: { $ref: "#/$defs/textSlotOverride" },
|
|
236
|
+
headlineLarge: { $ref: "#/$defs/textSlotOverride" },
|
|
237
|
+
headlineMedium: { $ref: "#/$defs/textSlotOverride" },
|
|
238
|
+
headlineSmall: { $ref: "#/$defs/textSlotOverride" },
|
|
239
|
+
titleLarge: { $ref: "#/$defs/textSlotOverride" },
|
|
240
|
+
titleMedium: { $ref: "#/$defs/textSlotOverride" },
|
|
241
|
+
titleSmall: { $ref: "#/$defs/textSlotOverride" },
|
|
242
|
+
bodyLarge: { $ref: "#/$defs/textSlotOverride" },
|
|
243
|
+
bodyMedium: { $ref: "#/$defs/textSlotOverride" },
|
|
244
|
+
bodySmall: { $ref: "#/$defs/textSlotOverride" },
|
|
245
|
+
labelLarge: { $ref: "#/$defs/textSlotOverride" },
|
|
246
|
+
labelMedium: { $ref: "#/$defs/textSlotOverride" },
|
|
247
|
+
labelSmall: { $ref: "#/$defs/textSlotOverride" },
|
|
248
|
+
labelXSmall: { $ref: "#/$defs/textSlotOverride" }
|
|
249
|
+
}
|
|
226
250
|
}
|
|
227
251
|
}
|
|
228
252
|
},
|
|
@@ -262,6 +286,17 @@ var visor_theme_schema_default = {
|
|
|
262
286
|
xl: { type: "string" }
|
|
263
287
|
}
|
|
264
288
|
},
|
|
289
|
+
strokeWidths: {
|
|
290
|
+
type: "object",
|
|
291
|
+
description: "Stroke-width scale in pixels \u2014 used for borders, outlines, dividers, progress-indicator strokes. Defaults: thin=1, regular=1.5, medium=2, thick=2.5.",
|
|
292
|
+
additionalProperties: false,
|
|
293
|
+
properties: {
|
|
294
|
+
thin: { type: "number", minimum: 0 },
|
|
295
|
+
regular: { type: "number", minimum: 0 },
|
|
296
|
+
medium: { type: "number", minimum: 0 },
|
|
297
|
+
thick: { type: "number", minimum: 0 }
|
|
298
|
+
}
|
|
299
|
+
},
|
|
265
300
|
motion: {
|
|
266
301
|
type: "object",
|
|
267
302
|
description: "Motion/animation configuration.",
|
|
@@ -304,6 +339,18 @@ var visor_theme_schema_default = {
|
|
|
304
339
|
additionalProperties: { type: "string" }
|
|
305
340
|
}
|
|
306
341
|
}
|
|
342
|
+
},
|
|
343
|
+
migrate: {
|
|
344
|
+
type: "object",
|
|
345
|
+
description: "Migration metadata consumed by `visor migrate` commands. Does not affect CSS generation or theme application.",
|
|
346
|
+
additionalProperties: false,
|
|
347
|
+
properties: {
|
|
348
|
+
"token-substitution": {
|
|
349
|
+
type: "object",
|
|
350
|
+
description: "\xA73.1 V7-primitive \u2192 Visor-semantic substitution table. Maps V7 CSS custom property names (with -- prefix) to Visor semantic token names. Consumed by `visor migrate token-substitution`.",
|
|
351
|
+
additionalProperties: { type: "string" }
|
|
352
|
+
}
|
|
353
|
+
}
|
|
307
354
|
}
|
|
308
355
|
},
|
|
309
356
|
$defs: {
|
|
@@ -316,6 +363,28 @@ var visor_theme_schema_default = {
|
|
|
316
363
|
{ pattern: "^hsla?\\(" },
|
|
317
364
|
{ pattern: "^oklch\\(" }
|
|
318
365
|
]
|
|
366
|
+
},
|
|
367
|
+
textSlotOverride: {
|
|
368
|
+
type: "object",
|
|
369
|
+
description: "Per-slot override for one Material TextTheme entry.",
|
|
370
|
+
additionalProperties: false,
|
|
371
|
+
properties: {
|
|
372
|
+
size: {
|
|
373
|
+
type: "number",
|
|
374
|
+
exclusiveMinimum: 0,
|
|
375
|
+
description: "Font size in logical pixels (Flutter TextStyle.fontSize)."
|
|
376
|
+
},
|
|
377
|
+
weight: {
|
|
378
|
+
type: "integer",
|
|
379
|
+
minimum: 100,
|
|
380
|
+
maximum: 900,
|
|
381
|
+
description: "Font weight."
|
|
382
|
+
},
|
|
383
|
+
"letter-spacing": {
|
|
384
|
+
type: "number",
|
|
385
|
+
description: "Letter spacing in logical pixels (Flutter TextStyle.letterSpacing). Material defaults include negative values, e.g. -0.25 for displayLarge."
|
|
386
|
+
}
|
|
387
|
+
}
|
|
319
388
|
}
|
|
320
389
|
}
|
|
321
390
|
};
|
|
@@ -333,6 +402,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
333
402
|
"spacing",
|
|
334
403
|
"radius",
|
|
335
404
|
"shadows",
|
|
405
|
+
"strokeWidths",
|
|
336
406
|
"motion",
|
|
337
407
|
"overrides"
|
|
338
408
|
]);
|
|
@@ -353,14 +423,18 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
|
|
|
353
423
|
"body",
|
|
354
424
|
"mono",
|
|
355
425
|
"letter-spacing",
|
|
356
|
-
"scale"
|
|
426
|
+
"scale",
|
|
427
|
+
"slots"
|
|
357
428
|
]);
|
|
358
429
|
var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
|
|
359
430
|
var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family"]);
|
|
360
431
|
var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set(["tight", "normal", "wide"]);
|
|
432
|
+
var KNOWN_SLOT_NAMES = new Set(MATERIAL_TEXT_SLOTS);
|
|
433
|
+
var KNOWN_SLOT_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["size", "weight", "letter-spacing"]);
|
|
361
434
|
var KNOWN_SPACING_KEYS = /* @__PURE__ */ new Set(["base"]);
|
|
362
435
|
var KNOWN_RADIUS_KEYS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "pill"]);
|
|
363
436
|
var KNOWN_SHADOW_KEYS = /* @__PURE__ */ new Set(["xs", "sm", "md", "lg", "xl"]);
|
|
437
|
+
var KNOWN_STROKE_WIDTH_KEYS = /* @__PURE__ */ new Set(["thin", "regular", "medium", "thick"]);
|
|
364
438
|
var KNOWN_MOTION_KEYS = /* @__PURE__ */ new Set(["duration-fast", "duration-normal", "duration-slow", "easing"]);
|
|
365
439
|
var KNOWN_OVERRIDES_KEYS = /* @__PURE__ */ new Set(["light", "dark"]);
|
|
366
440
|
function checkUnknownKeys(obj, errors) {
|
|
@@ -425,6 +499,31 @@ function checkUnknownKeys(obj, errors) {
|
|
|
425
499
|
}
|
|
426
500
|
}
|
|
427
501
|
}
|
|
502
|
+
if (typeof typo.slots === "object" && typo.slots !== null) {
|
|
503
|
+
const slots = typo.slots;
|
|
504
|
+
for (const slotName of Object.keys(slots)) {
|
|
505
|
+
if (!KNOWN_SLOT_NAMES.has(slotName)) {
|
|
506
|
+
errors.push(
|
|
507
|
+
`Unknown key 'typography.slots.${slotName}'. Valid keys: ${[...MATERIAL_TEXT_SLOTS].join(", ")}`
|
|
508
|
+
);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const override = slots[slotName];
|
|
512
|
+
if (typeof override !== "object" || override === null) {
|
|
513
|
+
errors.push(
|
|
514
|
+
`'typography.slots.${slotName}' must be an object with optional size/weight/letter-spacing fields`
|
|
515
|
+
);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
for (const key of Object.keys(override)) {
|
|
519
|
+
if (!KNOWN_SLOT_OVERRIDE_KEYS.has(key)) {
|
|
520
|
+
errors.push(
|
|
521
|
+
`Unknown key 'typography.slots.${slotName}.${key}'. Valid keys: ${[...KNOWN_SLOT_OVERRIDE_KEYS].join(", ")}`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
428
527
|
}
|
|
429
528
|
if (typeof obj.spacing === "object" && obj.spacing !== null) {
|
|
430
529
|
for (const key of Object.keys(obj.spacing)) {
|
|
@@ -447,6 +546,13 @@ function checkUnknownKeys(obj, errors) {
|
|
|
447
546
|
}
|
|
448
547
|
}
|
|
449
548
|
}
|
|
549
|
+
if (typeof obj.strokeWidths === "object" && obj.strokeWidths !== null) {
|
|
550
|
+
for (const key of Object.keys(obj.strokeWidths)) {
|
|
551
|
+
if (!KNOWN_STROKE_WIDTH_KEYS.has(key)) {
|
|
552
|
+
errors.push(`Unknown key 'strokeWidths.${key}'. Valid keys: ${[...KNOWN_STROKE_WIDTH_KEYS].join(", ")}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
450
556
|
if (typeof obj.motion === "object" && obj.motion !== null) {
|
|
451
557
|
for (const key of Object.keys(obj.motion)) {
|
|
452
558
|
if (!KNOWN_MOTION_KEYS.has(key)) {
|
|
@@ -547,6 +653,23 @@ function validateConfig(config) {
|
|
|
547
653
|
}
|
|
548
654
|
}
|
|
549
655
|
}
|
|
656
|
+
if (typeof typo.slots === "object" && typo.slots !== null) {
|
|
657
|
+
const slots = typo.slots;
|
|
658
|
+
for (const slotName of Object.keys(slots)) {
|
|
659
|
+
const override = slots[slotName];
|
|
660
|
+
if (typeof override !== "object" || override === null) continue;
|
|
661
|
+
const o = override;
|
|
662
|
+
if (o.size !== void 0 && (typeof o.size !== "number" || o.size <= 0)) {
|
|
663
|
+
errors.push(`'typography.slots.${slotName}.size' must be a positive number (logical pixels)`);
|
|
664
|
+
}
|
|
665
|
+
if (o.weight !== void 0 && (typeof o.weight !== "number" || o.weight < 100 || o.weight > 900)) {
|
|
666
|
+
errors.push(`'typography.slots.${slotName}.weight' must be between 100 and 900`);
|
|
667
|
+
}
|
|
668
|
+
if (o["letter-spacing"] !== void 0 && typeof o["letter-spacing"] !== "number") {
|
|
669
|
+
errors.push(`'typography.slots.${slotName}.letter-spacing' must be a number (Flutter logical pixels)`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
550
673
|
}
|
|
551
674
|
if (obj.overrides !== void 0) {
|
|
552
675
|
if (typeof obj.overrides !== "object" || obj.overrides === null) {
|
|
@@ -595,6 +718,12 @@ var DEFAULTS = {
|
|
|
595
718
|
lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
|
|
596
719
|
xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)"
|
|
597
720
|
},
|
|
721
|
+
strokeWidths: {
|
|
722
|
+
thin: 1,
|
|
723
|
+
regular: 1.5,
|
|
724
|
+
medium: 2,
|
|
725
|
+
thick: 2.5
|
|
726
|
+
},
|
|
598
727
|
motion: {
|
|
599
728
|
"duration-fast": "100ms",
|
|
600
729
|
"duration-normal": "200ms",
|
|
@@ -671,7 +800,8 @@ function resolveConfig(config) {
|
|
|
671
800
|
},
|
|
672
801
|
mono: {
|
|
673
802
|
family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family
|
|
674
|
-
}
|
|
803
|
+
},
|
|
804
|
+
slots: config.typography?.slots ?? {}
|
|
675
805
|
},
|
|
676
806
|
spacing: {
|
|
677
807
|
base: config.spacing?.base ?? DEFAULTS.spacing.base
|
|
@@ -690,6 +820,12 @@ function resolveConfig(config) {
|
|
|
690
820
|
lg: config.shadows?.lg ?? DEFAULTS.shadows.lg,
|
|
691
821
|
xl: config.shadows?.xl ?? DEFAULTS.shadows.xl
|
|
692
822
|
},
|
|
823
|
+
strokeWidths: {
|
|
824
|
+
thin: config.strokeWidths?.thin ?? DEFAULTS.strokeWidths.thin,
|
|
825
|
+
regular: config.strokeWidths?.regular ?? DEFAULTS.strokeWidths.regular,
|
|
826
|
+
medium: config.strokeWidths?.medium ?? DEFAULTS.strokeWidths.medium,
|
|
827
|
+
thick: config.strokeWidths?.thick ?? DEFAULTS.strokeWidths.thick
|
|
828
|
+
},
|
|
693
829
|
motion: {
|
|
694
830
|
"duration-fast": config.motion?.["duration-fast"] ?? DEFAULTS.motion["duration-fast"],
|
|
695
831
|
"duration-normal": config.motion?.["duration-normal"] ?? DEFAULTS.motion["duration-normal"],
|
|
@@ -715,13 +851,17 @@ var SEMANTIC_TEXT_MAP = {
|
|
|
715
851
|
light: { role: "neutral", shade: 900 },
|
|
716
852
|
dark: { role: "neutral", shade: 50 }
|
|
717
853
|
},
|
|
854
|
+
// VI-346: rebalanced to fixed-L shades (700/300, 600/400) so AA contrast holds
|
|
855
|
+
// by default for any reasonable input neutral. Shade 500 is brand-anchored
|
|
856
|
+
// (variable L = input itself) and therefore forbidden for any text level that
|
|
857
|
+
// must clear AA. See docs/token-rules.md and SEMANTIC_TEXT_MAP rationale.
|
|
718
858
|
secondary: {
|
|
719
|
-
light: { role: "neutral", shade:
|
|
720
|
-
dark: { role: "neutral", shade:
|
|
859
|
+
light: { role: "neutral", shade: 700 },
|
|
860
|
+
dark: { role: "neutral", shade: 300 }
|
|
721
861
|
},
|
|
722
862
|
tertiary: {
|
|
723
|
-
light: { role: "neutral", shade:
|
|
724
|
-
dark: { role: "neutral", shade:
|
|
863
|
+
light: { role: "neutral", shade: 600 },
|
|
864
|
+
dark: { role: "neutral", shade: 400 }
|
|
725
865
|
},
|
|
726
866
|
disabled: {
|
|
727
867
|
light: { role: "neutral", shade: 300 },
|
|
@@ -769,6 +909,12 @@ var SEMANTIC_SURFACE_MAP = {
|
|
|
769
909
|
light: { constant: CONFIG_SURFACE },
|
|
770
910
|
dark: { constant: CONFIG_DARK_SURFACE }
|
|
771
911
|
},
|
|
912
|
+
// Distinct from card: glass themes (Blackout, Modern Minimal dark) set surface-card translucent.
|
|
913
|
+
// Floating panels rendered over arbitrary page content must be opaque — override this token there.
|
|
914
|
+
popover: {
|
|
915
|
+
light: { constant: CONFIG_SURFACE },
|
|
916
|
+
dark: { constant: CONFIG_DARK_SURFACE }
|
|
917
|
+
},
|
|
772
918
|
subtle: {
|
|
773
919
|
light: { role: "neutral", shade: 50 },
|
|
774
920
|
dark: { role: "neutral", shade: 800 }
|
|
@@ -797,6 +943,12 @@ var SEMANTIC_SURFACE_MAP = {
|
|
|
797
943
|
light: { role: "neutral", shade: 50 },
|
|
798
944
|
dark: { role: "neutral", shade: 800 }
|
|
799
945
|
},
|
|
946
|
+
// Persistent selected-state surface (active nav item, currently-selected list row).
|
|
947
|
+
// Distinct from interactive-active (transient press) and from accent-subtle (broader brand surface).
|
|
948
|
+
selected: {
|
|
949
|
+
light: { role: "primary", shade: 100 },
|
|
950
|
+
dark: { role: "primary", shade: 800 }
|
|
951
|
+
},
|
|
800
952
|
"accent-subtle": {
|
|
801
953
|
light: { role: "primary", shade: 50 },
|
|
802
954
|
dark: { role: "primary", shade: 900 }
|
|
@@ -840,6 +992,29 @@ var SEMANTIC_SURFACE_MAP = {
|
|
|
840
992
|
"info-default": {
|
|
841
993
|
light: { role: "info", shade: 500 },
|
|
842
994
|
dark: { role: "info", shade: 500 }
|
|
995
|
+
},
|
|
996
|
+
// 5-tier ordinal elevation scale — deepest (0) to highest (4)
|
|
997
|
+
// Light mode: BO-10 near-white ramp (white → neutral-300).
|
|
998
|
+
// Dark mode: deep neutral ramp (neutral-950 → neutral-600).
|
|
999
|
+
"elev-0": {
|
|
1000
|
+
light: { constant: "#ffffff" },
|
|
1001
|
+
dark: { role: "neutral", shade: 950 }
|
|
1002
|
+
},
|
|
1003
|
+
"elev-1": {
|
|
1004
|
+
light: { role: "neutral", shade: 50 },
|
|
1005
|
+
dark: { role: "neutral", shade: 900 }
|
|
1006
|
+
},
|
|
1007
|
+
"elev-2": {
|
|
1008
|
+
light: { role: "neutral", shade: 100 },
|
|
1009
|
+
dark: { role: "neutral", shade: 800 }
|
|
1010
|
+
},
|
|
1011
|
+
"elev-3": {
|
|
1012
|
+
light: { role: "neutral", shade: 200 },
|
|
1013
|
+
dark: { role: "neutral", shade: 700 }
|
|
1014
|
+
},
|
|
1015
|
+
"elev-4": {
|
|
1016
|
+
light: { role: "neutral", shade: 300 },
|
|
1017
|
+
dark: { role: "neutral", shade: 600 }
|
|
843
1018
|
}
|
|
844
1019
|
};
|
|
845
1020
|
var SEMANTIC_BORDER_MAP = {
|
|
@@ -977,28 +1152,28 @@ function resolveRef(ref, primitives, config) {
|
|
|
977
1152
|
return ref.constant;
|
|
978
1153
|
}
|
|
979
1154
|
}
|
|
980
|
-
function resolveMapping(mapping,
|
|
1155
|
+
function resolveMapping(mapping, lightPrimitives, darkPrimitives, config) {
|
|
981
1156
|
return {
|
|
982
|
-
light: resolveRef(mapping.light,
|
|
983
|
-
dark: resolveRef(mapping.dark,
|
|
1157
|
+
light: resolveRef(mapping.light, lightPrimitives, config),
|
|
1158
|
+
dark: resolveRef(mapping.dark, darkPrimitives, config)
|
|
984
1159
|
};
|
|
985
1160
|
}
|
|
986
|
-
function assignSemanticTokens(
|
|
1161
|
+
function assignSemanticTokens(lightPrimitives, darkPrimitives, config) {
|
|
987
1162
|
const text = {};
|
|
988
1163
|
const surface = {};
|
|
989
1164
|
const border = {};
|
|
990
1165
|
const interactive = {};
|
|
991
1166
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.text)) {
|
|
992
|
-
text[name] = resolveMapping(mapping,
|
|
1167
|
+
text[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
993
1168
|
}
|
|
994
1169
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.surface)) {
|
|
995
|
-
surface[name] = resolveMapping(mapping,
|
|
1170
|
+
surface[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
996
1171
|
}
|
|
997
1172
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.border)) {
|
|
998
|
-
border[name] = resolveMapping(mapping,
|
|
1173
|
+
border[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
999
1174
|
}
|
|
1000
1175
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.interactive)) {
|
|
1001
|
-
interactive[name] = resolveMapping(mapping,
|
|
1176
|
+
interactive[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
1002
1177
|
}
|
|
1003
1178
|
return { text, surface, border, interactive };
|
|
1004
1179
|
}
|
|
@@ -1071,6 +1246,18 @@ function generatePrimitives(config) {
|
|
|
1071
1246
|
info: generateShadeScale(config.colors.info, "info")
|
|
1072
1247
|
};
|
|
1073
1248
|
}
|
|
1249
|
+
function generateDarkPrimitives(config, lightPrimitives) {
|
|
1250
|
+
const cd = config["colors-dark"];
|
|
1251
|
+
return {
|
|
1252
|
+
primary: cd?.primary ? generateShadeScale(cd.primary, "primary") : lightPrimitives.primary,
|
|
1253
|
+
accent: cd?.accent ? generateShadeScale(cd.accent, "accent") : lightPrimitives.accent,
|
|
1254
|
+
neutral: cd?.neutral ? config.colors.neutral === null ? TAILWIND_GRAY : generateShadeScale(cd.neutral, "neutral") : lightPrimitives.neutral,
|
|
1255
|
+
success: cd?.success ? generateShadeScale(cd.success, "success") : lightPrimitives.success,
|
|
1256
|
+
warning: cd?.warning ? generateShadeScale(cd.warning, "warning") : lightPrimitives.warning,
|
|
1257
|
+
error: cd?.error ? generateShadeScale(cd.error, "error") : lightPrimitives.error,
|
|
1258
|
+
info: cd?.info ? generateShadeScale(cd.info, "info") : lightPrimitives.info
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1074
1261
|
function parseConfig(yamlString) {
|
|
1075
1262
|
const parsed = parseYaml(yamlString);
|
|
1076
1263
|
const result = validateConfig(parsed);
|
|
@@ -1103,7 +1290,8 @@ ${validation.errors.map((e) => ` - ${e}`).join("\n")}`
|
|
|
1103
1290
|
}
|
|
1104
1291
|
const resolved = resolveConfig(config);
|
|
1105
1292
|
const primitives = generatePrimitives(resolved);
|
|
1106
|
-
|
|
1293
|
+
const darkPrimitives = generateDarkPrimitives(resolved, primitives);
|
|
1294
|
+
let tokens = assignSemanticTokens(primitives, darkPrimitives, resolved);
|
|
1107
1295
|
tokens = applyOverrides(tokens, resolved.overrides);
|
|
1108
1296
|
const output = {
|
|
1109
1297
|
primitivesCss: generatePrimitivesCss(primitives, resolved),
|
|
@@ -1677,35 +1865,75 @@ function colorToRgb(color) {
|
|
|
1677
1865
|
const parsed = parseColor(color);
|
|
1678
1866
|
return parsed ? parsed.rgb : [0, 0, 0];
|
|
1679
1867
|
}
|
|
1868
|
+
var STANDARD_TEXT_TOKENS = ["primary", "secondary", "tertiary"];
|
|
1869
|
+
var STATUS_TEXT_TOKENS = ["error", "warning", "success", "info"];
|
|
1680
1870
|
function checkContrastWarnings(config, issues) {
|
|
1681
1871
|
const resolved = resolveConfig(config);
|
|
1872
|
+
const lightPrimitives = generatePrimitives(resolved);
|
|
1873
|
+
const darkPrimitives = generateDarkPrimitives(resolved, lightPrimitives);
|
|
1874
|
+
const tokens = applyOverrides(
|
|
1875
|
+
assignSemanticTokens(lightPrimitives, darkPrimitives, resolved),
|
|
1876
|
+
resolved.overrides
|
|
1877
|
+
);
|
|
1682
1878
|
const lightBg = resolved.colors.background;
|
|
1683
1879
|
const lightSurface = resolved.colors.surface;
|
|
1684
1880
|
const primary = resolved.colors.primary;
|
|
1685
1881
|
const lightBgRgb = colorToRgb(lightBg);
|
|
1686
1882
|
const lightSurfaceRgb = colorToRgb(lightSurface);
|
|
1687
|
-
const
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
);
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
);
|
|
1883
|
+
const darkBg = resolved["colors-dark"]?.background ?? "#0a0a0a";
|
|
1884
|
+
const darkSurface = resolved["colors-dark"]?.surface ?? "#171717";
|
|
1885
|
+
const darkPrimary = resolved["colors-dark"]?.primary ?? primary;
|
|
1886
|
+
const darkBgRgb = colorToRgb(darkBg);
|
|
1887
|
+
const darkSurfaceRgb = colorToRgb(darkSurface);
|
|
1888
|
+
const textTokensToCheck = [...STANDARD_TEXT_TOKENS, ...STATUS_TEXT_TOKENS];
|
|
1889
|
+
for (const tokenName of textTokensToCheck) {
|
|
1890
|
+
const tokenValue = tokens.text[tokenName];
|
|
1891
|
+
if (!tokenValue) continue;
|
|
1892
|
+
const fullName = `text-${tokenName}`;
|
|
1893
|
+
const lightOnBg = getContrastRatio(tokenValue.light, lightBg, lightBgRgb);
|
|
1894
|
+
if (lightOnBg < CONTRAST_TEXT_AA) {
|
|
1895
|
+
issues.push(
|
|
1896
|
+
issue(
|
|
1897
|
+
"warning",
|
|
1898
|
+
"WCAG_CONTRAST",
|
|
1899
|
+
`Light mode: ${fullName} on background has contrast ratio ${lightOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1900
|
+
"colors.background"
|
|
1901
|
+
)
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
const lightOnSurface = getContrastRatio(tokenValue.light, lightSurface, lightSurfaceRgb);
|
|
1905
|
+
if (lightOnSurface < CONTRAST_TEXT_AA) {
|
|
1906
|
+
issues.push(
|
|
1907
|
+
issue(
|
|
1908
|
+
"warning",
|
|
1909
|
+
"WCAG_CONTRAST",
|
|
1910
|
+
`Light mode: ${fullName} on surface has contrast ratio ${lightOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1911
|
+
"colors.surface"
|
|
1912
|
+
)
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
const darkOnBg = getContrastRatio(tokenValue.dark, darkBg, darkBgRgb);
|
|
1916
|
+
if (darkOnBg < CONTRAST_TEXT_AA) {
|
|
1917
|
+
issues.push(
|
|
1918
|
+
issue(
|
|
1919
|
+
"warning",
|
|
1920
|
+
"WCAG_CONTRAST",
|
|
1921
|
+
`Dark mode: ${fullName} on background has contrast ratio ${darkOnBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1922
|
+
"colors-dark.background"
|
|
1923
|
+
)
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
const darkOnSurface = getContrastRatio(tokenValue.dark, darkSurface, darkSurfaceRgb);
|
|
1927
|
+
if (darkOnSurface < CONTRAST_TEXT_AA) {
|
|
1928
|
+
issues.push(
|
|
1929
|
+
issue(
|
|
1930
|
+
"warning",
|
|
1931
|
+
"WCAG_CONTRAST",
|
|
1932
|
+
`Dark mode: ${fullName} on surface has contrast ratio ${darkOnSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1933
|
+
"colors-dark.surface"
|
|
1934
|
+
)
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1709
1937
|
}
|
|
1710
1938
|
const primaryOnBg = getContrastRatio(primary, lightBg, lightBgRgb);
|
|
1711
1939
|
if (primaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
@@ -1729,34 +1957,6 @@ function checkContrastWarnings(config, issues) {
|
|
|
1729
1957
|
)
|
|
1730
1958
|
);
|
|
1731
1959
|
}
|
|
1732
|
-
const darkBg = resolved["colors-dark"]?.background ?? "#0a0a0a";
|
|
1733
|
-
const darkSurface = resolved["colors-dark"]?.surface ?? "#171717";
|
|
1734
|
-
const darkPrimary = resolved["colors-dark"]?.primary ?? primary;
|
|
1735
|
-
const darkBgRgb = colorToRgb(darkBg);
|
|
1736
|
-
const darkSurfaceRgb = colorToRgb(darkSurface);
|
|
1737
|
-
const textLight = "#f9fafb";
|
|
1738
|
-
const textOnDarkBg = getContrastRatio(textLight, darkBg, darkBgRgb);
|
|
1739
|
-
if (textOnDarkBg < CONTRAST_TEXT_AA) {
|
|
1740
|
-
issues.push(
|
|
1741
|
-
issue(
|
|
1742
|
-
"warning",
|
|
1743
|
-
"WCAG_CONTRAST",
|
|
1744
|
-
`Dark mode: text-primary on background has contrast ratio ${textOnDarkBg.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1745
|
-
"colors-dark.background"
|
|
1746
|
-
)
|
|
1747
|
-
);
|
|
1748
|
-
}
|
|
1749
|
-
const textOnDarkSurface = getContrastRatio(textLight, darkSurface, darkSurfaceRgb);
|
|
1750
|
-
if (textOnDarkSurface < CONTRAST_TEXT_AA) {
|
|
1751
|
-
issues.push(
|
|
1752
|
-
issue(
|
|
1753
|
-
"warning",
|
|
1754
|
-
"WCAG_CONTRAST",
|
|
1755
|
-
`Dark mode: text-primary on surface has contrast ratio ${textOnDarkSurface.toFixed(2)}:1 (needs >= ${CONTRAST_TEXT_AA}:1)`,
|
|
1756
|
-
"colors-dark.surface"
|
|
1757
|
-
)
|
|
1758
|
-
);
|
|
1759
|
-
}
|
|
1760
1960
|
const darkPrimaryOnBg = getContrastRatio(darkPrimary, darkBg, darkBgRgb);
|
|
1761
1961
|
if (darkPrimaryOnBg < CONTRAST_INTERACTIVE_AA) {
|
|
1762
1962
|
issues.push(
|
|
@@ -124,6 +124,23 @@ interface ParsedColor {
|
|
|
124
124
|
format: ColorFormat;
|
|
125
125
|
original: string;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* The 15 Material 3 type-scale slots plus Visor's `labelXSmall` extension.
|
|
129
|
+
*
|
|
130
|
+
* Per-slot size / weight / letter-spacing can be overridden in
|
|
131
|
+
* `typography.slots` to tune the generated Flutter `TextTheme` without
|
|
132
|
+
* touching the global font families.
|
|
133
|
+
*/
|
|
134
|
+
type MaterialTextSlot = "displayLarge" | "displayMedium" | "displaySmall" | "headlineLarge" | "headlineMedium" | "headlineSmall" | "titleLarge" | "titleMedium" | "titleSmall" | "bodyLarge" | "bodyMedium" | "bodySmall" | "labelLarge" | "labelMedium" | "labelSmall" | "labelXSmall";
|
|
135
|
+
/** Per-slot override in `typography.slots.<slot>`. All fields optional. */
|
|
136
|
+
interface TextSlotOverride {
|
|
137
|
+
/** Font size in logical pixels (Flutter `TextStyle.fontSize`). */
|
|
138
|
+
size?: number;
|
|
139
|
+
/** Font weight (100–900, matching Flutter `FontWeight.w100..w900`). */
|
|
140
|
+
weight?: number;
|
|
141
|
+
/** Letter spacing in logical pixels (Flutter `TextStyle.letterSpacing`). */
|
|
142
|
+
"letter-spacing"?: number;
|
|
143
|
+
}
|
|
127
144
|
interface VisorThemeConfig {
|
|
128
145
|
name: string;
|
|
129
146
|
version: 1;
|
|
@@ -189,6 +206,13 @@ interface VisorThemeConfig {
|
|
|
189
206
|
normal?: string;
|
|
190
207
|
wide?: string;
|
|
191
208
|
};
|
|
209
|
+
/**
|
|
210
|
+
* Per-slot overrides for the generated Flutter `TextTheme`. Any subset
|
|
211
|
+
* of the 16 Material slots may be specified; omitted slots fall
|
|
212
|
+
* through to `VisorTextStylesData.defaults` (Material 3 2024 scale).
|
|
213
|
+
* Flutter-only — ignored by CSS/NextJS adapters.
|
|
214
|
+
*/
|
|
215
|
+
slots?: Partial<Record<MaterialTextSlot, TextSlotOverride>>;
|
|
192
216
|
};
|
|
193
217
|
spacing?: {
|
|
194
218
|
base?: number;
|
|
@@ -207,6 +231,12 @@ interface VisorThemeConfig {
|
|
|
207
231
|
lg?: string;
|
|
208
232
|
xl?: string;
|
|
209
233
|
};
|
|
234
|
+
strokeWidths?: {
|
|
235
|
+
thin?: number;
|
|
236
|
+
regular?: number;
|
|
237
|
+
medium?: number;
|
|
238
|
+
thick?: number;
|
|
239
|
+
};
|
|
210
240
|
motion?: {
|
|
211
241
|
"duration-fast"?: string;
|
|
212
242
|
"duration-normal"?: string;
|
|
@@ -217,6 +247,21 @@ interface VisorThemeConfig {
|
|
|
217
247
|
light?: Record<string, string>;
|
|
218
248
|
dark?: Record<string, string>;
|
|
219
249
|
};
|
|
250
|
+
/**
|
|
251
|
+
* Migration metadata — optional, additive, consumed by `visor migrate` commands.
|
|
252
|
+
* Does not affect CSS generation or theme application.
|
|
253
|
+
*/
|
|
254
|
+
migrate?: {
|
|
255
|
+
/**
|
|
256
|
+
* §3.1 V7-primitive → Visor-semantic substitution table.
|
|
257
|
+
* Maps V7 CSS custom property names (without `var()`) to Visor semantic
|
|
258
|
+
* token names. Consumed by `visor migrate token-substitution`.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* { "--panel": "--surface-card", "--text": "--text-primary" }
|
|
262
|
+
*/
|
|
263
|
+
"token-substitution"?: Record<string, string>;
|
|
264
|
+
};
|
|
220
265
|
}
|
|
221
266
|
/** Config with all defaults resolved */
|
|
222
267
|
interface ResolvedThemeConfig {
|
|
@@ -260,6 +305,12 @@ interface ResolvedThemeConfig {
|
|
|
260
305
|
mono: {
|
|
261
306
|
family: string;
|
|
262
307
|
};
|
|
308
|
+
/**
|
|
309
|
+
* Per-slot Material `TextTheme` overrides, passed through from the
|
|
310
|
+
* raw config. Empty object when none supplied. Flutter adapter
|
|
311
|
+
* consumes these; other adapters may ignore them.
|
|
312
|
+
*/
|
|
313
|
+
slots: Partial<Record<MaterialTextSlot, TextSlotOverride>>;
|
|
263
314
|
};
|
|
264
315
|
spacing: {
|
|
265
316
|
base: number;
|
|
@@ -278,6 +329,12 @@ interface ResolvedThemeConfig {
|
|
|
278
329
|
lg: string;
|
|
279
330
|
xl: string;
|
|
280
331
|
};
|
|
332
|
+
strokeWidths: {
|
|
333
|
+
thin: number;
|
|
334
|
+
regular: number;
|
|
335
|
+
medium: number;
|
|
336
|
+
thick: number;
|
|
337
|
+
};
|
|
281
338
|
motion: {
|
|
282
339
|
"duration-fast": string;
|
|
283
340
|
"duration-normal": string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loworbitstudio/visor-theme-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Theme engine for the Visor design system — shade generation, token mapping, font resolution, and import/export for .visor.yaml themes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|