@rojaostudio/ds-core 1.1.0-next.0 → 1.1.0-next.10
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-CFCKPKAF.js → chunk-LP6UA4EW.js} +575 -170
- package/dist/{chunk-3QTDO2HW.js → chunk-PV6GFEAM.js} +1 -2
- package/dist/generate.d.ts +116 -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 +4 -3
|
@@ -675,27 +675,209 @@ 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");
|
|
684
|
-
|
|
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
|
+
}
|
|
786
|
+
function palette(name, custom) {
|
|
787
|
+
const own = custom == null ? void 0 : custom[name];
|
|
788
|
+
if (typeof own === "string") {
|
|
789
|
+
if (!isHex(own)) throw new Error(`rdsTheme: palette "${name}" is not a hex colour`);
|
|
790
|
+
return buildScale(own);
|
|
791
|
+
}
|
|
792
|
+
if (own) return own;
|
|
685
793
|
const p = palettes[name];
|
|
686
794
|
if (!p || typeof p === "string") throw new Error(`rdsTheme: unknown palette "${name}"`);
|
|
687
795
|
return p;
|
|
688
796
|
}
|
|
689
|
-
function ramp(c) {
|
|
797
|
+
function ramp(c, custom) {
|
|
690
798
|
var _a;
|
|
691
799
|
if (isHex(c)) return { base: c, scale: buildScale(c) };
|
|
692
|
-
const m = c.match(/^([a-zA-Z]
|
|
693
|
-
if (m)
|
|
800
|
+
const m = c.match(/^([a-zA-Z][a-zA-Z0-9]*)-(\d{2,3})$/);
|
|
801
|
+
if (m) {
|
|
802
|
+
const scale = palette(m[1], custom);
|
|
803
|
+
return { base: (_a = scale[Number(m[2])]) != null ? _a : refToHex(c), scale };
|
|
804
|
+
}
|
|
694
805
|
const hex = refToHex(c);
|
|
695
806
|
if (!hex) throw new Error(`rdsTheme: cannot resolve colour "${c}"`);
|
|
696
807
|
return { base: hex, scale: buildScale(hex) };
|
|
697
808
|
}
|
|
698
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 defaultWarn = (m) => {
|
|
878
|
+
var _a;
|
|
879
|
+
return (_a = globalThis.console) == null ? void 0 : _a.warn(m);
|
|
880
|
+
};
|
|
699
881
|
var ROLES = [
|
|
700
882
|
["colors/primary/light", "d", "b"],
|
|
701
883
|
["colors/primary/default", "d", "b"],
|
|
@@ -799,22 +981,38 @@ var ROLES = [
|
|
|
799
981
|
["colors/state/info-strong", "d", "d"],
|
|
800
982
|
["colors/state/neutral-strong", "d", "d"],
|
|
801
983
|
["text/on/primary-subtle", "d", "b"],
|
|
802
|
-
["surface/tint/subtle", "d", "b"]
|
|
984
|
+
["surface/tint/subtle", "d", "b"],
|
|
985
|
+
["border/error", "d", "d"]
|
|
803
986
|
];
|
|
987
|
+
var awayFrom = (bg) => contrastRatio(bg, BLACK) >= contrastRatio(bg, WHITE) ? "darker" : "lighter";
|
|
804
988
|
var TINT_DEFAULT = 200;
|
|
805
989
|
var lighterStep = (s) => s - 100 ;
|
|
806
990
|
var roleVar = (role) => `--${role.replaceAll("/", "-")}`;
|
|
807
|
-
function generateRdsTheme(def) {
|
|
991
|
+
function generateRdsTheme(def, opts = {}) {
|
|
992
|
+
var _a, _b;
|
|
993
|
+
const warn = (_a = opts.warn) != null ? _a : defaultWarn;
|
|
994
|
+
const where = `generateRdsTheme(${preview(def == null ? void 0 : def.name)})`;
|
|
995
|
+
if (!(def == null ? void 0 : def.brand) || typeof def.brand !== "object") throw new Error(`${where}: brand is missing`);
|
|
996
|
+
checkPalettes(def.palettes, where);
|
|
808
997
|
const b = def.brand;
|
|
809
|
-
const
|
|
810
|
-
const
|
|
811
|
-
const
|
|
812
|
-
const
|
|
998
|
+
const own = def.palettes;
|
|
999
|
+
const P = ramp(b.primary, own);
|
|
1000
|
+
const S = b.secondary ? ramp(b.secondary, own) : P;
|
|
1001
|
+
const A = b.accent ? ramp(b.accent, own) : P;
|
|
1002
|
+
const N = def.text && ((own == null ? void 0 : own[def.text]) || palettes[def.text] && typeof palettes[def.text] !== "string") ? palette(def.text, own) : palette("zinc");
|
|
813
1003
|
const L = palette("blue");
|
|
814
1004
|
const red = palette("red"), green = palette("green"), orange = palette("orange");
|
|
815
1005
|
const teal = palette("teal"), purple = palette("purple");
|
|
816
|
-
const
|
|
817
|
-
|
|
1006
|
+
const lightBgs = [WHITE, N[100]];
|
|
1007
|
+
let heading;
|
|
1008
|
+
if (b.heading) {
|
|
1009
|
+
heading = ramp(b.heading, own).base;
|
|
1010
|
+
if (!readsOn(heading, lightBgs)) {
|
|
1011
|
+
const worst = Math.min(...lightBgs.map((bg) => contrastRatio(heading, bg)));
|
|
1012
|
+
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.`);
|
|
1013
|
+
}
|
|
1014
|
+
} else heading = readableFrom(P.base, P.scale, lightBgs, "darker", BLACK);
|
|
1015
|
+
const invert = b.invert ? ramp(b.invert, own).base : WHITE;
|
|
818
1016
|
const l = {
|
|
819
1017
|
"colors/primary/light": P.scale[500],
|
|
820
1018
|
"colors/primary/default": P.base,
|
|
@@ -850,13 +1048,12 @@ function generateRdsTheme(def) {
|
|
|
850
1048
|
"border/strong-hover": N[600],
|
|
851
1049
|
"surface/tint/default": A.scale[TINT_DEFAULT],
|
|
852
1050
|
"surface/tint/strong": A.scale[300],
|
|
853
|
-
"text/on/tint": BLACK,
|
|
854
1051
|
// Light selection fill (pressed Toggle, choice tile): one step lighter than the default tint
|
|
855
1052
|
// (200 → 100, 100 → 50).
|
|
856
1053
|
"surface/tint/subtle": A.scale[lighterStep(TINT_DEFAULT)],
|
|
857
1054
|
"surface/action/default": L[100],
|
|
858
1055
|
"surface/action/strong": L[200],
|
|
859
|
-
"text/on/action-tonal": P.scale[700],
|
|
1056
|
+
"text/on/action-tonal": readableFrom(P.scale[700], P.scale, [L[100]], "darker", BLACK),
|
|
860
1057
|
"surface/lift/action": P.scale[50],
|
|
861
1058
|
"surface/lift/action-strong": L[100],
|
|
862
1059
|
"surface/band/action": L[100],
|
|
@@ -891,8 +1088,7 @@ function generateRdsTheme(def) {
|
|
|
891
1088
|
"text/warning": orange[700],
|
|
892
1089
|
"surface/neutral": N[100],
|
|
893
1090
|
"text/neutral": N[800],
|
|
894
|
-
"chart/series/
|
|
895
|
-
"chart/series/2": orange[500],
|
|
1091
|
+
"chart/series/2": orange[600],
|
|
896
1092
|
"chart/series/3": teal[500],
|
|
897
1093
|
"chart/series/4": purple[500],
|
|
898
1094
|
"chart/series/5": green[500],
|
|
@@ -917,8 +1113,19 @@ function generateRdsTheme(def) {
|
|
|
917
1113
|
["colors/state/neutral", "text/on/neutral"],
|
|
918
1114
|
["surface/cover", "text/on/cover"],
|
|
919
1115
|
["surface/lift/action", "text/on/lift-action"],
|
|
920
|
-
["colors/primary/dark", "text/on/primary-strong"]
|
|
1116
|
+
["colors/primary/dark", "text/on/primary-strong"],
|
|
1117
|
+
["surface/tint/default", "text/on/tint"]
|
|
921
1118
|
]) l[text] = on(l[fill]);
|
|
1119
|
+
[l["colors/primary/active"]] = stateFills(P.scale, l["colors/primary/default"], l["text/on/primary"], [l["colors/primary/active"]]);
|
|
1120
|
+
[l["colors/secondary/hover"], l["colors/secondary/active"]] = stateFills(
|
|
1121
|
+
S.scale,
|
|
1122
|
+
l["colors/secondary/default"],
|
|
1123
|
+
l["text/on/secondary"],
|
|
1124
|
+
[l["colors/secondary/hover"], l["colors/secondary/active"]]
|
|
1125
|
+
);
|
|
1126
|
+
[l["colors/accent/hover"]] = stateFills(A.scale, l["colors/accent/default"], l["text/on/accent"], [l["colors/accent/hover"]]);
|
|
1127
|
+
l["border/error"] = readableFrom(l["colors/state/error"], red, [l["surface/card"]], awayFrom(l["surface/card"]), BLACK, NON_TEXT);
|
|
1128
|
+
l["chart/series/1"] = readableFrom(P.scale[600], P.scale, [l["surface/card"]], awayFrom(l["surface/card"]), BLACK, NON_TEXT);
|
|
922
1129
|
l["logo/primary"] = l["colors/primary/default"];
|
|
923
1130
|
l["logo/signature"] = l["colors/primary/default"];
|
|
924
1131
|
l["logo/accent"] = l["colors/accent/logo"];
|
|
@@ -935,7 +1142,7 @@ function generateRdsTheme(def) {
|
|
|
935
1142
|
"border/default": N[800],
|
|
936
1143
|
"border/strong": N[500],
|
|
937
1144
|
"border/strong-hover": N[400],
|
|
938
|
-
"text/heading": N[50],
|
|
1145
|
+
"text/heading": readableFrom(N[50], N, [N[900]], "lighter", WHITE),
|
|
939
1146
|
"text/body": N[100],
|
|
940
1147
|
"text/muted": N[300],
|
|
941
1148
|
"text/subtle": N[400],
|
|
@@ -962,15 +1169,13 @@ function generateRdsTheme(def) {
|
|
|
962
1169
|
"colors/accent/mark": A.scale[300],
|
|
963
1170
|
"surface/tint/default": A.scale[900],
|
|
964
1171
|
"surface/tint/strong": A.scale[800],
|
|
965
|
-
"text/on/tint": WHITE,
|
|
966
1172
|
// No step below 900 in dark: subtle is the default tint.
|
|
967
1173
|
"surface/tint/subtle": A.scale[900],
|
|
968
1174
|
"surface/action/default": L[900],
|
|
969
1175
|
"surface/action/strong": L[800],
|
|
970
|
-
"text/on/action-tonal": P.scale[200],
|
|
1176
|
+
"text/on/action-tonal": readableFrom(P.scale[200], P.scale, [L[900]], "lighter", WHITE),
|
|
971
1177
|
"surface/lift/action": P.scale[800],
|
|
972
1178
|
"surface/lift/action-strong": P.scale[700],
|
|
973
|
-
"text/on/lift-action": WHITE,
|
|
974
1179
|
"colors/state/error-strong": red[400],
|
|
975
1180
|
"colors/state/success-strong": green[300],
|
|
976
1181
|
"colors/state/warning-strong": orange[400],
|
|
@@ -985,7 +1190,6 @@ function generateRdsTheme(def) {
|
|
|
985
1190
|
"text/warning": orange[300],
|
|
986
1191
|
"surface/neutral": N[800],
|
|
987
1192
|
"text/neutral": N[200],
|
|
988
|
-
"chart/series/1": P.scale[400],
|
|
989
1193
|
"chart/series/2": orange[400],
|
|
990
1194
|
"chart/series/3": teal[400],
|
|
991
1195
|
"chart/series/4": purple[400],
|
|
@@ -1004,12 +1208,28 @@ function generateRdsTheme(def) {
|
|
|
1004
1208
|
["colors/secondary/default", "text/on/secondary"],
|
|
1005
1209
|
["colors/accent/default", "text/on/accent"],
|
|
1006
1210
|
["colors/accent/mark", "text/on/accent-mark"],
|
|
1007
|
-
["colors/primary/dark", "text/on/primary-strong"]
|
|
1211
|
+
["colors/primary/dark", "text/on/primary-strong"],
|
|
1212
|
+
["surface/tint/default", "text/on/tint"],
|
|
1213
|
+
["surface/lift/action", "text/on/lift-action"]
|
|
1008
1214
|
]) d[text] = on(d[fill]);
|
|
1215
|
+
[d["colors/primary/active"]] = stateFills(P.scale, d["colors/primary/default"], d["text/on/primary"], [d["colors/primary/active"]]);
|
|
1216
|
+
[d["colors/secondary/hover"], d["colors/secondary/active"]] = stateFills(
|
|
1217
|
+
S.scale,
|
|
1218
|
+
d["colors/secondary/default"],
|
|
1219
|
+
d["text/on/secondary"],
|
|
1220
|
+
[d["colors/secondary/hover"], d["colors/secondary/active"]]
|
|
1221
|
+
);
|
|
1222
|
+
[d["colors/accent/hover"]] = stateFills(A.scale, d["colors/accent/default"], d["text/on/accent"], [d["colors/accent/hover"]]);
|
|
1223
|
+
d["border/error"] = readableFrom(d["colors/state/error-strong"], red, [d["surface/card"]], awayFrom(d["surface/card"]), WHITE, NON_TEXT);
|
|
1224
|
+
d["chart/series/1"] = readableFrom(P.scale[400], P.scale, [d["surface/card"]], awayFrom(d["surface/card"]), WHITE, NON_TEXT);
|
|
1009
1225
|
const plate = l["colors/primary/default"];
|
|
1010
1226
|
const ink = on(plate);
|
|
1011
1227
|
const inkA = ink === WHITE ? white : black;
|
|
1012
1228
|
const antiInk = ink === WHITE ? black : white;
|
|
1229
|
+
const lp = relativeLuminance(plate);
|
|
1230
|
+
const isPlate = (c) => c.toLowerCase() === plate.toLowerCase();
|
|
1231
|
+
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;
|
|
1232
|
+
const onPrimaryStrong = contrastRatio(plate, P.scale[100]) >= AA ? plate : on(P.scale[100]);
|
|
1013
1233
|
const br = {
|
|
1014
1234
|
"colors/primary/light": P.scale[300],
|
|
1015
1235
|
"colors/primary/default": ink,
|
|
@@ -1023,7 +1243,7 @@ function generateRdsTheme(def) {
|
|
|
1023
1243
|
"text/body": ink,
|
|
1024
1244
|
"text/link": contrastRatio(l["colors/accent/invert"], plate) >= 4.5 ? l["colors/accent/invert"] : ink,
|
|
1025
1245
|
"surface/page": plate,
|
|
1026
|
-
"surface/card":
|
|
1246
|
+
"surface/card": plateCard,
|
|
1027
1247
|
"surface/panel": plate,
|
|
1028
1248
|
"border/default": inkA(20),
|
|
1029
1249
|
"surface/tint/default": inkA(10),
|
|
@@ -1045,7 +1265,7 @@ function generateRdsTheme(def) {
|
|
|
1045
1265
|
"surface/lift/action": inkA(10),
|
|
1046
1266
|
"text/on/lift-action": ink,
|
|
1047
1267
|
"surface/lift/action-strong": inkA(20),
|
|
1048
|
-
"text/on/primary-strong":
|
|
1268
|
+
"text/on/primary-strong": onPrimaryStrong,
|
|
1049
1269
|
"text/subtle": inkA(60),
|
|
1050
1270
|
"surface/muted": inkA(10),
|
|
1051
1271
|
"surface/muted-strong": inkA(20),
|
|
@@ -1053,11 +1273,19 @@ function generateRdsTheme(def) {
|
|
|
1053
1273
|
"border/strong-hover": inkA(80),
|
|
1054
1274
|
"text/on/primary-subtle": inkA(30)
|
|
1055
1275
|
};
|
|
1276
|
+
[br["colors/primary/active"]] = stateFills(P.scale, br["colors/primary/default"], br["text/on/primary"], [br["colors/primary/active"]]);
|
|
1277
|
+
[br["colors/secondary/hover"], br["colors/secondary/active"]] = stateFills(
|
|
1278
|
+
S.scale,
|
|
1279
|
+
br["colors/secondary/default"],
|
|
1280
|
+
br["text/on/secondary"],
|
|
1281
|
+
[br["colors/secondary/hover"], br["colors/secondary/active"]]
|
|
1282
|
+
);
|
|
1283
|
+
const brandError = readableFrom(red[300], red, [plateCard], awayFrom(plateCard), ink, NON_TEXT);
|
|
1056
1284
|
const out = { light: {}, dark: {}, brand: {} };
|
|
1057
1285
|
const pick = (src, role) => {
|
|
1058
|
-
var
|
|
1286
|
+
var _a2;
|
|
1059
1287
|
const table = src === "l" ? l : src === "d" ? d : br;
|
|
1060
|
-
const v = (
|
|
1288
|
+
const v = (_a2 = table[role]) != null ? _a2 : l[role];
|
|
1061
1289
|
if (v === void 0) throw new Error(`rdsTheme: no value for "${role}"`);
|
|
1062
1290
|
return v;
|
|
1063
1291
|
};
|
|
@@ -1066,25 +1294,65 @@ function generateRdsTheme(def) {
|
|
|
1066
1294
|
out.dark[roleVar(role)] = pick(darkSrc, role);
|
|
1067
1295
|
out.brand[roleVar(role)] = pick(brandSrc, role);
|
|
1068
1296
|
}
|
|
1297
|
+
out.brand[roleVar("border/error")] = brandError;
|
|
1298
|
+
assertSafeTheme(out, where);
|
|
1069
1299
|
return out;
|
|
1070
1300
|
}
|
|
1301
|
+
var RDS_SCOPE_SELECTORS = [
|
|
1302
|
+
":root",
|
|
1303
|
+
".ds-scope",
|
|
1304
|
+
"[data-rds-scope]",
|
|
1305
|
+
".dark",
|
|
1306
|
+
"[data-rds-mode]",
|
|
1307
|
+
".ds-plate",
|
|
1308
|
+
"[data-rds-plate]"
|
|
1309
|
+
];
|
|
1310
|
+
var RDS_TOKEN_SCOPE = RDS_SCOPE_SELECTORS.join(", ");
|
|
1311
|
+
var split = (list) => list.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1312
|
+
var rootAnchored = (sel) => /^(:root|html)(?![\w-])/i.test(sel);
|
|
1313
|
+
var subject = (sel) => {
|
|
1314
|
+
var _a;
|
|
1315
|
+
return (_a = sel.trim().split(/\s*[>+~]\s*|\s+/).pop()) != null ? _a : sel;
|
|
1316
|
+
};
|
|
1317
|
+
var COVERING = /\.(ds-scope|dark|ds-plate)(?![\w-])|\[\s*data-rds-(scope|mode|plate)\s*([~|^$*]?=[^\]]*)?\]/;
|
|
1318
|
+
var covered = (sel) => {
|
|
1319
|
+
const s = subject(sel);
|
|
1320
|
+
return /^(:root|html)(?![\w-])/i.test(s) || COVERING.test(s);
|
|
1321
|
+
};
|
|
1071
1322
|
function emitRdsCss(theme, opts = {}) {
|
|
1072
1323
|
var _a, _b, _c;
|
|
1073
|
-
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
1324
|
+
assertSafeTheme(theme, "emitRdsCss");
|
|
1325
|
+
const badSel = ["scope", "dark", "plate"].filter((k) => opts[k] !== void 0 && !isSafeSelectorList(opts[k]));
|
|
1326
|
+
if (badSel.length)
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
`emitRdsCss: invalid selector in ${badSel.map((k) => `${k} ${preview(opts[k])}`).join(", ")} (no braces, ";", comments, backslash, "@", "<" or line breaks).`
|
|
1329
|
+
);
|
|
1330
|
+
const scopes = split((_a = opts.scope) != null ? _a : ":root, .ds-scope, [data-rds-scope]");
|
|
1331
|
+
const darks = split((_b = opts.dark) != null ? _b : '.dark, [data-rds-mode="dark"]');
|
|
1332
|
+
const plates = split((_c = opts.plate) != null ? _c : ".ds-plate, [data-rds-plate]");
|
|
1333
|
+
const block2 = (sels, map, only) => {
|
|
1077
1334
|
const lines = Object.entries(map).filter(([k, v]) => !only || only[k] !== v).map(([k, v]) => ` ${k}: ${k === "--type-font-mono" ? `"${v}", monospace` : v};`);
|
|
1078
|
-
return `${
|
|
1335
|
+
return `${sels.join(", ")} {
|
|
1079
1336
|
${lines.join("\n")}
|
|
1080
1337
|
}`;
|
|
1081
1338
|
};
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1339
|
+
const darkSels = scopes.flatMap(
|
|
1340
|
+
(s) => darks.flatMap((d) => {
|
|
1341
|
+
if (rootAnchored(d)) return s === ":root" ? [d] : [`${d} ${s}`];
|
|
1342
|
+
return s === ":root" ? [`:root${d}`, d] : [`${s}${d}`, `${d} ${s}`];
|
|
1343
|
+
})
|
|
1344
|
+
);
|
|
1345
|
+
if (!opts.allowUncovered) {
|
|
1346
|
+
const bad = [...new Set([...scopes, ...darkSels, ...plates].filter((sel) => !covered(sel)))];
|
|
1347
|
+
if (bad.length)
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
`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.`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1084
1352
|
return [
|
|
1085
|
-
block2(
|
|
1086
|
-
block2(
|
|
1087
|
-
block2(
|
|
1353
|
+
block2(scopes, __spreadValues(__spreadValues({}, theme.light), theme.vars)),
|
|
1354
|
+
block2(darkSels, theme.dark, theme.light),
|
|
1355
|
+
block2(plates, theme.brand, theme.light)
|
|
1088
1356
|
].join("\n\n") + "\n";
|
|
1089
1357
|
}
|
|
1090
1358
|
function rdsContrast(theme, mode, fg, bg) {
|
|
@@ -1092,6 +1360,121 @@ function rdsContrast(theme, mode, fg, bg) {
|
|
|
1092
1360
|
if (!/^#[0-9a-f]{6}$/i.test(a) || !/^#[0-9a-f]{6}$/i.test(c)) return null;
|
|
1093
1361
|
return contrastRatio(a, c);
|
|
1094
1362
|
}
|
|
1363
|
+
var RDS_CONTRAST_PAIRS = [
|
|
1364
|
+
["text/heading", "surface/page"],
|
|
1365
|
+
["text/heading", "surface/card"],
|
|
1366
|
+
["text/body", "surface/page"],
|
|
1367
|
+
["text/body", "surface/card"],
|
|
1368
|
+
["text/muted", "surface/card"],
|
|
1369
|
+
["text/link", "surface/card"],
|
|
1370
|
+
["text/on/primary", "colors/primary/default"],
|
|
1371
|
+
["text/on/primary", "colors/primary/active"],
|
|
1372
|
+
["text/on/secondary", "colors/secondary/default"],
|
|
1373
|
+
["text/on/secondary", "colors/secondary/hover"],
|
|
1374
|
+
["text/on/secondary", "colors/secondary/active"],
|
|
1375
|
+
["text/on/accent", "colors/accent/default"],
|
|
1376
|
+
["text/on/accent", "colors/accent/hover"],
|
|
1377
|
+
["text/on/accent-mark", "colors/accent/mark"],
|
|
1378
|
+
["text/on/tint", "surface/tint/default"],
|
|
1379
|
+
["text/on/action-tonal", "surface/action/default"],
|
|
1380
|
+
["text/on/lift-action", "surface/lift/action"],
|
|
1381
|
+
["text/on/primary-strong", "colors/primary/dark"],
|
|
1382
|
+
["text/on/cover", "surface/cover"],
|
|
1383
|
+
["text/on/error", "colors/state/error"],
|
|
1384
|
+
["text/on/success", "colors/state/success"],
|
|
1385
|
+
["text/on/info", "colors/state/info"],
|
|
1386
|
+
["text/on/warning", "colors/state/warning"],
|
|
1387
|
+
["text/on/neutral", "colors/state/neutral"],
|
|
1388
|
+
["text/error", "surface/error"]
|
|
1389
|
+
];
|
|
1390
|
+
function rdsContrastReport(theme) {
|
|
1391
|
+
const out = [];
|
|
1392
|
+
for (const mode of ["light", "dark", "brand"])
|
|
1393
|
+
for (const [fg, bg] of RDS_CONTRAST_PAIRS) {
|
|
1394
|
+
const ratio = rdsContrast(theme, mode, fg, bg);
|
|
1395
|
+
if (ratio !== null && ratio < AA) out.push({ mode, fg, bg, ratio: Math.round(ratio * 100) / 100 });
|
|
1396
|
+
}
|
|
1397
|
+
return out;
|
|
1398
|
+
}
|
|
1399
|
+
var isColourRef = (v) => /^[a-z][a-z0-9-]*\/[a-z]?\d+$/i.test(v);
|
|
1400
|
+
var ADDED_ROLES = {
|
|
1401
|
+
// Figma: light → base colors/state/error, dark → base dark/colors/state/error-strong, brand → base dark/text/error.
|
|
1402
|
+
"border/error": { light: "colors/state/error", dark: "colors/state/error-strong", brand: "text/error" }
|
|
1403
|
+
};
|
|
1404
|
+
function rdsThemeFromTable(table, opts = {}) {
|
|
1405
|
+
var _a, _b, _c;
|
|
1406
|
+
const where = `rdsThemeFromTable(${preview(table == null ? void 0 : table.name)})`;
|
|
1407
|
+
if (!table || typeof table !== "object") throw new Error(`${where}: the table is not an object`);
|
|
1408
|
+
const check = collector(where);
|
|
1409
|
+
if (!table.primitives || typeof table.primitives !== "object" || Array.isArray(table.primitives))
|
|
1410
|
+
check.problems.push("primitives: not an object");
|
|
1411
|
+
else
|
|
1412
|
+
for (const [name, value] of Object.entries(table.primitives)) {
|
|
1413
|
+
check.name(`primitives ${preview(name)}`, name);
|
|
1414
|
+
check.color(`primitives.${name}`, value);
|
|
1415
|
+
}
|
|
1416
|
+
if (table.vars !== void 0) {
|
|
1417
|
+
if (!table.vars || typeof table.vars !== "object" || Array.isArray(table.vars)) check.problems.push("vars: not an object");
|
|
1418
|
+
else
|
|
1419
|
+
for (const [name, value] of Object.entries(table.vars)) {
|
|
1420
|
+
check.name(`vars ${preview(name)}`, name);
|
|
1421
|
+
if (typeof value !== "string") check.problems.push(`vars.${name}: not a string`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
check.done();
|
|
1425
|
+
const out = { light: {}, dark: {}, brand: {} };
|
|
1426
|
+
const problems = [];
|
|
1427
|
+
const derived = /* @__PURE__ */ new Set();
|
|
1428
|
+
for (const mode of ["light", "dark", "brand"]) {
|
|
1429
|
+
const roles = (_a = table.modes) == null ? void 0 : _a[mode];
|
|
1430
|
+
if (!roles) {
|
|
1431
|
+
problems.push(`mode "${mode}" is missing`);
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
for (const [role] of ROLES) {
|
|
1435
|
+
let ref2 = roles[role];
|
|
1436
|
+
const added = (_b = ADDED_ROLES[role]) == null ? void 0 : _b[mode];
|
|
1437
|
+
if (ref2 === void 0 && added && roles[added] !== void 0) {
|
|
1438
|
+
ref2 = roles[added];
|
|
1439
|
+
derived.add(role);
|
|
1440
|
+
}
|
|
1441
|
+
if (ref2 === void 0) problems.push(`${mode}: role "${role}" is missing`);
|
|
1442
|
+
else if (isColourRef(ref2)) {
|
|
1443
|
+
const value = table.primitives[ref2];
|
|
1444
|
+
if (value === void 0) problems.push(`${mode}: "${role}" points to unknown primitive "${ref2}"`);
|
|
1445
|
+
else out[mode][roleVar(role)] = value.toLowerCase();
|
|
1446
|
+
} else out[mode][roleVar(role)] = ref2;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const valueOf = (field, ref2) => {
|
|
1450
|
+
if (!isColourRef(ref2)) return ref2;
|
|
1451
|
+
const value = table.primitives[ref2];
|
|
1452
|
+
if (value === void 0) problems.push(`${field} points to unknown primitive "${ref2}"`);
|
|
1453
|
+
return value == null ? void 0 : value.toLowerCase();
|
|
1454
|
+
};
|
|
1455
|
+
if (table.vars) {
|
|
1456
|
+
out.vars = {};
|
|
1457
|
+
for (const [name, ref2] of Object.entries(table.vars)) {
|
|
1458
|
+
const value = valueOf(`var "${name}"`, ref2);
|
|
1459
|
+
if (value !== void 0) out.vars[roleVar(name)] = value;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (problems.length) throw new Error(`${where}:
|
|
1463
|
+
${problems.join("\n ")}`);
|
|
1464
|
+
assertSafeTheme(out, where);
|
|
1465
|
+
const warn = (_c = opts.warn) != null ? _c : defaultWarn;
|
|
1466
|
+
if (derived.size)
|
|
1467
|
+
warn(
|
|
1468
|
+
`${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.`
|
|
1469
|
+
);
|
|
1470
|
+
const fails = rdsContrastReport(out);
|
|
1471
|
+
if (fails.length)
|
|
1472
|
+
warn(
|
|
1473
|
+
`${where}: ${fails.length} text pair(s) below 4.5:1
|
|
1474
|
+
` + fails.map((f) => `${f.mode}: ${f.fg} on ${f.bg} ${f.ratio}:1`).join("\n ")
|
|
1475
|
+
);
|
|
1476
|
+
return out;
|
|
1477
|
+
}
|
|
1095
1478
|
|
|
1096
1479
|
// generate/emitCss.ts
|
|
1097
1480
|
var SECTIONS = [
|
|
@@ -1164,153 +1547,175 @@ ${lightBlock}${darkBlock}
|
|
|
1164
1547
|
}
|
|
1165
1548
|
|
|
1166
1549
|
// generate/emitClaudeMd.ts
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
{
|
|
1185
|
-
{
|
|
1186
|
-
{
|
|
1187
|
-
{
|
|
1188
|
-
{
|
|
1189
|
-
{
|
|
1190
|
-
{
|
|
1191
|
-
{
|
|
1192
|
-
{
|
|
1193
|
-
{
|
|
1194
|
-
{
|
|
1195
|
-
{ key: "--border-subtle", use: "divis\xF3rias fracas" },
|
|
1196
|
-
{ key: "--border-default", use: "bordas de inputs e cards" },
|
|
1197
|
-
{ key: "--border-strong", use: "bordas enfatizadas" },
|
|
1198
|
-
{ key: "--border-focus", use: "anel de foco" },
|
|
1199
|
-
{ key: "--text-primary", use: "t\xEDtulos, corpo" },
|
|
1200
|
-
{ key: "--text-secondary", use: "texto de apoio" },
|
|
1201
|
-
{ key: "--text-muted", use: "legendas, metadados" },
|
|
1202
|
-
{ key: "--text-inverse", use: "texto sobre superf\xEDcies invertidas" },
|
|
1203
|
-
{ key: "--icon-default", use: "cor padr\xE3o de \xEDcone" }
|
|
1550
|
+
var MARKERS = /rojao-ds:(start|end)/i;
|
|
1551
|
+
var NAME = /^[a-z0-9][a-z0-9 _-]{0,63}$/i;
|
|
1552
|
+
var CSS_FILE = /^[a-z0-9_@./-]+\.css$/i;
|
|
1553
|
+
var CSS_URL = /^https?:\/\/[^\s"'`<>()\\]+$/i;
|
|
1554
|
+
var INSTALL = /^(?:pnpm add|yarn add|bun add|npm install|npm i) @rojaostudio\/ds(?:@[a-z0-9][a-z0-9.+-]*)?$/;
|
|
1555
|
+
var ROLE_DOCS = [
|
|
1556
|
+
{ role: "colors/primary/default", use: "primary action, brand fills" },
|
|
1557
|
+
{ role: "text/on/primary", use: "text/icon on the primary fill" },
|
|
1558
|
+
{ role: "colors/primary/active", use: "pressed primary" },
|
|
1559
|
+
{ role: "colors/secondary/default", use: "secondary brand fill (with `--text-on-secondary`)" },
|
|
1560
|
+
{ role: "colors/accent/default", use: "highlight \u2014 sparingly (with `--text-on-accent`)" },
|
|
1561
|
+
{ role: "surface/page", use: "page background" },
|
|
1562
|
+
{ role: "surface/card", use: "cards" },
|
|
1563
|
+
{ role: "surface/panel", use: "panels, side areas" },
|
|
1564
|
+
{ role: "surface/muted", use: "muted areas, wells" },
|
|
1565
|
+
{ role: "surface/tint/default", use: "selected / tinted fill (with `--text-on-tint`)" },
|
|
1566
|
+
{ role: "border/default", use: "dividers, card borders" },
|
|
1567
|
+
{ role: "border/strong", use: "control borders, emphasis" },
|
|
1568
|
+
{ role: "focus/ring", use: "focus ring" },
|
|
1569
|
+
{ role: "text/heading", use: "headings" },
|
|
1570
|
+
{ role: "text/body", use: "body text" },
|
|
1571
|
+
{ role: "text/muted", use: "supporting text" },
|
|
1572
|
+
{ role: "text/subtle", use: "captions, metadata" },
|
|
1573
|
+
{ role: "text/link", use: "links" },
|
|
1574
|
+
{ role: "colors/state/error", use: "error fill (with `--text-on-error`)" },
|
|
1575
|
+
{ role: "text/error", use: "error message text" },
|
|
1576
|
+
{ role: "colors/state/success", use: "success fill (with `--text-on-success`)" },
|
|
1577
|
+
{ role: "colors/state/warning", use: "warning fill (with `--text-on-warning`)" }
|
|
1204
1578
|
];
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1579
|
+
var CLAUDE_MD_COMPONENTS = [
|
|
1580
|
+
"button",
|
|
1581
|
+
"icon-button",
|
|
1582
|
+
"tooltip",
|
|
1583
|
+
"input",
|
|
1584
|
+
"textarea",
|
|
1585
|
+
"select",
|
|
1586
|
+
"checkbox",
|
|
1587
|
+
"radio-group",
|
|
1588
|
+
"switch",
|
|
1589
|
+
"card",
|
|
1590
|
+
"dialog",
|
|
1591
|
+
"sheet",
|
|
1592
|
+
"tabs",
|
|
1593
|
+
"table",
|
|
1594
|
+
"toast",
|
|
1595
|
+
"badge",
|
|
1596
|
+
"alert",
|
|
1597
|
+
"avatar",
|
|
1598
|
+
"dropdown-menu",
|
|
1599
|
+
"page-header",
|
|
1600
|
+
"sidebar",
|
|
1601
|
+
"empty",
|
|
1602
|
+
"skeleton",
|
|
1603
|
+
"pagination"
|
|
1604
|
+
];
|
|
1605
|
+
var CLAUDE_MD_FOUNDATION = {
|
|
1606
|
+
space: ["--space-4", "--space-8", "--space-12", "--space-16", "--space-24", "--space-32", "--space-40", "--space-48", "--space-56", "--space-64"],
|
|
1607
|
+
radius: ["--radius-xs", "--radius-control", "--radius-field", "--radius-card", "--radius-container", "--radius-full"],
|
|
1608
|
+
type: ["heading", "lead", "body", "label", "button", "small", "caption"],
|
|
1609
|
+
font: "--type-font-stack"
|
|
1610
|
+
};
|
|
1611
|
+
function isTable(x) {
|
|
1612
|
+
return typeof x === "object" && x !== null && "modes" in x && "primitives" in x;
|
|
1219
1613
|
}
|
|
1614
|
+
var TARGET_FILES = { claude: "CLAUDE.md", cursor: ".cursorrules", agents: "AGENTS.md" };
|
|
1220
1615
|
function fileLabel(target) {
|
|
1221
|
-
return target
|
|
1616
|
+
return TARGET_FILES[target];
|
|
1222
1617
|
}
|
|
1223
|
-
function
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1618
|
+
function roleTable(theme) {
|
|
1619
|
+
const rows = ROLE_DOCS.filter((r) => roleVar(r.role) in theme.light).map((r) => {
|
|
1620
|
+
var _a;
|
|
1621
|
+
const v = roleVar(r.role);
|
|
1622
|
+
return `| \`${v}\` | ${r.use} | \`${theme.light[v]}\` | \`${(_a = theme.dark[v]) != null ? _a : theme.light[v]}\` |`;
|
|
1623
|
+
});
|
|
1624
|
+
return ["| Token | Use for | Light | Dark |", "|---|---|---|---|", ...rows].join("\n");
|
|
1625
|
+
}
|
|
1626
|
+
function emitClaudeMd(source, opts = {}) {
|
|
1627
|
+
var _a, _b, _c, _d;
|
|
1628
|
+
const theme = (_a = opts.theme) != null ? _a : isTable(source) ? rdsThemeFromTable(source) : generateRdsTheme(source);
|
|
1629
|
+
assertSafeTheme(theme, "emitClaudeMd");
|
|
1630
|
+
const name = source.name;
|
|
1631
|
+
if (name !== void 0 && name !== "" && !(typeof name === "string" && NAME.test(name)))
|
|
1632
|
+
throw new Error(`emitClaudeMd: name ${preview(name)} is not valid (letters, digits, space, "-" and "_", up to 64)`);
|
|
1633
|
+
const Name = name ? name[0].toUpperCase() + name.slice(1) : "Brand";
|
|
1634
|
+
const desc = !isTable(source) ? oneLine(source.description) : "";
|
|
1635
|
+
const description = desc ? ` \u2014 ${desc}` : "";
|
|
1636
|
+
const cssFile = (_b = opts.cssFile) != null ? _b : "rds-theme.css";
|
|
1637
|
+
if (!CSS_FILE.test(cssFile)) throw new Error(`emitClaudeMd: cssFile ${preview(cssFile)} is not a valid .css path`);
|
|
1638
|
+
if (opts.cssUrl !== void 0 && !CSS_URL.test(opts.cssUrl))
|
|
1639
|
+
throw new Error(`emitClaudeMd: cssUrl ${preview(opts.cssUrl)} is not a valid http(s) URL`);
|
|
1640
|
+
const install = (_c = opts.install) != null ? _c : "pnpm add @rojaostudio/ds";
|
|
1641
|
+
if (!INSTALL.test(install)) throw new Error(`emitClaudeMd: install ${preview(install)} is not an install command of @rojaostudio/ds`);
|
|
1642
|
+
const target = (_d = opts.target) != null ? _d : "claude";
|
|
1643
|
+
if (!Object.prototype.hasOwnProperty.call(TARGET_FILES, target)) throw new Error(`emitClaudeMd: unknown target ${preview(target)}`);
|
|
1644
|
+
const f = CLAUDE_MD_FOUNDATION;
|
|
1645
|
+
const own = theme.vars ? Object.keys(theme.vars) : [];
|
|
1646
|
+
const themeImport = opts.cssUrl ? `@import "@rojaostudio/ds/styles/rds.css";
|
|
1647
|
+
@import url("${opts.cssUrl}"); /* the brand theme, AFTER rds.css */` : `@import "@rojaostudio/ds/styles/rds.css";
|
|
1648
|
+
@import "./${cssFile}"; /* the brand theme, AFTER rds.css */`;
|
|
1649
|
+
const text = `# Design System \u2014 ${Name}
|
|
1241
1650
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
- **Body font:** ${fontTitle(bodyFont)} \xB7 **Display font:** ${fontTitle(displayFont)} (load from Google Fonts; never substitute)
|
|
1246
|
-
- **Default radius:** \`${primitives.radius.md}px\` (\`rounded-lg\`). Pills/avatars fully round.
|
|
1247
|
-
- **Light AND dark are first-class** \u2014 every screen must work in both.
|
|
1651
|
+
> This project uses **Roj\xE3o DS 2.0** with the **${Name}** brand${description}. When you build any
|
|
1652
|
+
> UI \u2014 components, pages, screens \u2014 follow the rules below so every screen looks like the same
|
|
1653
|
+
> product. Do not invent colors, fonts, spacing or radii.
|
|
1248
1654
|
|
|
1249
|
-
##
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
> One sheet can hold **several themes** \u2014 each as its own \`theme-<name>\` class.
|
|
1265
|
-
> Switch with the class (one product per app, or many on one page).${cssUrl ? `
|
|
1266
|
-
> Edit the theme at the source and this same URL updates **all your apps at once**.` : ""}
|
|
1267
|
-
3. Style everything with the semantic tokens below, as CSS variables \u2014 they already
|
|
1268
|
-
flip between light and dark. You never write a hex value in a component.
|
|
1269
|
-
\`\`\`css
|
|
1270
|
-
.card { background: var(--surface-default); color: var(--text-primary);
|
|
1271
|
-
border: 1px solid var(--border-default); border-radius: ${primitives.radius.md}px; }
|
|
1272
|
-
\`\`\`
|
|
1655
|
+
## Setup (once per app)
|
|
1656
|
+
\`\`\`bash
|
|
1657
|
+
${install}
|
|
1658
|
+
\`\`\`
|
|
1659
|
+
\`\`\`css
|
|
1660
|
+
/* root stylesheet */
|
|
1661
|
+
${themeImport}
|
|
1662
|
+
\`\`\`
|
|
1663
|
+
- \`rds.css\` carries the tokens and every component's styles, in cascade layers
|
|
1664
|
+
(\`rds.theme\`, \`rds.tokens\`, \`rds.components\`). No Tailwind, no other stylesheet.
|
|
1665
|
+
- ${opts.cssUrl ? `The theme is served from \`${opts.cssUrl}\`.` : `\`${cssFile}\` is the brand theme, generated \u2014 a file **you own**; commit it. Regenerate it
|
|
1666
|
+
with \`npx rojao-ds init\` instead of editing it by hand.`}
|
|
1667
|
+
- Dark mode: \`class="dark"\` on \`<html>\` (or any element). Light AND dark are first-class.
|
|
1668
|
+
- Brand plate: \`class="ds-plate"\` on a section paints it with the primary color; the roles inside
|
|
1669
|
+
flip so text and components stay legible.
|
|
1273
1670
|
|
|
1274
|
-
##
|
|
1275
|
-
|
|
1671
|
+
## Components first
|
|
1672
|
+
Use the design system components before writing your own markup. Import each from its own path:
|
|
1673
|
+
\`\`\`tsx
|
|
1674
|
+
import { Button } from "@rojaostudio/ds/components/button";
|
|
1675
|
+
import { IconButton } from "@rojaostudio/ds/components/icon-button";
|
|
1676
|
+
import { Tooltip } from "@rojaostudio/ds/components/tooltip";
|
|
1677
|
+
\`\`\`
|
|
1678
|
+
Available, among others: ${CLAUDE_MD_COMPONENTS.map((c) => `\`${c}\``).join(" \xB7 ")}.
|
|
1679
|
+
- Components bring their own CSS (inside \`rds.css\`). Don't restyle their internals, don't wrap
|
|
1680
|
+
them in Tailwind classes, don't rebuild them with shadcn/ui.
|
|
1681
|
+
- Every \`IconButton\` goes inside a \`Tooltip\` with the same text as its \`label\`.
|
|
1276
1682
|
|
|
1277
|
-
##
|
|
1278
|
-
|
|
1683
|
+
## Theme roles \u2014 ALWAYS use these in your own CSS, NEVER hardcode a color
|
|
1684
|
+
They already flip between light, dark and the brand plate.
|
|
1685
|
+
${roleTable(theme)}
|
|
1279
1686
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1687
|
+
Every role pairs a fill with its \`--text-on-*\` text. Example:
|
|
1688
|
+
\`\`\`css
|
|
1689
|
+
.panel { background: var(--surface-card); color: var(--text-body);
|
|
1690
|
+
border: var(--border-width) solid var(--border-default);
|
|
1691
|
+
border-radius: var(--radius-card); padding: var(--space-16); }
|
|
1692
|
+
\`\`\`${own.length ? `
|
|
1282
1693
|
|
|
1283
|
-
|
|
1284
|
-
**Line heights:** ${Object.entries(primitives.lineHeight).map(([k, v]) => `\`${k}\`=${v}`).join(" \xB7 ")}
|
|
1694
|
+
Brand's own variables (same value in every mode): ${own.map((v) => `\`${v}\``).join(" \xB7 ")}.` : ""}
|
|
1285
1695
|
|
|
1286
|
-
##
|
|
1287
|
-
${
|
|
1696
|
+
## Foundation tokens
|
|
1697
|
+
- **Spacing** (padding, margin, gap) \u2014 ONLY: ${f.space.map((v) => `\`${v}\``).join(" \xB7 ")}
|
|
1698
|
+
- **Radius:** ${f.radius.map((v) => `\`${v}\``).join(" \xB7 ")}
|
|
1699
|
+
- **Type** \u2014 \`--type-<style>-size\` / \`--type-<style>-line\`, styles: ${f.type.map((v) => `\`${v}\``).join(" \xB7 ")}
|
|
1700
|
+
- **Font:** \`font-family: var(${f.font})\` \u2014 never substitute.
|
|
1288
1701
|
|
|
1289
1702
|
## Rules (do / don't)
|
|
1290
|
-
- \u2705 Primary
|
|
1291
|
-
- \u2705
|
|
1292
|
-
- \u2705
|
|
1293
|
-
- \u2705
|
|
1294
|
-
- \u2705
|
|
1295
|
-
- \
|
|
1296
|
-
- \u274C Never
|
|
1297
|
-
|
|
1298
|
-
- \u274C Never
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
You are NOT replacing shadcn. Use shadcn/ui components and apply THIS brand as the theme:
|
|
1302
|
-
map the tokens above onto shadcn's CSS variables (\`--background\` \u2192 \`--surface-page\`,
|
|
1303
|
-
\`--foreground\` \u2192 \`--text-primary\`, \`--primary\` \u2192 \`--brand-primary\`, \`--border\` \u2192
|
|
1304
|
-
\`--border-default\`, \`--ring\` \u2192 \`--border-focus\`, \`--radius\` \u2192 \`${primitives.radius.md}px\`).
|
|
1305
|
-
Build with shadcn's component shapes; paint them with these tokens. Don't restyle components from scratch.
|
|
1306
|
-
|
|
1307
|
-
## Primitive palette (reference \u2014 prefer the semantic tokens above)
|
|
1308
|
-
\`\`\`
|
|
1309
|
-
neutral 50 ${neutral[50]} \xB7 500 ${neutral[500]} \xB7 900 ${neutral[900]}
|
|
1310
|
-
\`\`\`
|
|
1703
|
+
- \u2705 Primary action = \`Button\` (it already paints \`--colors-primary-default\` + \`--text-on-primary\`).
|
|
1704
|
+
- \u2705 Page uses \`--surface-page\`; cards \`--surface-card\`; body text \`--text-body\`.
|
|
1705
|
+
- \u2705 \`--colors-accent-default\` is the highlight \u2014 use it sparingly.
|
|
1706
|
+
- \u2705 Spacing, radius and type come ONLY from the foundation tokens above.
|
|
1707
|
+
- \u2705 Your own CSS is unlayered, so it already wins over the design system \u2014 no \`!important\`.
|
|
1708
|
+
- \u274C Never write a raw hex (\`#000\`) or a Tailwind palette class (\`bg-emerald-500\`) in a component.
|
|
1709
|
+
- \u274C Never use a brand fill as a border or as text on a surface: contrast is guaranteed only for a
|
|
1710
|
+
fill + its \`--text-on-*\`.
|
|
1711
|
+
- \u274C Never hardcode dark-mode colors \u2014 the roles already flip. Don't add a second accent color.
|
|
1712
|
+
- \u274C Don't use the 1.x stylesheet (\`styles/base.css\`), its utilities (\`bg-surface-*\`, \`text-fg-*\`) or
|
|
1713
|
+
\`theme-<name>\` classes.
|
|
1311
1714
|
|
|
1312
|
-
<!-- Generated by Roj\xE3o DS \u2014 file: ${fileLabel(
|
|
1715
|
+
<!-- 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. -->
|
|
1313
1716
|
`;
|
|
1717
|
+
if (MARKERS.test(text)) throw new Error("emitClaudeMd: the generated text contains a rojao-ds marker");
|
|
1718
|
+
return text;
|
|
1314
1719
|
}
|
|
1315
1720
|
|
|
1316
|
-
export { ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, refToHex, relativeLuminance, resolveTheme, roleVar };
|
|
1721
|
+
export { RDS_CONTRAST_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 };
|