@rojaostudio/ds-core 1.1.0-next.0 → 1.1.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,8 +5,7 @@ var rojao = {
5
5
  brand: {
6
6
  primary: "navy-900",
7
7
  secondary: "navy-900",
8
- accent: "flare-700",
9
- heading: "flare-700"
8
+ accent: "flare-700"
10
9
  },
11
10
  surface: "zinc",
12
11
  text: "zinc",
@@ -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
- /** Selector of the dark mode, combined with the scope. Default `.dark`. */
36
+ /**
37
+ * Selector of the dark mode, combined with each scope. Default `.dark, [data-rds-mode="dark"]`. A selector
38
+ * anchored at the root (`:root[data-theme="dark"]`, `html.dark`) is used as is: the theme switches on <html>,
39
+ * as next-themes does.
40
+ */
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 de um CSS de tema hospedado, se houver. Sem default: o padrão é arquivo local. */
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(def: BrandDef, opts?: ClaudeMdOptions): string;
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-CFCKPKAF.js';
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-3QTDO2HW.js';
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-CFCKPKAF.js';
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
- /** rojao — navy + flare accent on zinc, Inter. The `rojao` brand of the Figma [RDS] (2.0). */
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-3QTDO2HW.js';
1
+ export { recipes, rojao } from './chunk-PV6GFEAM.js';
2
2
  import './chunk-ORMEWXMH.js';
package/dist/themes.js CHANGED
@@ -1,4 +1,4 @@
1
- import { generateTheme } from './chunk-CFCKPKAF.js';
1
+ import { generateTheme } from './chunk-LP6UA4EW.js';
2
2
  import './chunk-FRWRLCYM.js';
3
3
  import { __spreadValues, __objRest } from './chunk-ORMEWXMH.js';
4
4
 
@@ -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.0",
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",
@@ -68,7 +69,7 @@
68
69
  "publishConfig": {
69
70
  "registry": "https://registry.npmjs.org",
70
71
  "access": "public",
71
- "provenance": false
72
+ "provenance": true
72
73
  },
73
74
  "scripts": {
74
75
  "build": "tsup",