@nebutra/theme 0.2.1 → 2.0.0

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/brand-package-surface.d.ts +2 -0
  3. package/dist/brand-package-surface.js +36 -0
  4. package/dist/brand-package-surface.js.map +1 -0
  5. package/dist/chunk-4CNXYWPJ.js +259 -0
  6. package/dist/chunk-4CNXYWPJ.js.map +1 -0
  7. package/dist/chunk-XDRU7ACJ.js +91 -0
  8. package/dist/chunk-XDRU7ACJ.js.map +1 -0
  9. package/dist/client.d.ts +45 -0
  10. package/dist/client.js +43 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/index.d.ts +11 -0
  13. package/dist/index.js +77 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/language-switcher.d.ts +62 -0
  16. package/dist/language-switcher.js +242 -0
  17. package/dist/language-switcher.js.map +1 -0
  18. package/dist/languages.d.ts +62 -0
  19. package/dist/languages.js +19 -0
  20. package/dist/languages.js.map +1 -0
  21. package/morph.css +99 -0
  22. package/package.json +53 -11
  23. package/skins.css +718 -969
  24. package/src/languages.json +44 -0
  25. package/styles/sources.css +16 -0
  26. package/.turbo/turbo-build.log +0 -10
  27. package/.turbo/turbo-typecheck.log +0 -4
  28. package/AGENTS.md +0 -28
  29. package/DESIGN.md +0 -124
  30. package/scripts/sync-languages.mjs +0 -189
  31. package/scripts/sync-skins.mjs +0 -83
  32. package/scripts/sync-themes.mjs +0 -40
  33. package/src/__tests__/apply-language.test.ts +0 -70
  34. package/src/__tests__/languages.test.ts +0 -100
  35. package/src/__tests__/theme-registry.test.ts +0 -27
  36. package/src/apply-language.ts +0 -87
  37. package/src/brand-package-surface.ts +0 -30
  38. package/src/built-in-packages.generated.ts +0 -39
  39. package/src/built-in-packages.ts +0 -14
  40. package/src/client.ts +0 -35
  41. package/src/index.ts +0 -53
  42. package/src/languages.meta.json +0 -62
  43. package/src/languages.ts +0 -70
  44. package/src/registry.json +0 -7
  45. package/tsconfig.json +0 -10
  46. package/turbo.json +0 -25
@@ -0,0 +1,62 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { DesignLanguageEntry } from './languages.js';
3
+
4
+ /** Catalog order, factory first — it is the way back. */
5
+ declare const LANGUAGES: DesignLanguageEntry[];
6
+ /**
7
+ * Flip the language, with the change animated rather than cut.
8
+ *
9
+ * The attribute swap rewrites some two hundred custom properties in one frame,
10
+ * and custom properties do not interpolate — so the whole page used to change
11
+ * between two frames and read as a reload. The properties that *read* those
12
+ * variables do interpolate, so a blanket transition is armed just before the
13
+ * flip and disarmed after: alive for the length of the change and no longer,
14
+ * because leaving it on would put 400ms of lag on every hover afterwards.
15
+ *
16
+ * Where the browser has view transitions, they replace that entirely. A
17
+ * cross-fade carries the discrete changes a property transition cannot — the
18
+ * typeface, gradients with different stop counts, shadow stacks of different
19
+ * lengths — because by then they are pixels rather than properties. Only one of
20
+ * the two ever runs: both together animate the same change twice, and two clocks
21
+ * beating against each other is its own kind of jitter.
22
+ *
23
+ * `requestAnimationFrame` before writing the attribute matters on the fallback
24
+ * path — set the class and the attribute in the same frame and the browser has
25
+ * no previous computed value to transition from, so it cuts anyway.
26
+ */
27
+ declare function applyLanguageAttribute(id: string, animate?: boolean): void;
28
+ /**
29
+ * Shared state for every mounted switcher.
30
+ *
31
+ * The state lives on the document element rather than in any one component, so
32
+ * two mounted controls broadcast to each other instead of each keeping a private
33
+ * copy that goes stale the moment the other is clicked.
34
+ */
35
+ declare function useDesignLanguage(): {
36
+ active: string;
37
+ select: (id: string) => void;
38
+ ready: boolean;
39
+ };
40
+ interface DesignLanguageSwitcherProps {
41
+ className?: string;
42
+ /** `compact` for a header rail, `picker` for a primary on-page control. */
43
+ variant?: "compact" | "picker";
44
+ /** Shown under a `picker`: names the active language and what it changes. */
45
+ caption?: boolean;
46
+ /**
47
+ * Visible "Design language" label. Off where the surrounding surface already
48
+ * says it — on the marketing card the legend sat directly under a heading
49
+ * reading the same thing. Still rendered for assistive tech either way.
50
+ */
51
+ showLegend?: boolean;
52
+ }
53
+ /**
54
+ * Eight buttons and, optionally, a line saying what the current one does.
55
+ *
56
+ * Deliberately built from plain elements rather than the component library: this
57
+ * ships inside the package that defines the languages, and a control that
58
+ * re-skins itself mid-press is a distraction from the page it is re-skinning.
59
+ */
60
+ declare function DesignLanguageSwitcher({ className, variant, caption, showLegend, }: DesignLanguageSwitcherProps): react_jsx_runtime.JSX.Element;
61
+
62
+ export { DesignLanguageSwitcher, type DesignLanguageSwitcherProps, LANGUAGES, applyLanguageAttribute, useDesignLanguage };
@@ -0,0 +1,242 @@
1
+ "use client";
2
+ import {
3
+ LANGUAGE_REGISTRY
4
+ } from "./chunk-4CNXYWPJ.js";
5
+
6
+ // src/language-switcher.tsx
7
+ import * as React from "react";
8
+
9
+ // src/skin-fonts.generated.json
10
+ var skin_fonts_generated_default = {
11
+ cosmos: [
12
+ "var(--font-reg-fraunces)",
13
+ "Fraunces",
14
+ "GT Super",
15
+ "var(--font-noto-sans-sc",
16
+ 'Noto Sans SC")',
17
+ "PingFang SC",
18
+ "var(--font-jetbrains-mono)",
19
+ "JetBrains Mono"
20
+ ],
21
+ gsap: [
22
+ "var(--font-reg-inter-tight)",
23
+ "Mori",
24
+ "Inter Tight",
25
+ "DM Sans",
26
+ "var(--font-noto-sans-sc",
27
+ 'Noto Sans SC")',
28
+ "PingFang SC"
29
+ ],
30
+ linear: [
31
+ "var(--font-inter)",
32
+ "Inter Variable",
33
+ "Inter",
34
+ "var(--font-noto-sans-sc",
35
+ 'Noto Sans SC")',
36
+ "PingFang SC",
37
+ "var(--font-jetbrains-mono)",
38
+ "Berkeley Mono",
39
+ "JetBrains Mono"
40
+ ],
41
+ notion: [
42
+ "var(--font-inter)",
43
+ "NotionInter",
44
+ "Inter",
45
+ "var(--font-noto-sans-sc",
46
+ 'Noto Sans SC")',
47
+ "PingFang SC"
48
+ ],
49
+ raycast: [
50
+ "var(--font-inter)",
51
+ "Inter",
52
+ "var(--font-noto-sans-sc",
53
+ 'Noto Sans SC")',
54
+ "PingFang SC",
55
+ "var(--font-geist-mono)",
56
+ "Geist Mono",
57
+ "GeistMono",
58
+ "Menlo"
59
+ ],
60
+ stripe: [
61
+ "var(--font-reg-inter-tight)",
62
+ "sohne-var",
63
+ "Inter Tight",
64
+ "Inter",
65
+ "var(--font-noto-sans-sc",
66
+ 'Noto Sans SC")',
67
+ "PingFang SC"
68
+ ],
69
+ vanta: [
70
+ "var(--font-inter)",
71
+ "Inter Variable",
72
+ "Inter",
73
+ "var(--font-noto-sans-sc",
74
+ 'Noto Sans SC")',
75
+ "PingFang SC",
76
+ "var(--font-reg-source-serif-4)",
77
+ "Reckless",
78
+ "Source Serif 4",
79
+ "Lora"
80
+ ],
81
+ vercel: [
82
+ "var(--font-geist-sans)",
83
+ "Geist Sans",
84
+ "Geist",
85
+ "var(--font-noto-sans-sc",
86
+ 'Noto Sans SC")',
87
+ "PingFang SC",
88
+ "var(--font-geist-mono)",
89
+ "Geist Mono",
90
+ "Menlo"
91
+ ]
92
+ };
93
+
94
+ // src/language-switcher.tsx
95
+ import { jsx, jsxs } from "react/jsx-runtime";
96
+ var STORAGE_KEY = "nebutra.design.brand";
97
+ var CHANGE_EVENT = "nebutra.design.brandchange";
98
+ var MORPH_MS = 460;
99
+ var FONT_BUDGET_MS = 220;
100
+ var morphTimer;
101
+ var FONTS = skin_fonts_generated_default;
102
+ var LANGUAGES = [
103
+ ...LANGUAGE_REGISTRY.languages.filter((l) => l.id === "factory"),
104
+ ...LANGUAGE_REGISTRY.languages.filter((l) => l.id !== "factory")
105
+ ];
106
+ function readStored() {
107
+ if (typeof window === "undefined") return "factory";
108
+ try {
109
+ const value = sessionStorage.getItem(STORAGE_KEY);
110
+ if (value && LANGUAGES.some((l) => l.id === value)) return value;
111
+ } catch {
112
+ }
113
+ return "factory";
114
+ }
115
+ async function ensureFontsFor(id) {
116
+ const families = FONTS[id];
117
+ if (!families?.length || !document.fonts) return;
118
+ await Promise.race([
119
+ Promise.allSettled(
120
+ families.flatMap((family) => [
121
+ document.fonts.load(`400 1rem "${family}"`),
122
+ document.fonts.load(`600 1rem "${family}"`)
123
+ ])
124
+ ),
125
+ new Promise((resolve) => setTimeout(resolve, FONT_BUDGET_MS))
126
+ ]);
127
+ }
128
+ function supportsViewTransition() {
129
+ if (typeof document === "undefined") return false;
130
+ if (typeof document.startViewTransition !== "function") return false;
131
+ return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
132
+ }
133
+ function applyLanguageAttribute(id, animate = true) {
134
+ const root = document.documentElement;
135
+ const write = () => {
136
+ if (id === "factory") delete root.dataset.brand;
137
+ else root.dataset.brand = id;
138
+ };
139
+ if (!animate) {
140
+ write();
141
+ } else if (supportsViewTransition()) {
142
+ void ensureFontsFor(id).then(() => {
143
+ document.startViewTransition?.(() => {
144
+ write();
145
+ });
146
+ });
147
+ } else {
148
+ void ensureFontsFor(id).then(() => {
149
+ root.classList.add("brand-morphing");
150
+ clearTimeout(morphTimer);
151
+ requestAnimationFrame(() => {
152
+ write();
153
+ morphTimer = setTimeout(() => root.classList.remove("brand-morphing"), MORPH_MS);
154
+ });
155
+ });
156
+ }
157
+ try {
158
+ sessionStorage.setItem(STORAGE_KEY, id);
159
+ } catch {
160
+ }
161
+ }
162
+ function useDesignLanguage() {
163
+ const [active, setActive] = React.useState("factory");
164
+ const [ready, setReady] = React.useState(false);
165
+ React.useEffect(() => {
166
+ const stored = readStored();
167
+ applyLanguageAttribute(stored, false);
168
+ setActive(stored);
169
+ setReady(true);
170
+ const onChange = (event) => setActive(event.detail);
171
+ window.addEventListener(CHANGE_EVENT, onChange);
172
+ return () => window.removeEventListener(CHANGE_EVENT, onChange);
173
+ }, []);
174
+ const select = React.useCallback((id) => {
175
+ applyLanguageAttribute(id);
176
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: id }));
177
+ }, []);
178
+ return { active, select, ready };
179
+ }
180
+ function join(...parts) {
181
+ return parts.filter(Boolean).join(" ");
182
+ }
183
+ function DesignLanguageSwitcher({
184
+ className,
185
+ variant = "compact",
186
+ caption = false,
187
+ showLegend = true
188
+ }) {
189
+ const { active, select, ready } = useDesignLanguage();
190
+ const current = LANGUAGES.find((l) => l.id === active) ?? LANGUAGES[0];
191
+ const isPicker = variant === "picker";
192
+ return /* @__PURE__ */ jsxs("div", { className: join("flex flex-col gap-3", className), "data-ready": ready ? "true" : "false", children: [
193
+ /* @__PURE__ */ jsxs(
194
+ "fieldset",
195
+ {
196
+ className: join(
197
+ "m-0 flex flex-wrap items-center border-0 p-0",
198
+ isPicker ? "gap-1.5" : "gap-1.5"
199
+ ),
200
+ children: [
201
+ /* @__PURE__ */ jsx(
202
+ "legend",
203
+ {
204
+ className: isPicker || !showLegend ? "sr-only" : "mr-1 float-left w-auto font-mono text-[10px] text-muted-foreground uppercase tracking-wider",
205
+ children: "Design language"
206
+ }
207
+ ),
208
+ LANGUAGES.map((language) => {
209
+ const isActive = active === language.id;
210
+ return /* @__PURE__ */ jsx(
211
+ "button",
212
+ {
213
+ "aria-pressed": isActive,
214
+ className: join(
215
+ "transition-[background-color,color,box-shadow] duration-micro ease-out",
216
+ isPicker ? "rounded-panel px-3.5 py-2 font-medium text-[13px]" : "rounded-[var(--radius-sm,0.25rem)] px-2 py-1 font-medium text-[11px]",
217
+ isActive ? join("bg-foreground text-background", isPicker && "shadow-ambient-sm") : "bg-muted/70 text-muted-foreground hover:bg-muted hover:text-foreground"
218
+ ),
219
+ onClick: () => select(language.id),
220
+ type: "button",
221
+ children: language.shortName
222
+ },
223
+ language.id
224
+ );
225
+ })
226
+ ]
227
+ }
228
+ ),
229
+ caption && current && /* @__PURE__ */ jsxs("p", { className: "text-[13px] text-muted-foreground leading-relaxed", children: [
230
+ /* @__PURE__ */ jsx("span", { className: "text-foreground", children: current.name }),
231
+ current.tagline ? ` \u2014 ${current.tagline}.` : ".",
232
+ " Colour roles, radii, elevation, type and easing move together, not just the palette."
233
+ ] })
234
+ ] });
235
+ }
236
+ export {
237
+ DesignLanguageSwitcher,
238
+ LANGUAGES,
239
+ applyLanguageAttribute,
240
+ useDesignLanguage
241
+ };
242
+ //# sourceMappingURL=language-switcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/language-switcher.tsx","../src/skin-fonts.generated.json"],"sourcesContent":["\"use client\";\n\n/**\n * The control that switches design language, shared by every surface that has one.\n *\n * This was built inside the design site, where it was the whole point of the\n * page. When the marketing site wanted the same thing, the choice was to copy it\n * — a second list of eight languages, a second morph, a second storage key that\n * disagrees the moment either is edited — or to move it next to the mechanism.\n * `@nebutra/tokens` already ships the light/dark control; the language control\n * is its sibling and belongs here, beside skins.css and the catalog.\n *\n * Consumers import `@nebutra/theme/skins.css` and `@nebutra/theme/morph.css`,\n * then mount one of these. Nothing else is needed: the languages come from the\n * registry, the captions from the catalog, the typefaces from the map emitted\n * with the sheet.\n */\n\nimport * as React from \"react\";\nimport { type DesignLanguageEntry, LANGUAGE_REGISTRY } from \"./languages\";\nimport fontsByBrand from \"./skin-fonts.generated.json\";\n\nconst STORAGE_KEY = \"nebutra.design.brand\";\nconst CHANGE_EVENT = \"nebutra.design.brandchange\";\n\n/** How long the fallback blanket transition stays armed. Matches\n * --brand-morph-duration in morph.css, plus a frame so the last interpolation\n * lands before it is removed. */\nconst MORPH_MS = 460;\n\n/** How long a font may hold the switch. A proprietary family that will never\n * resolve on this origin must not make the control look unresponsive; a\n * dissolve of the fallback is a far better failure than one that does not run. */\nconst FONT_BUDGET_MS = 220;\n\nlet morphTimer: ReturnType<typeof setTimeout> | undefined;\n\nconst FONTS: Record<string, string[]> = fontsByBrand;\n\n/** Catalog order, factory first — it is the way back. */\nexport const LANGUAGES: DesignLanguageEntry[] = [\n ...LANGUAGE_REGISTRY.languages.filter((l) => l.id === \"factory\"),\n ...LANGUAGE_REGISTRY.languages.filter((l) => l.id !== \"factory\"),\n];\n\nfunction readStored(): string {\n if (typeof window === \"undefined\") return \"factory\";\n try {\n const value = sessionStorage.getItem(STORAGE_KEY);\n if (value && LANGUAGES.some((l) => l.id === value)) return value;\n } catch {\n /* private mode */\n }\n return \"factory\";\n}\n\n/**\n * Make the incoming typefaces resident before the snapshot is taken.\n *\n * Without this the dissolve photographs a fallback face and the real one swaps\n * in after the animation has finished — a jump landing on a settled page, which\n * is the one that actually reads as broken. `document.fonts.load` resolves\n * immediately for a face already resident, so the common path costs nothing.\n */\nasync function ensureFontsFor(id: string): Promise<void> {\n const families = FONTS[id];\n if (!families?.length || !document.fonts) return;\n await Promise.race([\n Promise.allSettled(\n families.flatMap((family) => [\n document.fonts.load(`400 1rem \"${family}\"`),\n document.fonts.load(`600 1rem \"${family}\"`),\n ]),\n ),\n new Promise((resolve) => setTimeout(resolve, FONT_BUDGET_MS)),\n ]);\n}\n\nfunction supportsViewTransition(): boolean {\n if (typeof document === \"undefined\") return false;\n if (typeof document.startViewTransition !== \"function\") return false;\n return !window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/**\n * Flip the language, with the change animated rather than cut.\n *\n * The attribute swap rewrites some two hundred custom properties in one frame,\n * and custom properties do not interpolate — so the whole page used to change\n * between two frames and read as a reload. The properties that *read* those\n * variables do interpolate, so a blanket transition is armed just before the\n * flip and disarmed after: alive for the length of the change and no longer,\n * because leaving it on would put 400ms of lag on every hover afterwards.\n *\n * Where the browser has view transitions, they replace that entirely. A\n * cross-fade carries the discrete changes a property transition cannot — the\n * typeface, gradients with different stop counts, shadow stacks of different\n * lengths — because by then they are pixels rather than properties. Only one of\n * the two ever runs: both together animate the same change twice, and two clocks\n * beating against each other is its own kind of jitter.\n *\n * `requestAnimationFrame` before writing the attribute matters on the fallback\n * path — set the class and the attribute in the same frame and the browser has\n * no previous computed value to transition from, so it cuts anyway.\n */\nexport function applyLanguageAttribute(id: string, animate = true): void {\n const root = document.documentElement;\n\n const write = () => {\n if (id === \"factory\") delete root.dataset.brand;\n else root.dataset.brand = id;\n };\n\n if (!animate) {\n write();\n } else if (supportsViewTransition()) {\n void ensureFontsFor(id).then(() => {\n document.startViewTransition?.(() => {\n write();\n });\n });\n } else {\n void ensureFontsFor(id).then(() => {\n root.classList.add(\"brand-morphing\");\n clearTimeout(morphTimer);\n requestAnimationFrame(() => {\n write();\n morphTimer = setTimeout(() => root.classList.remove(\"brand-morphing\"), MORPH_MS);\n });\n });\n }\n\n try {\n sessionStorage.setItem(STORAGE_KEY, id);\n } catch {\n /* private mode */\n }\n}\n\n/**\n * Shared state for every mounted switcher.\n *\n * The state lives on the document element rather than in any one component, so\n * two mounted controls broadcast to each other instead of each keeping a private\n * copy that goes stale the moment the other is clicked.\n */\nexport function useDesignLanguage(): {\n active: string;\n select: (id: string) => void;\n ready: boolean;\n} {\n const [active, setActive] = React.useState(\"factory\");\n const [ready, setReady] = React.useState(false);\n\n React.useEffect(() => {\n const stored = readStored();\n // No animation on restore: there is no previous language to move away from,\n // and a page that fades into its own colours on load looks like a bug.\n applyLanguageAttribute(stored, false);\n setActive(stored);\n setReady(true);\n\n const onChange = (event: Event) => setActive((event as CustomEvent<string>).detail);\n window.addEventListener(CHANGE_EVENT, onChange);\n return () => window.removeEventListener(CHANGE_EVENT, onChange);\n }, []);\n\n const select = React.useCallback((id: string) => {\n applyLanguageAttribute(id);\n window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: id }));\n }, []);\n\n return { active, select, ready };\n}\n\nfunction join(...parts: (string | false | null | undefined)[]): string {\n return parts.filter(Boolean).join(\" \");\n}\n\nexport interface DesignLanguageSwitcherProps {\n className?: string;\n /** `compact` for a header rail, `picker` for a primary on-page control. */\n variant?: \"compact\" | \"picker\";\n /** Shown under a `picker`: names the active language and what it changes. */\n caption?: boolean;\n /**\n * Visible \"Design language\" label. Off where the surrounding surface already\n * says it — on the marketing card the legend sat directly under a heading\n * reading the same thing. Still rendered for assistive tech either way.\n */\n showLegend?: boolean;\n}\n\n/**\n * Eight buttons and, optionally, a line saying what the current one does.\n *\n * Deliberately built from plain elements rather than the component library: this\n * ships inside the package that defines the languages, and a control that\n * re-skins itself mid-press is a distraction from the page it is re-skinning.\n */\nexport function DesignLanguageSwitcher({\n className,\n variant = \"compact\",\n caption = false,\n showLegend = true,\n}: DesignLanguageSwitcherProps) {\n const { active, select, ready } = useDesignLanguage();\n const current = LANGUAGES.find((l) => l.id === active) ?? LANGUAGES[0];\n const isPicker = variant === \"picker\";\n\n return (\n <div className={join(\"flex flex-col gap-3\", className)} data-ready={ready ? \"true\" : \"false\"}>\n <fieldset\n className={join(\n \"m-0 flex flex-wrap items-center border-0 p-0\",\n isPicker ? \"gap-1.5\" : \"gap-1.5\",\n )}\n >\n <legend\n className={\n isPicker || !showLegend\n ? \"sr-only\"\n : \"mr-1 float-left w-auto font-mono text-[10px] text-muted-foreground uppercase tracking-wider\"\n }\n >\n Design language\n </legend>\n {LANGUAGES.map((language) => {\n const isActive = active === language.id;\n return (\n <button\n aria-pressed={isActive}\n className={join(\n \"transition-[background-color,color,box-shadow] duration-micro ease-out\",\n isPicker\n ? \"rounded-panel px-3.5 py-2 font-medium text-[13px]\"\n : \"rounded-[var(--radius-sm,0.25rem)] px-2 py-1 font-medium text-[11px]\",\n isActive\n ? join(\"bg-foreground text-background\", isPicker && \"shadow-ambient-sm\")\n : \"bg-muted/70 text-muted-foreground hover:bg-muted hover:text-foreground\",\n )}\n key={language.id}\n onClick={() => select(language.id)}\n type=\"button\"\n >\n {language.shortName}\n </button>\n );\n })}\n </fieldset>\n {caption && current && (\n <p className=\"text-[13px] text-muted-foreground leading-relaxed\">\n <span className=\"text-foreground\">{current.name}</span>\n {current.tagline ? ` — ${current.tagline}.` : \".\"} Colour roles, radii, elevation, type\n and easing move together, not just the palette.\n </p>\n )}\n </div>\n );\n}\n","{\n \"cosmos\": [\n \"var(--font-reg-fraunces)\",\n \"Fraunces\",\n \"GT Super\",\n \"var(--font-noto-sans-sc\",\n \"Noto Sans SC\\\")\",\n \"PingFang SC\",\n \"var(--font-jetbrains-mono)\",\n \"JetBrains Mono\"\n ],\n \"gsap\": [\n \"var(--font-reg-inter-tight)\",\n \"Mori\",\n \"Inter Tight\",\n \"DM Sans\",\n \"var(--font-noto-sans-sc\",\n \"Noto Sans SC\\\")\",\n \"PingFang SC\"\n ],\n \"linear\": [\n \"var(--font-inter)\",\n \"Inter Variable\",\n \"Inter\",\n \"var(--font-noto-sans-sc\",\n \"Noto Sans SC\\\")\",\n \"PingFang SC\",\n \"var(--font-jetbrains-mono)\",\n \"Berkeley Mono\",\n \"JetBrains Mono\"\n ],\n \"notion\": [\n \"var(--font-inter)\",\n \"NotionInter\",\n \"Inter\",\n \"var(--font-noto-sans-sc\",\n \"Noto Sans SC\\\")\",\n \"PingFang SC\"\n ],\n \"raycast\": [\n \"var(--font-inter)\",\n \"Inter\",\n \"var(--font-noto-sans-sc\",\n \"Noto Sans SC\\\")\",\n \"PingFang SC\",\n \"var(--font-geist-mono)\",\n \"Geist Mono\",\n \"GeistMono\",\n \"Menlo\"\n ],\n \"stripe\": [\n \"var(--font-reg-inter-tight)\",\n \"sohne-var\",\n \"Inter Tight\",\n \"Inter\",\n \"var(--font-noto-sans-sc\",\n \"Noto Sans SC\\\")\",\n \"PingFang SC\"\n ],\n \"vanta\": [\n \"var(--font-inter)\",\n \"Inter Variable\",\n \"Inter\",\n \"var(--font-noto-sans-sc\",\n \"Noto Sans SC\\\")\",\n \"PingFang SC\",\n \"var(--font-reg-source-serif-4)\",\n \"Reckless\",\n \"Source Serif 4\",\n \"Lora\"\n ],\n \"vercel\": [\n \"var(--font-geist-sans)\",\n \"Geist Sans\",\n \"Geist\",\n \"var(--font-noto-sans-sc\",\n \"Noto Sans SC\\\")\",\n \"PingFang SC\",\n \"var(--font-geist-mono)\",\n \"Geist Mono\",\n \"Menlo\"\n ]\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Design-language catalog — the real product surface of @nebutra/theme.
3
+ *
4
+ * A "language" is a full Brand Package (roles + recipe + elev), proven by
5
+ * external DS stress tests (Linear / GSAP / Raycast / Vercel / Vanta / Stripe / Notion).
6
+ * Swapping `html[data-brand]` (or applyLanguage) recolors product chrome without
7
+ * call-site edits.
8
+ *
9
+ * Multi-mood oklch catalog was removed. Use LANGUAGE_REGISTRY / isLanguageId only.
10
+ */
11
+ interface DesignLanguageInstall {
12
+ command: string;
13
+ /** Single-skin import for demos; null for factory */
14
+ cssImport: string | null;
15
+ }
16
+ interface DesignLanguageCompatibility {
17
+ tailwind: "4";
18
+ cssVariables: boolean;
19
+ brandPackage: boolean;
20
+ dataBrand: boolean;
21
+ }
22
+ interface DesignLanguageEntry {
23
+ id: string;
24
+ name: string;
25
+ kind: "design-language";
26
+ description: string;
27
+ /**
28
+ * One line naming what this language does, for a picker's caption. Short on
29
+ * purpose — `description` is the catalog entry, this is the label under eight
30
+ * buttons. Null for a language whose meta has not written one.
31
+ */
32
+ tagline: string | null;
33
+ /** Button-width label. Falls back to `name` for the languages that fit. */
34
+ shortName: string;
35
+ /** Contract proofs this fixture exercises */
36
+ proves: string[];
37
+ darkDefault: boolean;
38
+ /** Relative to @nebutra/tokens package root; null = factory (no override) */
39
+ brandPath: string | null;
40
+ skinPath: string | null;
41
+ install: DesignLanguageInstall;
42
+ compatibility: DesignLanguageCompatibility;
43
+ }
44
+ interface DesignLanguageRegistry {
45
+ $schema: string;
46
+ version: string;
47
+ defaultLanguage: string;
48
+ description: string;
49
+ languages: DesignLanguageEntry[];
50
+ }
51
+ declare const LANGUAGE_REGISTRY: DesignLanguageRegistry;
52
+ declare const LANGUAGE_IDS: string[];
53
+ declare const DEFAULT_LANGUAGE: string;
54
+ /** Alias — “theme” in product speak = design language */
55
+ declare const BUILT_IN_LANGUAGE_IDS: string[];
56
+ type LanguageId = string;
57
+ declare function isLanguageId(id: string): boolean;
58
+ declare function getLanguageById(id: string): DesignLanguageEntry | undefined;
59
+ /** Fixture languages that ship a Brand Package (excludes factory). */
60
+ declare function listSkinLanguages(): DesignLanguageEntry[];
61
+
62
+ export { BUILT_IN_LANGUAGE_IDS, DEFAULT_LANGUAGE, type DesignLanguageCompatibility, type DesignLanguageEntry, type DesignLanguageInstall, type DesignLanguageRegistry, LANGUAGE_IDS, LANGUAGE_REGISTRY, type LanguageId, getLanguageById, isLanguageId, listSkinLanguages };
@@ -0,0 +1,19 @@
1
+ import {
2
+ BUILT_IN_LANGUAGE_IDS,
3
+ DEFAULT_LANGUAGE,
4
+ LANGUAGE_IDS,
5
+ LANGUAGE_REGISTRY,
6
+ getLanguageById,
7
+ isLanguageId,
8
+ listSkinLanguages
9
+ } from "./chunk-4CNXYWPJ.js";
10
+ export {
11
+ BUILT_IN_LANGUAGE_IDS,
12
+ DEFAULT_LANGUAGE,
13
+ LANGUAGE_IDS,
14
+ LANGUAGE_REGISTRY,
15
+ getLanguageById,
16
+ isLanguageId,
17
+ listSkinLanguages
18
+ };
19
+ //# sourceMappingURL=languages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/morph.css ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @nebutra/theme — morph.css
3
+ *
4
+ * How a design-language change moves. Imported by any app that imports
5
+ * skins.css and lets people switch at runtime.
6
+ *
7
+ * This lived in the design site's globals.css, which was fine while the design
8
+ * site was the only thing that could switch. The moment a second app wanted the
9
+ * same control, the choice was to copy sixty lines of timing into it or to put
10
+ * them where the mechanism already lives. A language that morphs on one site and
11
+ * cuts on another is not one design system.
12
+ */
13
+
14
+ /**
15
+ * The language change, as a transition rather than a cut.
16
+ *
17
+ * A Brand Package swap rewrites about two hundred custom properties at once,
18
+ * and custom properties do not animate — so every surface on the page changed
19
+ * in a single frame. Correct, and it read as a page reload.
20
+ *
21
+ * The properties that *read* those variables do animate, though: a
22
+ * `background-color` sourced from `hsl(var(--primary))` interpolates normally
23
+ * when the variable underneath it changes. So the switch turns this on, flips
24
+ * `data-brand`, and turns it off again — one blanket transition, alive for the
25
+ * length of the change and no longer. Leaving it on permanently would put a
26
+ * 400ms lag on every hover in the product surfaces this site renders, which is
27
+ * the opposite of what a design system should demonstrate.
28
+ *
29
+ * `font-family` is absent because it cannot interpolate — a typeface is a
30
+ * discrete value, and it snaps at the start while everything around it moves.
31
+ * Saying so is better than implying a morph that CSS cannot perform.
32
+ */
33
+ html.brand-morphing,
34
+ html.brand-morphing *,
35
+ html.brand-morphing *::before,
36
+ html.brand-morphing *::after {
37
+ transition-property:
38
+ background-color, background-image, color, border-color, border-radius,
39
+ box-shadow, fill, stroke, outline-color, text-decoration-color, opacity, padding, gap,
40
+ letter-spacing, font-size, font-weight, line-height, filter, backdrop-filter;
41
+ transition-duration: var(--brand-morph-duration, 420ms);
42
+ transition-timing-function: var(--ease-out, cubic-bezier(0, 0, 0.2, 1));
43
+ }
44
+
45
+ /* Anything already carrying its own transition keeps it: a switch that reveals
46
+ the language should not also overwrite how the language moves. */
47
+ html.brand-morphing [style*="transition"],
48
+ html.brand-morphing .transition-none {
49
+ transition-property: none;
50
+ }
51
+
52
+ @media (prefers-reduced-motion: reduce) {
53
+ html.brand-morphing,
54
+ html.brand-morphing *,
55
+ html.brand-morphing *::before,
56
+ html.brand-morphing *::after {
57
+ transition-duration: 0s;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * The change as a cross-fade, where the browser can do one.
63
+ *
64
+ * The blanket transition above interpolates every property that *can*
65
+ * interpolate, and several of the most visible ones cannot. A typeface is a
66
+ * discrete value. Two gradients with different stop counts do not blend. Skins
67
+ * carry shadow stacks of different lengths — Raycast's is inset + ring + inset
68
+ * where Notion's is a single hairline — and mismatched lists snap. So the page
69
+ * would glide through its colours while the type and the elevation cut
70
+ * underneath, which is exactly the seam that reads as "not smooth".
71
+ *
72
+ * A view transition has no such limit: it photographs the old page, applies the
73
+ * change, and dissolves one image into the other. Fonts, gradients and shadows
74
+ * come along because they are pixels by then, not properties.
75
+ *
76
+ * Slightly slower than the property transition it replaces. A cross-fade needs
77
+ * the extra time to read as a dissolve rather than a flicker, and it is the
78
+ * whole page changing material — the one moment on this site where taking a
79
+ * beat is the point.
80
+ */
81
+ ::view-transition-old(root),
82
+ ::view-transition-new(root) {
83
+ animation-duration: 560ms;
84
+ animation-timing-function: cubic-bezier(0.32, 0.08, 0.24, 1);
85
+ }
86
+
87
+ /* Both layers paint at once so it dissolves rather than wiping through the
88
+ backdrop — the default cross-fade leaves a pale frame mid-way on a light-to-
89
+ dark change, which is the flicker people notice. */
90
+ ::view-transition-old(root) {
91
+ mix-blend-mode: normal;
92
+ }
93
+
94
+ @media (prefers-reduced-motion: reduce) {
95
+ ::view-transition-old(root),
96
+ ::view-transition-new(root) {
97
+ animation: none;
98
+ }
99
+ }
package/package.json CHANGED
@@ -1,35 +1,64 @@
1
1
  {
2
2
  "name": "@nebutra/theme",
3
- "version": "0.2.1",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Design-language catalog for global product chrome swap (Brand Packages). Multi-mood oklch catalog removed.",
6
6
  "private": false,
7
+ "nebutra": {
8
+ "status": "foundation",
9
+ "graph": "core",
10
+ "productionReady": false
11
+ },
7
12
  "license": "MIT",
8
- "main": "./src/index.ts",
9
- "types": "./src/index.ts",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
10
15
  "sideEffects": [
11
16
  "./keyframes.css",
12
17
  "./themes.css",
13
18
  "./skins.css"
14
19
  ],
15
20
  "exports": {
16
- ".": "./src/index.ts",
17
- "./client": "./src/client.ts",
18
- "./languages": "./src/languages.ts",
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./client": {
27
+ "types": "./dist/client.d.ts",
28
+ "import": "./dist/client.js",
29
+ "default": "./dist/client.js"
30
+ },
31
+ "./language-switcher": {
32
+ "types": "./dist/language-switcher.d.ts",
33
+ "import": "./dist/language-switcher.js",
34
+ "default": "./dist/language-switcher.js"
35
+ },
36
+ "./languages": {
37
+ "types": "./dist/languages.d.ts",
38
+ "import": "./dist/languages.js",
39
+ "default": "./dist/languages.js"
40
+ },
19
41
  "./languages.json": "./src/languages.json",
20
- "./brand-package": "./src/brand-package-surface.ts",
42
+ "./brand-package": {
43
+ "types": "./dist/brand-package-surface.d.ts",
44
+ "import": "./dist/brand-package-surface.js",
45
+ "default": "./dist/brand-package-surface.js"
46
+ },
21
47
  "./keyframes.css": "./keyframes.css",
22
48
  "./themes.css": "./themes.css",
23
- "./skins.css": "./skins.css"
49
+ "./skins.css": "./skins.css",
50
+ "./morph.css": "./morph.css",
51
+ "./styles/sources.css": "./styles/sources.css"
24
52
  },
25
53
  "dependencies": {
26
- "@nebutra/tokens": "0.1.2"
54
+ "@nebutra/tokens": "2.0.0"
27
55
  },
28
56
  "devDependencies": {
29
57
  "@types/react": "^19.2.14",
30
58
  "tsx": "^4.21.0",
31
59
  "typescript": "^5.9.3",
32
- "vitest": "^4.1.4"
60
+ "vitest": "^4.1.4",
61
+ "tsup": "^8.5.1"
33
62
  },
34
63
  "peerDependencies": {
35
64
  "react": "^19"
@@ -46,8 +75,21 @@
46
75
  "publishConfig": {
47
76
  "access": "public"
48
77
  },
78
+ "files": [
79
+ "dist",
80
+ "src/languages.json",
81
+ "keyframes.css",
82
+ "themes.css",
83
+ "skins.css",
84
+ "README.md",
85
+ "LICENSE",
86
+ "CHANGELOG.md",
87
+ "morph.css",
88
+ "styles"
89
+ ],
49
90
  "scripts": {
50
- "build": "node scripts/sync-languages.mjs && node ../tokens/scripts/emit-skins.mjs && node scripts/sync-skins.mjs",
91
+ "build": "tsup",
92
+ "build:codegen": "node scripts/sync-languages.mjs && node ../tokens/scripts/emit-skins.mjs && node scripts/sync-skins.mjs && tsup",
51
93
  "sync": "node scripts/sync-languages.mjs && node scripts/sync-skins.mjs",
52
94
  "sync:languages": "node scripts/sync-languages.mjs",
53
95
  "sync:skins": "node scripts/sync-skins.mjs",