@rojaostudio/ds-core 1.1.0-next.2 → 1.1.0-next.4

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/README.md CHANGED
@@ -9,7 +9,7 @@ npm i @rojaostudio/ds-core
9
9
  ```
10
10
 
11
11
  ```ts
12
- import { generateTheme, emitCss, emitClaudeMd } from '@rojaostudio/ds-core/generate';
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
- emitCss(brand); // theme.css — light and dark, derived from one color
23
- emitClaudeMd(brand); // the same brand as rules for Claude Code, Cursor or AGENTS.md
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` | `generateTheme`, `emitCss`, `emitClaudeMd`, `buildScale`, `contrastRatio` |
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 |
@@ -1092,7 +1092,7 @@ ${lines.join("\n")}
1092
1092
  const scopes = scope.split(",").map((s) => s.trim());
1093
1093
  const darkSel = scopes.map((s) => s === ":root" ? `:root${dark}, ${dark}` : `${s}${dark}, ${dark} ${s}`).join(", ");
1094
1094
  return [
1095
- block2(scope, theme.light),
1095
+ block2(scope, __spreadValues(__spreadValues({}, theme.light), theme.vars)),
1096
1096
  block2(darkSel, theme.dark, theme.light),
1097
1097
  block2(plate, theme.brand, theme.light)
1098
1098
  ].join("\n\n") + "\n";
@@ -1123,6 +1123,19 @@ function rdsThemeFromTable(table) {
1123
1123
  } else out[mode][roleVar(role)] = ref2;
1124
1124
  }
1125
1125
  }
1126
+ const valueOf = (where, ref2) => {
1127
+ if (!isColourRef(ref2)) return ref2;
1128
+ const value = table.primitives[ref2];
1129
+ if (value === void 0) problems.push(`${where} points to unknown primitive "${ref2}"`);
1130
+ return value == null ? void 0 : value.toLowerCase();
1131
+ };
1132
+ if (table.vars) {
1133
+ out.vars = {};
1134
+ for (const [name, ref2] of Object.entries(table.vars)) {
1135
+ const value = valueOf(`var "${name}"`, ref2);
1136
+ if (value !== void 0) out.vars[roleVar(name)] = value;
1137
+ }
1138
+ }
1126
1139
  if (problems.length) throw new Error(`rdsThemeFromTable(${table.name}):
1127
1140
  ${problems.join("\n ")}`);
1128
1141
  return out;
@@ -1199,152 +1212,156 @@ ${lightBlock}${darkBlock}
1199
1212
  }
1200
1213
 
1201
1214
  // generate/emitClaudeMd.ts
1202
- function resolveColor(v) {
1203
- var _a, _b;
1204
- if (!v) return v;
1205
- if (v.startsWith("color-mix")) {
1206
- const mix = v.match(/var\(--color-([a-zA-Z]+)(?:-(\d+))?\)\s+(\d+)%/);
1207
- return mix ? `${mix[1]}${mix[2] ? "-" + mix[2] : ""} a ${mix[3]}%` : v;
1208
- }
1209
- const scale = v.match(/var\(--color-([a-zA-Z]+)-(\d+)\)/);
1210
- if (scale) {
1211
- const pal = primitives.color[scale[1]];
1212
- return (_a = pal == null ? void 0 : pal[scale[2]]) != null ? _a : v;
1213
- }
1214
- const flat = v.match(/var\(--color-(white|black)\)/);
1215
- if (flat) return (_b = primitives.color[flat[1]]) != null ? _b : v;
1216
- return v;
1217
- }
1218
- var TOKEN_DOCS = [
1219
- { key: "--brand-primary", use: "bot\xE3o prim\xE1rio, fills de marca" },
1220
- { key: "--brand-on-primary", use: "texto/\xEDcone sobre o prim\xE1rio" },
1221
- { key: "--brand-hover", use: "hover do prim\xE1rio" },
1222
- { key: "--brand-secondary", use: "2\xAA cor de marca \u2014 S\xD3 como fill (com --brand-on-secondary); nunca borda/texto sobre superf\xEDcie" },
1223
- { key: "--brand-on-secondary", use: "texto sobre o fill secund\xE1rio" },
1224
- { key: "--brand-accent", use: "destaque, links, sucesso (use com parcim\xF4nia)" },
1225
- { key: "--brand-accent-light", use: "fundo tingido do accent" },
1226
- { key: "--brand-on-accent", use: "texto sobre o accent" },
1227
- { key: "--surface-page", use: "fundo da p\xE1gina" },
1228
- { key: "--surface-default", use: "cards, pain\xE9is" },
1229
- { key: "--surface-raised", use: "superf\xEDcie elevada/hover" },
1230
- { key: "--border-subtle", use: "divis\xF3rias fracas" },
1231
- { key: "--border-default", use: "bordas de inputs e cards" },
1232
- { key: "--border-strong", use: "bordas enfatizadas" },
1233
- { key: "--border-focus", use: "anel de foco" },
1234
- { key: "--text-primary", use: "t\xEDtulos, corpo" },
1235
- { key: "--text-secondary", use: "texto de apoio" },
1236
- { key: "--text-muted", use: "legendas, metadados" },
1237
- { key: "--text-inverse", use: "texto sobre superf\xEDcies invertidas" },
1238
- { key: "--icon-default", use: "cor padr\xE3o de \xEDcone" }
1215
+ var ROLE_DOCS = [
1216
+ { role: "colors/primary/default", use: "primary action, brand fills" },
1217
+ { role: "text/on/primary", use: "text/icon on the primary fill" },
1218
+ { role: "colors/primary/active", use: "pressed primary" },
1219
+ { role: "colors/secondary/default", use: "secondary brand fill (with `--text-on-secondary`)" },
1220
+ { role: "colors/accent/default", use: "highlight \u2014 sparingly (with `--text-on-accent`)" },
1221
+ { role: "surface/page", use: "page background" },
1222
+ { role: "surface/card", use: "cards" },
1223
+ { role: "surface/panel", use: "panels, side areas" },
1224
+ { role: "surface/muted", use: "muted areas, wells" },
1225
+ { role: "surface/tint/default", use: "selected / tinted fill (with `--text-on-tint`)" },
1226
+ { role: "border/default", use: "dividers, card borders" },
1227
+ { role: "border/strong", use: "control borders, emphasis" },
1228
+ { role: "focus/ring", use: "focus ring" },
1229
+ { role: "text/heading", use: "headings" },
1230
+ { role: "text/body", use: "body text" },
1231
+ { role: "text/muted", use: "supporting text" },
1232
+ { role: "text/subtle", use: "captions, metadata" },
1233
+ { role: "text/link", use: "links" },
1234
+ { role: "colors/state/error", use: "error fill (with `--text-on-error`)" },
1235
+ { role: "text/error", use: "error message text" },
1236
+ { role: "colors/state/success", use: "success fill (with `--text-on-success`)" },
1237
+ { role: "colors/state/warning", use: "warning fill (with `--text-on-warning`)" }
1239
1238
  ];
1240
- function tokenTable(light2, dark) {
1241
- const rows = TOKEN_DOCS.filter((t) => t.key in light2).map((t) => {
1242
- const l = resolveColor(light2[t.key]);
1243
- const d = t.key in dark ? resolveColor(dark[t.key]) : "\u2014";
1244
- return `| \`${t.key}\` | ${t.use} | \`${l}\` | \`${d}\` |`;
1245
- });
1246
- return [
1247
- "| Token | Use para | Light | Dark |",
1248
- "|---|---|---|---|",
1249
- ...rows
1250
- ].join("\n");
1251
- }
1252
- function scaleLine(obj, unit = "px") {
1253
- return Object.entries(obj).map(([k, v]) => `\`${k}\`=${v}${v === 9999 ? "" : unit}`).join(" \xB7 ");
1239
+ var CLAUDE_MD_COMPONENTS = [
1240
+ "button",
1241
+ "icon-button",
1242
+ "tooltip",
1243
+ "input",
1244
+ "textarea",
1245
+ "select",
1246
+ "checkbox",
1247
+ "radio-group",
1248
+ "switch",
1249
+ "card",
1250
+ "dialog",
1251
+ "sheet",
1252
+ "tabs",
1253
+ "table",
1254
+ "toast",
1255
+ "badge",
1256
+ "alert",
1257
+ "avatar",
1258
+ "dropdown-menu",
1259
+ "page-header",
1260
+ "sidebar",
1261
+ "empty",
1262
+ "skeleton",
1263
+ "pagination"
1264
+ ];
1265
+ var CLAUDE_MD_FOUNDATION = {
1266
+ space: ["--space-4", "--space-8", "--space-12", "--space-16", "--space-24", "--space-32", "--space-40", "--space-48", "--space-56", "--space-64"],
1267
+ radius: ["--radius-xs", "--radius-control", "--radius-field", "--radius-card", "--radius-container", "--radius-full"],
1268
+ type: ["heading", "lead", "body", "label", "button", "small", "caption"],
1269
+ font: "--type-font-stack"
1270
+ };
1271
+ function isTable(x) {
1272
+ return typeof x === "object" && x !== null && "modes" in x && "primitives" in x;
1254
1273
  }
1255
1274
  function fileLabel(target) {
1256
1275
  return target === "cursor" ? ".cursorrules" : target === "agents" ? "AGENTS.md" : "CLAUDE.md";
1257
1276
  }
1258
- function emitClaudeMd(def, opts = {}) {
1277
+ function roleTable(theme) {
1278
+ const rows = ROLE_DOCS.filter((r) => roleVar(r.role) in theme.light).map((r) => {
1279
+ var _a;
1280
+ const v = roleVar(r.role);
1281
+ return `| \`${v}\` | ${r.use} | \`${theme.light[v]}\` | \`${(_a = theme.dark[v]) != null ? _a : theme.light[v]}\` |`;
1282
+ });
1283
+ return ["| Token | Use for | Light | Dark |", "|---|---|---|---|", ...rows].join("\n");
1284
+ }
1285
+ function emitClaudeMd(source, opts = {}) {
1259
1286
  var _a, _b, _c;
1260
- const { supported, light: light2, dark, note } = generateTheme(def);
1261
- if (!supported) throw new Error(`emitClaudeMd: ${def.name} n\xE3o suportado \u2014 ${note}`);
1262
- const Name = def.name[0].toUpperCase() + def.name.slice(1);
1263
- const cssUrl = (_a = opts.cssUrl) != null ? _a : null;
1264
- const bodyFont = typeof ((_b = def.fonts) == null ? void 0 : _b.body) === "string" ? def.fonts.body : "inter";
1265
- const displayFont = typeof ((_c = def.fonts) == null ? void 0 : _c.display) === "string" ? def.fonts.display : bodyFont;
1266
- const fontTitle = (s) => s.split("-").map((w) => {
1267
- var _a2;
1268
- return ((_a2 = w[0]) == null ? void 0 : _a2.toUpperCase()) + w.slice(1);
1269
- }).join(" ");
1270
- const neutral = primitives.color.neutral;
1271
- return `# Design System \u2014 ${Name} (brand: \`${def.name}\`)
1272
-
1273
- > This project uses the **${Name}** design system. When you build any UI \u2014
1274
- > components, pages, screens \u2014 follow the rules below so every screen looks like the
1275
- > same product. Do not invent colors, fonts, spacing, or radii.
1287
+ const theme = (_a = opts.theme) != null ? _a : isTable(source) ? rdsThemeFromTable(source) : generateRdsTheme(source);
1288
+ const name = source.name;
1289
+ const Name = name ? name[0].toUpperCase() + name.slice(1) : "Brand";
1290
+ const description = !isTable(source) && source.description ? ` \u2014 ${source.description}` : "";
1291
+ const cssFile = (_b = opts.cssFile) != null ? _b : "rds-theme.css";
1292
+ const target = (_c = opts.target) != null ? _c : "claude";
1293
+ const f = CLAUDE_MD_FOUNDATION;
1294
+ const own = theme.vars ? Object.keys(theme.vars) : [];
1295
+ const themeImport = opts.cssUrl ? `@import "@rojaostudio/ds/styles/rds.css";
1296
+ @import url("${opts.cssUrl}"); /* the brand theme, AFTER rds.css */` : `@import "@rojaostudio/ds/styles/rds.css";
1297
+ @import "./${cssFile}"; /* the brand theme, AFTER rds.css */`;
1298
+ return `# Design System \u2014 ${Name}
1276
1299
 
1277
- ## Brand identity
1278
- - **Name:** ${Name}
1279
- - **Look:** ${isNeutralBrand(def) ? "neutral \u2014 black & white lead, the accent does the talking" : "chromatic \u2014 the brand color leads"}${def.description ? ` \u2014 ${def.description}` : ""}
1280
- - **Body font:** ${fontTitle(bodyFont)} \xB7 **Display font:** ${fontTitle(displayFont)} (load from Google Fonts; never substitute)
1281
- - **Default radius:** \`${primitives.radius.md}px\` (\`rounded-lg\`). Pills/avatars fully round.
1282
- - **Light AND dark are first-class** \u2014 every screen must work in both.
1300
+ > This project uses **Roj\xE3o DS 2.0** with the **${Name}** brand${description}. When you build any
1301
+ > UI \u2014 components, pages, screens \u2014 follow the rules below so every screen looks like the same
1302
+ > product. Do not invent colors, fonts, spacing or radii.
1283
1303
 
1284
- ## How to apply
1285
- 1. ${cssUrl ? `Import the brand stylesheet once at the app root:
1286
- \`\`\`html
1287
- <link rel="stylesheet" href="${cssUrl}" />
1288
- \`\`\`` : `Put \`${def.name}.css\` (downloaded with this file) in your project and import it
1289
- once at the app root:
1290
- \`\`\`css
1291
- @import "./${def.name}.css";
1292
- \`\`\`
1293
- The theme is a file **you own** \u2014 commit it. Nothing here depends on an external host.`}
1294
- 2. Put the theme class on \`<html>\`:
1295
- \`\`\`html
1296
- <html class="theme-${def.name}"> <!-- light -->
1297
- <html class="theme-${def.name} dark"> <!-- dark -->
1298
- \`\`\`
1299
- > One sheet can hold **several themes** \u2014 each as its own \`theme-<name>\` class.
1300
- > Switch with the class (one product per app, or many on one page).${cssUrl ? `
1301
- > Edit the theme at the source and this same URL updates **all your apps at once**.` : ""}
1302
- 3. Style everything with the semantic tokens below, as CSS variables \u2014 they already
1303
- flip between light and dark. You never write a hex value in a component.
1304
- \`\`\`css
1305
- .card { background: var(--surface-default); color: var(--text-primary);
1306
- border: 1px solid var(--border-default); border-radius: ${primitives.radius.md}px; }
1307
- \`\`\`
1304
+ ## Setup (once per app)
1305
+ \`\`\`bash
1306
+ pnpm add @rojaostudio/ds
1307
+ \`\`\`
1308
+ \`\`\`css
1309
+ /* root stylesheet */
1310
+ ${themeImport}
1311
+ \`\`\`
1312
+ - \`rds.css\` carries the tokens and every component's styles, in cascade layers
1313
+ (\`rds.theme\`, \`rds.tokens\`, \`rds.components\`). No Tailwind, no other stylesheet.
1314
+ - ${opts.cssUrl ? `The theme is served from \`${opts.cssUrl}\`.` : `\`${cssFile}\` is the brand theme, generated \u2014 a file **you own**; commit it. Regenerate it
1315
+ with \`npx rojao-ds init\` instead of editing it by hand.`}
1316
+ - Dark mode: \`class="dark"\` on \`<html>\` (or any element). Light AND dark are first-class.
1317
+ - Brand plate: \`class="ds-plate"\` on a section paints it with the primary color; the roles inside
1318
+ flip so text and components stay legible.
1308
1319
 
1309
- ## Semantic color tokens \u2014 ALWAYS use these, NEVER hardcode a color
1310
- ${tokenTable(light2, dark)}
1320
+ ## Components first
1321
+ Use the design system components before writing your own markup. Import each from its own path:
1322
+ \`\`\`tsx
1323
+ import { Button } from "@rojaostudio/ds/components/button";
1324
+ import { IconButton } from "@rojaostudio/ds/components/icon-button";
1325
+ import { Tooltip } from "@rojaostudio/ds/components/tooltip";
1326
+ \`\`\`
1327
+ Available, among others: ${CLAUDE_MD_COMPONENTS.map((c) => `\`${c}\``).join(" \xB7 ")}.
1328
+ - Components bring their own CSS (inside \`rds.css\`). Don't restyle their internals, don't wrap
1329
+ them in Tailwind classes, don't rebuild them with shadcn/ui.
1330
+ - Every \`IconButton\` goes inside a \`Tooltip\` with the same text as its \`label\`.
1311
1331
 
1312
- ## Spacing scale (px) \u2014 use ONLY these steps for padding, margin, gap
1313
- ${scaleLine(primitives.space)}
1332
+ ## Theme roles \u2014 ALWAYS use these in your own CSS, NEVER hardcode a color
1333
+ They already flip between light, dark and the brand plate.
1334
+ ${roleTable(theme)}
1314
1335
 
1315
- ## Type scale (font-size, px)
1316
- ${scaleLine(primitives.fontSize)}
1336
+ Every role pairs a fill with its \`--text-on-*\` text. Example:
1337
+ \`\`\`css
1338
+ .panel { background: var(--surface-card); color: var(--text-body);
1339
+ border: var(--border-width) solid var(--border-default);
1340
+ border-radius: var(--radius-card); padding: var(--space-16); }
1341
+ \`\`\`${own.length ? `
1317
1342
 
1318
- **Font weights:** ${scaleLine(primitives.fontWeight, "")}
1319
- **Line heights:** ${Object.entries(primitives.lineHeight).map(([k, v]) => `\`${k}\`=${v}`).join(" \xB7 ")}
1343
+ Brand's own variables (same value in every mode): ${own.map((v) => `\`${v}\``).join(" \xB7 ")}.` : ""}
1320
1344
 
1321
- ## Radius (px)
1322
- ${scaleLine(primitives.radius)}
1345
+ ## Foundation tokens
1346
+ - **Spacing** (padding, margin, gap) \u2014 ONLY: ${f.space.map((v) => `\`${v}\``).join(" \xB7 ")}
1347
+ - **Radius:** ${f.radius.map((v) => `\`${v}\``).join(" \xB7 ")}
1348
+ - **Type** \u2014 \`--type-<style>-size\` / \`--type-<style>-line\`, styles: ${f.type.map((v) => `\`${v}\``).join(" \xB7 ")}
1349
+ - **Font:** \`font-family: var(${f.font})\` \u2014 never substitute.
1323
1350
 
1324
1351
  ## Rules (do / don't)
1325
- - \u2705 Primary button = \`--brand-primary\` bg + \`--brand-on-primary\` text; hover \u2192 \`--brand-hover\`.
1326
- - \u2705 Low-emphasis / secondary action = NEUTRAL outline: \`--border-default\` border + \`--text-secondary\` text (legible by rule, light AND dark). A brand color belongs to the PRIMARY (filled) action, not a secondary outline.
1327
- - \u2705 Page wrapper uses \`--surface-page\`; cards use \`--surface-default\`.
1328
- - \u2705 Body text is \`--text-primary\`; never pure black/white text directly.
1329
- - \u2705 \`--brand-accent\` is the ONLY saturated color \u2014 use it sparingly (links, success, highlights).
1330
- - \u2705 Spacing, font-size and radius come ONLY from the scales above.
1331
- - \u274C Never write a raw hex (\`#000\`, \`bg-black\`) or a Tailwind palette color (\`bg-emerald-500\`, \`text-zinc-700\`) in a component \u2014 map it to a token above.
1332
- - \u274C Never use a brand color (incl. \`--brand-secondary\`) as a border/ring or as text directly on a surface. The engine guarantees contrast ONLY for a fill + its \`--brand-on-*\` text. As a stroke (WCAG 1.4.11, 3:1) or label (1.4.3, 4.5:1) on the surface it can fail \u2014 and in one mode but not the other. Brand colors = fills.
1333
- - \u274C Never introduce a second accent color. Don't hardcode dark-mode colors \u2014 tokens already flip.
1334
-
1335
- ## Use with shadcn/ui
1336
- You are NOT replacing shadcn. Use shadcn/ui components and apply THIS brand as the theme:
1337
- map the tokens above onto shadcn's CSS variables (\`--background\` \u2192 \`--surface-page\`,
1338
- \`--foreground\` \u2192 \`--text-primary\`, \`--primary\` \u2192 \`--brand-primary\`, \`--border\` \u2192
1339
- \`--border-default\`, \`--ring\` \u2192 \`--border-focus\`, \`--radius\` \u2192 \`${primitives.radius.md}px\`).
1340
- Build with shadcn's component shapes; paint them with these tokens. Don't restyle components from scratch.
1341
-
1342
- ## Primitive palette (reference \u2014 prefer the semantic tokens above)
1343
- \`\`\`
1344
- neutral 50 ${neutral[50]} \xB7 500 ${neutral[500]} \xB7 900 ${neutral[900]}
1345
- \`\`\`
1352
+ - \u2705 Primary action = \`Button\` (it already paints \`--colors-primary-default\` + \`--text-on-primary\`).
1353
+ - \u2705 Page uses \`--surface-page\`; cards \`--surface-card\`; body text \`--text-body\`.
1354
+ - \u2705 \`--colors-accent-default\` is the highlight \u2014 use it sparingly.
1355
+ - \u2705 Spacing, radius and type come ONLY from the foundation tokens above.
1356
+ - \u2705 Your own CSS is unlayered, so it already wins over the design system \u2014 no \`!important\`.
1357
+ - \u274C Never write a raw hex (\`#000\`) or a Tailwind palette class (\`bg-emerald-500\`) in a component.
1358
+ - \u274C Never use a brand fill as a border or as text on a surface: contrast is guaranteed only for a
1359
+ fill + its \`--text-on-*\`.
1360
+ - \u274C Never hardcode dark-mode colors \u2014 the roles already flip. Don't add a second accent color.
1361
+ - \u274C Don't use the 1.x stylesheet (\`styles/base.css\`), its utilities (\`bg-surface-*\`, \`text-fg-*\`) or
1362
+ \`theme-<name>\` classes.
1346
1363
 
1347
- <!-- Generated by Roj\xE3o DS \u2014 file: ${fileLabel("claude")} / ${fileLabel("cursor")} / ${fileLabel("agents")}. Drop it in your project root; Claude Code & Cursor read it automatically. -->
1364
+ <!-- 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. -->
1348
1365
  `;
1349
1366
  }
1350
1367
 
@@ -3,7 +3,10 @@ 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
+ };
7
10
  /**
8
11
  * Roles of the [RDS] theme collection, in Figma order: [name, dark source, brand source].
9
12
  * The light mode always reads the plain base token. In dark and brand, Figma points either to the
@@ -38,6 +41,8 @@ type RdsBrandTable = {
38
41
  name: string;
39
42
  primitives: Record<string, string>;
40
43
  modes: Record<RdsMode, Record<string, string>>;
44
+ /** The brand's own variables of the `brand` collection, by Figma path ("acassius/cyan") → primitive or value. */
45
+ vars?: Record<string, string>;
41
46
  };
42
47
  /** The theme of a brand table. Fails on a missing role or an unknown primitive, listing them all. */
43
48
  declare function rdsThemeFromTable(table: RdsBrandTable): RdsTheme;
@@ -50,12 +55,33 @@ declare function rdsThemeFromTable(table: RdsBrandTable): RdsTheme;
50
55
 
51
56
  declare function emitCss(def: BrandDef): string;
52
57
 
58
+ /**
59
+ * emitClaudeMd.ts — a brand → the rules file (CLAUDE.md / .cursorrules / AGENTS.md) that teaches
60
+ * the AI (Claude Code, Cursor, any agent) to build with the brand on Rojão DS 2.0.
61
+ *
62
+ * 2.0 (issue #5): the file describes the [RDS] setup — components with their own CSS
63
+ * (`@rojaostudio/ds/styles/rds.css`), the generated theme file imported after it, the theme roles
64
+ * (`--surface-card`, `--text-on-primary`…) and the foundation tokens (`--space-*`, `--radius-*`,
65
+ * `--type-*`). The 1.x text (Tailwind utilities, base.css, `theme-<name>`, shadcn mapping) is gone:
66
+ * no component reads it any more.
67
+ *
68
+ * The input is either a recipe (BrandDef, theme derived by `generateRdsTheme`) or a brand table
69
+ * exported from Figma (`rdsThemeFromTable`). Colours in the table are the theme's own hex values,
70
+ * so the AI "sees" the brand. Pure (no I/O).
71
+ */
72
+
53
73
  type ClaudeMdTarget = "claude" | "cursor" | "agents";
54
74
  interface ClaudeMdOptions {
55
- /** URL de um CSS de tema hospedado, se houver. Sem default: o padrão é arquivo local. */
75
+ /** URL of a hosted theme stylesheet, if any. No default: the theme is a local file. */
56
76
  cssUrl?: string;
77
+ /** Name of the local theme file the consumer imports. Default `rds-theme.css`. */
78
+ cssFile?: string;
79
+ /** The theme, if it was already generated. Default: derived from the input. */
80
+ theme?: RdsTheme;
81
+ /** File the text is written to (only the footer changes). Default `claude`. */
82
+ target?: ClaudeMdTarget;
57
83
  }
58
- declare function emitClaudeMd(def: BrandDef, opts?: ClaudeMdOptions): string;
84
+ declare function emitClaudeMd(source: BrandDef | RdsBrandTable, opts?: ClaudeMdOptions): string;
59
85
 
60
86
  /**
61
87
  * scale.ts — deriva uma escala 50–900 a partir de UMA cor custom (1 hex). #28.
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, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-DZAJKHNW.js';
1
+ export { ROLES as RDS_ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-UD53AUQQ.js';
2
2
  import './chunk-FRWRLCYM.js';
3
3
  import './chunk-ORMEWXMH.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
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, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-DZAJKHNW.js';
2
+ export { ROLES as RDS_ROLES, SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, emitRdsCss, generateRdsTheme, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, rdsContrast, rdsThemeFromTable, refToHex, relativeLuminance, resolveTheme, roleVar } from './chunk-UD53AUQQ.js';
3
3
  export { primitives, tokens } from './chunk-FRWRLCYM.js';
4
4
  import './chunk-ORMEWXMH.js';
package/dist/themes.js CHANGED
@@ -1,4 +1,4 @@
1
- import { generateTheme } from './chunk-DZAJKHNW.js';
1
+ import { generateTheme } from './chunk-UD53AUQQ.js';
2
2
  import './chunk-FRWRLCYM.js';
3
3
  import { __spreadValues, __objRest } from './chunk-ORMEWXMH.js';
4
4
 
@@ -5,6 +5,7 @@
5
5
  //
6
6
  // Each theme role is followed through the aliases of the `base` collection (in the brand's mode) down to a
7
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 acassius/cyan) come along in `vars`.
8
9
  const BRAND = "rojao";
9
10
 
10
11
  const cols = await figma.variables.getLocalVariableCollectionsAsync();
@@ -47,5 +48,11 @@ for (const m of theme.modes) {
47
48
  }
48
49
  modes[m.name] = roles;
49
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
+ }
50
57
  const sorted = Object.fromEntries(Object.entries(primitives).sort(([a], [b]) => a.localeCompare(b)));
51
- return { $schema: "rds-brand-table/1", name: BRAND, primitives: sorted, modes };
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.2",
3
+ "version": "1.1.0-next.4",
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)",