@loworbitstudio/visor-theme-engine 0.1.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 +29 -0
- package/dist/adapters/index.d.ts +64 -3
- package/dist/adapters/index.js +729 -2
- package/dist/{chunk-ZLXFCNYF.js → chunk-G4B57FB3.js} +51 -23
- package/dist/index.d.ts +108 -6
- package/dist/index.js +230 -16
- package/dist/{types-r7ae3WP2.d.ts → types-gAlkt__C.d.ts} +58 -0
- package/package.json +7 -2
- package/src/visor-theme.schema.json +56 -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-G4B57FB3.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.",
|
|
@@ -316,6 +351,28 @@ var visor_theme_schema_default = {
|
|
|
316
351
|
{ pattern: "^hsla?\\(" },
|
|
317
352
|
{ pattern: "^oklch\\(" }
|
|
318
353
|
]
|
|
354
|
+
},
|
|
355
|
+
textSlotOverride: {
|
|
356
|
+
type: "object",
|
|
357
|
+
description: "Per-slot override for one Material TextTheme entry.",
|
|
358
|
+
additionalProperties: false,
|
|
359
|
+
properties: {
|
|
360
|
+
size: {
|
|
361
|
+
type: "number",
|
|
362
|
+
exclusiveMinimum: 0,
|
|
363
|
+
description: "Font size in logical pixels (Flutter TextStyle.fontSize)."
|
|
364
|
+
},
|
|
365
|
+
weight: {
|
|
366
|
+
type: "integer",
|
|
367
|
+
minimum: 100,
|
|
368
|
+
maximum: 900,
|
|
369
|
+
description: "Font weight."
|
|
370
|
+
},
|
|
371
|
+
"letter-spacing": {
|
|
372
|
+
type: "number",
|
|
373
|
+
description: "Letter spacing in logical pixels (Flutter TextStyle.letterSpacing). Material defaults include negative values, e.g. -0.25 for displayLarge."
|
|
374
|
+
}
|
|
375
|
+
}
|
|
319
376
|
}
|
|
320
377
|
}
|
|
321
378
|
};
|
|
@@ -325,12 +382,15 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
325
382
|
"name",
|
|
326
383
|
"version",
|
|
327
384
|
"group",
|
|
385
|
+
"label",
|
|
386
|
+
"default-mode",
|
|
328
387
|
"colors",
|
|
329
388
|
"colors-dark",
|
|
330
389
|
"typography",
|
|
331
390
|
"spacing",
|
|
332
391
|
"radius",
|
|
333
392
|
"shadows",
|
|
393
|
+
"strokeWidths",
|
|
334
394
|
"motion",
|
|
335
395
|
"overrides"
|
|
336
396
|
]);
|
|
@@ -351,14 +411,18 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
|
|
|
351
411
|
"body",
|
|
352
412
|
"mono",
|
|
353
413
|
"letter-spacing",
|
|
354
|
-
"scale"
|
|
414
|
+
"scale",
|
|
415
|
+
"slots"
|
|
355
416
|
]);
|
|
356
|
-
var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "source", "org"]);
|
|
417
|
+
var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
|
|
357
418
|
var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family"]);
|
|
358
419
|
var KNOWN_LETTER_SPACING_KEYS = /* @__PURE__ */ new Set(["tight", "normal", "wide"]);
|
|
420
|
+
var KNOWN_SLOT_NAMES = new Set(MATERIAL_TEXT_SLOTS);
|
|
421
|
+
var KNOWN_SLOT_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["size", "weight", "letter-spacing"]);
|
|
359
422
|
var KNOWN_SPACING_KEYS = /* @__PURE__ */ new Set(["base"]);
|
|
360
423
|
var KNOWN_RADIUS_KEYS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "pill"]);
|
|
361
424
|
var KNOWN_SHADOW_KEYS = /* @__PURE__ */ new Set(["xs", "sm", "md", "lg", "xl"]);
|
|
425
|
+
var KNOWN_STROKE_WIDTH_KEYS = /* @__PURE__ */ new Set(["thin", "regular", "medium", "thick"]);
|
|
362
426
|
var KNOWN_MOTION_KEYS = /* @__PURE__ */ new Set(["duration-fast", "duration-normal", "duration-slow", "easing"]);
|
|
363
427
|
var KNOWN_OVERRIDES_KEYS = /* @__PURE__ */ new Set(["light", "dark"]);
|
|
364
428
|
function checkUnknownKeys(obj, errors) {
|
|
@@ -423,6 +487,31 @@ function checkUnknownKeys(obj, errors) {
|
|
|
423
487
|
}
|
|
424
488
|
}
|
|
425
489
|
}
|
|
490
|
+
if (typeof typo.slots === "object" && typo.slots !== null) {
|
|
491
|
+
const slots = typo.slots;
|
|
492
|
+
for (const slotName of Object.keys(slots)) {
|
|
493
|
+
if (!KNOWN_SLOT_NAMES.has(slotName)) {
|
|
494
|
+
errors.push(
|
|
495
|
+
`Unknown key 'typography.slots.${slotName}'. Valid keys: ${[...MATERIAL_TEXT_SLOTS].join(", ")}`
|
|
496
|
+
);
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const override = slots[slotName];
|
|
500
|
+
if (typeof override !== "object" || override === null) {
|
|
501
|
+
errors.push(
|
|
502
|
+
`'typography.slots.${slotName}' must be an object with optional size/weight/letter-spacing fields`
|
|
503
|
+
);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
for (const key of Object.keys(override)) {
|
|
507
|
+
if (!KNOWN_SLOT_OVERRIDE_KEYS.has(key)) {
|
|
508
|
+
errors.push(
|
|
509
|
+
`Unknown key 'typography.slots.${slotName}.${key}'. Valid keys: ${[...KNOWN_SLOT_OVERRIDE_KEYS].join(", ")}`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
426
515
|
}
|
|
427
516
|
if (typeof obj.spacing === "object" && obj.spacing !== null) {
|
|
428
517
|
for (const key of Object.keys(obj.spacing)) {
|
|
@@ -445,6 +534,13 @@ function checkUnknownKeys(obj, errors) {
|
|
|
445
534
|
}
|
|
446
535
|
}
|
|
447
536
|
}
|
|
537
|
+
if (typeof obj.strokeWidths === "object" && obj.strokeWidths !== null) {
|
|
538
|
+
for (const key of Object.keys(obj.strokeWidths)) {
|
|
539
|
+
if (!KNOWN_STROKE_WIDTH_KEYS.has(key)) {
|
|
540
|
+
errors.push(`Unknown key 'strokeWidths.${key}'. Valid keys: ${[...KNOWN_STROKE_WIDTH_KEYS].join(", ")}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
448
544
|
if (typeof obj.motion === "object" && obj.motion !== null) {
|
|
449
545
|
for (const key of Object.keys(obj.motion)) {
|
|
450
546
|
if (!KNOWN_MOTION_KEYS.has(key)) {
|
|
@@ -473,6 +569,15 @@ function validateConfig(config) {
|
|
|
473
569
|
if (obj.version !== 1) {
|
|
474
570
|
errors.push("'version' must be 1");
|
|
475
571
|
}
|
|
572
|
+
if (obj.label !== void 0 && typeof obj.label !== "string") {
|
|
573
|
+
errors.push("'label' must be a string (optional display name override)");
|
|
574
|
+
}
|
|
575
|
+
if (obj["default-mode"] !== void 0) {
|
|
576
|
+
const mode = obj["default-mode"];
|
|
577
|
+
if (mode !== "dark" && mode !== "light") {
|
|
578
|
+
errors.push("'default-mode' must be either 'dark' or 'light'");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
476
581
|
if (typeof obj.colors !== "object" || obj.colors === null) {
|
|
477
582
|
errors.push("'colors' is required and must be an object");
|
|
478
583
|
return { valid: false, errors };
|
|
@@ -530,6 +635,28 @@ function validateConfig(config) {
|
|
|
530
635
|
if (font && font.source === "visor-fonts" && !font.org) {
|
|
531
636
|
errors.push(`'typography.${slot}.org' is required when source is 'visor-fonts'`);
|
|
532
637
|
}
|
|
638
|
+
if (font && font.weights !== void 0) {
|
|
639
|
+
if (!Array.isArray(font.weights) || !font.weights.every((w) => typeof w === "number" && w > 0)) {
|
|
640
|
+
errors.push(`'typography.${slot}.weights' must be an array of positive numbers (e.g., [300, 500])`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (typeof typo.slots === "object" && typo.slots !== null) {
|
|
645
|
+
const slots = typo.slots;
|
|
646
|
+
for (const slotName of Object.keys(slots)) {
|
|
647
|
+
const override = slots[slotName];
|
|
648
|
+
if (typeof override !== "object" || override === null) continue;
|
|
649
|
+
const o = override;
|
|
650
|
+
if (o.size !== void 0 && (typeof o.size !== "number" || o.size <= 0)) {
|
|
651
|
+
errors.push(`'typography.slots.${slotName}.size' must be a positive number (logical pixels)`);
|
|
652
|
+
}
|
|
653
|
+
if (o.weight !== void 0 && (typeof o.weight !== "number" || o.weight < 100 || o.weight > 900)) {
|
|
654
|
+
errors.push(`'typography.slots.${slotName}.weight' must be between 100 and 900`);
|
|
655
|
+
}
|
|
656
|
+
if (o["letter-spacing"] !== void 0 && typeof o["letter-spacing"] !== "number") {
|
|
657
|
+
errors.push(`'typography.slots.${slotName}.letter-spacing' must be a number (Flutter logical pixels)`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
533
660
|
}
|
|
534
661
|
}
|
|
535
662
|
if (obj.overrides !== void 0) {
|
|
@@ -579,6 +706,12 @@ var DEFAULTS = {
|
|
|
579
706
|
lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
|
|
580
707
|
xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)"
|
|
581
708
|
},
|
|
709
|
+
strokeWidths: {
|
|
710
|
+
thin: 1,
|
|
711
|
+
regular: 1.5,
|
|
712
|
+
medium: 2,
|
|
713
|
+
thick: 2.5
|
|
714
|
+
},
|
|
582
715
|
motion: {
|
|
583
716
|
"duration-fast": "100ms",
|
|
584
717
|
"duration-normal": "200ms",
|
|
@@ -636,23 +769,27 @@ function resolveConfig(config) {
|
|
|
636
769
|
family: config.typography?.heading?.family ?? DEFAULTS.typography.heading.family,
|
|
637
770
|
weight: config.typography?.heading?.weight ?? DEFAULTS.typography.heading.weight,
|
|
638
771
|
...config.typography?.heading?.source && { source: config.typography.heading.source },
|
|
639
|
-
...config.typography?.heading?.org && { org: config.typography.heading.org }
|
|
772
|
+
...config.typography?.heading?.org && { org: config.typography.heading.org },
|
|
773
|
+
...config.typography?.heading?.weights && { weights: config.typography.heading.weights }
|
|
640
774
|
},
|
|
641
775
|
display: {
|
|
642
776
|
family: config.typography?.display?.family ?? config.typography?.heading?.family ?? DEFAULTS.typography.heading.family,
|
|
643
777
|
weight: config.typography?.display?.weight ?? 400,
|
|
644
778
|
...config.typography?.display?.source && { source: config.typography.display.source },
|
|
645
|
-
...config.typography?.display?.org && { org: config.typography.display.org }
|
|
779
|
+
...config.typography?.display?.org && { org: config.typography.display.org },
|
|
780
|
+
...config.typography?.display?.weights && { weights: config.typography.display.weights }
|
|
646
781
|
},
|
|
647
782
|
body: {
|
|
648
783
|
family: config.typography?.body?.family ?? DEFAULTS.typography.body.family,
|
|
649
784
|
weight: config.typography?.body?.weight ?? DEFAULTS.typography.body.weight,
|
|
650
785
|
...config.typography?.body?.source && { source: config.typography.body.source },
|
|
651
|
-
...config.typography?.body?.org && { org: config.typography.body.org }
|
|
786
|
+
...config.typography?.body?.org && { org: config.typography.body.org },
|
|
787
|
+
...config.typography?.body?.weights && { weights: config.typography.body.weights }
|
|
652
788
|
},
|
|
653
789
|
mono: {
|
|
654
790
|
family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family
|
|
655
|
-
}
|
|
791
|
+
},
|
|
792
|
+
slots: config.typography?.slots ?? {}
|
|
656
793
|
},
|
|
657
794
|
spacing: {
|
|
658
795
|
base: config.spacing?.base ?? DEFAULTS.spacing.base
|
|
@@ -671,6 +808,12 @@ function resolveConfig(config) {
|
|
|
671
808
|
lg: config.shadows?.lg ?? DEFAULTS.shadows.lg,
|
|
672
809
|
xl: config.shadows?.xl ?? DEFAULTS.shadows.xl
|
|
673
810
|
},
|
|
811
|
+
strokeWidths: {
|
|
812
|
+
thin: config.strokeWidths?.thin ?? DEFAULTS.strokeWidths.thin,
|
|
813
|
+
regular: config.strokeWidths?.regular ?? DEFAULTS.strokeWidths.regular,
|
|
814
|
+
medium: config.strokeWidths?.medium ?? DEFAULTS.strokeWidths.medium,
|
|
815
|
+
thick: config.strokeWidths?.thick ?? DEFAULTS.strokeWidths.thick
|
|
816
|
+
},
|
|
674
817
|
motion: {
|
|
675
818
|
"duration-fast": config.motion?.["duration-fast"] ?? DEFAULTS.motion["duration-fast"],
|
|
676
819
|
"duration-normal": config.motion?.["duration-normal"] ?? DEFAULTS.motion["duration-normal"],
|
|
@@ -750,6 +893,12 @@ var SEMANTIC_SURFACE_MAP = {
|
|
|
750
893
|
light: { constant: CONFIG_SURFACE },
|
|
751
894
|
dark: { constant: CONFIG_DARK_SURFACE }
|
|
752
895
|
},
|
|
896
|
+
// Distinct from card: glass themes (Blackout, Modern Minimal dark) set surface-card translucent.
|
|
897
|
+
// Floating panels rendered over arbitrary page content must be opaque — override this token there.
|
|
898
|
+
popover: {
|
|
899
|
+
light: { constant: CONFIG_SURFACE },
|
|
900
|
+
dark: { constant: CONFIG_DARK_SURFACE }
|
|
901
|
+
},
|
|
753
902
|
subtle: {
|
|
754
903
|
light: { role: "neutral", shade: 50 },
|
|
755
904
|
dark: { role: "neutral", shade: 800 }
|
|
@@ -778,6 +927,12 @@ var SEMANTIC_SURFACE_MAP = {
|
|
|
778
927
|
light: { role: "neutral", shade: 50 },
|
|
779
928
|
dark: { role: "neutral", shade: 800 }
|
|
780
929
|
},
|
|
930
|
+
// Persistent selected-state surface (active nav item, currently-selected list row).
|
|
931
|
+
// Distinct from interactive-active (transient press) and from accent-subtle (broader brand surface).
|
|
932
|
+
selected: {
|
|
933
|
+
light: { role: "primary", shade: 100 },
|
|
934
|
+
dark: { role: "primary", shade: 800 }
|
|
935
|
+
},
|
|
781
936
|
"accent-subtle": {
|
|
782
937
|
light: { role: "primary", shade: 50 },
|
|
783
938
|
dark: { role: "primary", shade: 900 }
|
|
@@ -958,28 +1113,28 @@ function resolveRef(ref, primitives, config) {
|
|
|
958
1113
|
return ref.constant;
|
|
959
1114
|
}
|
|
960
1115
|
}
|
|
961
|
-
function resolveMapping(mapping,
|
|
1116
|
+
function resolveMapping(mapping, lightPrimitives, darkPrimitives, config) {
|
|
962
1117
|
return {
|
|
963
|
-
light: resolveRef(mapping.light,
|
|
964
|
-
dark: resolveRef(mapping.dark,
|
|
1118
|
+
light: resolveRef(mapping.light, lightPrimitives, config),
|
|
1119
|
+
dark: resolveRef(mapping.dark, darkPrimitives, config)
|
|
965
1120
|
};
|
|
966
1121
|
}
|
|
967
|
-
function assignSemanticTokens(
|
|
1122
|
+
function assignSemanticTokens(lightPrimitives, darkPrimitives, config) {
|
|
968
1123
|
const text = {};
|
|
969
1124
|
const surface = {};
|
|
970
1125
|
const border = {};
|
|
971
1126
|
const interactive = {};
|
|
972
1127
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.text)) {
|
|
973
|
-
text[name] = resolveMapping(mapping,
|
|
1128
|
+
text[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
974
1129
|
}
|
|
975
1130
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.surface)) {
|
|
976
|
-
surface[name] = resolveMapping(mapping,
|
|
1131
|
+
surface[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
977
1132
|
}
|
|
978
1133
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.border)) {
|
|
979
|
-
border[name] = resolveMapping(mapping,
|
|
1134
|
+
border[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
980
1135
|
}
|
|
981
1136
|
for (const [name, mapping] of Object.entries(SEMANTIC_MAP.interactive)) {
|
|
982
|
-
interactive[name] = resolveMapping(mapping,
|
|
1137
|
+
interactive[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
|
|
983
1138
|
}
|
|
984
1139
|
return { text, surface, border, interactive };
|
|
985
1140
|
}
|
|
@@ -1052,6 +1207,18 @@ function generatePrimitives(config) {
|
|
|
1052
1207
|
info: generateShadeScale(config.colors.info, "info")
|
|
1053
1208
|
};
|
|
1054
1209
|
}
|
|
1210
|
+
function generateDarkPrimitives(config, lightPrimitives) {
|
|
1211
|
+
const cd = config["colors-dark"];
|
|
1212
|
+
return {
|
|
1213
|
+
primary: cd?.primary ? generateShadeScale(cd.primary, "primary") : lightPrimitives.primary,
|
|
1214
|
+
accent: cd?.accent ? generateShadeScale(cd.accent, "accent") : lightPrimitives.accent,
|
|
1215
|
+
neutral: cd?.neutral ? config.colors.neutral === null ? TAILWIND_GRAY : generateShadeScale(cd.neutral, "neutral") : lightPrimitives.neutral,
|
|
1216
|
+
success: cd?.success ? generateShadeScale(cd.success, "success") : lightPrimitives.success,
|
|
1217
|
+
warning: cd?.warning ? generateShadeScale(cd.warning, "warning") : lightPrimitives.warning,
|
|
1218
|
+
error: cd?.error ? generateShadeScale(cd.error, "error") : lightPrimitives.error,
|
|
1219
|
+
info: cd?.info ? generateShadeScale(cd.info, "info") : lightPrimitives.info
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1055
1222
|
function parseConfig(yamlString) {
|
|
1056
1223
|
const parsed = parseYaml(yamlString);
|
|
1057
1224
|
const result = validateConfig(parsed);
|
|
@@ -1084,7 +1251,8 @@ ${validation.errors.map((e) => ` - ${e}`).join("\n")}`
|
|
|
1084
1251
|
}
|
|
1085
1252
|
const resolved = resolveConfig(config);
|
|
1086
1253
|
const primitives = generatePrimitives(resolved);
|
|
1087
|
-
|
|
1254
|
+
const darkPrimitives = generateDarkPrimitives(resolved, primitives);
|
|
1255
|
+
let tokens = assignSemanticTokens(primitives, darkPrimitives, resolved);
|
|
1088
1256
|
tokens = applyOverrides(tokens, resolved.overrides);
|
|
1089
1257
|
const output = {
|
|
1090
1258
|
primitivesCss: generatePrimitivesCss(primitives, resolved),
|
|
@@ -1835,6 +2003,51 @@ function checkRadiusScale(config, issues) {
|
|
|
1835
2003
|
);
|
|
1836
2004
|
}
|
|
1837
2005
|
}
|
|
2006
|
+
function checkDarkLightParity(config, issues) {
|
|
2007
|
+
if (!config.colors) return;
|
|
2008
|
+
const colorKeys = Object.keys(config.colors).filter((k) => k !== "primary");
|
|
2009
|
+
const hasDarkSection = config["colors-dark"] !== void 0;
|
|
2010
|
+
if (colorKeys.length > 0 && !hasDarkSection) {
|
|
2011
|
+
issues.push(
|
|
2012
|
+
issue(
|
|
2013
|
+
"warning",
|
|
2014
|
+
"DARK_LIGHT_PARITY",
|
|
2015
|
+
"Custom colors are set but no colors-dark section exists. Dark mode will use generated defaults which may not match your brand.",
|
|
2016
|
+
"colors-dark"
|
|
2017
|
+
)
|
|
2018
|
+
);
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
if (colorKeys.length > 0 && hasDarkSection) {
|
|
2022
|
+
const lightKeys = new Set(Object.keys(config.colors));
|
|
2023
|
+
const darkKeys = new Set(Object.keys(config["colors-dark"]));
|
|
2024
|
+
for (const key of lightKeys) {
|
|
2025
|
+
if (key === "primary") continue;
|
|
2026
|
+
if (!darkKeys.has(key)) {
|
|
2027
|
+
issues.push(
|
|
2028
|
+
issue(
|
|
2029
|
+
"warning",
|
|
2030
|
+
"DARK_LIGHT_PARITY",
|
|
2031
|
+
`Color "${key}" is set in colors but missing from colors-dark. Dark mode will use a generated default.`,
|
|
2032
|
+
"colors-dark"
|
|
2033
|
+
)
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
for (const key of darkKeys) {
|
|
2038
|
+
if (!lightKeys.has(key)) {
|
|
2039
|
+
issues.push(
|
|
2040
|
+
issue(
|
|
2041
|
+
"warning",
|
|
2042
|
+
"DARK_LIGHT_PARITY",
|
|
2043
|
+
`Color "${key}" is set in colors-dark but missing from colors. Light mode will use a generated default.`,
|
|
2044
|
+
"colors"
|
|
2045
|
+
)
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
1838
2051
|
function validate(config) {
|
|
1839
2052
|
const errors = [];
|
|
1840
2053
|
const warnings = [];
|
|
@@ -1866,6 +2079,7 @@ function validate(config) {
|
|
|
1866
2079
|
checkColorSimilarity(typedConfig, warnings);
|
|
1867
2080
|
checkMissingGlowShadow(typedConfig, warnings);
|
|
1868
2081
|
checkRadiusScale(typedConfig, warnings);
|
|
2082
|
+
checkDarkLightParity(typedConfig, warnings);
|
|
1869
2083
|
}
|
|
1870
2084
|
return {
|
|
1871
2085
|
valid: errors.length === 0,
|
|
@@ -46,18 +46,24 @@ interface VisorTypography {
|
|
|
46
46
|
heading?: {
|
|
47
47
|
family: string;
|
|
48
48
|
weight?: number;
|
|
49
|
+
/** Explicit list of font weights to load (overrides engine defaults) */
|
|
50
|
+
weights?: number[];
|
|
49
51
|
source?: FontSource;
|
|
50
52
|
org?: string;
|
|
51
53
|
};
|
|
52
54
|
display?: {
|
|
53
55
|
family: string;
|
|
54
56
|
weight?: number;
|
|
57
|
+
/** Explicit list of font weights to load (overrides engine defaults) */
|
|
58
|
+
weights?: number[];
|
|
55
59
|
source?: FontSource;
|
|
56
60
|
org?: string;
|
|
57
61
|
};
|
|
58
62
|
body?: {
|
|
59
63
|
family: string;
|
|
60
64
|
weight?: number;
|
|
65
|
+
/** Explicit list of font weights to load (overrides engine defaults) */
|
|
66
|
+
weights?: number[];
|
|
61
67
|
source?: FontSource;
|
|
62
68
|
org?: string;
|
|
63
69
|
};
|
|
@@ -118,11 +124,32 @@ interface ParsedColor {
|
|
|
118
124
|
format: ColorFormat;
|
|
119
125
|
original: string;
|
|
120
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
|
+
}
|
|
121
144
|
interface VisorThemeConfig {
|
|
122
145
|
name: string;
|
|
123
146
|
version: 1;
|
|
124
147
|
/** Theme group for the docs site theme switcher (e.g. 'Visor', 'Client', 'Low Orbit'). Used by `visor theme sync`. */
|
|
125
148
|
group?: string;
|
|
149
|
+
/** Optional display label override for the theme switcher (e.g. 'ENTR', 'SoleSpark'). Falls back to title-cased name. */
|
|
150
|
+
label?: string;
|
|
151
|
+
/** Default color mode to force when the theme is activated ('dark' or 'light'). If unset, user/system preference applies. */
|
|
152
|
+
"default-mode"?: "dark" | "light";
|
|
126
153
|
colors: {
|
|
127
154
|
primary: string;
|
|
128
155
|
accent?: string;
|
|
@@ -150,18 +177,21 @@ interface VisorThemeConfig {
|
|
|
150
177
|
heading?: {
|
|
151
178
|
family?: string;
|
|
152
179
|
weight?: number;
|
|
180
|
+
weights?: number[];
|
|
153
181
|
source?: FontSource;
|
|
154
182
|
org?: string;
|
|
155
183
|
};
|
|
156
184
|
display?: {
|
|
157
185
|
family?: string;
|
|
158
186
|
weight?: number;
|
|
187
|
+
weights?: number[];
|
|
159
188
|
source?: FontSource;
|
|
160
189
|
org?: string;
|
|
161
190
|
};
|
|
162
191
|
body?: {
|
|
163
192
|
family?: string;
|
|
164
193
|
weight?: number;
|
|
194
|
+
weights?: number[];
|
|
165
195
|
source?: FontSource;
|
|
166
196
|
org?: string;
|
|
167
197
|
};
|
|
@@ -176,6 +206,13 @@ interface VisorThemeConfig {
|
|
|
176
206
|
normal?: string;
|
|
177
207
|
wide?: string;
|
|
178
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>>;
|
|
179
216
|
};
|
|
180
217
|
spacing?: {
|
|
181
218
|
base?: number;
|
|
@@ -194,6 +231,12 @@ interface VisorThemeConfig {
|
|
|
194
231
|
lg?: string;
|
|
195
232
|
xl?: string;
|
|
196
233
|
};
|
|
234
|
+
strokeWidths?: {
|
|
235
|
+
thin?: number;
|
|
236
|
+
regular?: number;
|
|
237
|
+
medium?: number;
|
|
238
|
+
thick?: number;
|
|
239
|
+
};
|
|
197
240
|
motion?: {
|
|
198
241
|
"duration-fast"?: string;
|
|
199
242
|
"duration-normal"?: string;
|
|
@@ -226,24 +269,33 @@ interface ResolvedThemeConfig {
|
|
|
226
269
|
heading: {
|
|
227
270
|
family: string;
|
|
228
271
|
weight: number;
|
|
272
|
+
weights?: number[];
|
|
229
273
|
source?: FontSource;
|
|
230
274
|
org?: string;
|
|
231
275
|
};
|
|
232
276
|
display: {
|
|
233
277
|
family: string;
|
|
234
278
|
weight: number;
|
|
279
|
+
weights?: number[];
|
|
235
280
|
source?: FontSource;
|
|
236
281
|
org?: string;
|
|
237
282
|
};
|
|
238
283
|
body: {
|
|
239
284
|
family: string;
|
|
240
285
|
weight: number;
|
|
286
|
+
weights?: number[];
|
|
241
287
|
source?: FontSource;
|
|
242
288
|
org?: string;
|
|
243
289
|
};
|
|
244
290
|
mono: {
|
|
245
291
|
family: string;
|
|
246
292
|
};
|
|
293
|
+
/**
|
|
294
|
+
* Per-slot Material `TextTheme` overrides, passed through from the
|
|
295
|
+
* raw config. Empty object when none supplied. Flutter adapter
|
|
296
|
+
* consumes these; other adapters may ignore them.
|
|
297
|
+
*/
|
|
298
|
+
slots: Partial<Record<MaterialTextSlot, TextSlotOverride>>;
|
|
247
299
|
};
|
|
248
300
|
spacing: {
|
|
249
301
|
base: number;
|
|
@@ -262,6 +314,12 @@ interface ResolvedThemeConfig {
|
|
|
262
314
|
lg: string;
|
|
263
315
|
xl: string;
|
|
264
316
|
};
|
|
317
|
+
strokeWidths: {
|
|
318
|
+
thin: number;
|
|
319
|
+
regular: number;
|
|
320
|
+
medium: number;
|
|
321
|
+
thick: number;
|
|
322
|
+
};
|
|
265
323
|
motion: {
|
|
266
324
|
"duration-fast": string;
|
|
267
325
|
"duration-normal": string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loworbitstudio/visor-theme-engine",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -35,7 +35,12 @@
|
|
|
35
35
|
"design-system",
|
|
36
36
|
"theme",
|
|
37
37
|
"oklch",
|
|
38
|
-
"tokens"
|
|
38
|
+
"tokens",
|
|
39
|
+
"react",
|
|
40
|
+
"css-variables",
|
|
41
|
+
"theming",
|
|
42
|
+
"color-system",
|
|
43
|
+
"wcag"
|
|
39
44
|
],
|
|
40
45
|
"license": "MIT",
|
|
41
46
|
"repository": {
|
|
@@ -184,6 +184,29 @@
|
|
|
184
184
|
"normal": { "type": "string" },
|
|
185
185
|
"wide": { "type": "string" }
|
|
186
186
|
}
|
|
187
|
+
},
|
|
188
|
+
"slots": {
|
|
189
|
+
"type": "object",
|
|
190
|
+
"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.",
|
|
191
|
+
"additionalProperties": false,
|
|
192
|
+
"properties": {
|
|
193
|
+
"displayLarge": { "$ref": "#/$defs/textSlotOverride" },
|
|
194
|
+
"displayMedium": { "$ref": "#/$defs/textSlotOverride" },
|
|
195
|
+
"displaySmall": { "$ref": "#/$defs/textSlotOverride" },
|
|
196
|
+
"headlineLarge": { "$ref": "#/$defs/textSlotOverride" },
|
|
197
|
+
"headlineMedium": { "$ref": "#/$defs/textSlotOverride" },
|
|
198
|
+
"headlineSmall": { "$ref": "#/$defs/textSlotOverride" },
|
|
199
|
+
"titleLarge": { "$ref": "#/$defs/textSlotOverride" },
|
|
200
|
+
"titleMedium": { "$ref": "#/$defs/textSlotOverride" },
|
|
201
|
+
"titleSmall": { "$ref": "#/$defs/textSlotOverride" },
|
|
202
|
+
"bodyLarge": { "$ref": "#/$defs/textSlotOverride" },
|
|
203
|
+
"bodyMedium": { "$ref": "#/$defs/textSlotOverride" },
|
|
204
|
+
"bodySmall": { "$ref": "#/$defs/textSlotOverride" },
|
|
205
|
+
"labelLarge": { "$ref": "#/$defs/textSlotOverride" },
|
|
206
|
+
"labelMedium": { "$ref": "#/$defs/textSlotOverride" },
|
|
207
|
+
"labelSmall": { "$ref": "#/$defs/textSlotOverride" },
|
|
208
|
+
"labelXSmall": { "$ref": "#/$defs/textSlotOverride" }
|
|
209
|
+
}
|
|
187
210
|
}
|
|
188
211
|
}
|
|
189
212
|
},
|
|
@@ -223,6 +246,17 @@
|
|
|
223
246
|
"xl": { "type": "string" }
|
|
224
247
|
}
|
|
225
248
|
},
|
|
249
|
+
"strokeWidths": {
|
|
250
|
+
"type": "object",
|
|
251
|
+
"description": "Stroke-width scale in pixels — used for borders, outlines, dividers, progress-indicator strokes. Defaults: thin=1, regular=1.5, medium=2, thick=2.5.",
|
|
252
|
+
"additionalProperties": false,
|
|
253
|
+
"properties": {
|
|
254
|
+
"thin": { "type": "number", "minimum": 0 },
|
|
255
|
+
"regular": { "type": "number", "minimum": 0 },
|
|
256
|
+
"medium": { "type": "number", "minimum": 0 },
|
|
257
|
+
"thick": { "type": "number", "minimum": 0 }
|
|
258
|
+
}
|
|
259
|
+
},
|
|
226
260
|
"motion": {
|
|
227
261
|
"type": "object",
|
|
228
262
|
"description": "Motion/animation configuration.",
|
|
@@ -277,6 +311,28 @@
|
|
|
277
311
|
{ "pattern": "^hsla?\\(" },
|
|
278
312
|
{ "pattern": "^oklch\\(" }
|
|
279
313
|
]
|
|
314
|
+
},
|
|
315
|
+
"textSlotOverride": {
|
|
316
|
+
"type": "object",
|
|
317
|
+
"description": "Per-slot override for one Material TextTheme entry.",
|
|
318
|
+
"additionalProperties": false,
|
|
319
|
+
"properties": {
|
|
320
|
+
"size": {
|
|
321
|
+
"type": "number",
|
|
322
|
+
"exclusiveMinimum": 0,
|
|
323
|
+
"description": "Font size in logical pixels (Flutter TextStyle.fontSize)."
|
|
324
|
+
},
|
|
325
|
+
"weight": {
|
|
326
|
+
"type": "integer",
|
|
327
|
+
"minimum": 100,
|
|
328
|
+
"maximum": 900,
|
|
329
|
+
"description": "Font weight."
|
|
330
|
+
},
|
|
331
|
+
"letter-spacing": {
|
|
332
|
+
"type": "number",
|
|
333
|
+
"description": "Letter spacing in logical pixels (Flutter TextStyle.letterSpacing). Material defaults include negative values, e.g. -0.25 for displayLarge."
|
|
334
|
+
}
|
|
335
|
+
}
|
|
280
336
|
}
|
|
281
337
|
}
|
|
282
338
|
}
|