Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 54 additions & 26 deletions scripts/build-sparkle-design-theme.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,39 @@ function sortObjectKeys(obj) {
}

/**
* ブロック抽出ヘルパー: 指定トークン直後の {} をパース
* en: Extracts the brace block following a token (e.g. :root, @theme inline)
* `token` に一致する {} ブロックをすべて抽出する。sparkle-design.css には
* `:root { ... }` が2つある(プリミティブブロックと、実行時に参照できる
* セマンティックトークンブロック)ため、両方を集める必要がある。
* en: Extract every brace block matching `token`. sparkle-design.css
* declares two `:root { ... }` blocks (a primitive block and a
* semantic-token block meant to be usable at runtime), both of which need
* to be collected here.
*/
function extractBraceBlock(source, token) {
const start = source.indexOf(token);
if (start === -1) return null;
const open = source.indexOf("{", start);
if (open === -1) return null;
let depth = 0;
for (let i = open; i < source.length; i++) {
if (source[i] === "{") depth++;
else if (source[i] === "}") {
depth--;
if (depth === 0) {
// return inner content (without braces)
return source.slice(open + 1, i);
function extractAllBraceBlocks(source, token) {
const blocks = [];
let searchFrom = 0;
while (true) {
const start = source.indexOf(token, searchFrom);
if (start === -1) break;
const open = source.indexOf("{", start);
if (open === -1) break;
let depth = 0;
let end = -1;
for (let i = open; i < source.length; i++) {
if (source[i] === "{") depth++;
else if (source[i] === "}") {
depth--;
if (depth === 0) {
end = i;
break;
}
}
}
if (end === -1) break; // unbalanced
blocks.push(source.slice(open + 1, end));
searchFrom = end + 1;
}
return null;
return blocks;
}

/**
Expand Down Expand Up @@ -139,15 +152,31 @@ function buildTheme() {
const importRules = parseImports(noComments);

// 2) :root variables
const rootBlock = extractBraceBlock(noComments, ":root");
const rootVars = rootBlock ? parseDeclarations(rootBlock) : {};

// 3) @theme inline variables (same treatment as vars)
const themeInlineBlock = extractBraceBlock(noComments, "@theme inline");
const themeVars = themeInlineBlock ? parseDeclarations(themeInlineBlock) : {};

// Merge vars (semantic overrides primitive)
const cssVarsTheme = { ...rootVars, ...themeVars };
// sparkle-design.css には `:root { ... }` が2つある
// (プリミティブトークンブロックと、実行時に参照できるセマンティックトークン
// ブロック)。どちらも実際に解決可能な値(リテラル、または他の :root 変数への
// 参照)を持つため、両方をマージしてレジストリの cssVars に使う。
//
// `@theme inline` は意図的にここでは使わない: sparkle-design-cli 2.4.1+ の
// `@theme inline` は各セマンティック宣言が同名変数へ自己参照する
// (例: `--color-primary-500: var(--color-primary-500)`)設計になっており、
// これは Tailwind の compiled utility class 生成のためだけの仕組みで、
// レジストリ消費者へそのまま渡すと解決不能な循環参照になってしまう
// (goodpatch/sparkle-design#289 のレビュー指摘)。
// en: sparkle-design.css declares two `:root { ... }` blocks (primitive
// tokens, and semantic tokens meant to be usable at runtime). Both resolve
// to real values (literals or references to other :root vars), so merge
// both for the registry's cssVars.
// `@theme inline` is intentionally NOT used here: as of sparkle-design-cli
// 2.4.1+, every `@theme inline` declaration self-references its own
// semantic variable name (e.g. `--color-primary-500: var(--color-primary-500)`)
// — a Tailwind-compiler-only construct — which would become an unresolvable
// circular reference if handed to registry consumers as-is.
const rootBlocks = extractAllBraceBlocks(noComments, ":root");
const cssVarsTheme = rootBlocks.reduce(
(acc, block) => ({ ...acc, ...parseDeclarations(block) }),
{}
);

// 4) @utility blocks -> css
const utilities = parseUtilities(noComments);
Expand Down Expand Up @@ -190,4 +219,3 @@ if (import.meta.url === `file://${process.argv[1]}`) {
}

export default buildTheme;

Loading