@rojaostudio/ds-core 1.1.0-next.6 → 1.1.0-next.8

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.
@@ -705,6 +705,27 @@ function ramp(c, custom) {
705
705
  return { base: hex, scale: buildScale(hex) };
706
706
  }
707
707
  var on = (bg) => onColor(bg);
708
+ var AA = 4.5;
709
+ var NON_TEXT = 3;
710
+ var readsOn = (fg, bgs, min = AA) => bgs.every((bg) => contrastRatio(fg, bg) >= min);
711
+ function readableFrom(base, scale, bgs, toward, fallback, min = AA) {
712
+ var _a;
713
+ if (readsOn(base, bgs, min)) return base;
714
+ const lb = relativeLuminance(base);
715
+ const steps = SCALE_STEPS.map((s2) => scale[s2]).filter((c) => !!c && isHex(c)).filter((c) => toward === "darker" ? relativeLuminance(c) < lb : relativeLuminance(c) > lb).sort((a, b) => Math.abs(relativeLuminance(a) - lb) - Math.abs(relativeLuminance(b) - lb));
716
+ const step2 = steps.find((c) => readsOn(c, bgs, min));
717
+ if (step2) return step2;
718
+ const [h, s, l0] = hexToHsl((_a = steps[steps.length - 1]) != null ? _a : base);
719
+ for (let l = l0; toward === "darker" ? l >= 0 : l <= 100; l += toward === "darker" ? -2 : 2) {
720
+ const c = hslToHex(h, s, l);
721
+ if (readsOn(c, bgs, min)) return c;
722
+ }
723
+ return fallback;
724
+ }
725
+ var defaultWarn = (m) => {
726
+ var _a;
727
+ return (_a = globalThis.console) == null ? void 0 : _a.warn(m);
728
+ };
708
729
  var ROLES = [
709
730
  ["colors/primary/light", "d", "b"],
710
731
  ["colors/primary/default", "d", "b"],
@@ -808,12 +829,16 @@ var ROLES = [
808
829
  ["colors/state/info-strong", "d", "d"],
809
830
  ["colors/state/neutral-strong", "d", "d"],
810
831
  ["text/on/primary-subtle", "d", "b"],
811
- ["surface/tint/subtle", "d", "b"]
832
+ ["surface/tint/subtle", "d", "b"],
833
+ ["border/error", "d", "d"]
812
834
  ];
835
+ var awayFrom = (bg) => contrastRatio(bg, BLACK) >= contrastRatio(bg, WHITE) ? "darker" : "lighter";
813
836
  var TINT_DEFAULT = 200;
814
837
  var lighterStep = (s) => s - 100 ;
815
838
  var roleVar = (role) => `--${role.replaceAll("/", "-")}`;
816
- function generateRdsTheme(def) {
839
+ function generateRdsTheme(def, opts = {}) {
840
+ var _a, _b;
841
+ const warn = (_a = opts.warn) != null ? _a : defaultWarn;
817
842
  const b = def.brand;
818
843
  const own = def.palettes;
819
844
  const P = ramp(b.primary, own);
@@ -823,7 +848,15 @@ function generateRdsTheme(def) {
823
848
  const L = palette("blue");
824
849
  const red = palette("red"), green = palette("green"), orange = palette("orange");
825
850
  const teal = palette("teal"), purple = palette("purple");
826
- const heading = b.heading ? ramp(b.heading, own).base : P.base;
851
+ const lightBgs = [WHITE, N[100]];
852
+ let heading;
853
+ if (b.heading) {
854
+ heading = ramp(b.heading, own).base;
855
+ if (!readsOn(heading, lightBgs)) {
856
+ const worst = Math.min(...lightBgs.map((bg) => contrastRatio(heading, bg)));
857
+ warn(`generateRdsTheme(${def.name}): heading ${heading} is ${worst.toFixed(2)}:1 on surface/card or surface/page (AA asks 4.5:1). Kept as given.`);
858
+ }
859
+ } else heading = readableFrom(P.base, P.scale, lightBgs, "darker", BLACK);
827
860
  const invert = b.invert ? ramp(b.invert, own).base : WHITE;
828
861
  const l = {
829
862
  "colors/primary/light": P.scale[500],
@@ -860,13 +893,12 @@ function generateRdsTheme(def) {
860
893
  "border/strong-hover": N[600],
861
894
  "surface/tint/default": A.scale[TINT_DEFAULT],
862
895
  "surface/tint/strong": A.scale[300],
863
- "text/on/tint": BLACK,
864
896
  // Light selection fill (pressed Toggle, choice tile): one step lighter than the default tint
865
897
  // (200 → 100, 100 → 50).
866
898
  "surface/tint/subtle": A.scale[lighterStep(TINT_DEFAULT)],
867
899
  "surface/action/default": L[100],
868
900
  "surface/action/strong": L[200],
869
- "text/on/action-tonal": P.scale[700],
901
+ "text/on/action-tonal": readableFrom(P.scale[700], P.scale, [L[100]], "darker", BLACK),
870
902
  "surface/lift/action": P.scale[50],
871
903
  "surface/lift/action-strong": L[100],
872
904
  "surface/band/action": L[100],
@@ -901,8 +933,7 @@ function generateRdsTheme(def) {
901
933
  "text/warning": orange[700],
902
934
  "surface/neutral": N[100],
903
935
  "text/neutral": N[800],
904
- "chart/series/1": P.scale[600],
905
- "chart/series/2": orange[500],
936
+ "chart/series/2": orange[600],
906
937
  "chart/series/3": teal[500],
907
938
  "chart/series/4": purple[500],
908
939
  "chart/series/5": green[500],
@@ -927,8 +958,11 @@ function generateRdsTheme(def) {
927
958
  ["colors/state/neutral", "text/on/neutral"],
928
959
  ["surface/cover", "text/on/cover"],
929
960
  ["surface/lift/action", "text/on/lift-action"],
930
- ["colors/primary/dark", "text/on/primary-strong"]
961
+ ["colors/primary/dark", "text/on/primary-strong"],
962
+ ["surface/tint/default", "text/on/tint"]
931
963
  ]) l[text] = on(l[fill]);
964
+ l["border/error"] = readableFrom(l["colors/state/error"], red, [l["surface/card"]], awayFrom(l["surface/card"]), BLACK, NON_TEXT);
965
+ l["chart/series/1"] = readableFrom(P.scale[600], P.scale, [l["surface/card"]], awayFrom(l["surface/card"]), BLACK, NON_TEXT);
932
966
  l["logo/primary"] = l["colors/primary/default"];
933
967
  l["logo/signature"] = l["colors/primary/default"];
934
968
  l["logo/accent"] = l["colors/accent/logo"];
@@ -945,7 +979,7 @@ function generateRdsTheme(def) {
945
979
  "border/default": N[800],
946
980
  "border/strong": N[500],
947
981
  "border/strong-hover": N[400],
948
- "text/heading": N[50],
982
+ "text/heading": readableFrom(N[50], N, [N[900]], "lighter", WHITE),
949
983
  "text/body": N[100],
950
984
  "text/muted": N[300],
951
985
  "text/subtle": N[400],
@@ -972,15 +1006,13 @@ function generateRdsTheme(def) {
972
1006
  "colors/accent/mark": A.scale[300],
973
1007
  "surface/tint/default": A.scale[900],
974
1008
  "surface/tint/strong": A.scale[800],
975
- "text/on/tint": WHITE,
976
1009
  // No step below 900 in dark: subtle is the default tint.
977
1010
  "surface/tint/subtle": A.scale[900],
978
1011
  "surface/action/default": L[900],
979
1012
  "surface/action/strong": L[800],
980
- "text/on/action-tonal": P.scale[200],
1013
+ "text/on/action-tonal": readableFrom(P.scale[200], P.scale, [L[900]], "lighter", WHITE),
981
1014
  "surface/lift/action": P.scale[800],
982
1015
  "surface/lift/action-strong": P.scale[700],
983
- "text/on/lift-action": WHITE,
984
1016
  "colors/state/error-strong": red[400],
985
1017
  "colors/state/success-strong": green[300],
986
1018
  "colors/state/warning-strong": orange[400],
@@ -995,7 +1027,6 @@ function generateRdsTheme(def) {
995
1027
  "text/warning": orange[300],
996
1028
  "surface/neutral": N[800],
997
1029
  "text/neutral": N[200],
998
- "chart/series/1": P.scale[400],
999
1030
  "chart/series/2": orange[400],
1000
1031
  "chart/series/3": teal[400],
1001
1032
  "chart/series/4": purple[400],
@@ -1014,12 +1045,20 @@ function generateRdsTheme(def) {
1014
1045
  ["colors/secondary/default", "text/on/secondary"],
1015
1046
  ["colors/accent/default", "text/on/accent"],
1016
1047
  ["colors/accent/mark", "text/on/accent-mark"],
1017
- ["colors/primary/dark", "text/on/primary-strong"]
1048
+ ["colors/primary/dark", "text/on/primary-strong"],
1049
+ ["surface/tint/default", "text/on/tint"],
1050
+ ["surface/lift/action", "text/on/lift-action"]
1018
1051
  ]) d[text] = on(d[fill]);
1052
+ d["border/error"] = readableFrom(d["colors/state/error-strong"], red, [d["surface/card"]], awayFrom(d["surface/card"]), WHITE, NON_TEXT);
1053
+ d["chart/series/1"] = readableFrom(P.scale[400], P.scale, [d["surface/card"]], awayFrom(d["surface/card"]), WHITE, NON_TEXT);
1019
1054
  const plate = l["colors/primary/default"];
1020
1055
  const ink = on(plate);
1021
1056
  const inkA = ink === WHITE ? white : black;
1022
1057
  const antiInk = ink === WHITE ? black : white;
1058
+ const lp = relativeLuminance(plate);
1059
+ const isPlate = (c) => c.toLowerCase() === plate.toLowerCase();
1060
+ const plateCard = P.scale[800] && !isPlate(P.scale[800]) && contrastRatio(ink, P.scale[800]) >= AA ? P.scale[800] : (_b = SCALE_STEPS.map((st) => P.scale[st]).filter((c) => !!c && isHex(c) && !isPlate(c)).sort((a, b2) => Math.abs(relativeLuminance(a) - lp) - Math.abs(relativeLuminance(b2) - lp)).find((c) => contrastRatio(ink, c) >= AA)) != null ? _b : plate;
1061
+ const onPrimaryStrong = contrastRatio(plate, P.scale[100]) >= AA ? plate : on(P.scale[100]);
1023
1062
  const br = {
1024
1063
  "colors/primary/light": P.scale[300],
1025
1064
  "colors/primary/default": ink,
@@ -1033,7 +1072,7 @@ function generateRdsTheme(def) {
1033
1072
  "text/body": ink,
1034
1073
  "text/link": contrastRatio(l["colors/accent/invert"], plate) >= 4.5 ? l["colors/accent/invert"] : ink,
1035
1074
  "surface/page": plate,
1036
- "surface/card": l["colors/primary/light"],
1075
+ "surface/card": plateCard,
1037
1076
  "surface/panel": plate,
1038
1077
  "border/default": inkA(20),
1039
1078
  "surface/tint/default": inkA(10),
@@ -1055,7 +1094,7 @@ function generateRdsTheme(def) {
1055
1094
  "surface/lift/action": inkA(10),
1056
1095
  "text/on/lift-action": ink,
1057
1096
  "surface/lift/action-strong": inkA(20),
1058
- "text/on/primary-strong": plate,
1097
+ "text/on/primary-strong": onPrimaryStrong,
1059
1098
  "text/subtle": inkA(60),
1060
1099
  "surface/muted": inkA(10),
1061
1100
  "surface/muted-strong": inkA(20),
@@ -1063,11 +1102,12 @@ function generateRdsTheme(def) {
1063
1102
  "border/strong-hover": inkA(80),
1064
1103
  "text/on/primary-subtle": inkA(30)
1065
1104
  };
1105
+ const brandError = readableFrom(red[300], red, [plateCard], awayFrom(plateCard), ink, NON_TEXT);
1066
1106
  const out = { light: {}, dark: {}, brand: {} };
1067
1107
  const pick = (src, role) => {
1068
- var _a;
1108
+ var _a2;
1069
1109
  const table = src === "l" ? l : src === "d" ? d : br;
1070
- const v = (_a = table[role]) != null ? _a : l[role];
1110
+ const v = (_a2 = table[role]) != null ? _a2 : l[role];
1071
1111
  if (v === void 0) throw new Error(`rdsTheme: no value for "${role}"`);
1072
1112
  return v;
1073
1113
  };
@@ -1076,25 +1116,58 @@ function generateRdsTheme(def) {
1076
1116
  out.dark[roleVar(role)] = pick(darkSrc, role);
1077
1117
  out.brand[roleVar(role)] = pick(brandSrc, role);
1078
1118
  }
1119
+ out.brand[roleVar("border/error")] = brandError;
1079
1120
  return out;
1080
1121
  }
1122
+ var RDS_SCOPE_SELECTORS = [
1123
+ ":root",
1124
+ ".ds-scope",
1125
+ "[data-rds-scope]",
1126
+ ".dark",
1127
+ "[data-rds-mode]",
1128
+ ".ds-plate",
1129
+ "[data-rds-plate]"
1130
+ ];
1131
+ var RDS_TOKEN_SCOPE = RDS_SCOPE_SELECTORS.join(", ");
1132
+ var split = (list) => list.split(",").map((s) => s.trim()).filter(Boolean);
1133
+ var rootAnchored = (sel) => /^(:root|html)(?![\w-])/i.test(sel);
1134
+ var subject = (sel) => {
1135
+ var _a;
1136
+ return (_a = sel.trim().split(/\s*[>+~]\s*|\s+/).pop()) != null ? _a : sel;
1137
+ };
1138
+ var COVERING = /\.(ds-scope|dark|ds-plate)(?![\w-])|\[\s*data-rds-(scope|mode|plate)\s*([~|^$*]?=[^\]]*)?\]/;
1139
+ var covered = (sel) => {
1140
+ const s = subject(sel);
1141
+ return /^(:root|html)(?![\w-])/i.test(s) || COVERING.test(s);
1142
+ };
1081
1143
  function emitRdsCss(theme, opts = {}) {
1082
1144
  var _a, _b, _c;
1083
- const scope = (_a = opts.scope) != null ? _a : ":root, .ds-scope";
1084
- const dark = (_b = opts.dark) != null ? _b : ".dark";
1085
- const plate = (_c = opts.plate) != null ? _c : ".ds-plate";
1086
- const block2 = (sel, map, only) => {
1145
+ const scopes = split((_a = opts.scope) != null ? _a : ":root, .ds-scope, [data-rds-scope]");
1146
+ const darks = split((_b = opts.dark) != null ? _b : '.dark, [data-rds-mode="dark"]');
1147
+ const plates = split((_c = opts.plate) != null ? _c : ".ds-plate, [data-rds-plate]");
1148
+ const block2 = (sels, map, only) => {
1087
1149
  const lines = Object.entries(map).filter(([k, v]) => !only || only[k] !== v).map(([k, v]) => ` ${k}: ${k === "--type-font-mono" ? `"${v}", monospace` : v};`);
1088
- return `${sel} {
1150
+ return `${sels.join(", ")} {
1089
1151
  ${lines.join("\n")}
1090
1152
  }`;
1091
1153
  };
1092
- const scopes = scope.split(",").map((s) => s.trim());
1093
- const darkSel = scopes.map((s) => s === ":root" ? `:root${dark}, ${dark}` : `${s}${dark}, ${dark} ${s}`).join(", ");
1154
+ const darkSels = scopes.flatMap(
1155
+ (s) => darks.flatMap((d) => {
1156
+ if (rootAnchored(d)) return s === ":root" ? [d] : [`${d} ${s}`];
1157
+ return s === ":root" ? [`:root${d}`, d] : [`${s}${d}`, `${d} ${s}`];
1158
+ })
1159
+ );
1160
+ if (!opts.allowUncovered) {
1161
+ const bad = [...new Set([...scopes, ...darkSels, ...plates].filter((sel) => !covered(sel)))];
1162
+ if (bad.length)
1163
+ throw new Error(
1164
+ `emitRdsCss: ${bad.map((b) => `"${b}"`).join(", ")} ${bad.length > 1 ? "are" : "is"} not covered by the component tokens of @rojaostudio/ds, redeclared only on ${RDS_TOKEN_SCOPE}. The components inside would keep the colours resolved at :root. Anchor the selector at the root (':root[data-theme="dark"]', as next-themes sets it on <html>), or add the attribute the tokens cover: data-rds-scope on a scope ('.my-scope[data-rds-scope]'), data-rds-mode on a dark element ('[data-rds-mode="dark"]'), data-rds-plate on a plate. Pass allowUncovered: true only for a theme without the components.`
1165
+ );
1166
+ }
1094
1167
  return [
1095
- block2(scope, __spreadValues(__spreadValues({}, theme.light), theme.vars)),
1096
- block2(darkSel, theme.dark, theme.light),
1097
- block2(plate, theme.brand, theme.light)
1168
+ block2(scopes, __spreadValues(__spreadValues({}, theme.light), theme.vars)),
1169
+ block2(darkSels, theme.dark, theme.light),
1170
+ block2(plates, theme.brand, theme.light)
1098
1171
  ].join("\n\n") + "\n";
1099
1172
  }
1100
1173
  function rdsContrast(theme, mode, fg, bg) {
@@ -1102,11 +1175,48 @@ function rdsContrast(theme, mode, fg, bg) {
1102
1175
  if (!/^#[0-9a-f]{6}$/i.test(a) || !/^#[0-9a-f]{6}$/i.test(c)) return null;
1103
1176
  return contrastRatio(a, c);
1104
1177
  }
1178
+ var RDS_CONTRAST_PAIRS = [
1179
+ ["text/heading", "surface/page"],
1180
+ ["text/heading", "surface/card"],
1181
+ ["text/body", "surface/page"],
1182
+ ["text/body", "surface/card"],
1183
+ ["text/muted", "surface/card"],
1184
+ ["text/link", "surface/card"],
1185
+ ["text/on/primary", "colors/primary/default"],
1186
+ ["text/on/secondary", "colors/secondary/default"],
1187
+ ["text/on/accent", "colors/accent/default"],
1188
+ ["text/on/accent-mark", "colors/accent/mark"],
1189
+ ["text/on/tint", "surface/tint/default"],
1190
+ ["text/on/action-tonal", "surface/action/default"],
1191
+ ["text/on/lift-action", "surface/lift/action"],
1192
+ ["text/on/primary-strong", "colors/primary/dark"],
1193
+ ["text/on/cover", "surface/cover"],
1194
+ ["text/on/error", "colors/state/error"],
1195
+ ["text/on/success", "colors/state/success"],
1196
+ ["text/on/info", "colors/state/info"],
1197
+ ["text/on/warning", "colors/state/warning"],
1198
+ ["text/on/neutral", "colors/state/neutral"],
1199
+ ["text/error", "surface/error"]
1200
+ ];
1201
+ function rdsContrastReport(theme) {
1202
+ const out = [];
1203
+ for (const mode of ["light", "dark", "brand"])
1204
+ for (const [fg, bg] of RDS_CONTRAST_PAIRS) {
1205
+ const ratio = rdsContrast(theme, mode, fg, bg);
1206
+ if (ratio !== null && ratio < AA) out.push({ mode, fg, bg, ratio: Math.round(ratio * 100) / 100 });
1207
+ }
1208
+ return out;
1209
+ }
1105
1210
  var isColourRef = (v) => /^[a-z][a-z0-9-]*\/[a-z]?\d+$/i.test(v);
1106
- function rdsThemeFromTable(table) {
1107
- var _a;
1211
+ var ADDED_ROLES = {
1212
+ // Figma: light → base colors/state/error, dark → base dark/colors/state/error-strong, brand → base dark/text/error.
1213
+ "border/error": { light: "colors/state/error", dark: "colors/state/error-strong", brand: "text/error" }
1214
+ };
1215
+ function rdsThemeFromTable(table, opts = {}) {
1216
+ var _a, _b, _c;
1108
1217
  const out = { light: {}, dark: {}, brand: {} };
1109
1218
  const problems = [];
1219
+ const derived = /* @__PURE__ */ new Set();
1110
1220
  for (const mode of ["light", "dark", "brand"]) {
1111
1221
  const roles = (_a = table.modes) == null ? void 0 : _a[mode];
1112
1222
  if (!roles) {
@@ -1114,7 +1224,12 @@ function rdsThemeFromTable(table) {
1114
1224
  continue;
1115
1225
  }
1116
1226
  for (const [role] of ROLES) {
1117
- const ref2 = roles[role];
1227
+ let ref2 = roles[role];
1228
+ const added = (_b = ADDED_ROLES[role]) == null ? void 0 : _b[mode];
1229
+ if (ref2 === void 0 && added && roles[added] !== void 0) {
1230
+ ref2 = roles[added];
1231
+ derived.add(role);
1232
+ }
1118
1233
  if (ref2 === void 0) problems.push(`${mode}: role "${role}" is missing`);
1119
1234
  else if (isColourRef(ref2)) {
1120
1235
  const value = table.primitives[ref2];
@@ -1138,6 +1253,17 @@ function rdsThemeFromTable(table) {
1138
1253
  }
1139
1254
  if (problems.length) throw new Error(`rdsThemeFromTable(${table.name}):
1140
1255
  ${problems.join("\n ")}`);
1256
+ const warn = (_c = opts.warn) != null ? _c : defaultWarn;
1257
+ if (derived.size)
1258
+ warn(
1259
+ `rdsThemeFromTable(${table.name}): the table was exported before ${[...derived].map((r) => `"${r}"`).join(", ")} existed in the [RDS] theme. Taken from the tokens Figma aliases it to (` + [...derived].map((r) => Object.entries(ADDED_ROLES[r]).map(([m, src]) => `${m}: ${src}`).join(", ")).join("; ") + `). Export the table again with figma/export-brand.js.`
1260
+ );
1261
+ const fails = rdsContrastReport(out);
1262
+ if (fails.length)
1263
+ warn(
1264
+ `rdsThemeFromTable(${table.name}): ${fails.length} text pair(s) below 4.5:1
1265
+ ` + fails.map((f) => `${f.mode}: ${f.fg} on ${f.bg} ${f.ratio}:1`).join("\n ")
1266
+ );
1141
1267
  return out;
1142
1268
  }
1143
1269
 
@@ -1365,4 +1491,4 @@ Brand's own variables (same value in every mode): ${own.map((v) => `\`${v}\``).j
1365
1491
  `;
1366
1492
  }
1367
1493
 
1368
- export { ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar };
1494
+ export { RDS_CONTRAST_PAIRS, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar };
@@ -7,6 +7,10 @@ type RdsTheme = Record<RdsMode, Record<string, string>> & {
7
7
  /** The brand's own variables (Figma `brand` collection, `<brand>/<name>`), one value for every mode. */
8
8
  vars?: Record<string, string>;
9
9
  };
10
+ type RdsThemeOptions = {
11
+ /** Where the warnings go (an explicit BrandDef.heading that fails AA). Default console.warn. */
12
+ warn?: (message: string) => void;
13
+ };
10
14
  /**
11
15
  * Roles of the [RDS] theme collection, in Figma order: [name, dark source, brand source].
12
16
  * The light mode always reads the plain base token. In dark and brand, Figma points either to the
@@ -15,22 +19,57 @@ type RdsTheme = Record<RdsMode, Record<string, string>> & {
15
19
  declare const ROLES: ReadonlyArray<readonly [string, "l" | "d", "l" | "d" | "b"]>;
16
20
  /** CSS custom property of a role: the Figma path with hyphens (the [RDS] codeSyntax rule). */
17
21
  declare const roleVar: (role: string) => string;
18
- declare function generateRdsTheme(def: BrandDef): RdsTheme;
22
+ declare function generateRdsTheme(def: BrandDef, opts?: RdsThemeOptions): RdsTheme;
23
+ /**
24
+ * The selectors on which @rojaostudio/ds redeclares its component tokens (styles/rds/components.css and
25
+ * foundation.css). A component token holds `var(--theme-role)`, and a custom property is resolved on the element
26
+ * where it is declared: a theme scope that none of these match would repaint the roles but not the components
27
+ * inside it, which keep the colours resolved at :root. The attributes are the generic way in: put
28
+ * `data-rds-scope` on a scope element, `data-rds-mode="dark"` on a dark one, `data-rds-plate` on a plate.
29
+ */
30
+ declare const RDS_SCOPE_SELECTORS: readonly [":root", ".ds-scope", "[data-rds-scope]", ".dark", "[data-rds-mode]", ".ds-plate", "[data-rds-plate]"];
31
+ /** RDS_SCOPE_SELECTORS as one selector list, as the component token layer is emitted. */
32
+ declare const RDS_TOKEN_SCOPE: string;
19
33
  type RdsCssOptions = {
20
- /** Scope of the light mode. Default `:root, .ds-scope`. */
34
+ /** Scope of the light mode. Default `:root, .ds-scope, [data-rds-scope]`. */
21
35
  scope?: string;
22
- /** Selector of the dark mode, combined with the scope. Default `.dark`. */
36
+ /**
37
+ * Selector of the dark mode, combined with each scope. Default `.dark, [data-rds-mode="dark"]`. A selector
38
+ * anchored at the root (`:root[data-theme="dark"]`, `html.dark`) is used as is: the theme switches on <html>,
39
+ * as next-themes does.
40
+ */
23
41
  dark?: string;
24
- /** Selector of the brand plate (a section painted with the primary). Default `.ds-plate`. */
42
+ /** Selector of the brand plate (a section painted with the primary). Default `.ds-plate, [data-rds-plate]`. */
25
43
  plate?: string;
44
+ /**
45
+ * Skip the check that every selector is covered by the component tokens of @rojaostudio/ds (RDS_SCOPE_SELECTORS).
46
+ * Only for a theme read by your own CSS, without the components.
47
+ */
48
+ allowUncovered?: boolean;
26
49
  };
27
50
  /**
28
51
  * The theme as CSS. Dark and plate only carry what differs from light: they inherit the rest
29
52
  * through the cascade, like the [RDS] modes that point back to the plain token.
53
+ *
54
+ * Throws when a selector would leave the components of @rojaostudio/ds on the root colours (see
55
+ * RDS_SCOPE_SELECTORS), unless `allowUncovered`.
30
56
  */
31
57
  declare function emitRdsCss(theme: RdsTheme, opts?: RdsCssOptions): string;
32
58
  /** Contrast of a role pair in one mode (alpha colours are not measured: they depend on what is below). */
33
59
  declare function rdsContrast(theme: RdsTheme, mode: RdsMode, fg: string, bg: string): number | null;
60
+ /**
61
+ * The main text pairs of the theme: [text role, the surface it sits on]. Measured in every mode; a pair with an
62
+ * alpha colour is skipped (its contrast depends on what is below).
63
+ */
64
+ declare const RDS_CONTRAST_PAIRS: ReadonlyArray<readonly [string, string]>;
65
+ type RdsContrastFailure = {
66
+ mode: RdsMode;
67
+ fg: string;
68
+ bg: string;
69
+ ratio: number;
70
+ };
71
+ /** The pairs of RDS_CONTRAST_PAIRS that fail WCAG AA (4.5:1), mode by mode. Empty means every pair passes. */
72
+ declare function rdsContrastReport(theme: RdsTheme): RdsContrastFailure[];
34
73
  /**
35
74
  * A brand as drawn in the [RDS] Base Tokens file: for each theme mode, every role points to a primitive of the
36
75
  * [RDS] Primitives library by name ("accyan/400"), and `primitives` holds the colour Figma resolves for each.
@@ -44,8 +83,13 @@ type RdsBrandTable = {
44
83
  /** The brand's own variables of the `brand` collection, by Figma path ("marca/ciano") → primitive or value. */
45
84
  vars?: Record<string, string>;
46
85
  };
47
- /** The theme of a brand table. Fails on a missing role or an unknown primitive, listing them all. */
48
- declare function rdsThemeFromTable(table: RdsBrandTable): RdsTheme;
86
+ /**
87
+ * The theme of a brand table. Fails on a missing role or an unknown primitive, listing them all. A table exported
88
+ * before a role of ADDED_ROLES existed is the exception: the role is taken from the token Figma aliases it to, and
89
+ * `opts.warn` asks to export the table again. The contrast of the main text pairs (rdsContrastReport) is only
90
+ * reported, through `opts.warn`: the table is the brand as drawn in Figma, kept one to one even where a pair fails.
91
+ */
92
+ declare function rdsThemeFromTable(table: RdsBrandTable, opts?: RdsThemeOptions): RdsTheme;
49
93
 
50
94
  /**
51
95
  * emitCss.ts — formata o output do generateTheme num theme.css legível.
@@ -149,4 +193,4 @@ declare function isNeutralBrand(def: {
149
193
  palettes?: PaletteDefs;
150
194
  }): boolean;
151
195
 
152
- export { BrandDef, type ClaudeMdOptions, type ClaudeMdTarget, ROLES as RDS_ROLES, type RdsBrandTable, type RdsCssOptions, type RdsMode, type RdsTheme, SCALE_STEPS, type Scale, type ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsThemeFromTable, refToHex, relativeLuminance, roleVar };
196
+ export { BrandDef, type ClaudeMdOptions, type ClaudeMdTarget, RDS_CONTRAST_PAIRS, ROLES as RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, type RdsBrandTable, type RdsContrastFailure, type RdsCssOptions, type RdsMode, type RdsTheme, type RdsThemeOptions, SCALE_STEPS, type Scale, type ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, roleVar };
package/dist/generate.js CHANGED
@@ -1,3 +1,3 @@
1
- export { ROLES as RDS_ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-UD53AUQQ.js';
1
+ export { RDS_CONTRAST_PAIRS, ROLES as RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-6XIFYDSU.js';
2
2
  import './chunk-FRWRLCYM.js';
3
3
  import './chunk-ORMEWXMH.js';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { G as GenResult, R as ResolvedTheme, T as TokenMap, g as generateTheme, r as resolveTheme } from './generateTheme-CUfNpsKs.js';
2
- export { ClaudeMdOptions, ClaudeMdTarget, RDS_ROLES, RdsBrandTable, RdsCssOptions, RdsMode, RdsTheme, SCALE_STEPS, Scale, ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsThemeFromTable, refToHex, relativeLuminance, roleVar } from './generate.js';
2
+ export { ClaudeMdOptions, ClaudeMdTarget, RDS_CONTRAST_PAIRS, RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, RdsBrandTable, RdsContrastFailure, RdsCssOptions, RdsMode, RdsTheme, RdsThemeOptions, SCALE_STEPS, Scale, ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, roleVar } from './generate.js';
3
3
  export { A as Archetype, B as BrandColors, a as BrandDef, C as ColorRef, D as DarkStrategy, F as FontDef, P as PaletteRef, S as ScaleToken, T as TextStyle, b as TextStyleName, c as TypeScale } from './recipe.schema-Cq7e82nx.js';
4
4
  export { primitives, tokens } from './tokens.js';
5
5
  export { RecipeName, recipes } from './recipes.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { recipes } from './chunk-PV6GFEAM.js';
2
- export { ROLES as RDS_ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-UD53AUQQ.js';
2
+ export { RDS_CONTRAST_PAIRS, ROLES as RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-6XIFYDSU.js';
3
3
  export { primitives, tokens } from './chunk-FRWRLCYM.js';
4
4
  import './chunk-ORMEWXMH.js';
package/dist/themes.js CHANGED
@@ -1,4 +1,4 @@
1
- import { generateTheme } from './chunk-UD53AUQQ.js';
1
+ import { generateTheme } from './chunk-6XIFYDSU.js';
2
2
  import './chunk-FRWRLCYM.js';
3
3
  import { __spreadValues, __objRest } from './chunk-ORMEWXMH.js';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rojaostudio/ds-core",
3
- "version": "1.1.0-next.6",
3
+ "version": "1.1.0-next.8",
4
4
  "description": "O motor do Rojão DS — tokens, derivação de tema e emissores. Sem React, sem Tailwind, sem CSS.",
5
5
  "license": "MIT",
6
6
  "author": "Rojão Studio (https://rojao.studio)",