Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ r/
.DS_Store
site/dist/
site/src/generated/

# JetBrains IDE settings
.idea/
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ pnpm site:dev # run the component showcase locally (one-time: pnpm --dir site
pnpm site:build # typecheck and build the showcase
```

`tests/hosts/*.json` pin the `--crowdin-*` variables each Crowdin host delivers to app iframes through `AP.getCssVariables`, trimmed to what the `app-theme` bridge reads plus the variables those values reference. The tests require every variable the bridge reads to be present on every host, and the `theme` literals to equal what crowdin.com sends - so when a host changes its variables, refresh the fixture and let the tests tell you what to update. To refresh one, open the host in a browser and copy `window.cssVariables` from the console: it is the same map the host sends to iframes.

`registry/src/**` is generated by `pnpm sync` - never edit it by hand. Crowdin styling changes go into the `theme`/`style`/`app-theme` items in `registry.json`. `registry/crowdin/**` is hand-written (the host-bridge sources shipped by `app-theme`) and survives sync.

To refresh the components from upstream shadcn, run the **Sync upstream** workflow from the Actions tab - it syncs, runs the full check suite, and opens a PR with the diff (only when something actually changed). New upstream components are opt-in: add the name to `scripts/upstream-items.json` first.

The showcase at [crowdin.github.io/ui-registry](https://crowdin.github.io/ui-registry/) is built from `site/` and renders `registry/src/**` directly with the generated Crowdin theme - use its header toggles to preview dark mode and simulated Crowdin host variables.
The showcase at [crowdin.github.io/ui-registry](https://crowdin.github.io/ui-registry/) is built from `site/` and renders `registry/src/**` directly with the generated Crowdin theme - use its header toggles to preview dark mode and the captured host palettes from `tests/hosts/`.
208 changes: 104 additions & 104 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,65 +21,65 @@
},
"light": {
"radius": "0.5rem",
"background": "oklch(1 0 0)",
"foreground": "oklch(0.268 0.022 230)",
"card": "oklch(1 0 0)",
"card-foreground": "oklch(0.268 0.022 230)",
"popover": "oklch(1 0 0)",
"popover-foreground": "oklch(0.268 0.022 230)",
"primary": "oklch(0.268 0.022 230)",
"primary-foreground": "oklch(1 0 0)",
"secondary": "oklch(0.935 0.007 228)",
"secondary-foreground": "oklch(0.268 0.022 230)",
"muted": "oklch(0.935 0.007 228)",
"muted-foreground": "oklch(0.577 0.014 228)",
"accent": "oklch(0.965 0.004 228)",
"accent-foreground": "oklch(0.268 0.022 230)",
"destructive": "oklch(0.577 0.21 27.3)",
"border": "oklch(0.91 0.006 228)",
"input": "oklch(0.91 0.006 228)",
"ring": "oklch(0.268 0.022 230)",
"background": "#ffffff",
"foreground": "rgba(38, 38, 38, 0.87)",
"card": "#ffffff",
"card-foreground": "rgba(38, 38, 38, 0.87)",
"popover": "#ffffff",
"popover-foreground": "rgba(38, 38, 38, 0.87)",
"primary": "#262626",
"primary-foreground": "#ffffff",
"secondary": "rgba(38, 38, 38, 0.1)",
"secondary-foreground": "rgba(38, 38, 38, 0.87)",
"muted": "rgba(38, 38, 38, 0.05)",
"muted-foreground": "rgba(38, 38, 38, 0.67)",
"accent": "rgba(38, 38, 38, 0.05)",
"accent-foreground": "rgba(38, 38, 38, 0.87)",
"destructive": "#dc5242",
"border": "rgba(38, 38, 38, 0.1)",
"input": "rgba(38, 38, 38, 0.1)",
"ring": "#262626",
"chart-1": "#3b82f6",
"chart-2": "#22c55e",
"chart-3": "#eab308",
"chart-4": "#c99eff",
"chart-5": "#dc5242",
"sidebar": "oklch(0.975 0.003 228)",
"sidebar-foreground": "oklch(0.268 0.022 230)",
"sidebar-primary": "oklch(0.268 0.022 230)",
"sidebar-primary-foreground": "oklch(1 0 0)",
"sidebar-accent": "oklch(0.965 0.004 228)",
"sidebar-accent-foreground": "oklch(0.268 0.022 230)",
"sidebar-border": "oklch(0.91 0.006 228)",
"sidebar-ring": "oklch(0.268 0.022 230)"
"sidebar": "#eeeeee",
"sidebar-foreground": "rgba(38, 38, 38, 0.87)",
"sidebar-primary": "#262626",
"sidebar-primary-foreground": "#ffffff",
"sidebar-accent": "rgba(38, 38, 38, 0.05)",
"sidebar-accent-foreground": "rgba(38, 38, 38, 0.87)",
"sidebar-border": "rgba(38, 38, 38, 0.1)",
"sidebar-ring": "#262626"
},
"dark": {
"background": "oklch(0.253 0.02 248)",
"foreground": "oklch(1 0 0)",
"card": "oklch(0.306 0.02 248)",
"card-foreground": "oklch(1 0 0)",
"popover": "oklch(0.306 0.02 248)",
"popover-foreground": "oklch(1 0 0)",
"primary": "oklch(0.636 0.165 142.5)",
"primary-foreground": "oklch(1 0 0)",
"secondary": "oklch(0.4 0.022 248)",
"secondary-foreground": "oklch(1 0 0)",
"muted": "oklch(0.21 0.017 248)",
"muted-foreground": "oklch(0.644 0.009 248)",
"accent": "oklch(0.385 0.016 248)",
"accent-foreground": "oklch(1 0 0)",
"destructive": "oklch(0.704 0.191 22.216)",
"border": "oklch(1 0 0 / 10%)",
"input": "oklch(1 0 0 / 10%)",
"ring": "oklch(0.636 0.165 142.5)",
"sidebar": "oklch(0.21 0.017 248)",
"sidebar-foreground": "oklch(1 0 0)",
"sidebar-primary": "oklch(0.636 0.165 142.5)",
"sidebar-primary-foreground": "oklch(1 0 0)",
"sidebar-accent": "oklch(0.385 0.016 248)",
"sidebar-accent-foreground": "oklch(1 0 0)",
"sidebar-border": "oklch(1 0 0 / 10%)",
"sidebar-ring": "oklch(0.636 0.165 142.5)"
"background": "#1c1c1c",
"foreground": "rgba(244, 244, 244, 0.87)",
"card": "#262626",
"card-foreground": "rgba(244, 244, 244, 0.87)",
"popover": "#1c1c1c",
"popover-foreground": "rgba(244, 244, 244, 0.87)",
"primary": "#f4f4f4",
"primary-foreground": "#262626",
"secondary": "rgba(244, 244, 244, 0.1)",
"secondary-foreground": "rgba(244, 244, 244, 0.87)",
"muted": "rgba(244, 244, 244, 0.05)",
"muted-foreground": "rgba(244, 244, 244, 0.54)",
"accent": "rgba(244, 244, 244, 0.05)",
"accent-foreground": "rgba(244, 244, 244, 0.87)",
"destructive": "#fd413c",
"border": "rgba(244, 244, 244, 0.1)",
"input": "rgba(244, 244, 244, 0.1)",
"ring": "#f4f4f4",
"sidebar": "#141414",
"sidebar-foreground": "rgba(244, 244, 244, 0.87)",
"sidebar-primary": "#f4f4f4",
"sidebar-primary-foreground": "#262626",
"sidebar-accent": "rgba(244, 244, 244, 0.05)",
"sidebar-accent-foreground": "rgba(244, 244, 244, 0.87)",
"sidebar-border": "rgba(244, 244, 244, 0.1)",
"sidebar-ring": "#f4f4f4"
}
}
},
Expand Down Expand Up @@ -144,60 +144,60 @@
],
"css": {
":root:root": {
"--background": "var(--crowdin-level-2-bg, oklch(1 0 0))",
"--foreground": "var(--crowdin-body-color, oklch(0.268 0.022 230))",
"--card": "var(--crowdin-level-3-bg, oklch(1 0 0))",
"--card-foreground": "var(--crowdin-body-color, oklch(0.268 0.022 230))",
"--popover": "var(--crowdin-level-2-bg, oklch(1 0 0))",
"--popover-foreground": "var(--crowdin-body-color, oklch(0.268 0.022 230))",
"--primary": "var(--crowdin-primary, oklch(0.268 0.022 230))",
"--primary-foreground": "var(--crowdin-primary-btn-color, oklch(1 0 0))",
"--secondary": "var(--crowdin-gray-01, oklch(0.935 0.007 228))",
"--secondary-foreground": "var(--crowdin-body-color, oklch(0.268 0.022 230))",
"--muted": "var(--crowdin-gray-005, oklch(0.935 0.007 228))",
"--muted-foreground": "var(--crowdin-text-muted, oklch(0.577 0.014 228))",
"--accent": "var(--crowdin-gray-005, oklch(0.965 0.004 228))",
"--accent-foreground": "var(--crowdin-body-color, oklch(0.268 0.022 230))",
"--destructive": "var(--crowdin-danger, oklch(0.577 0.21 27.3))",
"--border": "var(--crowdin-border-color, oklch(0.91 0.006 228))",
"--input": "var(--crowdin-border-color, oklch(0.91 0.006 228))",
"--ring": "var(--crowdin-primary, oklch(0.268 0.022 230))",
"--sidebar": "var(--crowdin-level-0-bg, oklch(0.975 0.003 228))",
"--sidebar-foreground": "var(--crowdin-body-color, oklch(0.268 0.022 230))",
"--sidebar-primary": "var(--crowdin-primary, oklch(0.268 0.022 230))",
"--sidebar-primary-foreground": "var(--crowdin-primary-btn-color, oklch(1 0 0))",
"--sidebar-accent": "var(--crowdin-gray-005, oklch(0.965 0.004 228))",
"--sidebar-accent-foreground": "var(--crowdin-body-color, oklch(0.268 0.022 230))",
"--sidebar-border": "var(--crowdin-border-color, oklch(0.91 0.006 228))",
"--sidebar-ring": "var(--crowdin-primary, oklch(0.268 0.022 230))"
"--background": "var(--crowdin-level-2-bg, #ffffff)",
"--foreground": "var(--crowdin-body-color, rgba(38, 38, 38, 0.87))",
"--card": "var(--crowdin-level-3-bg, #ffffff)",
"--card-foreground": "var(--crowdin-body-color, rgba(38, 38, 38, 0.87))",
"--popover": "var(--crowdin-level-2-bg, #ffffff)",
"--popover-foreground": "var(--crowdin-body-color, rgba(38, 38, 38, 0.87))",
"--primary": "var(--crowdin-primary, #262626)",
"--primary-foreground": "var(--crowdin-primary-foreground, #ffffff)",
"--secondary": "var(--crowdin-gray-01, rgba(38, 38, 38, 0.1))",
"--secondary-foreground": "var(--crowdin-body-color, rgba(38, 38, 38, 0.87))",
"--muted": "var(--crowdin-gray-005, rgba(38, 38, 38, 0.05))",
"--muted-foreground": "var(--crowdin-text-muted, rgba(38, 38, 38, 0.67))",
"--accent": "var(--crowdin-gray-005, rgba(38, 38, 38, 0.05))",
"--accent-foreground": "var(--crowdin-body-color, rgba(38, 38, 38, 0.87))",
"--destructive": "var(--crowdin-danger, #dc5242)",
"--border": "var(--crowdin-border-color, rgba(38, 38, 38, 0.1))",
"--input": "var(--crowdin-border-color, rgba(38, 38, 38, 0.1))",
"--ring": "var(--crowdin-primary, #262626)",
"--sidebar": "var(--crowdin-level-0-bg, #eeeeee)",
"--sidebar-foreground": "var(--crowdin-body-color, rgba(38, 38, 38, 0.87))",
"--sidebar-primary": "var(--crowdin-primary, #262626)",
"--sidebar-primary-foreground": "var(--crowdin-primary-foreground, #ffffff)",
"--sidebar-accent": "var(--crowdin-gray-005, rgba(38, 38, 38, 0.05))",
"--sidebar-accent-foreground": "var(--crowdin-body-color, rgba(38, 38, 38, 0.87))",
"--sidebar-border": "var(--crowdin-border-color, rgba(38, 38, 38, 0.1))",
"--sidebar-ring": "var(--crowdin-primary, #262626)"
},
":root:root.dark": {
"--background": "var(--crowdin-level-2-bg, oklch(0.253 0.02 248))",
"--foreground": "var(--crowdin-body-color, oklch(1 0 0))",
"--card": "var(--crowdin-level-3-bg, oklch(0.306 0.02 248))",
"--card-foreground": "var(--crowdin-body-color, oklch(1 0 0))",
"--popover": "var(--crowdin-level-2-bg, oklch(0.306 0.02 248))",
"--popover-foreground": "var(--crowdin-body-color, oklch(1 0 0))",
"--primary": "var(--crowdin-primary, oklch(0.636 0.165 142.5))",
"--primary-foreground": "var(--crowdin-primary-btn-color, oklch(1 0 0))",
"--secondary": "var(--crowdin-gray-01, oklch(0.4 0.022 248))",
"--secondary-foreground": "var(--crowdin-body-color, oklch(1 0 0))",
"--muted": "var(--crowdin-gray-005, oklch(0.21 0.017 248))",
"--muted-foreground": "var(--crowdin-text-muted, oklch(0.644 0.009 248))",
"--accent": "var(--crowdin-gray-005, oklch(0.385 0.016 248))",
"--accent-foreground": "var(--crowdin-body-color, oklch(1 0 0))",
"--destructive": "var(--crowdin-danger, oklch(0.704 0.191 22.216))",
"--border": "var(--crowdin-border-color, oklch(1 0 0 / 10%))",
"--input": "var(--crowdin-border-color, oklch(1 0 0 / 10%))",
"--ring": "var(--crowdin-primary, oklch(0.636 0.165 142.5))",
"--sidebar": "var(--crowdin-level-0-bg, oklch(0.21 0.017 248))",
"--sidebar-foreground": "var(--crowdin-body-color, oklch(1 0 0))",
"--sidebar-primary": "var(--crowdin-primary, oklch(0.636 0.165 142.5))",
"--sidebar-primary-foreground": "var(--crowdin-primary-btn-color, oklch(1 0 0))",
"--sidebar-accent": "var(--crowdin-gray-005, oklch(0.385 0.016 248))",
"--sidebar-accent-foreground": "var(--crowdin-body-color, oklch(1 0 0))",
"--sidebar-border": "var(--crowdin-border-color, oklch(1 0 0 / 10%))",
"--sidebar-ring": "var(--crowdin-primary, oklch(0.636 0.165 142.5))"
"--background": "var(--crowdin-level-2-bg, #1c1c1c)",
"--foreground": "var(--crowdin-body-color, rgba(244, 244, 244, 0.87))",
"--card": "var(--crowdin-level-3-bg, #262626)",
"--card-foreground": "var(--crowdin-body-color, rgba(244, 244, 244, 0.87))",
"--popover": "var(--crowdin-level-2-bg, #1c1c1c)",
"--popover-foreground": "var(--crowdin-body-color, rgba(244, 244, 244, 0.87))",
"--primary": "var(--crowdin-primary, #f4f4f4)",
"--primary-foreground": "var(--crowdin-primary-foreground, #262626)",
"--secondary": "var(--crowdin-gray-01, rgba(244, 244, 244, 0.1))",
"--secondary-foreground": "var(--crowdin-body-color, rgba(244, 244, 244, 0.87))",
"--muted": "var(--crowdin-gray-005, rgba(244, 244, 244, 0.05))",
"--muted-foreground": "var(--crowdin-text-muted, rgba(244, 244, 244, 0.54))",
"--accent": "var(--crowdin-gray-005, rgba(244, 244, 244, 0.05))",
"--accent-foreground": "var(--crowdin-body-color, rgba(244, 244, 244, 0.87))",
"--destructive": "var(--crowdin-danger, #fd413c)",
"--border": "var(--crowdin-border-color, rgba(244, 244, 244, 0.1))",
"--input": "var(--crowdin-border-color, rgba(244, 244, 244, 0.1))",
"--ring": "var(--crowdin-primary, #f4f4f4)",
"--sidebar": "var(--crowdin-level-0-bg, #141414)",
"--sidebar-foreground": "var(--crowdin-body-color, rgba(244, 244, 244, 0.87))",
"--sidebar-primary": "var(--crowdin-primary, #f4f4f4)",
"--sidebar-primary-foreground": "var(--crowdin-primary-foreground, #262626)",
"--sidebar-accent": "var(--crowdin-gray-005, rgba(244, 244, 244, 0.05))",
"--sidebar-accent-foreground": "var(--crowdin-body-color, rgba(244, 244, 244, 0.87))",
"--sidebar-border": "var(--crowdin-border-color, rgba(244, 244, 244, 0.1))",
"--sidebar-ring": "var(--crowdin-primary, #f4f4f4)"
}
}
}
Expand Down
58 changes: 55 additions & 3 deletions scripts/registry.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { readdirSync, readFileSync } from "node:fs";
import { test } from "node:test";

const registry = JSON.parse(
Expand Down Expand Up @@ -116,7 +116,7 @@ const HOST_VAR_BY_TOKEN = {
"popover": "--crowdin-level-2-bg",
"popover-foreground": "--crowdin-body-color",
"primary": "--crowdin-primary",
"primary-foreground": "--crowdin-primary-btn-color",
"primary-foreground": "--crowdin-primary-foreground",
"secondary": "--crowdin-gray-01",
"secondary-foreground": "--crowdin-body-color",
"muted": "--crowdin-gray-005",
Expand All @@ -130,7 +130,7 @@ const HOST_VAR_BY_TOKEN = {
"sidebar": "--crowdin-level-0-bg",
"sidebar-foreground": "--crowdin-body-color",
"sidebar-primary": "--crowdin-primary",
"sidebar-primary-foreground": "--crowdin-primary-btn-color",
"sidebar-primary-foreground": "--crowdin-primary-foreground",
"sidebar-accent": "--crowdin-gray-005",
"sidebar-accent-foreground": "--crowdin-body-color",
"sidebar-border": "--crowdin-border-color",
Expand All @@ -149,3 +149,55 @@ test("each token reads its canonical host variable", () => {
}
}
});

// tests/hosts/*.json pin what each Crowdin host actually sends to app iframes
// (copied from the host's window.cssVariables, see README). They are the ground truth for
// the host variable names the bridge may read and for the canonical values.
const hosts = readdirSync(new URL("../tests/hosts/", import.meta.url))
.filter((file) => file.endsWith(".json"))
.map((file) =>
JSON.parse(readFileSync(new URL(`../tests/hosts/${file}`, import.meta.url), "utf8")),
);

test("every captured host sends every host variable the bridge reads", () => {
assert.ok(hosts.length > 0, "no host fixtures in tests/hosts/");
const hostVars = new Set(Object.values(HOST_VAR_BY_TOKEN));
for (const host of hosts) {
for (const mode of ["light", "dark"]) {
for (const hostVar of hostVars) {
assert.ok(
hostVar in host[mode],
`${host.host} (${mode}) does not send ${hostVar}`,
);
}
}
}
});

// Resolves a value the way the app sees it: the bridge applies the host's
// --crowdin-*/--color-* variables on <html>, so var() chains resolve through
// the captured map; an unknown variable falls back like the browser would.
function resolveHostValue(value, vars) {
const ref = /var\((--[a-zA-Z0-9_-]+)(?:,\s*([^()]*(?:\([^()]*\))*[^()]*))?\)/;
for (let match = value.match(ref), i = 0; match && i < 32; match = value.match(ref), i++) {
const [whole, name, fallback] = match;
value = value.replace(whole, name in vars ? vars[name] : (fallback ?? "<missing>"));
}
return value.trim();
}

// The theme literals (and so the bridge fallbacks) are the canonical Crowdin
// values: exactly what crowdin.com sends for each token's host variable.
test("theme literals equal the values crowdin.com sends", () => {
const com = hosts.find((host) => host.host === "crowdin-com");
assert.ok(com, "tests/hosts/crowdin-com.json is missing");
for (const { cssVars } of MODES) {
for (const [token, hostVar] of Object.entries(HOST_VAR_BY_TOKEN)) {
assert.equal(
theme.cssVars[cssVars][token],
resolveHostValue(`var(${hostVar})`, com[cssVars]),
`theme ${cssVars} ${token} drifted from crowdin.com ${hostVar}`,
);
}
}
});
4 changes: 2 additions & 2 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
"url": "https://github.com/andrii-bodnar/"
},
"scripts": {
"predev": "node scripts/gen-theme.mjs",
"predev": "node scripts/gen-theme.mjs && node scripts/gen-host-sim.mjs",
"dev": "vite",
"prebuild": "node scripts/gen-theme.mjs",
"prebuild": "node scripts/gen-theme.mjs && node scripts/gen-host-sim.mjs",
"build": "tsc --noEmit && vite build",
"preview": "vite preview"
},
Expand Down
Loading
Loading