@loworbitstudio/visor-theme-engine 0.22.0 → 0.24.0

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.
@@ -1,4 +1,4 @@
1
- import { k as GeneratedPrimitives, s as SemanticTokens, R as ResolvedThemeConfig } from '../types-Wy5n0W9F.js';
1
+ import { k as GeneratedPrimitives, s as SemanticTokens, R as ResolvedThemeConfig } from '../types-ZPTjTL_-.js';
2
2
 
3
3
  /**
4
4
  * Adapter types for the Visor theme engine.
@@ -25,7 +25,7 @@ import {
25
25
  resolveThemeBrand,
26
26
  resolveThemeFonts,
27
27
  sectionComment
28
- } from "../chunk-L5DQDEZV.js";
28
+ } from "../chunk-D5XSLWXP.js";
29
29
 
30
30
  // src/adapters/brand-passthrough.ts
31
31
  var SENTINEL_COLOR = "#ff00ff";
@@ -1271,6 +1271,7 @@ var FIELD = "components/ui/field/field.module.css";
1271
1271
  var TEXT = "components/ui/text/text.module.css";
1272
1272
  var INLINE_EDIT = "components/ui/inline-edit/inline-edit.module.css";
1273
1273
  var SAVE_STATUS = "components/ui/save-status/save-status.module.css";
1274
+ var TOOLTIP = "components/ui/tooltip/tooltip.module.css";
1274
1275
  function at(file, fallback) {
1275
1276
  return [{ file, fallback }];
1276
1277
  }
@@ -1300,7 +1301,7 @@ var tableFamily = {
1300
1301
  { key: "cell-font-size", property: "font-size", description: "Body cell type size.", consumers: at(TABLE, "inherit") },
1301
1302
  { key: "cell-color", property: "color", description: "Body cell text colour.", consumers: at(TABLE, "var(--text-primary, #111827)") },
1302
1303
  { key: "cell-bg", property: "background-color", description: "Body cell fill. Sits above the shared `--dt-row-bg` surface role.", consumers: at(TABLE, "var(--dt-row-bg, transparent)") },
1303
- { key: "cell-border-top", property: "border-top", description: "Cell hairline.", consumers: at(TABLE, "1px solid var(--hairline, transparent)") },
1304
+ { key: "cell-border-top", property: "border-top", description: "Cell hairline.", consumers: at(TABLE, "var(--hairline-width, 1px) solid var(--hairline, transparent)") },
1304
1305
  { key: "footer-bg", property: "background-color", description: "Footer row fill.", consumers: at(TABLE, "var(--surface-muted, #f3f4f6)") },
1305
1306
  { key: "caption-color", property: "color", description: "Caption text colour.", consumers: at(TABLE, "var(--text-secondary, #6b7280)") }
1306
1307
  ]
@@ -1404,7 +1405,49 @@ var badgeFamily = {
1404
1405
  ]
1405
1406
  },
1406
1407
  { key: "md-padding", property: "padding", description: "Default badge padding.", consumers: at(BADGE, "calc(var(--spacing-1, 0.25rem) / 2) var(--spacing-2, 0.5rem)") },
1407
- { key: "md-gap", property: "gap", description: "Default badge icon gap.", consumers: at(BADGE, "var(--spacing-1, 0.25rem)") }
1408
+ { key: "md-gap", property: "gap", description: "Default badge icon gap.", consumers: at(BADGE, "var(--spacing-1, 0.25rem)") },
1409
+ {
1410
+ key: "bg",
1411
+ property: "background-color",
1412
+ description: "Badge ground in every variant (an accent tint, an on-image scrim). Unset, each variant keeps its own surface.",
1413
+ consumers: [
1414
+ "var(--interactive-primary-bg, currentColor)",
1415
+ "var(--interactive-secondary-bg, var(--surface-subtle, transparent))",
1416
+ "var(--surface-card, #ffffff)",
1417
+ "transparent",
1418
+ "var(--surface-error-subtle, transparent)",
1419
+ "color-mix(in srgb, var(--destructive) 20%, transparent)",
1420
+ "var(--surface-success-subtle, transparent)",
1421
+ "color-mix(in srgb, var(--success) 18%, transparent)",
1422
+ "var(--surface-warning-subtle, transparent)",
1423
+ "color-mix(in srgb, var(--warning) 20%, transparent)",
1424
+ "var(--surface-info-subtle, transparent)",
1425
+ "color-mix(in srgb, var(--info) 18%, transparent)",
1426
+ "var(--surface-muted, #f3f4f6)",
1427
+ "var(--surface-error-default, transparent)",
1428
+ "var(--surface-success-default, transparent)",
1429
+ "var(--surface-warning-default, transparent)",
1430
+ "var(--surface-info-default, transparent)",
1431
+ "var(--color-neutral-600, #4b5563)"
1432
+ ].map((fallback) => ({ file: BADGE, fallback }))
1433
+ },
1434
+ {
1435
+ key: "ink",
1436
+ property: "color",
1437
+ description: "Badge label colour in every variant. Unset, each variant keeps its own text colour.",
1438
+ consumers: [
1439
+ "var(--interactive-primary-text, var(--text-inverse, #f9fafb))",
1440
+ "var(--interactive-secondary-text, var(--text-primary, currentColor))",
1441
+ "var(--text-primary, #111827)",
1442
+ "var(--text-error, currentColor)",
1443
+ "var(--text-success, currentColor)",
1444
+ "var(--text-warning, currentColor)",
1445
+ "var(--text-info, currentColor)",
1446
+ "var(--text-secondary, var(--text-primary, #374151))",
1447
+ "var(--text-inverse, currentColor)",
1448
+ "var(--color-white, #ffffff)"
1449
+ ].map((fallback) => ({ file: BADGE, fallback }))
1450
+ }
1408
1451
  ]
1409
1452
  };
1410
1453
  var statusBadgeFamily = {
@@ -1564,7 +1607,7 @@ var tabsFamily = {
1564
1607
  { key: "list-padding", property: "padding", description: "Tab-rail inset.", consumers: at(TABS, "calc(var(--spacing-1, 0.25rem) * 0.75)") },
1565
1608
  { key: "list-color", property: "color", description: "Tab-rail base text colour.", consumers: at(TABS, "var(--text-secondary, #6b7280)") },
1566
1609
  { key: "list-bg", property: "background-color", description: "Segmented-variant rail fill.", consumers: at(TABS, "var(--surface-muted, #f3f4f6)") },
1567
- { key: "line-border-bottom", property: "border-bottom", description: "Underlined-variant rail rule.", consumers: at(TABS, "1px solid var(--hairline, var(--border-default, #e5e7eb))") },
1610
+ { key: "line-border-bottom", property: "border-bottom", description: "Underlined-variant rail rule (width, style and colour). Unbound, the rail rule is painted at `--hairline-width`; binding this draws your border instead.", consumers: at(TABS, "1px solid transparent") },
1568
1611
  { key: "trigger-radius", property: "border-radius", description: "Trigger corner rounding.", consumers: at(TABS, "var(--radius-md, 0.375rem)") },
1569
1612
  { key: "trigger-padding", property: "padding", description: "Trigger inset.", consumers: at(TABS, "var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem)") },
1570
1613
  { key: "trigger-font-size", property: "font-size", description: "Trigger type size.", consumers: at(TABS, "var(--font-size-sm, 0.875rem)") },
@@ -1864,6 +1907,53 @@ var saveStatusFamily = {
1864
1907
  { key: "letter-spacing", property: "letter-spacing", description: "Readout tracking.", consumers: at(SAVE_STATUS, "revert-layer") }
1865
1908
  ]
1866
1909
  };
1910
+ var tooltipFamily = {
1911
+ family: "tooltip",
1912
+ prefix: "tooltip",
1913
+ description: "Tooltip ground and ink, so a dark theme can keep the label legible without retuning the shared inverse pair other components read.",
1914
+ tokens: [
1915
+ { key: "bg", property: "background-color", description: "Tooltip ground.", consumers: at(TOOLTIP, "var(--surface-overlay, #111827)") },
1916
+ { key: "text", property: "color", description: "Tooltip label colour.", consumers: at(TOOLTIP, "var(--text-inverse, #ffffff)") }
1917
+ ]
1918
+ };
1919
+ var HAIRLINE_PX = [
1920
+ "components/ui/bulk-action-bar/bulk-action-bar.module.css",
1921
+ "components/ui/dialog/dialog.module.css",
1922
+ "components/ui/dropdown-menu/dropdown-menu.module.css",
1923
+ "components/ui/matrix-table/matrix-table.module.css",
1924
+ TABLE,
1925
+ TABS
1926
+ ];
1927
+ var HAIRLINE_THIN = [
1928
+ "blocks/dialog-form/dialog-form.module.css",
1929
+ "components/ui/coherence-check/coherence-check.module.css",
1930
+ "components/ui/doc-frame/doc-frame.module.css",
1931
+ "components/ui/doc-nav/doc-nav.module.css",
1932
+ "components/ui/editable-block/editable-block.module.css",
1933
+ "components/ui/fidelity-mirror/fidelity-mirror.module.css",
1934
+ "components/ui/infographic-bar/infographic-bar.module.css",
1935
+ "components/ui/popover/popover.module.css",
1936
+ "components/ui/section-nav/section-nav.module.css",
1937
+ "components/ui/specimen-card/specimen-card.module.css",
1938
+ "components/ui/structured-prompt/structured-prompt.module.css",
1939
+ "components/visual/browser-frame/browser-frame.module.css"
1940
+ ];
1941
+ var hairlineFamily = {
1942
+ family: "hairline",
1943
+ prefix: "hairline",
1944
+ description: "The one switch for hairlines (VI-680). Every hairline rule, divider and ring reads its weight from `--hairline-width`, so `width: 0` removes them all at once. Independent of the `control` edge switch.",
1945
+ tokens: [
1946
+ {
1947
+ key: "width",
1948
+ property: "border-width / box-shadow spread / height",
1949
+ description: "THE switch. Hairline weight for every `--hairline` / `--hairline-strong` rule; `0` turns every hairline off. Focus and invalid edges are untouched.",
1950
+ consumers: [
1951
+ ...each(HAIRLINE_PX, "1px"),
1952
+ ...each(HAIRLINE_THIN, "var(--stroke-width-thin, 1px)")
1953
+ ]
1954
+ }
1955
+ ]
1956
+ };
1867
1957
  var adminUiFamily = {
1868
1958
  family: "admin-ui",
1869
1959
  prefix: "admin-ui",
@@ -1893,11 +1983,13 @@ var COMPONENT_TOKEN_FAMILIES = [
1893
1983
  spinnerFamily,
1894
1984
  checkboxFamily,
1895
1985
  controlFamily,
1986
+ hairlineFamily,
1896
1987
  fieldFamily,
1897
1988
  buttonFamily,
1898
1989
  textFamily,
1899
1990
  switchFamily,
1900
1991
  saveStatusFamily,
1992
+ tooltipFamily,
1901
1993
  adminUiFamily
1902
1994
  ];
1903
1995
  var COMPONENT_TOKEN_FAMILY_BY_NAME = new Map(
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, c as VisorBrand, B as BrandSlot, d as BrandSource, e as BrandResolution, C as ColorScheme, f as ThemeBrandResult, g as BrandStrategy, h as BrandStrategyContext, i as BrandStrategyIssue, j as BrandStrategyValidationResult, S as SerializedBrandStrategy, R as ResolvedThemeConfig, k as GeneratedPrimitives, l as ThemeOutput, m as ThemeData, n as VisorThemeConfig, o as FullShadeScale, p as ColorRole, q as SelectiveShadeScale, r as RGB, P as ParsedColor, O as OKLCH, s as SemanticTokens, t as ShadeStep } from './types-Wy5n0W9F.js';
2
- export { u as BRAND_VARIANTS, v as BRAND_VISIBILITIES, w as BrandAccessibility, x as BrandArchetype, y as BrandBoilerplate, z as BrandColorPairing, A as BrandColorUsage, D as BrandContrastTarget, E as BrandGoverns, H as BrandLexiconEntry, I as BrandMessaging, J as BrandPersonalityTrait, K as BrandPillar, L as BrandPositioning, M as BrandStrategyIssueSeverity, N as BrandToneEntry, Q as BrandVariant, U as BrandVisibility, W as BrandVoice, X as BrandVoiceTrait, Y as COMPONENT_TOKEN_FAMILIES, Z as COMPONENT_TOKEN_FAMILY_BY_NAME, _ as ColorFormat, $ as ComponentTokenBinding, a0 as ComponentTokenBindings, a1 as ComponentTokenConsumer, a2 as ComponentTokenFamily, a3 as ComponentTokenSpec, a4 as DEFAULT_BRAND_STRATEGY_SURFACES, a5 as DEFAULT_BRAND_STRATEGY_TONE_STATES, a6 as FontSource, a7 as GOVERNS_WILDCARD, a8 as RGBA, a9 as ResolvedComponentTokens, aa as SemanticTokenValue, ab as allComponentTokenNames, ac as componentTokenName, ad as hasComponentBindings, ae as resolveComponentBindings, af as validateComponentBindings } from './types-Wy5n0W9F.js';
1
+ import { F as FontResolveOptions, a as FontResolution, V as VisorTypography, b as FontDisplayStrategy, T as ThemeFontResult, G as GoogleFontEntry, c as VisorBrand, B as BrandSlot, d as BrandSource, e as BrandResolution, C as ColorScheme, f as ThemeBrandResult, g as BrandStrategy, h as BrandStrategyContext, i as BrandStrategyIssue, j as BrandStrategyValidationResult, S as SerializedBrandStrategy, R as ResolvedThemeConfig, k as GeneratedPrimitives, l as ThemeOutput, m as ThemeData, n as VisorThemeConfig, o as FullShadeScale, p as ColorRole, q as SelectiveShadeScale, r as RGB, P as ParsedColor, O as OKLCH, s as SemanticTokens, t as ShadeStep } from './types-ZPTjTL_-.js';
2
+ export { u as BRAND_VARIANTS, v as BRAND_VISIBILITIES, w as BrandAccessibility, x as BrandArchetype, y as BrandBoilerplate, z as BrandColorPairing, A as BrandColorUsage, D as BrandContrastTarget, E as BrandGoverns, H as BrandLexiconEntry, I as BrandMessaging, J as BrandPersonalityTrait, K as BrandPillar, L as BrandPositioning, M as BrandStrategyIssueSeverity, N as BrandToneEntry, Q as BrandVariant, U as BrandVisibility, W as BrandVoice, X as BrandVoiceTrait, Y as COMPONENT_TOKEN_FAMILIES, Z as COMPONENT_TOKEN_FAMILY_BY_NAME, _ as ColorFormat, $ as ComponentTokenBinding, a0 as ComponentTokenBindings, a1 as ComponentTokenConsumer, a2 as ComponentTokenFamily, a3 as ComponentTokenSpec, a4 as DEFAULT_BRAND_STRATEGY_SURFACES, a5 as DEFAULT_BRAND_STRATEGY_TONE_STATES, a6 as FontSource, a7 as GOVERNS_WILDCARD, a8 as RGBA, a9 as ResolvedComponentTokens, aa as SemanticTokenValue, ab as allComponentTokenNames, ac as componentTokenName, ad as hasComponentBindings, ae as resolveComponentBindings, af as validateComponentBindings } from './types-ZPTjTL_-.js';
3
3
 
4
4
  /**
5
5
  * Font resolver — maps font family names to loadable font resources.
@@ -408,6 +408,14 @@ var properties = {
408
408
  ],
409
409
  description: "The one switch for form-control edges (VI-655). 'off' removes the resting edge of every form control, dashed drop-target and empty-state edges included, with no layout shift; focus and invalid stay visible. Shorthand for components.control.edge-width: \"0\" (an explicit binding there wins). Defaults to 'on'. Optional."
410
410
  },
411
+ hairlines: {
412
+ type: "string",
413
+ "enum": [
414
+ "on",
415
+ "off"
416
+ ],
417
+ description: "The one switch for hairlines (VI-680). 'off' sets --hairline-width to 0, so every hairline rule, divider and ring disappears. Shorthand for components.hairline.width: \"0\" (an explicit binding there wins). Independent of 'edges'. Defaults to 'on'. Optional."
418
+ },
411
419
  colors: {
412
420
  type: "object",
413
421
  description: "Color definitions for light mode. Only primary is required — all others have sensible defaults.",
@@ -1061,7 +1069,7 @@ var properties = {
1061
1069
  },
1062
1070
  badge: {
1063
1071
  type: "object",
1064
- description: "Badge treatment — the D3 Tier-2 `--badge-text-transform` / `-letter-spacing` / `-font-size` / `-font-weight` set, promoted from the admin-ui audit to a bindable contract. Keys: radius, border, font-family, font-weight, text-transform, letter-spacing, font-size, md-padding, md-gap.",
1072
+ description: "Badge treatment — the D3 Tier-2 `--badge-text-transform` / `-letter-spacing` / `-font-size` / `-font-weight` set, promoted from the admin-ui audit to a bindable contract. Keys: radius, border, font-family, font-weight, text-transform, letter-spacing, font-size, md-padding, md-gap, bg, ink.",
1065
1073
  additionalProperties: {
1066
1074
  $ref: "#/$defs/componentTokenBinding"
1067
1075
  }
@@ -1136,6 +1144,13 @@ var properties = {
1136
1144
  $ref: "#/$defs/componentTokenBinding"
1137
1145
  }
1138
1146
  },
1147
+ hairline: {
1148
+ type: "object",
1149
+ description: "The one switch for hairlines (VI-680). Every hairline rule, divider and ring reads its weight from `--hairline-width`, so `width: 0` removes them all at once. Independent of the `control` edge switch. Keys: width.",
1150
+ additionalProperties: {
1151
+ $ref: "#/$defs/componentTokenBinding"
1152
+ }
1153
+ },
1139
1154
  control: {
1140
1155
  type: "object",
1141
1156
  description: "The one switch for form-control edges (VI-655). Every form control draws its resting edge from these, so `edge-width: 0` removes every resting edge at once, dashed edges included, with no layout shift. Keys: edge-width, edge-color, edge-style, drop-edge-width, drop-edge-style, state-edge-width.",
@@ -1185,6 +1200,13 @@ var properties = {
1185
1200
  $ref: "#/$defs/componentTokenBinding"
1186
1201
  }
1187
1202
  },
1203
+ tooltip: {
1204
+ type: "object",
1205
+ description: "Tooltip ground and ink, so a dark theme can keep the label legible without retuning the shared inverse pair other components read. Keys: bg, text.",
1206
+ additionalProperties: {
1207
+ $ref: "#/$defs/componentTokenBinding"
1208
+ }
1209
+ },
1188
1210
  "admin-ui": {
1189
1211
  type: "object",
1190
1212
  description: "Structural roles the admin-ui pattern owns. `marquee-family` is the display face for KPI hero figures and marquee page titles — the role the admin-ui portability audit named as the one piece of ENTR brand coupling a new theme must rebind. Keys: marquee-family.",
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  rgbToOklch,
64
64
  serializeColor,
65
65
  validateComponentBindings
66
- } from "./chunk-L5DQDEZV.js";
66
+ } from "./chunk-D5XSLWXP.js";
67
67
 
68
68
  // src/fonts/validate-coverage.ts
69
69
  var FONT_VAR_RE = /--font-(heading|display|body|sans|mono)\s*:\s*([^;]+);/g;
@@ -655,6 +655,11 @@ var visor_theme_schema_default = {
655
655
  enum: ["on", "off"],
656
656
  description: `The one switch for form-control edges (VI-655). 'off' removes the resting edge of every form control, dashed drop-target and empty-state edges included, with no layout shift; focus and invalid stay visible. Shorthand for components.control.edge-width: "0" (an explicit binding there wins). Defaults to 'on'. Optional.`
657
657
  },
658
+ hairlines: {
659
+ type: "string",
660
+ enum: ["on", "off"],
661
+ description: `The one switch for hairlines (VI-680). 'off' sets --hairline-width to 0, so every hairline rule, divider and ring disappears. Shorthand for components.hairline.width: "0" (an explicit binding there wins). Independent of 'edges'. Defaults to 'on'. Optional.`
662
+ },
658
663
  colors: {
659
664
  type: "object",
660
665
  description: "Color definitions for light mode. Only primary is required \u2014 all others have sensible defaults.",
@@ -1100,7 +1105,7 @@ var visor_theme_schema_default = {
1100
1105
  },
1101
1106
  badge: {
1102
1107
  type: "object",
1103
- description: "Badge treatment \u2014 the D3 Tier-2 `--badge-text-transform` / `-letter-spacing` / `-font-size` / `-font-weight` set, promoted from the admin-ui audit to a bindable contract. Keys: radius, border, font-family, font-weight, text-transform, letter-spacing, font-size, md-padding, md-gap.",
1108
+ description: "Badge treatment \u2014 the D3 Tier-2 `--badge-text-transform` / `-letter-spacing` / `-font-size` / `-font-weight` set, promoted from the admin-ui audit to a bindable contract. Keys: radius, border, font-family, font-weight, text-transform, letter-spacing, font-size, md-padding, md-gap, bg, ink.",
1104
1109
  additionalProperties: {
1105
1110
  $ref: "#/$defs/componentTokenBinding"
1106
1111
  }
@@ -1175,6 +1180,13 @@ var visor_theme_schema_default = {
1175
1180
  $ref: "#/$defs/componentTokenBinding"
1176
1181
  }
1177
1182
  },
1183
+ hairline: {
1184
+ type: "object",
1185
+ description: "The one switch for hairlines (VI-680). Every hairline rule, divider and ring reads its weight from `--hairline-width`, so `width: 0` removes them all at once. Independent of the `control` edge switch. Keys: width.",
1186
+ additionalProperties: {
1187
+ $ref: "#/$defs/componentTokenBinding"
1188
+ }
1189
+ },
1178
1190
  control: {
1179
1191
  type: "object",
1180
1192
  description: "The one switch for form-control edges (VI-655). Every form control draws its resting edge from these, so `edge-width: 0` removes every resting edge at once, dashed edges included, with no layout shift. Keys: edge-width, edge-color, edge-style, drop-edge-width, drop-edge-style, state-edge-width.",
@@ -1224,6 +1236,13 @@ var visor_theme_schema_default = {
1224
1236
  $ref: "#/$defs/componentTokenBinding"
1225
1237
  }
1226
1238
  },
1239
+ tooltip: {
1240
+ type: "object",
1241
+ description: "Tooltip ground and ink, so a dark theme can keep the label legible without retuning the shared inverse pair other components read. Keys: bg, text.",
1242
+ additionalProperties: {
1243
+ $ref: "#/$defs/componentTokenBinding"
1244
+ }
1245
+ },
1227
1246
  "admin-ui": {
1228
1247
  type: "object",
1229
1248
  description: "Structural roles the admin-ui pattern owns. `marquee-family` is the display face for KPI hero figures and marquee page titles \u2014 the role the admin-ui portability audit named as the one piece of ENTR brand coupling a new theme must rebind. Keys: marquee-family.",
@@ -1509,7 +1528,8 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
1509
1528
  "motion",
1510
1529
  "overrides",
1511
1530
  "components",
1512
- "edges"
1531
+ "edges",
1532
+ "hairlines"
1513
1533
  ]);
1514
1534
  var KNOWN_COLOR_KEYS = /* @__PURE__ */ new Set([
1515
1535
  "primary",
@@ -1791,6 +1811,9 @@ function validateConfig(config) {
1791
1811
  if (obj.edges !== void 0 && obj.edges !== "on" && obj.edges !== "off") {
1792
1812
  errors.push("'edges' must be either 'on' or 'off'");
1793
1813
  }
1814
+ if (obj.hairlines !== void 0 && obj.hairlines !== "on" && obj.hairlines !== "off") {
1815
+ errors.push("'hairlines' must be either 'on' or 'off'");
1816
+ }
1794
1817
  if (typeof obj.colors !== "object" || obj.colors === null) {
1795
1818
  errors.push("'colors' is required and must be an object");
1796
1819
  return { valid: false, errors };
@@ -2173,9 +2196,16 @@ function resolveConfig(config) {
2173
2196
  // component-tokens.ts owns their shape, there is nothing to default.
2174
2197
  // VI-655: `edges: off` folds in here as `control.edge-width: 0`, so every
2175
2198
  // adapter emits the switch through the same component-token path.
2176
- components: config.edges === "off" ? {
2199
+ // VI-680: `hairlines: off` folds in the same way, as `hairline.width: 0`.
2200
+ // The two switches are independent: each touches only its own family.
2201
+ components: config.edges === "off" || config.hairlines === "off" ? {
2177
2202
  ...config.components,
2178
- control: { "edge-width": "0", ...config.components?.control }
2203
+ ...config.edges === "off" && {
2204
+ control: { "edge-width": "0", ...config.components?.control }
2205
+ },
2206
+ ...config.hairlines === "off" && {
2207
+ hairline: { width: "0", ...config.components?.hairline }
2208
+ }
2179
2209
  } : config.components,
2180
2210
  originalColors,
2181
2211
  colorFormats
@@ -838,6 +838,14 @@ interface VisorThemeConfig {
838
838
  * `components.control.edge-width` wins over it. Default: `on`.
839
839
  */
840
840
  edges?: "on" | "off";
841
+ /**
842
+ * VI-680: the one switch for hairlines. `off` sets `--hairline-width` to 0 so
843
+ * every hairline rule, divider and ring (the `--hairline` / `--hairline-strong`
844
+ * family) disappears. Shorthand for `components.hairline.width: "0"`, and an
845
+ * explicit `components.hairline.width` wins over it. Independent of `edges`:
846
+ * neither switch touches the other's tokens. Default: `on`.
847
+ */
848
+ hairlines?: "on" | "off";
841
849
  /**
842
850
  * Migration metadata — optional, additive, consumed by `visor migrate` commands.
843
851
  * Does not affect CSS generation or theme application.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor-theme-engine",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
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",
@@ -39,6 +39,11 @@
39
39
  "enum": ["on", "off"],
40
40
  "description": "The one switch for form-control edges (VI-655). 'off' removes the resting edge of every form control, dashed drop-target and empty-state edges included, with no layout shift; focus and invalid stay visible. Shorthand for components.control.edge-width: \"0\" (an explicit binding there wins). Defaults to 'on'. Optional."
41
41
  },
42
+ "hairlines": {
43
+ "type": "string",
44
+ "enum": ["on", "off"],
45
+ "description": "The one switch for hairlines (VI-680). 'off' sets --hairline-width to 0, so every hairline rule, divider and ring disappears. Shorthand for components.hairline.width: \"0\" (an explicit binding there wins). Independent of 'edges'. Defaults to 'on'. Optional."
46
+ },
42
47
  "colors": {
43
48
  "type": "object",
44
49
  "description": "Color definitions for light mode. Only primary is required — all others have sensible defaults.",
@@ -484,7 +489,7 @@
484
489
  },
485
490
  "badge": {
486
491
  "type": "object",
487
- "description": "Badge treatment — the D3 Tier-2 `--badge-text-transform` / `-letter-spacing` / `-font-size` / `-font-weight` set, promoted from the admin-ui audit to a bindable contract. Keys: radius, border, font-family, font-weight, text-transform, letter-spacing, font-size, md-padding, md-gap.",
492
+ "description": "Badge treatment — the D3 Tier-2 `--badge-text-transform` / `-letter-spacing` / `-font-size` / `-font-weight` set, promoted from the admin-ui audit to a bindable contract. Keys: radius, border, font-family, font-weight, text-transform, letter-spacing, font-size, md-padding, md-gap, bg, ink.",
488
493
  "additionalProperties": {
489
494
  "$ref": "#/$defs/componentTokenBinding"
490
495
  }
@@ -559,6 +564,13 @@
559
564
  "$ref": "#/$defs/componentTokenBinding"
560
565
  }
561
566
  },
567
+ "hairline": {
568
+ "type": "object",
569
+ "description": "The one switch for hairlines (VI-680). Every hairline rule, divider and ring reads its weight from `--hairline-width`, so `width: 0` removes them all at once. Independent of the `control` edge switch. Keys: width.",
570
+ "additionalProperties": {
571
+ "$ref": "#/$defs/componentTokenBinding"
572
+ }
573
+ },
562
574
  "control": {
563
575
  "type": "object",
564
576
  "description": "The one switch for form-control edges (VI-655). Every form control draws its resting edge from these, so `edge-width: 0` removes every resting edge at once, dashed edges included, with no layout shift. Keys: edge-width, edge-color, edge-style, drop-edge-width, drop-edge-style, state-edge-width.",
@@ -608,6 +620,13 @@
608
620
  "$ref": "#/$defs/componentTokenBinding"
609
621
  }
610
622
  },
623
+ "tooltip": {
624
+ "type": "object",
625
+ "description": "Tooltip ground and ink, so a dark theme can keep the label legible without retuning the shared inverse pair other components read. Keys: bg, text.",
626
+ "additionalProperties": {
627
+ "$ref": "#/$defs/componentTokenBinding"
628
+ }
629
+ },
611
630
  "admin-ui": {
612
631
  "type": "object",
613
632
  "description": "Structural roles the admin-ui pattern owns. `marquee-family` is the display face for KPI hero figures and marquee page titles — the role the admin-ui portability audit named as the one piece of ENTR brand coupling a new theme must rebind. Keys: marquee-family.",