@rojaostudio/ds-core 1.1.0-next.1 → 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-CFOS2RAD.js → chunk-LP6UA4EW.js} +556 -161
- 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 +3 -2
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.
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ npm i @rojaostudio/ds-core
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
import {
|
|
12
|
+
import { generateRdsTheme, emitRdsCss, emitClaudeMd } from '@rojaostudio/ds-core/generate';
|
|
13
13
|
|
|
14
14
|
const brand = {
|
|
15
15
|
name: 'acme',
|
|
@@ -19,10 +19,13 @@ const brand = {
|
|
|
19
19
|
fonts: { body: 'inter' },
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
emitClaudeMd(brand);
|
|
22
|
+
emitRdsCss(generateRdsTheme(brand)); // rds-theme.css — import it AFTER @rojaostudio/ds/styles/rds.css
|
|
23
|
+
emitClaudeMd(brand); // the same brand as rules for Claude Code, Cursor or AGENTS.md
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Or, without writing code: `npx rojao-ds init` (the [`rojao-ds`](https://www.npmjs.com/package/rojao-ds) CLI)
|
|
27
|
+
writes both files into your project.
|
|
28
|
+
|
|
26
29
|
One color goes in. What comes out is an OKLCH scale with an adaptive contrast floor, semantic
|
|
27
30
|
roles for surface, text, border and brand, and the full light/dark pair — verified against
|
|
28
31
|
WCAG AA on all three surfaces, by test, on every build.
|
|
@@ -41,7 +44,7 @@ Components, styles and the React Native target live in
|
|
|
41
44
|
|
|
42
45
|
| Entry | What's in it |
|
|
43
46
|
|---|---|
|
|
44
|
-
| `@rojaostudio/ds-core/generate` | `
|
|
47
|
+
| `@rojaostudio/ds-core/generate` | `generateRdsTheme`, `rdsThemeFromTable`, `emitRdsCss`, `emitClaudeMd`, `generateTheme`/`emitCss` (1.x), `buildScale`, `contrastRatio` |
|
|
45
48
|
| `@rojaostudio/ds-core/tokens` | `primitives` — the raw palettes and scales |
|
|
46
49
|
| `@rojaostudio/ds-core/recipes` | the reference brand definition |
|
|
47
50
|
| `@rojaostudio/ds-core/themes` | the preset catalogue: a starting palette per niche |
|
|
@@ -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,77 @@ 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 defaultWarn = (m) => {
|
|
878
|
+
var _a;
|
|
879
|
+
return (_a = globalThis.console) == null ? void 0 : _a.warn(m);
|
|
880
|
+
};
|
|
708
881
|
var ROLES = [
|
|
709
882
|
["colors/primary/light", "d", "b"],
|
|
710
883
|
["colors/primary/default", "d", "b"],
|
|
@@ -808,12 +981,19 @@ var ROLES = [
|
|
|
808
981
|
["colors/state/info-strong", "d", "d"],
|
|
809
982
|
["colors/state/neutral-strong", "d", "d"],
|
|
810
983
|
["text/on/primary-subtle", "d", "b"],
|
|
811
|
-
["surface/tint/subtle", "d", "b"]
|
|
984
|
+
["surface/tint/subtle", "d", "b"],
|
|
985
|
+
["border/error", "d", "d"]
|
|
812
986
|
];
|
|
987
|
+
var awayFrom = (bg) => contrastRatio(bg, BLACK) >= contrastRatio(bg, WHITE) ? "darker" : "lighter";
|
|
813
988
|
var TINT_DEFAULT = 200;
|
|
814
989
|
var lighterStep = (s) => s - 100 ;
|
|
815
990
|
var roleVar = (role) => `--${role.replaceAll("/", "-")}`;
|
|
816
|
-
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);
|
|
817
997
|
const b = def.brand;
|
|
818
998
|
const own = def.palettes;
|
|
819
999
|
const P = ramp(b.primary, own);
|
|
@@ -823,7 +1003,15 @@ function generateRdsTheme(def) {
|
|
|
823
1003
|
const L = palette("blue");
|
|
824
1004
|
const red = palette("red"), green = palette("green"), orange = palette("orange");
|
|
825
1005
|
const teal = palette("teal"), purple = palette("purple");
|
|
826
|
-
const
|
|
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);
|
|
827
1015
|
const invert = b.invert ? ramp(b.invert, own).base : WHITE;
|
|
828
1016
|
const l = {
|
|
829
1017
|
"colors/primary/light": P.scale[500],
|
|
@@ -860,13 +1048,12 @@ function generateRdsTheme(def) {
|
|
|
860
1048
|
"border/strong-hover": N[600],
|
|
861
1049
|
"surface/tint/default": A.scale[TINT_DEFAULT],
|
|
862
1050
|
"surface/tint/strong": A.scale[300],
|
|
863
|
-
"text/on/tint": BLACK,
|
|
864
1051
|
// Light selection fill (pressed Toggle, choice tile): one step lighter than the default tint
|
|
865
1052
|
// (200 → 100, 100 → 50).
|
|
866
1053
|
"surface/tint/subtle": A.scale[lighterStep(TINT_DEFAULT)],
|
|
867
1054
|
"surface/action/default": L[100],
|
|
868
1055
|
"surface/action/strong": L[200],
|
|
869
|
-
"text/on/action-tonal": P.scale[700],
|
|
1056
|
+
"text/on/action-tonal": readableFrom(P.scale[700], P.scale, [L[100]], "darker", BLACK),
|
|
870
1057
|
"surface/lift/action": P.scale[50],
|
|
871
1058
|
"surface/lift/action-strong": L[100],
|
|
872
1059
|
"surface/band/action": L[100],
|
|
@@ -901,8 +1088,7 @@ function generateRdsTheme(def) {
|
|
|
901
1088
|
"text/warning": orange[700],
|
|
902
1089
|
"surface/neutral": N[100],
|
|
903
1090
|
"text/neutral": N[800],
|
|
904
|
-
"chart/series/
|
|
905
|
-
"chart/series/2": orange[500],
|
|
1091
|
+
"chart/series/2": orange[600],
|
|
906
1092
|
"chart/series/3": teal[500],
|
|
907
1093
|
"chart/series/4": purple[500],
|
|
908
1094
|
"chart/series/5": green[500],
|
|
@@ -927,8 +1113,19 @@ function generateRdsTheme(def) {
|
|
|
927
1113
|
["colors/state/neutral", "text/on/neutral"],
|
|
928
1114
|
["surface/cover", "text/on/cover"],
|
|
929
1115
|
["surface/lift/action", "text/on/lift-action"],
|
|
930
|
-
["colors/primary/dark", "text/on/primary-strong"]
|
|
1116
|
+
["colors/primary/dark", "text/on/primary-strong"],
|
|
1117
|
+
["surface/tint/default", "text/on/tint"]
|
|
931
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);
|
|
932
1129
|
l["logo/primary"] = l["colors/primary/default"];
|
|
933
1130
|
l["logo/signature"] = l["colors/primary/default"];
|
|
934
1131
|
l["logo/accent"] = l["colors/accent/logo"];
|
|
@@ -945,7 +1142,7 @@ function generateRdsTheme(def) {
|
|
|
945
1142
|
"border/default": N[800],
|
|
946
1143
|
"border/strong": N[500],
|
|
947
1144
|
"border/strong-hover": N[400],
|
|
948
|
-
"text/heading": N[50],
|
|
1145
|
+
"text/heading": readableFrom(N[50], N, [N[900]], "lighter", WHITE),
|
|
949
1146
|
"text/body": N[100],
|
|
950
1147
|
"text/muted": N[300],
|
|
951
1148
|
"text/subtle": N[400],
|
|
@@ -972,15 +1169,13 @@ function generateRdsTheme(def) {
|
|
|
972
1169
|
"colors/accent/mark": A.scale[300],
|
|
973
1170
|
"surface/tint/default": A.scale[900],
|
|
974
1171
|
"surface/tint/strong": A.scale[800],
|
|
975
|
-
"text/on/tint": WHITE,
|
|
976
1172
|
// No step below 900 in dark: subtle is the default tint.
|
|
977
1173
|
"surface/tint/subtle": A.scale[900],
|
|
978
1174
|
"surface/action/default": L[900],
|
|
979
1175
|
"surface/action/strong": L[800],
|
|
980
|
-
"text/on/action-tonal": P.scale[200],
|
|
1176
|
+
"text/on/action-tonal": readableFrom(P.scale[200], P.scale, [L[900]], "lighter", WHITE),
|
|
981
1177
|
"surface/lift/action": P.scale[800],
|
|
982
1178
|
"surface/lift/action-strong": P.scale[700],
|
|
983
|
-
"text/on/lift-action": WHITE,
|
|
984
1179
|
"colors/state/error-strong": red[400],
|
|
985
1180
|
"colors/state/success-strong": green[300],
|
|
986
1181
|
"colors/state/warning-strong": orange[400],
|
|
@@ -995,7 +1190,6 @@ function generateRdsTheme(def) {
|
|
|
995
1190
|
"text/warning": orange[300],
|
|
996
1191
|
"surface/neutral": N[800],
|
|
997
1192
|
"text/neutral": N[200],
|
|
998
|
-
"chart/series/1": P.scale[400],
|
|
999
1193
|
"chart/series/2": orange[400],
|
|
1000
1194
|
"chart/series/3": teal[400],
|
|
1001
1195
|
"chart/series/4": purple[400],
|
|
@@ -1014,12 +1208,28 @@ function generateRdsTheme(def) {
|
|
|
1014
1208
|
["colors/secondary/default", "text/on/secondary"],
|
|
1015
1209
|
["colors/accent/default", "text/on/accent"],
|
|
1016
1210
|
["colors/accent/mark", "text/on/accent-mark"],
|
|
1017
|
-
["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"]
|
|
1018
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);
|
|
1019
1225
|
const plate = l["colors/primary/default"];
|
|
1020
1226
|
const ink = on(plate);
|
|
1021
1227
|
const inkA = ink === WHITE ? white : black;
|
|
1022
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]);
|
|
1023
1233
|
const br = {
|
|
1024
1234
|
"colors/primary/light": P.scale[300],
|
|
1025
1235
|
"colors/primary/default": ink,
|
|
@@ -1033,7 +1243,7 @@ function generateRdsTheme(def) {
|
|
|
1033
1243
|
"text/body": ink,
|
|
1034
1244
|
"text/link": contrastRatio(l["colors/accent/invert"], plate) >= 4.5 ? l["colors/accent/invert"] : ink,
|
|
1035
1245
|
"surface/page": plate,
|
|
1036
|
-
"surface/card":
|
|
1246
|
+
"surface/card": plateCard,
|
|
1037
1247
|
"surface/panel": plate,
|
|
1038
1248
|
"border/default": inkA(20),
|
|
1039
1249
|
"surface/tint/default": inkA(10),
|
|
@@ -1055,7 +1265,7 @@ function generateRdsTheme(def) {
|
|
|
1055
1265
|
"surface/lift/action": inkA(10),
|
|
1056
1266
|
"text/on/lift-action": ink,
|
|
1057
1267
|
"surface/lift/action-strong": inkA(20),
|
|
1058
|
-
"text/on/primary-strong":
|
|
1268
|
+
"text/on/primary-strong": onPrimaryStrong,
|
|
1059
1269
|
"text/subtle": inkA(60),
|
|
1060
1270
|
"surface/muted": inkA(10),
|
|
1061
1271
|
"surface/muted-strong": inkA(20),
|
|
@@ -1063,11 +1273,19 @@ function generateRdsTheme(def) {
|
|
|
1063
1273
|
"border/strong-hover": inkA(80),
|
|
1064
1274
|
"text/on/primary-subtle": inkA(30)
|
|
1065
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);
|
|
1066
1284
|
const out = { light: {}, dark: {}, brand: {} };
|
|
1067
1285
|
const pick = (src, role) => {
|
|
1068
|
-
var
|
|
1286
|
+
var _a2;
|
|
1069
1287
|
const table = src === "l" ? l : src === "d" ? d : br;
|
|
1070
|
-
const v = (
|
|
1288
|
+
const v = (_a2 = table[role]) != null ? _a2 : l[role];
|
|
1071
1289
|
if (v === void 0) throw new Error(`rdsTheme: no value for "${role}"`);
|
|
1072
1290
|
return v;
|
|
1073
1291
|
};
|
|
@@ -1076,25 +1294,65 @@ function generateRdsTheme(def) {
|
|
|
1076
1294
|
out.dark[roleVar(role)] = pick(darkSrc, role);
|
|
1077
1295
|
out.brand[roleVar(role)] = pick(brandSrc, role);
|
|
1078
1296
|
}
|
|
1297
|
+
out.brand[roleVar("border/error")] = brandError;
|
|
1298
|
+
assertSafeTheme(out, where);
|
|
1079
1299
|
return out;
|
|
1080
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
|
+
};
|
|
1081
1322
|
function emitRdsCss(theme, opts = {}) {
|
|
1082
1323
|
var _a, _b, _c;
|
|
1083
|
-
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
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) => {
|
|
1087
1334
|
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 `${
|
|
1335
|
+
return `${sels.join(", ")} {
|
|
1089
1336
|
${lines.join("\n")}
|
|
1090
1337
|
}`;
|
|
1091
1338
|
};
|
|
1092
|
-
const
|
|
1093
|
-
|
|
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
|
+
}
|
|
1094
1352
|
return [
|
|
1095
|
-
block2(
|
|
1096
|
-
block2(
|
|
1097
|
-
block2(
|
|
1353
|
+
block2(scopes, __spreadValues(__spreadValues({}, theme.light), theme.vars)),
|
|
1354
|
+
block2(darkSels, theme.dark, theme.light),
|
|
1355
|
+
block2(plates, theme.brand, theme.light)
|
|
1098
1356
|
].join("\n\n") + "\n";
|
|
1099
1357
|
}
|
|
1100
1358
|
function rdsContrast(theme, mode, fg, bg) {
|
|
@@ -1102,6 +1360,121 @@ function rdsContrast(theme, mode, fg, bg) {
|
|
|
1102
1360
|
if (!/^#[0-9a-f]{6}$/i.test(a) || !/^#[0-9a-f]{6}$/i.test(c)) return null;
|
|
1103
1361
|
return contrastRatio(a, c);
|
|
1104
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
|
+
}
|
|
1105
1478
|
|
|
1106
1479
|
// generate/emitCss.ts
|
|
1107
1480
|
var SECTIONS = [
|
|
@@ -1174,153 +1547,175 @@ ${lightBlock}${darkBlock}
|
|
|
1174
1547
|
}
|
|
1175
1548
|
|
|
1176
1549
|
// 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" }
|
|
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`)" }
|
|
1214
1578
|
];
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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;
|
|
1229
1613
|
}
|
|
1614
|
+
var TARGET_FILES = { claude: "CLAUDE.md", cursor: ".cursorrules", agents: "AGENTS.md" };
|
|
1230
1615
|
function fileLabel(target) {
|
|
1231
|
-
return target
|
|
1616
|
+
return TARGET_FILES[target];
|
|
1232
1617
|
}
|
|
1233
|
-
function
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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}
|
|
1251
1650
|
|
|
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.
|
|
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.
|
|
1258
1654
|
|
|
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
|
-
\`\`\`
|
|
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.
|
|
1283
1670
|
|
|
1284
|
-
##
|
|
1285
|
-
|
|
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\`.
|
|
1286
1682
|
|
|
1287
|
-
##
|
|
1288
|
-
|
|
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)}
|
|
1289
1686
|
|
|
1290
|
-
|
|
1291
|
-
|
|
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 ? `
|
|
1292
1693
|
|
|
1293
|
-
|
|
1294
|
-
**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 ")}.` : ""}
|
|
1295
1695
|
|
|
1296
|
-
##
|
|
1297
|
-
${
|
|
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.
|
|
1298
1701
|
|
|
1299
1702
|
## 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
|
-
\`\`\`
|
|
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.
|
|
1321
1714
|
|
|
1322
|
-
<!-- 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. -->
|
|
1323
1716
|
`;
|
|
1717
|
+
if (MARKERS.test(text)) throw new Error("emitClaudeMd: the generated text contains a rojao-ds marker");
|
|
1718
|
+
return text;
|
|
1324
1719
|
}
|
|
1325
1720
|
|
|
1326
|
-
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 };
|
package/dist/generate.d.ts
CHANGED
|
@@ -3,7 +3,14 @@ import { a as BrandDef } from './recipe.schema-Cq7e82nx.js';
|
|
|
3
3
|
export { A as Archetype, B as BrandColors, 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
|
|
|
5
5
|
type RdsMode = "light" | "dark" | "brand";
|
|
6
|
-
type RdsTheme = Record<RdsMode, Record<string, string
|
|
6
|
+
type RdsTheme = Record<RdsMode, Record<string, string>> & {
|
|
7
|
+
/** The brand's own variables (Figma `brand` collection, `<brand>/<name>`), one value for every mode. */
|
|
8
|
+
vars?: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
type RdsThemeOptions = {
|
|
11
|
+
/** Where the warnings go (an explicit BrandDef.heading that fails AA). Default console.warn. */
|
|
12
|
+
warn?: (message: string) => void;
|
|
13
|
+
};
|
|
7
14
|
/**
|
|
8
15
|
* Roles of the [RDS] theme collection, in Figma order: [name, dark source, brand source].
|
|
9
16
|
* The light mode always reads the plain base token. In dark and brand, Figma points either to the
|
|
@@ -12,22 +19,97 @@ type RdsTheme = Record<RdsMode, Record<string, string>>;
|
|
|
12
19
|
declare const ROLES: ReadonlyArray<readonly [string, "l" | "d", "l" | "d" | "b"]>;
|
|
13
20
|
/** CSS custom property of a role: the Figma path with hyphens (the [RDS] codeSyntax rule). */
|
|
14
21
|
declare const roleVar: (role: string) => string;
|
|
15
|
-
declare function generateRdsTheme(def: BrandDef): RdsTheme;
|
|
22
|
+
declare function generateRdsTheme(def: BrandDef, opts?: RdsThemeOptions): RdsTheme;
|
|
23
|
+
/**
|
|
24
|
+
* The selectors on which @rojaostudio/ds redeclares its component tokens (styles/rds/components.css and
|
|
25
|
+
* foundation.css). A component token holds `var(--theme-role)`, and a custom property is resolved on the element
|
|
26
|
+
* where it is declared: a theme scope that none of these match would repaint the roles but not the components
|
|
27
|
+
* inside it, which keep the colours resolved at :root. The attributes are the generic way in: put
|
|
28
|
+
* `data-rds-scope` on a scope element, `data-rds-mode="dark"` on a dark one, `data-rds-plate` on a plate.
|
|
29
|
+
*/
|
|
30
|
+
declare const RDS_SCOPE_SELECTORS: readonly [":root", ".ds-scope", "[data-rds-scope]", ".dark", "[data-rds-mode]", ".ds-plate", "[data-rds-plate]"];
|
|
31
|
+
/** RDS_SCOPE_SELECTORS as one selector list, as the component token layer is emitted. */
|
|
32
|
+
declare const RDS_TOKEN_SCOPE: string;
|
|
16
33
|
type RdsCssOptions = {
|
|
17
|
-
/** Scope of the light mode. Default `:root, .ds-scope`. */
|
|
34
|
+
/** Scope of the light mode. Default `:root, .ds-scope, [data-rds-scope]`. */
|
|
18
35
|
scope?: string;
|
|
19
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Selector of the dark mode, combined with each scope. Default `.dark, [data-rds-mode="dark"]`. A selector
|
|
38
|
+
* anchored at the root (`:root[data-theme="dark"]`, `html.dark`) is used as is: the theme switches on <html>,
|
|
39
|
+
* as next-themes does.
|
|
40
|
+
*/
|
|
20
41
|
dark?: string;
|
|
21
|
-
/** Selector of the brand plate (a section painted with the primary). Default `.ds-plate`. */
|
|
42
|
+
/** Selector of the brand plate (a section painted with the primary). Default `.ds-plate, [data-rds-plate]`. */
|
|
22
43
|
plate?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Skip the check that every selector is covered by the component tokens of @rojaostudio/ds (RDS_SCOPE_SELECTORS).
|
|
46
|
+
* Only for a theme read by your own CSS, without the components.
|
|
47
|
+
*/
|
|
48
|
+
allowUncovered?: boolean;
|
|
23
49
|
};
|
|
24
50
|
/**
|
|
25
51
|
* The theme as CSS. Dark and plate only carry what differs from light: they inherit the rest
|
|
26
52
|
* through the cascade, like the [RDS] modes that point back to the plain token.
|
|
53
|
+
*
|
|
54
|
+
* Throws when a selector would leave the components of @rojaostudio/ds on the root colours (see
|
|
55
|
+
* RDS_SCOPE_SELECTORS), unless `allowUncovered`.
|
|
27
56
|
*/
|
|
28
57
|
declare function emitRdsCss(theme: RdsTheme, opts?: RdsCssOptions): string;
|
|
29
58
|
/** Contrast of a role pair in one mode (alpha colours are not measured: they depend on what is below). */
|
|
30
59
|
declare function rdsContrast(theme: RdsTheme, mode: RdsMode, fg: string, bg: string): number | null;
|
|
60
|
+
/**
|
|
61
|
+
* The main text pairs of the theme: [text role, the surface it sits on]. Measured in every mode; a pair with an
|
|
62
|
+
* alpha colour is skipped (its contrast depends on what is below).
|
|
63
|
+
*/
|
|
64
|
+
declare const RDS_CONTRAST_PAIRS: ReadonlyArray<readonly [string, string]>;
|
|
65
|
+
type RdsContrastFailure = {
|
|
66
|
+
mode: RdsMode;
|
|
67
|
+
fg: string;
|
|
68
|
+
bg: string;
|
|
69
|
+
ratio: number;
|
|
70
|
+
};
|
|
71
|
+
/** The pairs of RDS_CONTRAST_PAIRS that fail WCAG AA (4.5:1), mode by mode. Empty means every pair passes. */
|
|
72
|
+
declare function rdsContrastReport(theme: RdsTheme): RdsContrastFailure[];
|
|
73
|
+
/**
|
|
74
|
+
* A brand as drawn in the [RDS] Base Tokens file: for each theme mode, every role points to a primitive of the
|
|
75
|
+
* [RDS] Primitives library by name ("accyan/400"), and `primitives` holds the colour Figma resolves for each.
|
|
76
|
+
* Exported by `figma/export-brand.js`. The theme comes out one to one with the Figma brand mode.
|
|
77
|
+
*/
|
|
78
|
+
type RdsBrandTable = {
|
|
79
|
+
$schema?: "rds-brand-table/1";
|
|
80
|
+
name: string;
|
|
81
|
+
primitives: Record<string, string>;
|
|
82
|
+
modes: Record<RdsMode, Record<string, string>>;
|
|
83
|
+
/** The brand's own variables of the `brand` collection, by Figma path ("marca/ciano") → primitive or value. */
|
|
84
|
+
vars?: Record<string, string>;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The theme of a brand table. Fails on a missing role or an unknown primitive, listing them all. A table exported
|
|
88
|
+
* before a role of ADDED_ROLES existed is the exception: the role is taken from the token Figma aliases it to, and
|
|
89
|
+
* `opts.warn` asks to export the table again. The contrast of the main text pairs (rdsContrastReport) is only
|
|
90
|
+
* reported, through `opts.warn`: the table is the brand as drawn in Figma, kept one to one even where a pair fails.
|
|
91
|
+
*/
|
|
92
|
+
declare function rdsThemeFromTable(table: RdsBrandTable, opts?: RdsThemeOptions): RdsTheme;
|
|
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
|
+
}
|
|
31
113
|
|
|
32
114
|
/**
|
|
33
115
|
* emitCss.ts — formata o output do generateTheme num theme.css legível.
|
|
@@ -37,12 +119,38 @@ declare function rdsContrast(theme: RdsTheme, mode: RdsMode, fg: string, bg: str
|
|
|
37
119
|
|
|
38
120
|
declare function emitCss(def: BrandDef): string;
|
|
39
121
|
|
|
122
|
+
/**
|
|
123
|
+
* emitClaudeMd.ts — a brand → the rules file (CLAUDE.md / .cursorrules / AGENTS.md) that teaches
|
|
124
|
+
* the AI (Claude Code, Cursor, any agent) to build with the brand on Rojão DS 2.0.
|
|
125
|
+
*
|
|
126
|
+
* 2.0 (issue #5): the file describes the [RDS] setup — components with their own CSS
|
|
127
|
+
* (`@rojaostudio/ds/styles/rds.css`), the generated theme file imported after it, the theme roles
|
|
128
|
+
* (`--surface-card`, `--text-on-primary`…) and the foundation tokens (`--space-*`, `--radius-*`,
|
|
129
|
+
* `--type-*`). The 1.x text (Tailwind utilities, base.css, `theme-<name>`, shadcn mapping) is gone:
|
|
130
|
+
* no component reads it any more.
|
|
131
|
+
*
|
|
132
|
+
* The input is either a recipe (BrandDef, theme derived by `generateRdsTheme`) or a brand table
|
|
133
|
+
* exported from Figma (`rdsThemeFromTable`). Colours in the table are the theme's own hex values,
|
|
134
|
+
* so the AI "sees" the brand. Pure (no I/O).
|
|
135
|
+
*/
|
|
136
|
+
|
|
40
137
|
type ClaudeMdTarget = "claude" | "cursor" | "agents";
|
|
41
138
|
interface ClaudeMdOptions {
|
|
42
|
-
/** URL
|
|
139
|
+
/** URL of a hosted theme stylesheet, if any. No default: the theme is a local file. */
|
|
43
140
|
cssUrl?: string;
|
|
141
|
+
/** Name of the local theme file the consumer imports. Default `rds-theme.css`. */
|
|
142
|
+
cssFile?: string;
|
|
143
|
+
/** The theme, if it was already generated. Default: derived from the input. */
|
|
144
|
+
theme?: RdsTheme;
|
|
145
|
+
/** File the text is written to (only the footer changes). Default `claude`. */
|
|
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;
|
|
44
152
|
}
|
|
45
|
-
declare function emitClaudeMd(
|
|
153
|
+
declare function emitClaudeMd(source: BrandDef | RdsBrandTable, opts?: ClaudeMdOptions): string;
|
|
46
154
|
|
|
47
155
|
/**
|
|
48
156
|
* scale.ts — deriva uma escala 50–900 a partir de UMA cor custom (1 hex). #28.
|
|
@@ -110,4 +218,4 @@ declare function isNeutralBrand(def: {
|
|
|
110
218
|
palettes?: PaletteDefs;
|
|
111
219
|
}): boolean;
|
|
112
220
|
|
|
113
|
-
export { BrandDef, type ClaudeMdOptions, type ClaudeMdTarget, ROLES as RDS_ROLES, type RdsCssOptions, type RdsMode, type RdsTheme, SCALE_STEPS, type Scale, type ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, 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 { ROLES as RDS_ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, 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-LP6UA4EW.js';
|
|
2
2
|
import './chunk-FRWRLCYM.js';
|
|
3
3
|
import './chunk-ORMEWXMH.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { G as GenResult, R as ResolvedTheme, T as TokenMap, g as generateTheme, r as resolveTheme } from './generateTheme-CUfNpsKs.js';
|
|
2
|
-
export { ClaudeMdOptions, ClaudeMdTarget, RDS_ROLES, RdsCssOptions, RdsMode, RdsTheme, SCALE_STEPS, Scale, ScaleStep, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, 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
|
-
export { recipes } from './chunk-
|
|
2
|
-
export { ROLES as RDS_ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-
|
|
1
|
+
export { recipes } from './chunk-PV6GFEAM.js';
|
|
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-LP6UA4EW.js';
|
|
3
3
|
export { primitives, tokens } from './chunk-FRWRLCYM.js';
|
|
4
4
|
import './chunk-ORMEWXMH.js';
|
package/dist/recipes.d.ts
CHANGED
|
@@ -15,7 +15,11 @@ import { a as BrandDef } from './recipe.schema-Cq7e82nx.js';
|
|
|
15
15
|
* ele não descreve marca de ninguém, descreve ponto de partida.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* rojao — navy + flare accent on zinc, Inter. The `rojao` brand of the Figma [RDS] (2.0).
|
|
20
|
+
* text/heading is the primary (navy) on light, as in the [RDS] base collection: the logo pairs are navy and
|
|
21
|
+
* orange, white and orange. The orange title is the Heading's tone=accent, not the default.
|
|
22
|
+
*/
|
|
19
23
|
declare const rojao: BrandDef;
|
|
20
24
|
declare const recipes: {
|
|
21
25
|
readonly rojao: BrandDef;
|
package/dist/recipes.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { recipes, rojao } from './chunk-
|
|
1
|
+
export { recipes, rojao } from './chunk-PV6GFEAM.js';
|
|
2
2
|
import './chunk-ORMEWXMH.js';
|
package/dist/themes.js
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* global figma */
|
|
2
|
+
// Exports one brand of the [RDS] Base Tokens file as a brand table for rdsThemeFromTable().
|
|
3
|
+
// Run inside the Base Tokens file with the Figma Plugin API (a plugin console or an agent with use_figma),
|
|
4
|
+
// after setting BRAND to the mode name of the `base` collection. The return value is the JSON to save.
|
|
5
|
+
//
|
|
6
|
+
// Each theme role is followed through the aliases of the `base` collection (in the brand's mode) down to a
|
|
7
|
+
// primitive of the [RDS] Primitives library, kept by name ("accyan/400") with the colour Figma resolves.
|
|
8
|
+
// The brand's own variables in the `brand` collection (`<brand>/<name>`, as marca/ciano) come along in `vars`.
|
|
9
|
+
const BRAND = "rojao";
|
|
10
|
+
|
|
11
|
+
const cols = await figma.variables.getLocalVariableCollectionsAsync();
|
|
12
|
+
const base = cols.find((c) => c.name === "base");
|
|
13
|
+
const theme = cols.find((c) => c.name === "theme");
|
|
14
|
+
if (!base || !theme) throw new Error("open the [RDS] Base Tokens file");
|
|
15
|
+
const brandMode = base.modes.find((m) => m.name === BRAND);
|
|
16
|
+
if (!brandMode) throw new Error(`no brand "${BRAND}" in base: ${base.modes.map((m) => m.name).join(", ")}`);
|
|
17
|
+
const local = new Set(cols.map((c) => c.id));
|
|
18
|
+
|
|
19
|
+
const hex = (c) =>
|
|
20
|
+
"#" +
|
|
21
|
+
[c.r, c.g, c.b].map((x) => Math.round(x * 255).toString(16).padStart(2, "0")).join("") +
|
|
22
|
+
(c.a !== undefined && c.a < 1 ? Math.round(c.a * 255).toString(16).padStart(2, "0") : "");
|
|
23
|
+
const literal = (v, val) => (v.resolvedType === "COLOR" ? hex(val) : String(val));
|
|
24
|
+
const isAlias = (val) => val && typeof val === "object" && val.type === "VARIABLE_ALIAS";
|
|
25
|
+
|
|
26
|
+
const primitives = {};
|
|
27
|
+
async function resolve(v, themeMode) {
|
|
28
|
+
if (!local.has(v.variableCollectionId)) {
|
|
29
|
+
// A Primitives library variable: keep its name, record what it resolves to.
|
|
30
|
+
const val = Object.values(v.valuesByMode)[0];
|
|
31
|
+
if (isAlias(val)) return resolve(await figma.variables.getVariableByIdAsync(val.id), themeMode);
|
|
32
|
+
primitives[v.name] = literal(v, val);
|
|
33
|
+
return v.name;
|
|
34
|
+
}
|
|
35
|
+
const modeId = v.variableCollectionId === base.id ? brandMode.modeId
|
|
36
|
+
: v.variableCollectionId === theme.id ? themeMode : Object.keys(v.valuesByMode)[0];
|
|
37
|
+
const val = v.valuesByMode[modeId] ?? Object.values(v.valuesByMode)[0];
|
|
38
|
+
if (isAlias(val)) return resolve(await figma.variables.getVariableByIdAsync(val.id), themeMode);
|
|
39
|
+
return literal(v, val);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const modes = {};
|
|
43
|
+
for (const m of theme.modes) {
|
|
44
|
+
const roles = {};
|
|
45
|
+
for (const id of theme.variableIds) {
|
|
46
|
+
const v = await figma.variables.getVariableByIdAsync(id);
|
|
47
|
+
roles[v.name] = await resolve(v, m.modeId);
|
|
48
|
+
}
|
|
49
|
+
modes[m.name] = roles;
|
|
50
|
+
}
|
|
51
|
+
const vars = {};
|
|
52
|
+
const brandCol = cols.find((c) => c.name === "brand");
|
|
53
|
+
for (const id of brandCol?.variableIds ?? []) {
|
|
54
|
+
const v = await figma.variables.getVariableByIdAsync(id);
|
|
55
|
+
if (v.name.startsWith(`${BRAND}/`)) vars[v.name] = await resolve(v, theme.modes[0].modeId);
|
|
56
|
+
}
|
|
57
|
+
const sorted = Object.fromEntries(Object.entries(primitives).sort(([a], [b]) => a.localeCompare(b)));
|
|
58
|
+
return { $schema: "rds-brand-table/1", name: BRAND, primitives: sorted, modes, vars };
|
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.10",
|
|
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)",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"files": [
|
|
55
55
|
"LICENSE",
|
|
56
56
|
"README.md",
|
|
57
|
-
"dist"
|
|
57
|
+
"dist",
|
|
58
|
+
"figma/export-brand.js"
|
|
58
59
|
],
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"eslint": "^9.18.0",
|