@rojaostudio/ds-core 1.1.0-next.1 → 1.1.0-next.11
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/LICENSE +11 -0
- package/README.md +7 -4
- package/dist/{chunk-CFOS2RAD.js → chunk-D6HJEVJK.js} +671 -176
- package/dist/{chunk-3QTDO2HW.js → chunk-PV6GFEAM.js} +1 -2
- package/dist/generate.d.ts +126 -8
- package/dist/generate.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/recipes.d.ts +5 -1
- package/dist/recipes.js +1 -1
- package/dist/themes.js +1 -1
- package/figma/export-brand.js +58 -0
- package/package.json +3 -2
|
@@ -675,12 +675,114 @@ function resolveTheme(def, opts) {
|
|
|
675
675
|
return { tokens: isDark ? __spreadValues(__spreadValues({}, light2), dark) : light2, isDark };
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
// generate/validate.ts
|
|
679
|
+
var HEX = /^#[0-9a-f]{3,8}$/i;
|
|
680
|
+
var CH = String.raw`(?:[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?(?:%|deg)?|none)`;
|
|
681
|
+
var COLOR_FN = new RegExp(String.raw`^(?:rgba?|oklch)\(\s*${CH}(?:\s*[,/]?\s*${CH}){2,3}\s*\)$`, "i");
|
|
682
|
+
var VAR_NAME = /^[a-z0-9]+(?:[/-][a-z0-9]+)*$/;
|
|
683
|
+
var FONT_BAD = /["';{}\\<>\u0000-\u001f\u007f]/;
|
|
684
|
+
var isSafeColor = (v) => typeof v === "string" && (HEX.test(v) || COLOR_FN.test(v));
|
|
685
|
+
var isSafeVarName = (v) => typeof v === "string" && VAR_NAME.test(v);
|
|
686
|
+
var isSafeFont = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 100 && !FONT_BAD.test(v);
|
|
687
|
+
var isSafeCustomProperty = (k) => k.startsWith("--") && isSafeVarName(k.slice(2));
|
|
688
|
+
var FONT_ROLE_VAR = "--type-font-mono";
|
|
689
|
+
function preview(v) {
|
|
690
|
+
var _a;
|
|
691
|
+
const s = typeof v === "string" ? v : (_a = JSON.stringify(v)) != null ? _a : String(v);
|
|
692
|
+
const one = s.replace(/[\r\n\t]+/g, " ");
|
|
693
|
+
return JSON.stringify(one.length > 60 ? `${one.slice(0, 60)}\u2026` : one);
|
|
694
|
+
}
|
|
695
|
+
var RdsValidationError = class extends Error {
|
|
696
|
+
constructor(where, problems) {
|
|
697
|
+
super(`${where}: invalid input
|
|
698
|
+
${problems.join("\n ")}`);
|
|
699
|
+
this.problems = problems;
|
|
700
|
+
this.name = "RdsValidationError";
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
function collector(where) {
|
|
704
|
+
const problems = [];
|
|
705
|
+
return {
|
|
706
|
+
problems,
|
|
707
|
+
color(field, v) {
|
|
708
|
+
if (!isSafeColor(v)) problems.push(`${field}: ${preview(v)} is not a colour (hex, rgb(), rgba() or oklch())`);
|
|
709
|
+
},
|
|
710
|
+
name(field, v) {
|
|
711
|
+
if (!isSafeVarName(v)) problems.push(`${field}: ${preview(v)} is not a valid name (lowercase letters, digits, "-" and "/")`);
|
|
712
|
+
},
|
|
713
|
+
font(field, v) {
|
|
714
|
+
if (!isSafeFont(v)) problems.push(`${field}: ${preview(v)} is not a valid font family (no quotes, ";", braces, "\\" or line breaks)`);
|
|
715
|
+
},
|
|
716
|
+
done() {
|
|
717
|
+
if (problems.length) throw new RdsValidationError(where, problems);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function assertSafeTheme(theme, where) {
|
|
722
|
+
const c = collector(where);
|
|
723
|
+
if (!theme || typeof theme !== "object") {
|
|
724
|
+
c.problems.push("the theme is not an object");
|
|
725
|
+
c.done();
|
|
726
|
+
}
|
|
727
|
+
const check = (label, map, fontRole) => {
|
|
728
|
+
if (!map || typeof map !== "object" || Array.isArray(map)) {
|
|
729
|
+
c.problems.push(`${label}: not an object`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
for (const [k, v] of Object.entries(map)) {
|
|
733
|
+
if (!isSafeCustomProperty(k)) {
|
|
734
|
+
c.problems.push(`${label}: ${preview(k)} is not a valid CSS variable name`);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (fontRole && k === FONT_ROLE_VAR) c.font(`${label}.${k}`, v);
|
|
738
|
+
else c.color(`${label}.${k}`, v);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
for (const mode of ["light", "dark", "brand"]) check(mode, theme[mode], true);
|
|
742
|
+
if (theme.vars !== void 0) check("vars", theme.vars, false);
|
|
743
|
+
c.done();
|
|
744
|
+
}
|
|
745
|
+
var isSafeSelectorList = (v) => (
|
|
746
|
+
// eslint-disable-next-line no-control-regex -- control characters are exactly what is refused
|
|
747
|
+
typeof v === "string" && v.trim().length > 0 && !/[{};\\<@\u0000-\u001f\u007f]|\/\*|\*\//.test(v)
|
|
748
|
+
);
|
|
749
|
+
function oneLine(v, max = 200) {
|
|
750
|
+
if (typeof v !== "string") return "";
|
|
751
|
+
let s = v.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ").replace(/[<>`]/g, "").replace(/rojao-ds:(start|end)/gi, "").replace(/\s+/g, " ").trim();
|
|
752
|
+
if (s.length > max) s = `${s.slice(0, max - 1).trimEnd()}\u2026`;
|
|
753
|
+
return s;
|
|
754
|
+
}
|
|
755
|
+
|
|
678
756
|
// generate/rdsTheme.ts
|
|
679
757
|
var palettes = primitives.color;
|
|
680
758
|
var WHITE = "#ffffff";
|
|
681
759
|
var BLACK = "#000000";
|
|
682
760
|
var black = (pct) => BLACK + Math.round(pct / 100 * 255).toString(16).padStart(2, "0");
|
|
683
761
|
var white = (pct) => WHITE + Math.round(pct / 100 * 255).toString(16).padStart(2, "0");
|
|
762
|
+
var PALETTE_STEPS2 = /* @__PURE__ */ new Set([...SCALE_STEPS.map(String), "950"]);
|
|
763
|
+
function checkPalettes(custom, where) {
|
|
764
|
+
if (custom === void 0) return;
|
|
765
|
+
const c = collector(where);
|
|
766
|
+
if (!custom || typeof custom !== "object" || Array.isArray(custom)) {
|
|
767
|
+
c.problems.push("palettes: not an object");
|
|
768
|
+
c.done();
|
|
769
|
+
}
|
|
770
|
+
for (const [name, value] of Object.entries(custom)) {
|
|
771
|
+
if (!/^[a-z][a-z0-9]*$/i.test(name)) {
|
|
772
|
+
c.problems.push(`palettes: ${preview(name)} is not a valid palette name (letters and digits)`);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (typeof value === "string") {
|
|
776
|
+
if (!isHex(value)) c.problems.push(`palettes.${name}: ${preview(value)} is not a hex colour`);
|
|
777
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
778
|
+
for (const [step2, v] of Object.entries(value)) {
|
|
779
|
+
if (!PALETTE_STEPS2.has(step2)) c.problems.push(`palettes.${name}: ${preview(step2)} is not a scale step (50\u2013900)`);
|
|
780
|
+
else c.color(`palettes.${name}.${step2}`, v);
|
|
781
|
+
}
|
|
782
|
+
} else c.problems.push(`palettes.${name}: expected a hex or a scale of steps 50\u2013900`);
|
|
783
|
+
}
|
|
784
|
+
c.done();
|
|
785
|
+
}
|
|
684
786
|
function palette(name, custom) {
|
|
685
787
|
const own = custom == null ? void 0 : custom[name];
|
|
686
788
|
if (typeof own === "string") {
|
|
@@ -705,6 +807,87 @@ function ramp(c, custom) {
|
|
|
705
807
|
return { base: hex, scale: buildScale(hex) };
|
|
706
808
|
}
|
|
707
809
|
var on = (bg) => onColor(bg);
|
|
810
|
+
var AA = 4.5;
|
|
811
|
+
var NON_TEXT = 3;
|
|
812
|
+
var readsOn = (fg, bgs, min = AA) => bgs.every((bg) => contrastRatio(fg, bg) >= min);
|
|
813
|
+
function readableFrom(base, scale, bgs, toward, fallback, min = AA) {
|
|
814
|
+
var _a;
|
|
815
|
+
if (readsOn(base, bgs, min)) return base;
|
|
816
|
+
const lb = relativeLuminance(base);
|
|
817
|
+
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));
|
|
818
|
+
const step2 = steps.find((c) => readsOn(c, bgs, min));
|
|
819
|
+
if (step2) return step2;
|
|
820
|
+
const [h, s, l0] = hexToHsl((_a = steps[steps.length - 1]) != null ? _a : base);
|
|
821
|
+
for (let l = l0; toward === "darker" ? l >= 0 : l <= 100; l += toward === "darker" ? -2 : 2) {
|
|
822
|
+
const c = hslToHex(h, s, l);
|
|
823
|
+
if (readsOn(c, bgs, min)) return c;
|
|
824
|
+
}
|
|
825
|
+
return fallback;
|
|
826
|
+
}
|
|
827
|
+
function stateFills(scale, fill, text, preferred) {
|
|
828
|
+
const lf = relativeLuminance(fill);
|
|
829
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
830
|
+
const ramp2 = SCALE_STEPS.map((s) => scale[s]).filter((c) => !!c && isHex(c)).map((c) => c.toLowerCase()).sort((a, b) => relativeLuminance(a) - relativeLuminance(b));
|
|
831
|
+
const beyond = [];
|
|
832
|
+
if (ramp2.length) {
|
|
833
|
+
const [hd, sd, ld] = hexToHsl(ramp2[0]);
|
|
834
|
+
for (let x = ld - 3; x >= 0; x -= 3) beyond.push(hslToHex(hd, sd, x));
|
|
835
|
+
const [hl, sl, ll] = hexToHsl(ramp2[ramp2.length - 1]);
|
|
836
|
+
for (let x = ll + 3; x <= 100; x += 3) beyond.push(hslToHex(hl, sl, x));
|
|
837
|
+
}
|
|
838
|
+
const steps = [.../* @__PURE__ */ new Set([...ramp2, ...beyond.map((c) => c.toLowerCase())])].filter(
|
|
839
|
+
(c) => !same(c, fill) && relativeLuminance(c) !== lf
|
|
840
|
+
);
|
|
841
|
+
const side = (dir2) => steps.filter((c) => Math.sign(relativeLuminance(c) - lf) === dir2).sort((a, b) => Math.abs(relativeLuminance(a) - lf) - Math.abs(relativeLuminance(b) - lf));
|
|
842
|
+
const carrying = (list) => {
|
|
843
|
+
const out = [];
|
|
844
|
+
for (const c of list) {
|
|
845
|
+
if (contrastRatio(text, c) < AA) break;
|
|
846
|
+
out.push(c);
|
|
847
|
+
}
|
|
848
|
+
return out;
|
|
849
|
+
};
|
|
850
|
+
const awayFromText = relativeLuminance(text) <= lf ? 1 : -1;
|
|
851
|
+
const lp = relativeLuminance(preferred[0]);
|
|
852
|
+
const dir = lp === lf ? awayFromText : lp > lf ? 1 : -1;
|
|
853
|
+
const n = preferred.length;
|
|
854
|
+
for (const d of [dir, -dir]) {
|
|
855
|
+
const all = side(d);
|
|
856
|
+
const ok2 = carrying(all);
|
|
857
|
+
if (ok2.length < n) continue;
|
|
858
|
+
const wants = preferred.map((p, i) => {
|
|
859
|
+
const k = d === dir ? all.findIndex((c) => same(c, p)) : -1;
|
|
860
|
+
return k >= 0 ? k : i;
|
|
861
|
+
});
|
|
862
|
+
const order = wants.map((_, i) => i).sort((x, y) => wants[x] - wants[y] || x - y);
|
|
863
|
+
const idx = new Array(n);
|
|
864
|
+
let next = 0;
|
|
865
|
+
order.forEach((i, rank) => {
|
|
866
|
+
idx[i] = Math.min(Math.max(wants[i], next), ok2.length - (n - rank));
|
|
867
|
+
next = idx[i] + 1;
|
|
868
|
+
});
|
|
869
|
+
return idx.map((k) => ok2[k]);
|
|
870
|
+
}
|
|
871
|
+
const ok = [...carrying(side(dir)), ...carrying(side(-dir))];
|
|
872
|
+
return preferred.map((p, i) => {
|
|
873
|
+
var _a;
|
|
874
|
+
return (_a = ok[i]) != null ? _a : p;
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
var carry = (scale, fill, text, value) => contrastRatio(text, value) >= AA ? value : stateFills(scale, fill, text, [value])[0];
|
|
878
|
+
function over(top, below) {
|
|
879
|
+
const ch = (h, i) => parseInt(h.slice(1 + 2 * i, 3 + 2 * i), 16);
|
|
880
|
+
const a = top.length === 9 ? ch(top, 3) / 255 : 1;
|
|
881
|
+
return `#${[0, 1, 2].map((i) => Math.round(ch(top, i) * a + ch(below, i) * (1 - a)).toString(16).padStart(2, "0")).join("")}`;
|
|
882
|
+
}
|
|
883
|
+
var PLATE_MIN = 6;
|
|
884
|
+
function distinct(a, b) {
|
|
885
|
+
return contrastRatio(a, b) >= 1.5 || Math.abs(hexToHsl(a)[1] - hexToHsl(b)[1]) >= 40;
|
|
886
|
+
}
|
|
887
|
+
var defaultWarn = (m) => {
|
|
888
|
+
var _a;
|
|
889
|
+
return (_a = globalThis.console) == null ? void 0 : _a.warn(m);
|
|
890
|
+
};
|
|
708
891
|
var ROLES = [
|
|
709
892
|
["colors/primary/light", "d", "b"],
|
|
710
893
|
["colors/primary/default", "d", "b"],
|
|
@@ -808,12 +991,19 @@ var ROLES = [
|
|
|
808
991
|
["colors/state/info-strong", "d", "d"],
|
|
809
992
|
["colors/state/neutral-strong", "d", "d"],
|
|
810
993
|
["text/on/primary-subtle", "d", "b"],
|
|
811
|
-
["surface/tint/subtle", "d", "b"]
|
|
994
|
+
["surface/tint/subtle", "d", "b"],
|
|
995
|
+
["border/error", "d", "d"]
|
|
812
996
|
];
|
|
997
|
+
var awayFrom = (bg) => contrastRatio(bg, BLACK) >= contrastRatio(bg, WHITE) ? "darker" : "lighter";
|
|
813
998
|
var TINT_DEFAULT = 200;
|
|
814
999
|
var lighterStep = (s) => s - 100 ;
|
|
815
1000
|
var roleVar = (role) => `--${role.replaceAll("/", "-")}`;
|
|
816
|
-
function generateRdsTheme(def) {
|
|
1001
|
+
function generateRdsTheme(def, opts = {}) {
|
|
1002
|
+
var _a, _b, _c, _d;
|
|
1003
|
+
const warn = (_a = opts.warn) != null ? _a : defaultWarn;
|
|
1004
|
+
const where = `generateRdsTheme(${preview(def == null ? void 0 : def.name)})`;
|
|
1005
|
+
if (!(def == null ? void 0 : def.brand) || typeof def.brand !== "object") throw new Error(`${where}: brand is missing`);
|
|
1006
|
+
checkPalettes(def.palettes, where);
|
|
817
1007
|
const b = def.brand;
|
|
818
1008
|
const own = def.palettes;
|
|
819
1009
|
const P = ramp(b.primary, own);
|
|
@@ -823,7 +1013,15 @@ function generateRdsTheme(def) {
|
|
|
823
1013
|
const L = palette("blue");
|
|
824
1014
|
const red = palette("red"), green = palette("green"), orange = palette("orange");
|
|
825
1015
|
const teal = palette("teal"), purple = palette("purple");
|
|
826
|
-
const
|
|
1016
|
+
const lightBgs = [WHITE, N[100], N[200], A.scale[200]];
|
|
1017
|
+
let heading;
|
|
1018
|
+
if (b.heading) {
|
|
1019
|
+
heading = ramp(b.heading, own).base;
|
|
1020
|
+
if (!readsOn(heading, lightBgs)) {
|
|
1021
|
+
const worst = Math.min(...lightBgs.map((bg) => contrastRatio(heading, bg)));
|
|
1022
|
+
warn(`generateRdsTheme(${def.name}): heading ${heading} is ${worst.toFixed(2)}:1 on surface/card, surface/page, surface/muted-strong or colors/accent/highlight (AA asks 4.5:1). Kept as given.`);
|
|
1023
|
+
}
|
|
1024
|
+
} else heading = readableFrom(P.base, P.scale, lightBgs, "darker", BLACK);
|
|
827
1025
|
const invert = b.invert ? ramp(b.invert, own).base : WHITE;
|
|
828
1026
|
const l = {
|
|
829
1027
|
"colors/primary/light": P.scale[500],
|
|
@@ -860,13 +1058,12 @@ function generateRdsTheme(def) {
|
|
|
860
1058
|
"border/strong-hover": N[600],
|
|
861
1059
|
"surface/tint/default": A.scale[TINT_DEFAULT],
|
|
862
1060
|
"surface/tint/strong": A.scale[300],
|
|
863
|
-
"text/on/tint": BLACK,
|
|
864
1061
|
// Light selection fill (pressed Toggle, choice tile): one step lighter than the default tint
|
|
865
1062
|
// (200 → 100, 100 → 50).
|
|
866
1063
|
"surface/tint/subtle": A.scale[lighterStep(TINT_DEFAULT)],
|
|
867
1064
|
"surface/action/default": L[100],
|
|
868
1065
|
"surface/action/strong": L[200],
|
|
869
|
-
"text/on/action-tonal": P.scale[700],
|
|
1066
|
+
"text/on/action-tonal": readableFrom(P.scale[700], P.scale, [L[100]], "darker", BLACK),
|
|
870
1067
|
"surface/lift/action": P.scale[50],
|
|
871
1068
|
"surface/lift/action-strong": L[100],
|
|
872
1069
|
"surface/band/action": L[100],
|
|
@@ -901,8 +1098,7 @@ function generateRdsTheme(def) {
|
|
|
901
1098
|
"text/warning": orange[700],
|
|
902
1099
|
"surface/neutral": N[100],
|
|
903
1100
|
"text/neutral": N[800],
|
|
904
|
-
"chart/series/
|
|
905
|
-
"chart/series/2": orange[500],
|
|
1101
|
+
"chart/series/2": orange[600],
|
|
906
1102
|
"chart/series/3": teal[500],
|
|
907
1103
|
"chart/series/4": purple[500],
|
|
908
1104
|
"chart/series/5": green[500],
|
|
@@ -915,6 +1111,16 @@ function generateRdsTheme(def) {
|
|
|
915
1111
|
"logo/inverse-signature": WHITE,
|
|
916
1112
|
"social/ink": BLACK
|
|
917
1113
|
};
|
|
1114
|
+
const single = !b.secondary;
|
|
1115
|
+
const K = single ? N : P.scale;
|
|
1116
|
+
if (single) {
|
|
1117
|
+
const steps = [900, 800, 700, 600, 500];
|
|
1118
|
+
const k = Math.max(0, steps.findIndex((s) => distinct(N[s], l["colors/secondary/default"])));
|
|
1119
|
+
l["colors/primary/default"] = N[steps[Math.min(k, 2)]];
|
|
1120
|
+
l["colors/primary/dark"] = N[steps[Math.min(k, 2) + 1]];
|
|
1121
|
+
l["colors/primary/active"] = N[steps[Math.min(k, 2) + 2]];
|
|
1122
|
+
l["colors/primary/light"] = N[500];
|
|
1123
|
+
}
|
|
918
1124
|
for (const [fill, text] of [
|
|
919
1125
|
["colors/primary/default", "text/on/primary"],
|
|
920
1126
|
["colors/secondary/default", "text/on/secondary"],
|
|
@@ -927,10 +1133,23 @@ function generateRdsTheme(def) {
|
|
|
927
1133
|
["colors/state/neutral", "text/on/neutral"],
|
|
928
1134
|
["surface/cover", "text/on/cover"],
|
|
929
1135
|
["surface/lift/action", "text/on/lift-action"],
|
|
930
|
-
["colors/primary/
|
|
1136
|
+
["colors/primary/default", "text/on/primary-strong"],
|
|
1137
|
+
["surface/tint/default", "text/on/tint"]
|
|
931
1138
|
]) l[text] = on(l[fill]);
|
|
932
|
-
|
|
933
|
-
|
|
1139
|
+
for (const role of ["colors/primary/dark", "colors/primary/light"])
|
|
1140
|
+
l[role] = carry(K, l["colors/primary/default"], l["text/on/primary-strong"], l[role]);
|
|
1141
|
+
[l["colors/primary/active"]] = stateFills(K, l["colors/primary/default"], l["text/on/primary"], [l["colors/primary/active"]]);
|
|
1142
|
+
[l["colors/secondary/hover"], l["colors/secondary/active"]] = stateFills(
|
|
1143
|
+
S.scale,
|
|
1144
|
+
l["colors/secondary/default"],
|
|
1145
|
+
l["text/on/secondary"],
|
|
1146
|
+
[l["colors/secondary/hover"], l["colors/secondary/active"]]
|
|
1147
|
+
);
|
|
1148
|
+
[l["colors/accent/hover"]] = stateFills(A.scale, l["colors/accent/default"], l["text/on/accent"], [l["colors/accent/hover"]]);
|
|
1149
|
+
l["border/error"] = readableFrom(l["colors/state/error"], red, [l["surface/card"]], awayFrom(l["surface/card"]), BLACK, NON_TEXT);
|
|
1150
|
+
l["chart/series/1"] = readableFrom(P.scale[600], P.scale, [l["surface/card"], l["surface/page"]], awayFrom(l["surface/card"]), BLACK, NON_TEXT);
|
|
1151
|
+
l["logo/primary"] = readableFrom(P.base, P.scale, [l["surface/card"], l["surface/page"], l["surface/panel"]], "darker", BLACK, NON_TEXT);
|
|
1152
|
+
l["logo/signature"] = l["logo/primary"];
|
|
934
1153
|
l["logo/accent"] = l["colors/accent/logo"];
|
|
935
1154
|
l["logo/accent-2"] = l["colors/secondary/default"];
|
|
936
1155
|
l["logo/accent-3"] = l["colors/accent/default"];
|
|
@@ -945,7 +1164,7 @@ function generateRdsTheme(def) {
|
|
|
945
1164
|
"border/default": N[800],
|
|
946
1165
|
"border/strong": N[500],
|
|
947
1166
|
"border/strong-hover": N[400],
|
|
948
|
-
"text/heading": N[50],
|
|
1167
|
+
"text/heading": readableFrom(N[50], N, [N[900]], "lighter", WHITE),
|
|
949
1168
|
"text/body": N[100],
|
|
950
1169
|
"text/muted": N[300],
|
|
951
1170
|
"text/subtle": N[400],
|
|
@@ -972,15 +1191,13 @@ function generateRdsTheme(def) {
|
|
|
972
1191
|
"colors/accent/mark": A.scale[300],
|
|
973
1192
|
"surface/tint/default": A.scale[900],
|
|
974
1193
|
"surface/tint/strong": A.scale[800],
|
|
975
|
-
"text/on/tint": WHITE,
|
|
976
1194
|
// No step below 900 in dark: subtle is the default tint.
|
|
977
1195
|
"surface/tint/subtle": A.scale[900],
|
|
978
1196
|
"surface/action/default": L[900],
|
|
979
1197
|
"surface/action/strong": L[800],
|
|
980
|
-
"text/on/action-tonal": P.scale[200],
|
|
1198
|
+
"text/on/action-tonal": readableFrom(P.scale[200], P.scale, [L[900]], "lighter", WHITE),
|
|
981
1199
|
"surface/lift/action": P.scale[800],
|
|
982
1200
|
"surface/lift/action-strong": P.scale[700],
|
|
983
|
-
"text/on/lift-action": WHITE,
|
|
984
1201
|
"colors/state/error-strong": red[400],
|
|
985
1202
|
"colors/state/success-strong": green[300],
|
|
986
1203
|
"colors/state/warning-strong": orange[400],
|
|
@@ -995,7 +1212,6 @@ function generateRdsTheme(def) {
|
|
|
995
1212
|
"text/warning": orange[300],
|
|
996
1213
|
"surface/neutral": N[800],
|
|
997
1214
|
"text/neutral": N[200],
|
|
998
|
-
"chart/series/1": P.scale[400],
|
|
999
1215
|
"chart/series/2": orange[400],
|
|
1000
1216
|
"chart/series/3": teal[400],
|
|
1001
1217
|
"chart/series/4": purple[400],
|
|
@@ -1009,17 +1225,56 @@ function generateRdsTheme(def) {
|
|
|
1009
1225
|
"colors/state/neutral-strong": N[400],
|
|
1010
1226
|
"text/on/primary-subtle": black(30)
|
|
1011
1227
|
};
|
|
1228
|
+
if (single) {
|
|
1229
|
+
const steps = [200, 300, 100, 400];
|
|
1230
|
+
const s = (_b = steps.find((st) => distinct(N[st], d["colors/secondary/default"]))) != null ? _b : 200;
|
|
1231
|
+
d["colors/primary/default"] = N[s];
|
|
1232
|
+
d["colors/primary/dark"] = N[s === 100 ? 50 : s - 100];
|
|
1233
|
+
d["colors/primary/active"] = N[s + 100];
|
|
1234
|
+
d["colors/primary/light"] = N[s + 200];
|
|
1235
|
+
}
|
|
1236
|
+
d["surface/tint/default"] = readableFrom(d["surface/tint/default"], A.scale, [d["text/body"], d["text/muted"], d["colors/primary/default"]], "darker", N[900]);
|
|
1237
|
+
d["surface/tint/subtle"] = d["surface/tint/default"];
|
|
1238
|
+
d["colors/accent/highlight"] = readableFrom(d["colors/accent/highlight"], A.scale, [d["text/heading"]], "darker", N[900]);
|
|
1012
1239
|
for (const [fill, text] of [
|
|
1013
1240
|
["colors/primary/default", "text/on/primary"],
|
|
1014
1241
|
["colors/secondary/default", "text/on/secondary"],
|
|
1015
1242
|
["colors/accent/default", "text/on/accent"],
|
|
1016
1243
|
["colors/accent/mark", "text/on/accent-mark"],
|
|
1017
|
-
["colors/primary/
|
|
1244
|
+
["colors/primary/default", "text/on/primary-strong"],
|
|
1245
|
+
["surface/tint/default", "text/on/tint"],
|
|
1246
|
+
["surface/lift/action", "text/on/lift-action"]
|
|
1018
1247
|
]) d[text] = on(d[fill]);
|
|
1019
|
-
const
|
|
1248
|
+
for (const role of ["colors/primary/dark", "colors/primary/light"])
|
|
1249
|
+
d[role] = carry(K, d["colors/primary/default"], d["text/on/primary-strong"], d[role]);
|
|
1250
|
+
[d["colors/primary/active"]] = stateFills(K, d["colors/primary/default"], d["text/on/primary"], [d["colors/primary/active"]]);
|
|
1251
|
+
[d["colors/secondary/hover"], d["colors/secondary/active"]] = stateFills(
|
|
1252
|
+
S.scale,
|
|
1253
|
+
d["colors/secondary/default"],
|
|
1254
|
+
d["text/on/secondary"],
|
|
1255
|
+
[d["colors/secondary/hover"], d["colors/secondary/active"]]
|
|
1256
|
+
);
|
|
1257
|
+
[d["colors/accent/hover"]] = stateFills(A.scale, d["colors/accent/default"], d["text/on/accent"], [d["colors/accent/hover"]]);
|
|
1258
|
+
d["border/error"] = readableFrom(red[400], red, [d["surface/card"]], awayFrom(d["surface/card"]), WHITE, NON_TEXT);
|
|
1259
|
+
d["chart/series/1"] = readableFrom(P.scale[400], P.scale, [d["surface/card"]], awayFrom(d["surface/card"]), WHITE, NON_TEXT);
|
|
1260
|
+
d["colors/state/error-strong"] = carry(red, l["colors/state/error"], l["text/on/error"], d["colors/state/error-strong"]);
|
|
1261
|
+
const ink0 = on(P.base);
|
|
1262
|
+
const plate = contrastRatio(ink0, P.base) >= PLATE_MIN ? P.base : readableFrom(P.base, P.scale, [ink0], ink0 === WHITE ? "darker" : "lighter", ink0 === WHITE ? BLACK : WHITE, PLATE_MIN);
|
|
1020
1263
|
const ink = on(plate);
|
|
1021
1264
|
const inkA = ink === WHITE ? white : black;
|
|
1022
1265
|
const antiInk = ink === WHITE ? black : white;
|
|
1266
|
+
const lp = relativeLuminance(plate);
|
|
1267
|
+
const isPlate = (c) => c.toLowerCase() === plate.toLowerCase();
|
|
1268
|
+
const plateCard = P.scale[800] && !isPlate(P.scale[800]) && contrastRatio(ink, P.scale[800]) >= PLATE_MIN ? P.scale[800] : (_c = 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) >= PLATE_MIN)) != null ? _c : plate;
|
|
1269
|
+
const plateBgs = [plate, plateCard];
|
|
1270
|
+
const ov = plateBgs.every((bg) => contrastRatio(ink, over(inkA(20), bg)) >= AA) ? inkA : ink === WHITE ? black : white;
|
|
1271
|
+
const under = [...plateBgs, ...plateBgs.map((bg) => over(ov(10), bg))];
|
|
1272
|
+
const quiet = (pct, min = AA, bgs = under) => {
|
|
1273
|
+
for (let p = pct; p < 100; p += 5) if (bgs.every((bg) => contrastRatio(over(inkA(p), bg), bg) >= min)) return inkA(p);
|
|
1274
|
+
return ink;
|
|
1275
|
+
};
|
|
1276
|
+
const fieldBorder = quiet(60, NON_TEXT, plateBgs);
|
|
1277
|
+
const fieldAlpha = fieldBorder.length === 9 ? Math.round(parseInt(fieldBorder.slice(7), 16) / 255 * 100) : 100;
|
|
1023
1278
|
const br = {
|
|
1024
1279
|
"colors/primary/light": P.scale[300],
|
|
1025
1280
|
"colors/primary/default": ink,
|
|
@@ -1033,41 +1288,64 @@ function generateRdsTheme(def) {
|
|
|
1033
1288
|
"text/body": ink,
|
|
1034
1289
|
"text/link": contrastRatio(l["colors/accent/invert"], plate) >= 4.5 ? l["colors/accent/invert"] : ink,
|
|
1035
1290
|
"surface/page": plate,
|
|
1036
|
-
"surface/card":
|
|
1291
|
+
"surface/card": plateCard,
|
|
1037
1292
|
"surface/panel": plate,
|
|
1038
1293
|
"border/default": inkA(20),
|
|
1039
|
-
"surface/tint/default":
|
|
1040
|
-
"surface/tint/strong":
|
|
1041
|
-
"surface/tint/subtle":
|
|
1294
|
+
"surface/tint/default": ov(10),
|
|
1295
|
+
"surface/tint/strong": ov(20),
|
|
1296
|
+
"surface/tint/subtle": ov(5),
|
|
1042
1297
|
"text/on/action-tonal": ink,
|
|
1043
1298
|
"text/on/primary": plate,
|
|
1044
1299
|
"text/on/secondary": plate,
|
|
1045
1300
|
"text/on/tint": ink,
|
|
1046
|
-
"surface/action/default":
|
|
1047
|
-
"surface/action/strong":
|
|
1301
|
+
"surface/action/default": ov(10),
|
|
1302
|
+
"surface/action/strong": ov(20),
|
|
1048
1303
|
"focus/ring": ink,
|
|
1049
1304
|
"focus/ring-inset": plate,
|
|
1050
1305
|
"surface/disabled": inkA(10),
|
|
1051
1306
|
"text/disabled": inkA(40),
|
|
1052
|
-
"text/muted":
|
|
1307
|
+
"text/muted": quiet(70),
|
|
1053
1308
|
"text/disabled-invert": antiInk(40),
|
|
1054
1309
|
"text/action": ink,
|
|
1055
|
-
"surface/lift/action":
|
|
1310
|
+
"surface/lift/action": ov(10),
|
|
1056
1311
|
"text/on/lift-action": ink,
|
|
1057
|
-
"surface/lift/action-strong":
|
|
1312
|
+
"surface/lift/action-strong": ov(20),
|
|
1313
|
+
// The label of the neutral Button on the plate, whose fill is the ink: the plate colour.
|
|
1058
1314
|
"text/on/primary-strong": plate,
|
|
1059
|
-
"text/subtle":
|
|
1060
|
-
"surface/muted":
|
|
1061
|
-
"surface/muted-strong":
|
|
1062
|
-
"border/strong":
|
|
1063
|
-
"border/strong-hover": inkA(
|
|
1315
|
+
"text/subtle": quiet(60),
|
|
1316
|
+
"surface/muted": ov(10),
|
|
1317
|
+
"surface/muted-strong": ov(20),
|
|
1318
|
+
"border/strong": fieldBorder,
|
|
1319
|
+
"border/strong-hover": fieldAlpha + 20 < 100 ? inkA(fieldAlpha + 20) : ink,
|
|
1064
1320
|
"text/on/primary-subtle": inkA(30)
|
|
1065
1321
|
};
|
|
1322
|
+
for (const role of ["colors/primary/dark", "colors/primary/light"])
|
|
1323
|
+
br[role] = carry(P.scale, br["colors/primary/default"], br["text/on/primary-strong"], br[role]);
|
|
1324
|
+
br["colors/primary/dark"] = readableFrom(br["colors/primary/dark"], P.scale, [plateCard], awayFrom(plateCard), ink, NON_TEXT);
|
|
1325
|
+
[br["colors/primary/active"]] = stateFills(P.scale, br["colors/primary/default"], br["text/on/primary"], [br["colors/primary/active"]]);
|
|
1326
|
+
[br["colors/secondary/hover"], br["colors/secondary/active"]] = stateFills(
|
|
1327
|
+
S.scale,
|
|
1328
|
+
br["colors/secondary/default"],
|
|
1329
|
+
br["text/on/secondary"],
|
|
1330
|
+
[br["colors/secondary/hover"], br["colors/secondary/active"]]
|
|
1331
|
+
);
|
|
1332
|
+
const brandError = readableFrom(red[300], red, plateBgs.slice().reverse(), awayFrom(plateCard), ink, NON_TEXT);
|
|
1333
|
+
const lightPlate = ink === BLACK;
|
|
1334
|
+
const plateOwn = {};
|
|
1335
|
+
for (const [st, rampOf] of [["error", red], ["success", green], ["info", L], ["warning", orange], ["neutral", N]]) {
|
|
1336
|
+
const from = lightPlate ? l : d;
|
|
1337
|
+
const soft = [`surface/${st}`, ...st === "error" ? ["surface/error-strong"] : []];
|
|
1338
|
+
for (const s of soft) plateOwn[s] = from[s];
|
|
1339
|
+
plateOwn[`text/${st}`] = readableFrom(from[`text/${st}`], rampOf, [...under, ...soft.map((s) => from[s])], awayFrom(plate), ink);
|
|
1340
|
+
}
|
|
1341
|
+
plateOwn["colors/accent/highlight"] = (_d = [d["colors/accent/highlight"], l["colors/accent/highlight"]].find((c) => contrastRatio(ink, c) >= AA)) != null ? _d : readableFrom(l["colors/accent/highlight"], A.scale, [ink], lightPlate ? "lighter" : "darker", ink === WHITE ? BLACK : WHITE);
|
|
1342
|
+
plateOwn["chart/series/1"] = readableFrom(d["chart/series/1"], P.scale, plateBgs, awayFrom(plate), ink, NON_TEXT);
|
|
1343
|
+
for (const logo of ["logo/primary", "logo/signature", "logo/accent", "logo/mono", "social/ink"]) plateOwn[logo] = ink;
|
|
1066
1344
|
const out = { light: {}, dark: {}, brand: {} };
|
|
1067
1345
|
const pick = (src, role) => {
|
|
1068
|
-
var
|
|
1346
|
+
var _a2;
|
|
1069
1347
|
const table = src === "l" ? l : src === "d" ? d : br;
|
|
1070
|
-
const v = (
|
|
1348
|
+
const v = (_a2 = table[role]) != null ? _a2 : l[role];
|
|
1071
1349
|
if (v === void 0) throw new Error(`rdsTheme: no value for "${role}"`);
|
|
1072
1350
|
return v;
|
|
1073
1351
|
};
|
|
@@ -1076,25 +1354,66 @@ function generateRdsTheme(def) {
|
|
|
1076
1354
|
out.dark[roleVar(role)] = pick(darkSrc, role);
|
|
1077
1355
|
out.brand[roleVar(role)] = pick(brandSrc, role);
|
|
1078
1356
|
}
|
|
1357
|
+
out.brand[roleVar("border/error")] = brandError;
|
|
1358
|
+
for (const [role, v] of Object.entries(plateOwn)) out.brand[roleVar(role)] = v;
|
|
1359
|
+
assertSafeTheme(out, where);
|
|
1079
1360
|
return out;
|
|
1080
1361
|
}
|
|
1362
|
+
var RDS_SCOPE_SELECTORS = [
|
|
1363
|
+
":root",
|
|
1364
|
+
".ds-scope",
|
|
1365
|
+
"[data-rds-scope]",
|
|
1366
|
+
".dark",
|
|
1367
|
+
"[data-rds-mode]",
|
|
1368
|
+
".ds-plate",
|
|
1369
|
+
"[data-rds-plate]"
|
|
1370
|
+
];
|
|
1371
|
+
var RDS_TOKEN_SCOPE = RDS_SCOPE_SELECTORS.join(", ");
|
|
1372
|
+
var split = (list) => list.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1373
|
+
var rootAnchored = (sel) => /^(:root|html)(?![\w-])/i.test(sel);
|
|
1374
|
+
var subject = (sel) => {
|
|
1375
|
+
var _a;
|
|
1376
|
+
return (_a = sel.trim().split(/\s*[>+~]\s*|\s+/).pop()) != null ? _a : sel;
|
|
1377
|
+
};
|
|
1378
|
+
var COVERING = /\.(ds-scope|dark|ds-plate)(?![\w-])|\[\s*data-rds-(scope|mode|plate)\s*([~|^$*]?=[^\]]*)?\]/;
|
|
1379
|
+
var covered = (sel) => {
|
|
1380
|
+
const s = subject(sel);
|
|
1381
|
+
return /^(:root|html)(?![\w-])/i.test(s) || COVERING.test(s);
|
|
1382
|
+
};
|
|
1081
1383
|
function emitRdsCss(theme, opts = {}) {
|
|
1082
1384
|
var _a, _b, _c;
|
|
1083
|
-
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1385
|
+
assertSafeTheme(theme, "emitRdsCss");
|
|
1386
|
+
const badSel = ["scope", "dark", "plate"].filter((k) => opts[k] !== void 0 && !isSafeSelectorList(opts[k]));
|
|
1387
|
+
if (badSel.length)
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
`emitRdsCss: invalid selector in ${badSel.map((k) => `${k} ${preview(opts[k])}`).join(", ")} (no braces, ";", comments, backslash, "@", "<" or line breaks).`
|
|
1390
|
+
);
|
|
1391
|
+
const scopes = split((_a = opts.scope) != null ? _a : ":root, .ds-scope, [data-rds-scope]");
|
|
1392
|
+
const darks = split((_b = opts.dark) != null ? _b : '.dark, [data-rds-mode="dark"]');
|
|
1393
|
+
const plates = split((_c = opts.plate) != null ? _c : ".ds-plate, [data-rds-plate]");
|
|
1394
|
+
const block2 = (sels, map, only) => {
|
|
1087
1395
|
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 `${
|
|
1396
|
+
return `${sels.join(", ")} {
|
|
1089
1397
|
${lines.join("\n")}
|
|
1090
1398
|
}`;
|
|
1091
1399
|
};
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1400
|
+
const darkSels = scopes.flatMap(
|
|
1401
|
+
(s) => darks.flatMap((d) => {
|
|
1402
|
+
if (rootAnchored(d)) return s === ":root" ? [d] : [`${d} ${s}`];
|
|
1403
|
+
return s === ":root" ? [`:root${d}`, d] : [`${s}${d}`, `${d} ${s}`];
|
|
1404
|
+
})
|
|
1405
|
+
);
|
|
1406
|
+
if (!opts.allowUncovered) {
|
|
1407
|
+
const bad = [...new Set([...scopes, ...darkSels, ...plates].filter((sel) => !covered(sel)))];
|
|
1408
|
+
if (bad.length)
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
`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.`
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1094
1413
|
return [
|
|
1095
|
-
block2(
|
|
1096
|
-
block2(
|
|
1097
|
-
block2(
|
|
1414
|
+
block2(scopes, __spreadValues(__spreadValues({}, theme.light), theme.vars)),
|
|
1415
|
+
block2(darkSels, theme.dark, theme.light),
|
|
1416
|
+
block2(plates, theme.brand, theme.light)
|
|
1098
1417
|
].join("\n\n") + "\n";
|
|
1099
1418
|
}
|
|
1100
1419
|
function rdsContrast(theme, mode, fg, bg) {
|
|
@@ -1102,6 +1421,160 @@ function rdsContrast(theme, mode, fg, bg) {
|
|
|
1102
1421
|
if (!/^#[0-9a-f]{6}$/i.test(a) || !/^#[0-9a-f]{6}$/i.test(c)) return null;
|
|
1103
1422
|
return contrastRatio(a, c);
|
|
1104
1423
|
}
|
|
1424
|
+
var RDS_CONTRAST_PAIRS = [
|
|
1425
|
+
["text/heading", "surface/page"],
|
|
1426
|
+
["text/heading", "surface/card"],
|
|
1427
|
+
["text/body", "surface/page"],
|
|
1428
|
+
["text/body", "surface/card"],
|
|
1429
|
+
["text/muted", "surface/card"],
|
|
1430
|
+
["text/link", "surface/card"],
|
|
1431
|
+
["text/on/primary", "colors/primary/default"],
|
|
1432
|
+
["text/on/primary", "colors/primary/active"],
|
|
1433
|
+
["text/on/secondary", "colors/secondary/default"],
|
|
1434
|
+
["text/on/secondary", "colors/secondary/hover"],
|
|
1435
|
+
["text/on/secondary", "colors/secondary/active"],
|
|
1436
|
+
["text/on/accent", "colors/accent/default"],
|
|
1437
|
+
["text/on/accent", "colors/accent/hover"],
|
|
1438
|
+
["text/on/accent-mark", "colors/accent/mark"],
|
|
1439
|
+
["text/on/tint", "surface/tint/default"],
|
|
1440
|
+
["text/on/action-tonal", "surface/action/default"],
|
|
1441
|
+
["text/on/lift-action", "surface/lift/action"],
|
|
1442
|
+
["text/on/primary-strong", "colors/primary/dark"],
|
|
1443
|
+
["text/on/cover", "surface/cover"],
|
|
1444
|
+
["text/on/error", "colors/state/error"],
|
|
1445
|
+
["text/on/success", "colors/state/success"],
|
|
1446
|
+
["text/on/info", "colors/state/info"],
|
|
1447
|
+
["text/on/warning", "colors/state/warning"],
|
|
1448
|
+
["text/on/neutral", "colors/state/neutral"],
|
|
1449
|
+
["text/error", "surface/error"],
|
|
1450
|
+
// The pairs the components join (packages/ds, styles/rds/components.css): the label of the neutral Button and
|
|
1451
|
+
// Badge on its fill and hovers, the inverse label (colors/primary/default) on the card, the pressed outline Button,
|
|
1452
|
+
// the Badge highlight, the selected entry, quiet text and state text where they are placed.
|
|
1453
|
+
["text/on/primary-strong", "colors/primary/default"],
|
|
1454
|
+
["text/on/primary-strong", "colors/primary/light"],
|
|
1455
|
+
["text/on/primary-strong", "colors/primary/active"],
|
|
1456
|
+
["colors/primary/default", "surface/card"],
|
|
1457
|
+
["colors/primary/default", "surface/tint/default"],
|
|
1458
|
+
["text/heading", "surface/muted-strong"],
|
|
1459
|
+
["text/heading", "colors/accent/highlight"],
|
|
1460
|
+
["text/body", "surface/tint/default"],
|
|
1461
|
+
["text/muted", "surface/tint/default"],
|
|
1462
|
+
["text/on/tint", "surface/tint/subtle"],
|
|
1463
|
+
["text/action", "surface/action/strong"],
|
|
1464
|
+
["text/muted", "surface/page"],
|
|
1465
|
+
["text/muted", "surface/panel"],
|
|
1466
|
+
["text/subtle", "surface/card"],
|
|
1467
|
+
["text/subtle", "surface/panel"],
|
|
1468
|
+
["text/error", "surface/card"],
|
|
1469
|
+
["text/error", "surface/page"],
|
|
1470
|
+
["text/error", "surface/error-strong"],
|
|
1471
|
+
["text/success", "surface/success"],
|
|
1472
|
+
["text/success", "surface/card"],
|
|
1473
|
+
["text/info", "surface/info"],
|
|
1474
|
+
["text/warning", "surface/warning"],
|
|
1475
|
+
["text/neutral", "surface/neutral"]
|
|
1476
|
+
];
|
|
1477
|
+
var RDS_NON_TEXT_PAIRS = [
|
|
1478
|
+
["border/strong", "surface/card"],
|
|
1479
|
+
["border/strong", "surface/page"],
|
|
1480
|
+
["border/error", "surface/card"],
|
|
1481
|
+
["focus/ring", "surface/page"],
|
|
1482
|
+
["focus/ring", "surface/card"],
|
|
1483
|
+
["logo/primary", "surface/card"],
|
|
1484
|
+
["logo/primary", "surface/page"],
|
|
1485
|
+
["logo/primary", "surface/panel"],
|
|
1486
|
+
["chart/series/1", "surface/card"],
|
|
1487
|
+
["chart/series/1", "surface/page"]
|
|
1488
|
+
];
|
|
1489
|
+
function rdsContrastReport(theme) {
|
|
1490
|
+
const out = [];
|
|
1491
|
+
for (const mode of ["light", "dark", "brand"])
|
|
1492
|
+
for (const [pairs, min] of [[RDS_CONTRAST_PAIRS, AA], [RDS_NON_TEXT_PAIRS, NON_TEXT]])
|
|
1493
|
+
for (const [fg, bg] of pairs) {
|
|
1494
|
+
const ratio = rdsContrast(theme, mode, fg, bg);
|
|
1495
|
+
if (ratio !== null && ratio < min) out.push({ mode, fg, bg, ratio: Math.round(ratio * 100) / 100, min });
|
|
1496
|
+
}
|
|
1497
|
+
return out;
|
|
1498
|
+
}
|
|
1499
|
+
var isColourRef = (v) => /^[a-z][a-z0-9-]*\/[a-z]?\d+$/i.test(v);
|
|
1500
|
+
var ADDED_ROLES = {
|
|
1501
|
+
// Figma: light → base colors/state/error, dark → base dark/colors/state/error-strong, brand → base dark/text/error.
|
|
1502
|
+
"border/error": { light: "colors/state/error", dark: "colors/state/error-strong", brand: "text/error" }
|
|
1503
|
+
};
|
|
1504
|
+
function rdsThemeFromTable(table, opts = {}) {
|
|
1505
|
+
var _a, _b, _c;
|
|
1506
|
+
const where = `rdsThemeFromTable(${preview(table == null ? void 0 : table.name)})`;
|
|
1507
|
+
if (!table || typeof table !== "object") throw new Error(`${where}: the table is not an object`);
|
|
1508
|
+
const check = collector(where);
|
|
1509
|
+
if (!table.primitives || typeof table.primitives !== "object" || Array.isArray(table.primitives))
|
|
1510
|
+
check.problems.push("primitives: not an object");
|
|
1511
|
+
else
|
|
1512
|
+
for (const [name, value] of Object.entries(table.primitives)) {
|
|
1513
|
+
check.name(`primitives ${preview(name)}`, name);
|
|
1514
|
+
check.color(`primitives.${name}`, value);
|
|
1515
|
+
}
|
|
1516
|
+
if (table.vars !== void 0) {
|
|
1517
|
+
if (!table.vars || typeof table.vars !== "object" || Array.isArray(table.vars)) check.problems.push("vars: not an object");
|
|
1518
|
+
else
|
|
1519
|
+
for (const [name, value] of Object.entries(table.vars)) {
|
|
1520
|
+
check.name(`vars ${preview(name)}`, name);
|
|
1521
|
+
if (typeof value !== "string") check.problems.push(`vars.${name}: not a string`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
check.done();
|
|
1525
|
+
const out = { light: {}, dark: {}, brand: {} };
|
|
1526
|
+
const problems = [];
|
|
1527
|
+
const derived = /* @__PURE__ */ new Set();
|
|
1528
|
+
for (const mode of ["light", "dark", "brand"]) {
|
|
1529
|
+
const roles = (_a = table.modes) == null ? void 0 : _a[mode];
|
|
1530
|
+
if (!roles) {
|
|
1531
|
+
problems.push(`mode "${mode}" is missing`);
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
for (const [role] of ROLES) {
|
|
1535
|
+
let ref2 = roles[role];
|
|
1536
|
+
const added = (_b = ADDED_ROLES[role]) == null ? void 0 : _b[mode];
|
|
1537
|
+
if (ref2 === void 0 && added && roles[added] !== void 0) {
|
|
1538
|
+
ref2 = roles[added];
|
|
1539
|
+
derived.add(role);
|
|
1540
|
+
}
|
|
1541
|
+
if (ref2 === void 0) problems.push(`${mode}: role "${role}" is missing`);
|
|
1542
|
+
else if (isColourRef(ref2)) {
|
|
1543
|
+
const value = table.primitives[ref2];
|
|
1544
|
+
if (value === void 0) problems.push(`${mode}: "${role}" points to unknown primitive "${ref2}"`);
|
|
1545
|
+
else out[mode][roleVar(role)] = value.toLowerCase();
|
|
1546
|
+
} else out[mode][roleVar(role)] = ref2;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const valueOf = (field, ref2) => {
|
|
1550
|
+
if (!isColourRef(ref2)) return ref2;
|
|
1551
|
+
const value = table.primitives[ref2];
|
|
1552
|
+
if (value === void 0) problems.push(`${field} points to unknown primitive "${ref2}"`);
|
|
1553
|
+
return value == null ? void 0 : value.toLowerCase();
|
|
1554
|
+
};
|
|
1555
|
+
if (table.vars) {
|
|
1556
|
+
out.vars = {};
|
|
1557
|
+
for (const [name, ref2] of Object.entries(table.vars)) {
|
|
1558
|
+
const value = valueOf(`var "${name}"`, ref2);
|
|
1559
|
+
if (value !== void 0) out.vars[roleVar(name)] = value;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (problems.length) throw new Error(`${where}:
|
|
1563
|
+
${problems.join("\n ")}`);
|
|
1564
|
+
assertSafeTheme(out, where);
|
|
1565
|
+
const warn = (_c = opts.warn) != null ? _c : defaultWarn;
|
|
1566
|
+
if (derived.size)
|
|
1567
|
+
warn(
|
|
1568
|
+
`${where}: 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.`
|
|
1569
|
+
);
|
|
1570
|
+
const fails = rdsContrastReport(out);
|
|
1571
|
+
if (fails.length)
|
|
1572
|
+
warn(
|
|
1573
|
+
`${where}: ${fails.length} pair(s) below WCAG (4.5:1 text, 3:1 non-text)
|
|
1574
|
+
` + fails.map((f) => `${f.mode}: ${f.fg} on ${f.bg} ${f.ratio}:1 (min ${f.min}:1)`).join("\n ")
|
|
1575
|
+
);
|
|
1576
|
+
return out;
|
|
1577
|
+
}
|
|
1105
1578
|
|
|
1106
1579
|
// generate/emitCss.ts
|
|
1107
1580
|
var SECTIONS = [
|
|
@@ -1174,153 +1647,175 @@ ${lightBlock}${darkBlock}
|
|
|
1174
1647
|
}
|
|
1175
1648
|
|
|
1176
1649
|
// generate/emitClaudeMd.ts
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
{
|
|
1195
|
-
{
|
|
1196
|
-
{
|
|
1197
|
-
{
|
|
1198
|
-
{
|
|
1199
|
-
{
|
|
1200
|
-
{
|
|
1201
|
-
{
|
|
1202
|
-
{
|
|
1203
|
-
{
|
|
1204
|
-
{
|
|
1205
|
-
{ key: "--border-subtle", use: "divis\xF3rias fracas" },
|
|
1206
|
-
{ key: "--border-default", use: "bordas de inputs e cards" },
|
|
1207
|
-
{ key: "--border-strong", use: "bordas enfatizadas" },
|
|
1208
|
-
{ key: "--border-focus", use: "anel de foco" },
|
|
1209
|
-
{ key: "--text-primary", use: "t\xEDtulos, corpo" },
|
|
1210
|
-
{ key: "--text-secondary", use: "texto de apoio" },
|
|
1211
|
-
{ key: "--text-muted", use: "legendas, metadados" },
|
|
1212
|
-
{ key: "--text-inverse", use: "texto sobre superf\xEDcies invertidas" },
|
|
1213
|
-
{ key: "--icon-default", use: "cor padr\xE3o de \xEDcone" }
|
|
1650
|
+
var MARKERS = /rojao-ds:(start|end)/i;
|
|
1651
|
+
var NAME = /^[a-z0-9][a-z0-9 _-]{0,63}$/i;
|
|
1652
|
+
var CSS_FILE = /^[a-z0-9_@./-]+\.css$/i;
|
|
1653
|
+
var CSS_URL = /^https?:\/\/[^\s"'`<>()\\]+$/i;
|
|
1654
|
+
var INSTALL = /^(?:pnpm add|yarn add|bun add|npm install|npm i) @rojaostudio\/ds(?:@[a-z0-9][a-z0-9.+-]*)?$/;
|
|
1655
|
+
var ROLE_DOCS = [
|
|
1656
|
+
{ role: "colors/primary/default", use: "neutral ink fill: neutral Button and Badge, bars, Tooltip" },
|
|
1657
|
+
{ role: "text/on/primary", use: "text/icon on the neutral ink fill" },
|
|
1658
|
+
{ role: "colors/primary/active", use: "pressed neutral ink fill" },
|
|
1659
|
+
{ role: "colors/secondary/default", use: "action fill: the default Button, checkbox, switch (with `--text-on-secondary`)" },
|
|
1660
|
+
{ role: "colors/accent/default", use: "highlight \u2014 sparingly (with `--text-on-accent`)" },
|
|
1661
|
+
{ role: "surface/page", use: "page background" },
|
|
1662
|
+
{ role: "surface/card", use: "cards" },
|
|
1663
|
+
{ role: "surface/panel", use: "panels, side areas" },
|
|
1664
|
+
{ role: "surface/muted", use: "muted areas, wells" },
|
|
1665
|
+
{ role: "surface/tint/default", use: "selected / tinted fill (with `--text-on-tint`)" },
|
|
1666
|
+
{ role: "border/default", use: "dividers, card borders" },
|
|
1667
|
+
{ role: "border/strong", use: "control borders, emphasis" },
|
|
1668
|
+
{ role: "focus/ring", use: "focus ring" },
|
|
1669
|
+
{ role: "text/heading", use: "headings" },
|
|
1670
|
+
{ role: "text/body", use: "body text" },
|
|
1671
|
+
{ role: "text/muted", use: "supporting text" },
|
|
1672
|
+
{ role: "text/subtle", use: "captions, metadata" },
|
|
1673
|
+
{ role: "text/link", use: "links" },
|
|
1674
|
+
{ role: "colors/state/error", use: "error fill (with `--text-on-error`)" },
|
|
1675
|
+
{ role: "text/error", use: "error message text" },
|
|
1676
|
+
{ role: "colors/state/success", use: "success fill (with `--text-on-success`)" },
|
|
1677
|
+
{ role: "colors/state/warning", use: "warning fill (with `--text-on-warning`)" }
|
|
1214
1678
|
];
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1679
|
+
var CLAUDE_MD_COMPONENTS = [
|
|
1680
|
+
"button",
|
|
1681
|
+
"icon-button",
|
|
1682
|
+
"tooltip",
|
|
1683
|
+
"input",
|
|
1684
|
+
"textarea",
|
|
1685
|
+
"select",
|
|
1686
|
+
"checkbox",
|
|
1687
|
+
"radio-group",
|
|
1688
|
+
"switch",
|
|
1689
|
+
"card",
|
|
1690
|
+
"dialog",
|
|
1691
|
+
"sheet",
|
|
1692
|
+
"tabs",
|
|
1693
|
+
"table",
|
|
1694
|
+
"toast",
|
|
1695
|
+
"badge",
|
|
1696
|
+
"alert",
|
|
1697
|
+
"avatar",
|
|
1698
|
+
"dropdown-menu",
|
|
1699
|
+
"page-header",
|
|
1700
|
+
"sidebar",
|
|
1701
|
+
"empty",
|
|
1702
|
+
"skeleton",
|
|
1703
|
+
"pagination"
|
|
1704
|
+
];
|
|
1705
|
+
var CLAUDE_MD_FOUNDATION = {
|
|
1706
|
+
space: ["--space-4", "--space-8", "--space-12", "--space-16", "--space-24", "--space-32", "--space-40", "--space-48", "--space-56", "--space-64"],
|
|
1707
|
+
radius: ["--radius-xs", "--radius-control", "--radius-field", "--radius-card", "--radius-container", "--radius-full"],
|
|
1708
|
+
type: ["heading", "lead", "body", "label", "button", "small", "caption"],
|
|
1709
|
+
font: "--type-font-stack"
|
|
1710
|
+
};
|
|
1711
|
+
function isTable(x) {
|
|
1712
|
+
return typeof x === "object" && x !== null && "modes" in x && "primitives" in x;
|
|
1229
1713
|
}
|
|
1714
|
+
var TARGET_FILES = { claude: "CLAUDE.md", cursor: ".cursorrules", agents: "AGENTS.md" };
|
|
1230
1715
|
function fileLabel(target) {
|
|
1231
|
-
return target
|
|
1716
|
+
return TARGET_FILES[target];
|
|
1232
1717
|
}
|
|
1233
|
-
function
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1718
|
+
function roleTable(theme) {
|
|
1719
|
+
const rows = ROLE_DOCS.filter((r) => roleVar(r.role) in theme.light).map((r) => {
|
|
1720
|
+
var _a;
|
|
1721
|
+
const v = roleVar(r.role);
|
|
1722
|
+
return `| \`${v}\` | ${r.use} | \`${theme.light[v]}\` | \`${(_a = theme.dark[v]) != null ? _a : theme.light[v]}\` |`;
|
|
1723
|
+
});
|
|
1724
|
+
return ["| Token | Use for | Light | Dark |", "|---|---|---|---|", ...rows].join("\n");
|
|
1725
|
+
}
|
|
1726
|
+
function emitClaudeMd(source, opts = {}) {
|
|
1727
|
+
var _a, _b, _c, _d;
|
|
1728
|
+
const theme = (_a = opts.theme) != null ? _a : isTable(source) ? rdsThemeFromTable(source) : generateRdsTheme(source);
|
|
1729
|
+
assertSafeTheme(theme, "emitClaudeMd");
|
|
1730
|
+
const name = source.name;
|
|
1731
|
+
if (name !== void 0 && name !== "" && !(typeof name === "string" && NAME.test(name)))
|
|
1732
|
+
throw new Error(`emitClaudeMd: name ${preview(name)} is not valid (letters, digits, space, "-" and "_", up to 64)`);
|
|
1733
|
+
const Name = name ? name[0].toUpperCase() + name.slice(1) : "Brand";
|
|
1734
|
+
const desc = !isTable(source) ? oneLine(source.description) : "";
|
|
1735
|
+
const description = desc ? ` \u2014 ${desc}` : "";
|
|
1736
|
+
const cssFile = (_b = opts.cssFile) != null ? _b : "rds-theme.css";
|
|
1737
|
+
if (!CSS_FILE.test(cssFile)) throw new Error(`emitClaudeMd: cssFile ${preview(cssFile)} is not a valid .css path`);
|
|
1738
|
+
if (opts.cssUrl !== void 0 && !CSS_URL.test(opts.cssUrl))
|
|
1739
|
+
throw new Error(`emitClaudeMd: cssUrl ${preview(opts.cssUrl)} is not a valid http(s) URL`);
|
|
1740
|
+
const install = (_c = opts.install) != null ? _c : "pnpm add @rojaostudio/ds";
|
|
1741
|
+
if (!INSTALL.test(install)) throw new Error(`emitClaudeMd: install ${preview(install)} is not an install command of @rojaostudio/ds`);
|
|
1742
|
+
const target = (_d = opts.target) != null ? _d : "claude";
|
|
1743
|
+
if (!Object.prototype.hasOwnProperty.call(TARGET_FILES, target)) throw new Error(`emitClaudeMd: unknown target ${preview(target)}`);
|
|
1744
|
+
const f = CLAUDE_MD_FOUNDATION;
|
|
1745
|
+
const own = theme.vars ? Object.keys(theme.vars) : [];
|
|
1746
|
+
const themeImport = opts.cssUrl ? `@import "@rojaostudio/ds/styles/rds.css";
|
|
1747
|
+
@import url("${opts.cssUrl}"); /* the brand theme, AFTER rds.css */` : `@import "@rojaostudio/ds/styles/rds.css";
|
|
1748
|
+
@import "./${cssFile}"; /* the brand theme, AFTER rds.css */`;
|
|
1749
|
+
const text = `# Design System \u2014 ${Name}
|
|
1251
1750
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
- **Body font:** ${fontTitle(bodyFont)} \xB7 **Display font:** ${fontTitle(displayFont)} (load from Google Fonts; never substitute)
|
|
1256
|
-
- **Default radius:** \`${primitives.radius.md}px\` (\`rounded-lg\`). Pills/avatars fully round.
|
|
1257
|
-
- **Light AND dark are first-class** \u2014 every screen must work in both.
|
|
1751
|
+
> This project uses **Roj\xE3o DS 2.0** with the **${Name}** brand${description}. When you build any
|
|
1752
|
+
> UI \u2014 components, pages, screens \u2014 follow the rules below so every screen looks like the same
|
|
1753
|
+
> product. Do not invent colors, fonts, spacing or radii.
|
|
1258
1754
|
|
|
1259
|
-
##
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
> One sheet can hold **several themes** \u2014 each as its own \`theme-<name>\` class.
|
|
1275
|
-
> Switch with the class (one product per app, or many on one page).${cssUrl ? `
|
|
1276
|
-
> Edit the theme at the source and this same URL updates **all your apps at once**.` : ""}
|
|
1277
|
-
3. Style everything with the semantic tokens below, as CSS variables \u2014 they already
|
|
1278
|
-
flip between light and dark. You never write a hex value in a component.
|
|
1279
|
-
\`\`\`css
|
|
1280
|
-
.card { background: var(--surface-default); color: var(--text-primary);
|
|
1281
|
-
border: 1px solid var(--border-default); border-radius: ${primitives.radius.md}px; }
|
|
1282
|
-
\`\`\`
|
|
1755
|
+
## Setup (once per app)
|
|
1756
|
+
\`\`\`bash
|
|
1757
|
+
${install}
|
|
1758
|
+
\`\`\`
|
|
1759
|
+
\`\`\`css
|
|
1760
|
+
/* root stylesheet */
|
|
1761
|
+
${themeImport}
|
|
1762
|
+
\`\`\`
|
|
1763
|
+
- \`rds.css\` carries the tokens and every component's styles, in cascade layers
|
|
1764
|
+
(\`rds.theme\`, \`rds.tokens\`, \`rds.components\`). No Tailwind, no other stylesheet.
|
|
1765
|
+
- ${opts.cssUrl ? `The theme is served from \`${opts.cssUrl}\`.` : `\`${cssFile}\` is the brand theme, generated \u2014 a file **you own**; commit it. Regenerate it
|
|
1766
|
+
with \`npx rojao-ds init\` instead of editing it by hand.`}
|
|
1767
|
+
- Dark mode: \`class="dark"\` on \`<html>\` (or any element). Light AND dark are first-class.
|
|
1768
|
+
- Brand plate: \`class="ds-plate"\` on a section paints it with the brand color; the roles inside
|
|
1769
|
+
flip so text and components stay legible.
|
|
1283
1770
|
|
|
1284
|
-
##
|
|
1285
|
-
|
|
1771
|
+
## Components first
|
|
1772
|
+
Use the design system components before writing your own markup. Import each from its own path:
|
|
1773
|
+
\`\`\`tsx
|
|
1774
|
+
import { Button } from "@rojaostudio/ds/components/button";
|
|
1775
|
+
import { IconButton } from "@rojaostudio/ds/components/icon-button";
|
|
1776
|
+
import { Tooltip } from "@rojaostudio/ds/components/tooltip";
|
|
1777
|
+
\`\`\`
|
|
1778
|
+
Available, among others: ${CLAUDE_MD_COMPONENTS.map((c) => `\`${c}\``).join(" \xB7 ")}.
|
|
1779
|
+
- Components bring their own CSS (inside \`rds.css\`). Don't restyle their internals, don't wrap
|
|
1780
|
+
them in Tailwind classes, don't rebuild them with shadcn/ui.
|
|
1781
|
+
- Every \`IconButton\` goes inside a \`Tooltip\` with the same text as its \`label\`.
|
|
1286
1782
|
|
|
1287
|
-
##
|
|
1288
|
-
|
|
1783
|
+
## Theme roles \u2014 ALWAYS use these in your own CSS, NEVER hardcode a color
|
|
1784
|
+
They already flip between light, dark and the brand plate.
|
|
1785
|
+
${roleTable(theme)}
|
|
1289
1786
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1787
|
+
Every role pairs a fill with its \`--text-on-*\` text. Example:
|
|
1788
|
+
\`\`\`css
|
|
1789
|
+
.panel { background: var(--surface-card); color: var(--text-body);
|
|
1790
|
+
border: var(--border-width) solid var(--border-default);
|
|
1791
|
+
border-radius: var(--radius-card); padding: var(--space-16); }
|
|
1792
|
+
\`\`\`${own.length ? `
|
|
1292
1793
|
|
|
1293
|
-
|
|
1294
|
-
**Line heights:** ${Object.entries(primitives.lineHeight).map(([k, v]) => `\`${k}\`=${v}`).join(" \xB7 ")}
|
|
1794
|
+
Brand's own variables (same value in every mode): ${own.map((v) => `\`${v}\``).join(" \xB7 ")}.` : ""}
|
|
1295
1795
|
|
|
1296
|
-
##
|
|
1297
|
-
${
|
|
1796
|
+
## Foundation tokens
|
|
1797
|
+
- **Spacing** (padding, margin, gap) \u2014 ONLY: ${f.space.map((v) => `\`${v}\``).join(" \xB7 ")}
|
|
1798
|
+
- **Radius:** ${f.radius.map((v) => `\`${v}\``).join(" \xB7 ")}
|
|
1799
|
+
- **Type** \u2014 \`--type-<style>-size\` / \`--type-<style>-line\`, styles: ${f.type.map((v) => `\`${v}\``).join(" \xB7 ")}
|
|
1800
|
+
- **Font:** \`font-family: var(${f.font})\` \u2014 never substitute.
|
|
1298
1801
|
|
|
1299
1802
|
## Rules (do / don't)
|
|
1300
|
-
- \u2705 Primary
|
|
1301
|
-
- \u2705
|
|
1302
|
-
- \u2705
|
|
1303
|
-
- \u2705
|
|
1304
|
-
- \u2705
|
|
1305
|
-
- \
|
|
1306
|
-
- \u274C Never
|
|
1307
|
-
|
|
1308
|
-
- \u274C Never
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
You are NOT replacing shadcn. Use shadcn/ui components and apply THIS brand as the theme:
|
|
1312
|
-
map the tokens above onto shadcn's CSS variables (\`--background\` \u2192 \`--surface-page\`,
|
|
1313
|
-
\`--foreground\` \u2192 \`--text-primary\`, \`--primary\` \u2192 \`--brand-primary\`, \`--border\` \u2192
|
|
1314
|
-
\`--border-default\`, \`--ring\` \u2192 \`--border-focus\`, \`--radius\` \u2192 \`${primitives.radius.md}px\`).
|
|
1315
|
-
Build with shadcn's component shapes; paint them with these tokens. Don't restyle components from scratch.
|
|
1316
|
-
|
|
1317
|
-
## Primitive palette (reference \u2014 prefer the semantic tokens above)
|
|
1318
|
-
\`\`\`
|
|
1319
|
-
neutral 50 ${neutral[50]} \xB7 500 ${neutral[500]} \xB7 900 ${neutral[900]}
|
|
1320
|
-
\`\`\`
|
|
1803
|
+
- \u2705 Primary action = \`Button\` (tone action paints \`--colors-secondary-default\` + \`--text-on-secondary\`).
|
|
1804
|
+
- \u2705 Page uses \`--surface-page\`; cards \`--surface-card\`; body text \`--text-body\`.
|
|
1805
|
+
- \u2705 \`--colors-accent-default\` is the highlight \u2014 use it sparingly.
|
|
1806
|
+
- \u2705 Spacing, radius and type come ONLY from the foundation tokens above.
|
|
1807
|
+
- \u2705 Your own CSS is unlayered, so it already wins over the design system \u2014 no \`!important\`.
|
|
1808
|
+
- \u274C Never write a raw hex (\`#000\`) or a Tailwind palette class (\`bg-emerald-500\`) in a component.
|
|
1809
|
+
- \u274C Never use a brand fill as a border or as text on a surface: contrast is guaranteed only for a
|
|
1810
|
+
fill + its \`--text-on-*\`.
|
|
1811
|
+
- \u274C Never hardcode dark-mode colors \u2014 the roles already flip. Don't add a second accent color.
|
|
1812
|
+
- \u274C Don't use the 1.x stylesheet (\`styles/base.css\`), its utilities (\`bg-surface-*\`, \`text-fg-*\`) or
|
|
1813
|
+
\`theme-<name>\` classes.
|
|
1321
1814
|
|
|
1322
|
-
<!-- Generated by Roj\xE3o DS \u2014 file: ${fileLabel(
|
|
1815
|
+
<!-- Generated by Roj\xE3o DS (npx rojao-ds init) \u2014 file: ${fileLabel(target)}. Claude Code, Cursor and other agents read it from the project root. -->
|
|
1323
1816
|
`;
|
|
1817
|
+
if (MARKERS.test(text)) throw new Error("emitClaudeMd: the generated text contains a rojao-ds marker");
|
|
1818
|
+
return text;
|
|
1324
1819
|
}
|
|
1325
1820
|
|
|
1326
|
-
export { ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, refToHex, relativeLuminance, resolveTheme, roleVar };
|
|
1821
|
+
export { RDS_CONTRAST_PAIRS, RDS_NON_TEXT_PAIRS, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, ROLES, RdsValidationError, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, isSafeColor, isSafeFont, isSafeVarName, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar };
|