@loworbitstudio/visor-theme-engine 0.4.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.
@@ -1366,6 +1366,26 @@ function generateFullBundleCss(primitives, tokens, config) {
1366
1366
  return lines.join("\n");
1367
1367
  }
1368
1368
 
1369
+ // src/types.ts
1370
+ var MATERIAL_TEXT_SLOTS = [
1371
+ "displayLarge",
1372
+ "displayMedium",
1373
+ "displaySmall",
1374
+ "headlineLarge",
1375
+ "headlineMedium",
1376
+ "headlineSmall",
1377
+ "titleLarge",
1378
+ "titleMedium",
1379
+ "titleSmall",
1380
+ "bodyLarge",
1381
+ "bodyMedium",
1382
+ "bodySmall",
1383
+ "labelLarge",
1384
+ "labelMedium",
1385
+ "labelSmall",
1386
+ "labelXSmall"
1387
+ ];
1388
+
1369
1389
  export {
1370
1390
  googleFontsCatalog,
1371
1391
  lookupGoogleFont,
@@ -1392,6 +1412,7 @@ export {
1392
1412
  isValidColor,
1393
1413
  compositeOverBackground,
1394
1414
  serializeColor,
1415
+ MATERIAL_TEXT_SLOTS,
1395
1416
  FULL_SHADE_STEPS,
1396
1417
  SELECTIVE_SHADE_STEPS,
1397
1418
  TAILWIND_GRAY,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FontResolveOptions, a as FontResolution, V as VisorTypography, b as FontDisplayStrategy, T as ThemeFontResult, G as GoogleFontEntry, R as ResolvedThemeConfig, c as GeneratedPrimitives, d as ThemeOutput, e as ThemeData, f as VisorThemeConfig, g as FullShadeScale, C as ColorRole, S as SelectiveShadeScale, h as RGB, P as ParsedColor, O as OKLCH, i as SemanticTokens, j as ShadeStep } from './types-DgAumoCX.js';
2
- export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-DgAumoCX.js';
1
+ import { F as FontResolveOptions, a as FontResolution, V as VisorTypography, b as FontDisplayStrategy, T as ThemeFontResult, G as GoogleFontEntry, R as ResolvedThemeConfig, c as GeneratedPrimitives, d as ThemeOutput, e as ThemeData, f as VisorThemeConfig, g as FullShadeScale, C as ColorRole, S as SelectiveShadeScale, h as RGB, P as ParsedColor, O as OKLCH, i as SemanticTokens, j as ShadeStep } from './types-gAlkt__C.js';
2
+ export { k as ColorFormat, l as FontSource, m as RGBA, n as SemanticTokenValue } from './types-gAlkt__C.js';
3
3
 
4
4
  /**
5
5
  * Font resolver — maps font family names to loadable font resources.
@@ -97,7 +97,7 @@ declare function lookupGoogleFont(family: string): GoogleFontEntry | undefined;
97
97
  */
98
98
 
99
99
  /**
100
- * Generate all shade scales from a resolved config.
100
+ * Generate all shade scales from a resolved config (light-mode colors).
101
101
  * If neutral is null, uses Tailwind Gray verbatim.
102
102
  */
103
103
  declare function generatePrimitives(config: ResolvedThemeConfig): GeneratedPrimitives;
@@ -367,6 +367,61 @@ var properties = {
367
367
  type: "string"
368
368
  }
369
369
  }
370
+ },
371
+ slots: {
372
+ type: "object",
373
+ 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.",
374
+ additionalProperties: false,
375
+ properties: {
376
+ displayLarge: {
377
+ $ref: "#/$defs/textSlotOverride"
378
+ },
379
+ displayMedium: {
380
+ $ref: "#/$defs/textSlotOverride"
381
+ },
382
+ displaySmall: {
383
+ $ref: "#/$defs/textSlotOverride"
384
+ },
385
+ headlineLarge: {
386
+ $ref: "#/$defs/textSlotOverride"
387
+ },
388
+ headlineMedium: {
389
+ $ref: "#/$defs/textSlotOverride"
390
+ },
391
+ headlineSmall: {
392
+ $ref: "#/$defs/textSlotOverride"
393
+ },
394
+ titleLarge: {
395
+ $ref: "#/$defs/textSlotOverride"
396
+ },
397
+ titleMedium: {
398
+ $ref: "#/$defs/textSlotOverride"
399
+ },
400
+ titleSmall: {
401
+ $ref: "#/$defs/textSlotOverride"
402
+ },
403
+ bodyLarge: {
404
+ $ref: "#/$defs/textSlotOverride"
405
+ },
406
+ bodyMedium: {
407
+ $ref: "#/$defs/textSlotOverride"
408
+ },
409
+ bodySmall: {
410
+ $ref: "#/$defs/textSlotOverride"
411
+ },
412
+ labelLarge: {
413
+ $ref: "#/$defs/textSlotOverride"
414
+ },
415
+ labelMedium: {
416
+ $ref: "#/$defs/textSlotOverride"
417
+ },
418
+ labelSmall: {
419
+ $ref: "#/$defs/textSlotOverride"
420
+ },
421
+ labelXSmall: {
422
+ $ref: "#/$defs/textSlotOverride"
423
+ }
424
+ }
370
425
  }
371
426
  }
372
427
  },
@@ -432,6 +487,29 @@ var properties = {
432
487
  }
433
488
  }
434
489
  },
490
+ strokeWidths: {
491
+ type: "object",
492
+ 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.",
493
+ additionalProperties: false,
494
+ properties: {
495
+ thin: {
496
+ type: "number",
497
+ minimum: 0
498
+ },
499
+ regular: {
500
+ type: "number",
501
+ minimum: 0
502
+ },
503
+ medium: {
504
+ type: "number",
505
+ minimum: 0
506
+ },
507
+ thick: {
508
+ type: "number",
509
+ minimum: 0
510
+ }
511
+ }
512
+ },
435
513
  motion: {
436
514
  type: "object",
437
515
  description: "Motion/animation configuration.",
@@ -498,6 +576,28 @@ var $defs = {
498
576
  pattern: "^oklch\\("
499
577
  }
500
578
  ]
579
+ },
580
+ textSlotOverride: {
581
+ type: "object",
582
+ description: "Per-slot override for one Material TextTheme entry.",
583
+ additionalProperties: false,
584
+ properties: {
585
+ size: {
586
+ type: "number",
587
+ exclusiveMinimum: 0,
588
+ description: "Font size in logical pixels (Flutter TextStyle.fontSize)."
589
+ },
590
+ weight: {
591
+ type: "integer",
592
+ minimum: 100,
593
+ maximum: 900,
594
+ description: "Font weight."
595
+ },
596
+ "letter-spacing": {
597
+ type: "number",
598
+ description: "Letter spacing in logical pixels (Flutter TextStyle.letterSpacing). Material defaults include negative values, e.g. -0.25 for displayLarge."
599
+ }
600
+ }
501
601
  }
502
602
  };
503
603
  var visorTheme_schema = {
@@ -658,10 +758,12 @@ declare function resolveConfig(config: VisorThemeConfig): ResolvedThemeConfig;
658
758
  */
659
759
 
660
760
  /**
661
- * Assign semantic tokens from generated shade scales and resolved config.
662
- * Returns concrete hex values for all ~55 tokens in both light and dark modes.
761
+ * Assign semantic tokens from mode-specific shade scales and resolved config.
762
+ * lightPrimitives drives light-mode token values; darkPrimitives drives dark-mode
763
+ * values — allowing themes with colors-dark overrides to produce correct dark
764
+ * semantic tokens (e.g. surface-accent-default uses the dark brand color).
663
765
  */
664
- declare function assignSemanticTokens(primitives: GeneratedPrimitives, config: ResolvedThemeConfig): SemanticTokens;
766
+ declare function assignSemanticTokens(lightPrimitives: GeneratedPrimitives, darkPrimitives: GeneratedPrimitives, config: ResolvedThemeConfig): SemanticTokens;
665
767
 
666
768
  /**
667
769
  * Override Application (Stage 4)
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-NZF2MS4L.js";
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
  };
@@ -333,6 +390,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
333
390
  "spacing",
334
391
  "radius",
335
392
  "shadows",
393
+ "strokeWidths",
336
394
  "motion",
337
395
  "overrides"
338
396
  ]);
@@ -353,14 +411,18 @@ var KNOWN_TYPOGRAPHY_KEYS = /* @__PURE__ */ new Set([
353
411
  "body",
354
412
  "mono",
355
413
  "letter-spacing",
356
- "scale"
414
+ "scale",
415
+ "slots"
357
416
  ]);
358
417
  var KNOWN_TYPOGRAPHY_FONT_KEYS = /* @__PURE__ */ new Set(["family", "weight", "weights", "source", "org"]);
359
418
  var KNOWN_TYPOGRAPHY_MONO_KEYS = /* @__PURE__ */ new Set(["family"]);
360
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"]);
361
422
  var KNOWN_SPACING_KEYS = /* @__PURE__ */ new Set(["base"]);
362
423
  var KNOWN_RADIUS_KEYS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "pill"]);
363
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"]);
364
426
  var KNOWN_MOTION_KEYS = /* @__PURE__ */ new Set(["duration-fast", "duration-normal", "duration-slow", "easing"]);
365
427
  var KNOWN_OVERRIDES_KEYS = /* @__PURE__ */ new Set(["light", "dark"]);
366
428
  function checkUnknownKeys(obj, errors) {
@@ -425,6 +487,31 @@ function checkUnknownKeys(obj, errors) {
425
487
  }
426
488
  }
427
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
+ }
428
515
  }
429
516
  if (typeof obj.spacing === "object" && obj.spacing !== null) {
430
517
  for (const key of Object.keys(obj.spacing)) {
@@ -447,6 +534,13 @@ function checkUnknownKeys(obj, errors) {
447
534
  }
448
535
  }
449
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
+ }
450
544
  if (typeof obj.motion === "object" && obj.motion !== null) {
451
545
  for (const key of Object.keys(obj.motion)) {
452
546
  if (!KNOWN_MOTION_KEYS.has(key)) {
@@ -547,6 +641,23 @@ function validateConfig(config) {
547
641
  }
548
642
  }
549
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
+ }
660
+ }
550
661
  }
551
662
  if (obj.overrides !== void 0) {
552
663
  if (typeof obj.overrides !== "object" || obj.overrides === null) {
@@ -595,6 +706,12 @@ var DEFAULTS = {
595
706
  lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
596
707
  xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)"
597
708
  },
709
+ strokeWidths: {
710
+ thin: 1,
711
+ regular: 1.5,
712
+ medium: 2,
713
+ thick: 2.5
714
+ },
598
715
  motion: {
599
716
  "duration-fast": "100ms",
600
717
  "duration-normal": "200ms",
@@ -671,7 +788,8 @@ function resolveConfig(config) {
671
788
  },
672
789
  mono: {
673
790
  family: config.typography?.mono?.family ?? DEFAULTS.typography.mono.family
674
- }
791
+ },
792
+ slots: config.typography?.slots ?? {}
675
793
  },
676
794
  spacing: {
677
795
  base: config.spacing?.base ?? DEFAULTS.spacing.base
@@ -690,6 +808,12 @@ function resolveConfig(config) {
690
808
  lg: config.shadows?.lg ?? DEFAULTS.shadows.lg,
691
809
  xl: config.shadows?.xl ?? DEFAULTS.shadows.xl
692
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
+ },
693
817
  motion: {
694
818
  "duration-fast": config.motion?.["duration-fast"] ?? DEFAULTS.motion["duration-fast"],
695
819
  "duration-normal": config.motion?.["duration-normal"] ?? DEFAULTS.motion["duration-normal"],
@@ -769,6 +893,12 @@ var SEMANTIC_SURFACE_MAP = {
769
893
  light: { constant: CONFIG_SURFACE },
770
894
  dark: { constant: CONFIG_DARK_SURFACE }
771
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
+ },
772
902
  subtle: {
773
903
  light: { role: "neutral", shade: 50 },
774
904
  dark: { role: "neutral", shade: 800 }
@@ -797,6 +927,12 @@ var SEMANTIC_SURFACE_MAP = {
797
927
  light: { role: "neutral", shade: 50 },
798
928
  dark: { role: "neutral", shade: 800 }
799
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
+ },
800
936
  "accent-subtle": {
801
937
  light: { role: "primary", shade: 50 },
802
938
  dark: { role: "primary", shade: 900 }
@@ -977,28 +1113,28 @@ function resolveRef(ref, primitives, config) {
977
1113
  return ref.constant;
978
1114
  }
979
1115
  }
980
- function resolveMapping(mapping, primitives, config) {
1116
+ function resolveMapping(mapping, lightPrimitives, darkPrimitives, config) {
981
1117
  return {
982
- light: resolveRef(mapping.light, primitives, config),
983
- dark: resolveRef(mapping.dark, primitives, config)
1118
+ light: resolveRef(mapping.light, lightPrimitives, config),
1119
+ dark: resolveRef(mapping.dark, darkPrimitives, config)
984
1120
  };
985
1121
  }
986
- function assignSemanticTokens(primitives, config) {
1122
+ function assignSemanticTokens(lightPrimitives, darkPrimitives, config) {
987
1123
  const text = {};
988
1124
  const surface = {};
989
1125
  const border = {};
990
1126
  const interactive = {};
991
1127
  for (const [name, mapping] of Object.entries(SEMANTIC_MAP.text)) {
992
- text[name] = resolveMapping(mapping, primitives, config);
1128
+ text[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
993
1129
  }
994
1130
  for (const [name, mapping] of Object.entries(SEMANTIC_MAP.surface)) {
995
- surface[name] = resolveMapping(mapping, primitives, config);
1131
+ surface[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
996
1132
  }
997
1133
  for (const [name, mapping] of Object.entries(SEMANTIC_MAP.border)) {
998
- border[name] = resolveMapping(mapping, primitives, config);
1134
+ border[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
999
1135
  }
1000
1136
  for (const [name, mapping] of Object.entries(SEMANTIC_MAP.interactive)) {
1001
- interactive[name] = resolveMapping(mapping, primitives, config);
1137
+ interactive[name] = resolveMapping(mapping, lightPrimitives, darkPrimitives, config);
1002
1138
  }
1003
1139
  return { text, surface, border, interactive };
1004
1140
  }
@@ -1071,6 +1207,18 @@ function generatePrimitives(config) {
1071
1207
  info: generateShadeScale(config.colors.info, "info")
1072
1208
  };
1073
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
+ }
1074
1222
  function parseConfig(yamlString) {
1075
1223
  const parsed = parseYaml(yamlString);
1076
1224
  const result = validateConfig(parsed);
@@ -1103,7 +1251,8 @@ ${validation.errors.map((e) => ` - ${e}`).join("\n")}`
1103
1251
  }
1104
1252
  const resolved = resolveConfig(config);
1105
1253
  const primitives = generatePrimitives(resolved);
1106
- let tokens = assignSemanticTokens(primitives, resolved);
1254
+ const darkPrimitives = generateDarkPrimitives(resolved, primitives);
1255
+ let tokens = assignSemanticTokens(primitives, darkPrimitives, resolved);
1107
1256
  tokens = applyOverrides(tokens, resolved.overrides);
1108
1257
  const output = {
1109
1258
  primitivesCss: generatePrimitivesCss(primitives, resolved),
@@ -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;
@@ -260,6 +290,12 @@ interface ResolvedThemeConfig {
260
290
  mono: {
261
291
  family: string;
262
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>>;
263
299
  };
264
300
  spacing: {
265
301
  base: number;
@@ -278,6 +314,12 @@ interface ResolvedThemeConfig {
278
314
  lg: string;
279
315
  xl: string;
280
316
  };
317
+ strokeWidths: {
318
+ thin: number;
319
+ regular: number;
320
+ medium: number;
321
+ thick: number;
322
+ };
281
323
  motion: {
282
324
  "duration-fast": string;
283
325
  "duration-normal": string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor-theme-engine",
3
- "version": "0.4.0",
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",
@@ -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
  }