@rojaostudio/ds-core 1.1.0-next.7 → 1.1.0-next.9
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/dist/{chunk-Y25PXY26.js → chunk-N3HPD3U5.js} +197 -24
- package/dist/generate.d.ts +30 -4
- package/dist/generate.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/themes.js +1 -1
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -19,3 +19,14 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Arquivos gerados
|
|
26
|
+
|
|
27
|
+
Os arquivos que esta ferramenta escreve no seu projeto (o tema CSS e o bloco
|
|
28
|
+
entre os marcadores <!-- rojao-ds:start --> e <!-- rojao-ds:end --> em
|
|
29
|
+
CLAUDE.md, AGENTS.md ou .cursorrules) são de quem usa a ferramenta. Você pode
|
|
30
|
+
usá-los, alterá-los e distribuí-los como quiser, sem obrigação de manter aviso
|
|
31
|
+
de licença ou atribuição. A licença MIT acima vale para o código da ferramenta,
|
|
32
|
+
não para o que ela gera.
|
|
@@ -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") {
|
|
@@ -706,18 +808,19 @@ function ramp(c, custom) {
|
|
|
706
808
|
}
|
|
707
809
|
var on = (bg) => onColor(bg);
|
|
708
810
|
var AA = 4.5;
|
|
709
|
-
var
|
|
710
|
-
|
|
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) {
|
|
711
814
|
var _a;
|
|
712
|
-
if (readsOn(base, bgs)) return base;
|
|
815
|
+
if (readsOn(base, bgs, min)) return base;
|
|
713
816
|
const lb = relativeLuminance(base);
|
|
714
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));
|
|
715
|
-
const step2 = steps.find((c) => readsOn(c, bgs));
|
|
818
|
+
const step2 = steps.find((c) => readsOn(c, bgs, min));
|
|
716
819
|
if (step2) return step2;
|
|
717
820
|
const [h, s, l0] = hexToHsl((_a = steps[steps.length - 1]) != null ? _a : base);
|
|
718
821
|
for (let l = l0; toward === "darker" ? l >= 0 : l <= 100; l += toward === "darker" ? -2 : 2) {
|
|
719
822
|
const c = hslToHex(h, s, l);
|
|
720
|
-
if (readsOn(c, bgs)) return c;
|
|
823
|
+
if (readsOn(c, bgs, min)) return c;
|
|
721
824
|
}
|
|
722
825
|
return fallback;
|
|
723
826
|
}
|
|
@@ -828,14 +931,19 @@ var ROLES = [
|
|
|
828
931
|
["colors/state/info-strong", "d", "d"],
|
|
829
932
|
["colors/state/neutral-strong", "d", "d"],
|
|
830
933
|
["text/on/primary-subtle", "d", "b"],
|
|
831
|
-
["surface/tint/subtle", "d", "b"]
|
|
934
|
+
["surface/tint/subtle", "d", "b"],
|
|
935
|
+
["border/error", "d", "d"]
|
|
832
936
|
];
|
|
937
|
+
var awayFrom = (bg) => contrastRatio(bg, BLACK) >= contrastRatio(bg, WHITE) ? "darker" : "lighter";
|
|
833
938
|
var TINT_DEFAULT = 200;
|
|
834
939
|
var lighterStep = (s) => s - 100 ;
|
|
835
940
|
var roleVar = (role) => `--${role.replaceAll("/", "-")}`;
|
|
836
941
|
function generateRdsTheme(def, opts = {}) {
|
|
837
942
|
var _a, _b;
|
|
838
943
|
const warn = (_a = opts.warn) != null ? _a : defaultWarn;
|
|
944
|
+
const where = `generateRdsTheme(${preview(def == null ? void 0 : def.name)})`;
|
|
945
|
+
if (!(def == null ? void 0 : def.brand) || typeof def.brand !== "object") throw new Error(`${where}: brand is missing`);
|
|
946
|
+
checkPalettes(def.palettes, where);
|
|
839
947
|
const b = def.brand;
|
|
840
948
|
const own = def.palettes;
|
|
841
949
|
const P = ramp(b.primary, own);
|
|
@@ -930,8 +1038,7 @@ function generateRdsTheme(def, opts = {}) {
|
|
|
930
1038
|
"text/warning": orange[700],
|
|
931
1039
|
"surface/neutral": N[100],
|
|
932
1040
|
"text/neutral": N[800],
|
|
933
|
-
"chart/series/
|
|
934
|
-
"chart/series/2": orange[500],
|
|
1041
|
+
"chart/series/2": orange[600],
|
|
935
1042
|
"chart/series/3": teal[500],
|
|
936
1043
|
"chart/series/4": purple[500],
|
|
937
1044
|
"chart/series/5": green[500],
|
|
@@ -959,6 +1066,8 @@ function generateRdsTheme(def, opts = {}) {
|
|
|
959
1066
|
["colors/primary/dark", "text/on/primary-strong"],
|
|
960
1067
|
["surface/tint/default", "text/on/tint"]
|
|
961
1068
|
]) l[text] = on(l[fill]);
|
|
1069
|
+
l["border/error"] = readableFrom(l["colors/state/error"], red, [l["surface/card"]], awayFrom(l["surface/card"]), BLACK, NON_TEXT);
|
|
1070
|
+
l["chart/series/1"] = readableFrom(P.scale[600], P.scale, [l["surface/card"]], awayFrom(l["surface/card"]), BLACK, NON_TEXT);
|
|
962
1071
|
l["logo/primary"] = l["colors/primary/default"];
|
|
963
1072
|
l["logo/signature"] = l["colors/primary/default"];
|
|
964
1073
|
l["logo/accent"] = l["colors/accent/logo"];
|
|
@@ -1023,7 +1132,6 @@ function generateRdsTheme(def, opts = {}) {
|
|
|
1023
1132
|
"text/warning": orange[300],
|
|
1024
1133
|
"surface/neutral": N[800],
|
|
1025
1134
|
"text/neutral": N[200],
|
|
1026
|
-
"chart/series/1": P.scale[400],
|
|
1027
1135
|
"chart/series/2": orange[400],
|
|
1028
1136
|
"chart/series/3": teal[400],
|
|
1029
1137
|
"chart/series/4": purple[400],
|
|
@@ -1046,12 +1154,15 @@ function generateRdsTheme(def, opts = {}) {
|
|
|
1046
1154
|
["surface/tint/default", "text/on/tint"],
|
|
1047
1155
|
["surface/lift/action", "text/on/lift-action"]
|
|
1048
1156
|
]) d[text] = on(d[fill]);
|
|
1157
|
+
d["border/error"] = readableFrom(d["colors/state/error-strong"], red, [d["surface/card"]], awayFrom(d["surface/card"]), WHITE, NON_TEXT);
|
|
1158
|
+
d["chart/series/1"] = readableFrom(P.scale[400], P.scale, [d["surface/card"]], awayFrom(d["surface/card"]), WHITE, NON_TEXT);
|
|
1049
1159
|
const plate = l["colors/primary/default"];
|
|
1050
1160
|
const ink = on(plate);
|
|
1051
1161
|
const inkA = ink === WHITE ? white : black;
|
|
1052
1162
|
const antiInk = ink === WHITE ? black : white;
|
|
1053
1163
|
const lp = relativeLuminance(plate);
|
|
1054
|
-
const
|
|
1164
|
+
const isPlate = (c) => c.toLowerCase() === plate.toLowerCase();
|
|
1165
|
+
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;
|
|
1055
1166
|
const onPrimaryStrong = contrastRatio(plate, P.scale[100]) >= AA ? plate : on(P.scale[100]);
|
|
1056
1167
|
const br = {
|
|
1057
1168
|
"colors/primary/light": P.scale[300],
|
|
@@ -1096,6 +1207,7 @@ function generateRdsTheme(def, opts = {}) {
|
|
|
1096
1207
|
"border/strong-hover": inkA(80),
|
|
1097
1208
|
"text/on/primary-subtle": inkA(30)
|
|
1098
1209
|
};
|
|
1210
|
+
const brandError = readableFrom(red[300], red, [plateCard], awayFrom(plateCard), ink, NON_TEXT);
|
|
1099
1211
|
const out = { light: {}, dark: {}, brand: {} };
|
|
1100
1212
|
const pick = (src, role) => {
|
|
1101
1213
|
var _a2;
|
|
@@ -1109,6 +1221,8 @@ function generateRdsTheme(def, opts = {}) {
|
|
|
1109
1221
|
out.dark[roleVar(role)] = pick(darkSrc, role);
|
|
1110
1222
|
out.brand[roleVar(role)] = pick(brandSrc, role);
|
|
1111
1223
|
}
|
|
1224
|
+
out.brand[roleVar("border/error")] = brandError;
|
|
1225
|
+
assertSafeTheme(out, where);
|
|
1112
1226
|
return out;
|
|
1113
1227
|
}
|
|
1114
1228
|
var RDS_SCOPE_SELECTORS = [
|
|
@@ -1134,6 +1248,12 @@ var covered = (sel) => {
|
|
|
1134
1248
|
};
|
|
1135
1249
|
function emitRdsCss(theme, opts = {}) {
|
|
1136
1250
|
var _a, _b, _c;
|
|
1251
|
+
assertSafeTheme(theme, "emitRdsCss");
|
|
1252
|
+
const badSel = ["scope", "dark", "plate"].filter((k) => opts[k] !== void 0 && !isSafeSelectorList(opts[k]));
|
|
1253
|
+
if (badSel.length)
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
`emitRdsCss: invalid selector in ${badSel.map((k) => `${k} ${preview(opts[k])}`).join(", ")} (no braces, ";", comments, backslash, "@", "<" or line breaks).`
|
|
1256
|
+
);
|
|
1137
1257
|
const scopes = split((_a = opts.scope) != null ? _a : ":root, .ds-scope, [data-rds-scope]");
|
|
1138
1258
|
const darks = split((_b = opts.dark) != null ? _b : '.dark, [data-rds-mode="dark"]');
|
|
1139
1259
|
const plates = split((_c = opts.plate) != null ? _c : ".ds-plate, [data-rds-plate]");
|
|
@@ -1200,10 +1320,34 @@ function rdsContrastReport(theme) {
|
|
|
1200
1320
|
return out;
|
|
1201
1321
|
}
|
|
1202
1322
|
var isColourRef = (v) => /^[a-z][a-z0-9-]*\/[a-z]?\d+$/i.test(v);
|
|
1323
|
+
var ADDED_ROLES = {
|
|
1324
|
+
// Figma: light → base colors/state/error, dark → base dark/colors/state/error-strong, brand → base dark/text/error.
|
|
1325
|
+
"border/error": { light: "colors/state/error", dark: "colors/state/error-strong", brand: "text/error" }
|
|
1326
|
+
};
|
|
1203
1327
|
function rdsThemeFromTable(table, opts = {}) {
|
|
1204
|
-
var _a, _b;
|
|
1328
|
+
var _a, _b, _c;
|
|
1329
|
+
const where = `rdsThemeFromTable(${preview(table == null ? void 0 : table.name)})`;
|
|
1330
|
+
if (!table || typeof table !== "object") throw new Error(`${where}: the table is not an object`);
|
|
1331
|
+
const check = collector(where);
|
|
1332
|
+
if (!table.primitives || typeof table.primitives !== "object" || Array.isArray(table.primitives))
|
|
1333
|
+
check.problems.push("primitives: not an object");
|
|
1334
|
+
else
|
|
1335
|
+
for (const [name, value] of Object.entries(table.primitives)) {
|
|
1336
|
+
check.name(`primitives ${preview(name)}`, name);
|
|
1337
|
+
check.color(`primitives.${name}`, value);
|
|
1338
|
+
}
|
|
1339
|
+
if (table.vars !== void 0) {
|
|
1340
|
+
if (!table.vars || typeof table.vars !== "object" || Array.isArray(table.vars)) check.problems.push("vars: not an object");
|
|
1341
|
+
else
|
|
1342
|
+
for (const [name, value] of Object.entries(table.vars)) {
|
|
1343
|
+
check.name(`vars ${preview(name)}`, name);
|
|
1344
|
+
if (typeof value !== "string") check.problems.push(`vars.${name}: not a string`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
check.done();
|
|
1205
1348
|
const out = { light: {}, dark: {}, brand: {} };
|
|
1206
1349
|
const problems = [];
|
|
1350
|
+
const derived = /* @__PURE__ */ new Set();
|
|
1207
1351
|
for (const mode of ["light", "dark", "brand"]) {
|
|
1208
1352
|
const roles = (_a = table.modes) == null ? void 0 : _a[mode];
|
|
1209
1353
|
if (!roles) {
|
|
@@ -1211,7 +1355,12 @@ function rdsThemeFromTable(table, opts = {}) {
|
|
|
1211
1355
|
continue;
|
|
1212
1356
|
}
|
|
1213
1357
|
for (const [role] of ROLES) {
|
|
1214
|
-
|
|
1358
|
+
let ref2 = roles[role];
|
|
1359
|
+
const added = (_b = ADDED_ROLES[role]) == null ? void 0 : _b[mode];
|
|
1360
|
+
if (ref2 === void 0 && added && roles[added] !== void 0) {
|
|
1361
|
+
ref2 = roles[added];
|
|
1362
|
+
derived.add(role);
|
|
1363
|
+
}
|
|
1215
1364
|
if (ref2 === void 0) problems.push(`${mode}: role "${role}" is missing`);
|
|
1216
1365
|
else if (isColourRef(ref2)) {
|
|
1217
1366
|
const value = table.primitives[ref2];
|
|
@@ -1220,10 +1369,10 @@ function rdsThemeFromTable(table, opts = {}) {
|
|
|
1220
1369
|
} else out[mode][roleVar(role)] = ref2;
|
|
1221
1370
|
}
|
|
1222
1371
|
}
|
|
1223
|
-
const valueOf = (
|
|
1372
|
+
const valueOf = (field, ref2) => {
|
|
1224
1373
|
if (!isColourRef(ref2)) return ref2;
|
|
1225
1374
|
const value = table.primitives[ref2];
|
|
1226
|
-
if (value === void 0) problems.push(`${
|
|
1375
|
+
if (value === void 0) problems.push(`${field} points to unknown primitive "${ref2}"`);
|
|
1227
1376
|
return value == null ? void 0 : value.toLowerCase();
|
|
1228
1377
|
};
|
|
1229
1378
|
if (table.vars) {
|
|
@@ -1233,12 +1382,18 @@ function rdsThemeFromTable(table, opts = {}) {
|
|
|
1233
1382
|
if (value !== void 0) out.vars[roleVar(name)] = value;
|
|
1234
1383
|
}
|
|
1235
1384
|
}
|
|
1236
|
-
if (problems.length) throw new Error(
|
|
1385
|
+
if (problems.length) throw new Error(`${where}:
|
|
1237
1386
|
${problems.join("\n ")}`);
|
|
1387
|
+
assertSafeTheme(out, where);
|
|
1388
|
+
const warn = (_c = opts.warn) != null ? _c : defaultWarn;
|
|
1389
|
+
if (derived.size)
|
|
1390
|
+
warn(
|
|
1391
|
+
`${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.`
|
|
1392
|
+
);
|
|
1238
1393
|
const fails = rdsContrastReport(out);
|
|
1239
1394
|
if (fails.length)
|
|
1240
|
-
|
|
1241
|
-
|
|
1395
|
+
warn(
|
|
1396
|
+
`${where}: ${fails.length} text pair(s) below 4.5:1
|
|
1242
1397
|
` + fails.map((f) => `${f.mode}: ${f.fg} on ${f.bg} ${f.ratio}:1`).join("\n ")
|
|
1243
1398
|
);
|
|
1244
1399
|
return out;
|
|
@@ -1315,6 +1470,11 @@ ${lightBlock}${darkBlock}
|
|
|
1315
1470
|
}
|
|
1316
1471
|
|
|
1317
1472
|
// generate/emitClaudeMd.ts
|
|
1473
|
+
var MARKERS = /rojao-ds:(start|end)/i;
|
|
1474
|
+
var NAME = /^[a-z0-9][a-z0-9 _-]{0,63}$/i;
|
|
1475
|
+
var CSS_FILE = /^[a-z0-9_@./-]+\.css$/i;
|
|
1476
|
+
var CSS_URL = /^https?:\/\/[^\s"'`<>()\\]+$/i;
|
|
1477
|
+
var INSTALL = /^(?:pnpm add|yarn add|bun add|npm install|npm i) @rojaostudio\/ds(?:@[a-z0-9][a-z0-9.+-]*)?$/;
|
|
1318
1478
|
var ROLE_DOCS = [
|
|
1319
1479
|
{ role: "colors/primary/default", use: "primary action, brand fills" },
|
|
1320
1480
|
{ role: "text/on/primary", use: "text/icon on the primary fill" },
|
|
@@ -1374,8 +1534,9 @@ var CLAUDE_MD_FOUNDATION = {
|
|
|
1374
1534
|
function isTable(x) {
|
|
1375
1535
|
return typeof x === "object" && x !== null && "modes" in x && "primitives" in x;
|
|
1376
1536
|
}
|
|
1537
|
+
var TARGET_FILES = { claude: "CLAUDE.md", cursor: ".cursorrules", agents: "AGENTS.md" };
|
|
1377
1538
|
function fileLabel(target) {
|
|
1378
|
-
return target
|
|
1539
|
+
return TARGET_FILES[target];
|
|
1379
1540
|
}
|
|
1380
1541
|
function roleTable(theme) {
|
|
1381
1542
|
const rows = ROLE_DOCS.filter((r) => roleVar(r.role) in theme.light).map((r) => {
|
|
@@ -1386,19 +1547,29 @@ function roleTable(theme) {
|
|
|
1386
1547
|
return ["| Token | Use for | Light | Dark |", "|---|---|---|---|", ...rows].join("\n");
|
|
1387
1548
|
}
|
|
1388
1549
|
function emitClaudeMd(source, opts = {}) {
|
|
1389
|
-
var _a, _b, _c;
|
|
1550
|
+
var _a, _b, _c, _d;
|
|
1390
1551
|
const theme = (_a = opts.theme) != null ? _a : isTable(source) ? rdsThemeFromTable(source) : generateRdsTheme(source);
|
|
1552
|
+
assertSafeTheme(theme, "emitClaudeMd");
|
|
1391
1553
|
const name = source.name;
|
|
1554
|
+
if (name !== void 0 && name !== "" && !(typeof name === "string" && NAME.test(name)))
|
|
1555
|
+
throw new Error(`emitClaudeMd: name ${preview(name)} is not valid (letters, digits, space, "-" and "_", up to 64)`);
|
|
1392
1556
|
const Name = name ? name[0].toUpperCase() + name.slice(1) : "Brand";
|
|
1393
|
-
const
|
|
1557
|
+
const desc = !isTable(source) ? oneLine(source.description) : "";
|
|
1558
|
+
const description = desc ? ` \u2014 ${desc}` : "";
|
|
1394
1559
|
const cssFile = (_b = opts.cssFile) != null ? _b : "rds-theme.css";
|
|
1395
|
-
|
|
1560
|
+
if (!CSS_FILE.test(cssFile)) throw new Error(`emitClaudeMd: cssFile ${preview(cssFile)} is not a valid .css path`);
|
|
1561
|
+
if (opts.cssUrl !== void 0 && !CSS_URL.test(opts.cssUrl))
|
|
1562
|
+
throw new Error(`emitClaudeMd: cssUrl ${preview(opts.cssUrl)} is not a valid http(s) URL`);
|
|
1563
|
+
const install = (_c = opts.install) != null ? _c : "pnpm add @rojaostudio/ds";
|
|
1564
|
+
if (!INSTALL.test(install)) throw new Error(`emitClaudeMd: install ${preview(install)} is not an install command of @rojaostudio/ds`);
|
|
1565
|
+
const target = (_d = opts.target) != null ? _d : "claude";
|
|
1566
|
+
if (!Object.prototype.hasOwnProperty.call(TARGET_FILES, target)) throw new Error(`emitClaudeMd: unknown target ${preview(target)}`);
|
|
1396
1567
|
const f = CLAUDE_MD_FOUNDATION;
|
|
1397
1568
|
const own = theme.vars ? Object.keys(theme.vars) : [];
|
|
1398
1569
|
const themeImport = opts.cssUrl ? `@import "@rojaostudio/ds/styles/rds.css";
|
|
1399
1570
|
@import url("${opts.cssUrl}"); /* the brand theme, AFTER rds.css */` : `@import "@rojaostudio/ds/styles/rds.css";
|
|
1400
1571
|
@import "./${cssFile}"; /* the brand theme, AFTER rds.css */`;
|
|
1401
|
-
|
|
1572
|
+
const text = `# Design System \u2014 ${Name}
|
|
1402
1573
|
|
|
1403
1574
|
> This project uses **Roj\xE3o DS 2.0** with the **${Name}** brand${description}. When you build any
|
|
1404
1575
|
> UI \u2014 components, pages, screens \u2014 follow the rules below so every screen looks like the same
|
|
@@ -1406,7 +1577,7 @@ function emitClaudeMd(source, opts = {}) {
|
|
|
1406
1577
|
|
|
1407
1578
|
## Setup (once per app)
|
|
1408
1579
|
\`\`\`bash
|
|
1409
|
-
|
|
1580
|
+
${install}
|
|
1410
1581
|
\`\`\`
|
|
1411
1582
|
\`\`\`css
|
|
1412
1583
|
/* root stylesheet */
|
|
@@ -1466,6 +1637,8 @@ Brand's own variables (same value in every mode): ${own.map((v) => `\`${v}\``).j
|
|
|
1466
1637
|
|
|
1467
1638
|
<!-- 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. -->
|
|
1468
1639
|
`;
|
|
1640
|
+
if (MARKERS.test(text)) throw new Error("emitClaudeMd: the generated text contains a rojao-ds marker");
|
|
1641
|
+
return text;
|
|
1469
1642
|
}
|
|
1470
1643
|
|
|
1471
|
-
export { RDS_CONTRAST_PAIRS, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar };
|
|
1644
|
+
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 };
|
package/dist/generate.d.ts
CHANGED
|
@@ -84,12 +84,33 @@ type RdsBrandTable = {
|
|
|
84
84
|
vars?: Record<string, string>;
|
|
85
85
|
};
|
|
86
86
|
/**
|
|
87
|
-
* The theme of a brand table. Fails on a missing role or an unknown primitive, listing them all.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
87
|
+
* The theme of a brand table. Fails on a missing role or an unknown primitive, listing them all. A table exported
|
|
88
|
+
* before a role of ADDED_ROLES existed is the exception: the role is taken from the token Figma aliases it to, and
|
|
89
|
+
* `opts.warn` asks to export the table again. The contrast of the main text pairs (rdsContrastReport) is only
|
|
90
|
+
* reported, through `opts.warn`: the table is the brand as drawn in Figma, kept one to one even where a pair fails.
|
|
90
91
|
*/
|
|
91
92
|
declare function rdsThemeFromTable(table: RdsBrandTable, opts?: RdsThemeOptions): RdsTheme;
|
|
92
93
|
|
|
94
|
+
/**
|
|
95
|
+
* validate.ts — what the emitters accept from the outside (a recipe, a brand table, a theme object).
|
|
96
|
+
*
|
|
97
|
+
* ds-core is a public API, and its output lands in a stylesheet (`emitRdsCss`) and in the rules file an AI agent
|
|
98
|
+
* reads (`emitClaudeMd`). A value that is not a colour could close the CSS rule and open another one, pull an
|
|
99
|
+
* `url()`, or carry an instruction into CLAUDE.md. So every value is checked against an allow list before it is
|
|
100
|
+
* written, and the check runs again in each emitter (defence in depth: a theme object can be built by hand).
|
|
101
|
+
*
|
|
102
|
+
* Allow lists, never deny lists: a colour is a hex or a closed `rgb()/rgba()/oklch()`; a variable name is lowercase
|
|
103
|
+
* words joined by `-` or `/`; a font family has no quote, semicolon, brace, backslash or line break.
|
|
104
|
+
*/
|
|
105
|
+
declare const isSafeColor: (v: unknown) => v is string;
|
|
106
|
+
declare const isSafeVarName: (v: unknown) => v is string;
|
|
107
|
+
declare const isSafeFont: (v: unknown) => v is string;
|
|
108
|
+
/** Thrown when an input fails the allow list. The message lists every field that failed. */
|
|
109
|
+
declare class RdsValidationError extends Error {
|
|
110
|
+
readonly problems: string[];
|
|
111
|
+
constructor(where: string, problems: string[]);
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
/**
|
|
94
115
|
* emitCss.ts — formata o output do generateTheme num theme.css legível.
|
|
95
116
|
* Header + bloco light (.theme-<name>) + bloco dark (seletor duplo), agrupado
|
|
@@ -123,6 +144,11 @@ interface ClaudeMdOptions {
|
|
|
123
144
|
theme?: RdsTheme;
|
|
124
145
|
/** File the text is written to (only the footer changes). Default `claude`. */
|
|
125
146
|
target?: ClaudeMdTarget;
|
|
147
|
+
/**
|
|
148
|
+
* The install command shown in the setup, as the consumer's package manager runs it
|
|
149
|
+
* (`npm install @rojaostudio/ds@next`). Default `pnpm add @rojaostudio/ds`.
|
|
150
|
+
*/
|
|
151
|
+
install?: string;
|
|
126
152
|
}
|
|
127
153
|
declare function emitClaudeMd(source: BrandDef | RdsBrandTable, opts?: ClaudeMdOptions): string;
|
|
128
154
|
|
|
@@ -192,4 +218,4 @@ declare function isNeutralBrand(def: {
|
|
|
192
218
|
palettes?: PaletteDefs;
|
|
193
219
|
}): boolean;
|
|
194
220
|
|
|
195
|
-
export { BrandDef, type ClaudeMdOptions, type ClaudeMdTarget, RDS_CONTRAST_PAIRS, ROLES as RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, type RdsBrandTable, type RdsContrastFailure, type RdsCssOptions, type RdsMode, type RdsTheme, type RdsThemeOptions, SCALE_STEPS, type Scale, type ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, roleVar };
|
|
221
|
+
export { BrandDef, type ClaudeMdOptions, type ClaudeMdTarget, RDS_CONTRAST_PAIRS, ROLES as RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, type RdsBrandTable, type RdsContrastFailure, type RdsCssOptions, type RdsMode, type RdsTheme, type RdsThemeOptions, RdsValidationError, SCALE_STEPS, type Scale, type ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, isSafeColor, isSafeFont, isSafeVarName, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, roleVar };
|
package/dist/generate.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { RDS_CONTRAST_PAIRS, ROLES as RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-
|
|
1
|
+
export { RDS_CONTRAST_PAIRS, ROLES as RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, 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 } from './chunk-N3HPD3U5.js';
|
|
2
2
|
import './chunk-FRWRLCYM.js';
|
|
3
3
|
import './chunk-ORMEWXMH.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { G as GenResult, R as ResolvedTheme, T as TokenMap, g as generateTheme, r as resolveTheme } from './generateTheme-CUfNpsKs.js';
|
|
2
|
-
export { ClaudeMdOptions, ClaudeMdTarget, RDS_CONTRAST_PAIRS, RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, RdsBrandTable, RdsContrastFailure, RdsCssOptions, RdsMode, RdsTheme, RdsThemeOptions, SCALE_STEPS, Scale, ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, roleVar } from './generate.js';
|
|
2
|
+
export { ClaudeMdOptions, ClaudeMdTarget, RDS_CONTRAST_PAIRS, RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, RdsBrandTable, RdsContrastFailure, RdsCssOptions, RdsMode, RdsTheme, RdsThemeOptions, RdsValidationError, SCALE_STEPS, Scale, ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, isSafeColor, isSafeFont, isSafeVarName, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, roleVar } from './generate.js';
|
|
3
3
|
export { A as Archetype, B as BrandColors, a as BrandDef, C as ColorRef, D as DarkStrategy, F as FontDef, P as PaletteRef, S as ScaleToken, T as TextStyle, b as TextStyleName, c as TypeScale } from './recipe.schema-Cq7e82nx.js';
|
|
4
4
|
export { primitives, tokens } from './tokens.js';
|
|
5
5
|
export { RecipeName, recipes } from './recipes.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { recipes } from './chunk-PV6GFEAM.js';
|
|
2
|
-
export { RDS_CONTRAST_PAIRS, ROLES as RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsContrastReport, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-
|
|
2
|
+
export { RDS_CONTRAST_PAIRS, ROLES as RDS_ROLES, RDS_SCOPE_SELECTORS, RDS_TOKEN_SCOPE, 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 } from './chunk-N3HPD3U5.js';
|
|
3
3
|
export { primitives, tokens } from './chunk-FRWRLCYM.js';
|
|
4
4
|
import './chunk-ORMEWXMH.js';
|
package/dist/themes.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rojaostudio/ds-core",
|
|
3
|
-
"version": "1.1.0-next.
|
|
3
|
+
"version": "1.1.0-next.9",
|
|
4
4
|
"description": "O motor do Rojão DS — tokens, derivação de tema e emissores. Sem React, sem Tailwind, sem CSS.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Rojão Studio (https://rojao.studio)",
|