@softure-ai/theme-kit 0.0.0-stage → 0.1.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 (77) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +129 -2
  4. package/bin/softure-theme-css.mjs +14 -0
  5. package/dist/build/build-theme-package.d.ts +13 -0
  6. package/dist/build/build-theme-package.d.ts.map +1 -0
  7. package/dist/build/build-theme-package.js +42 -0
  8. package/dist/build/build-theme-package.js.map +1 -0
  9. package/dist/build/index.d.ts +4 -0
  10. package/dist/build/index.d.ts.map +1 -0
  11. package/dist/build/index.js +5 -0
  12. package/dist/build/index.js.map +1 -0
  13. package/dist/build/theme-package-config.d.ts +22 -0
  14. package/dist/build/theme-package-config.d.ts.map +1 -0
  15. package/dist/build/theme-package-config.js +34 -0
  16. package/dist/build/theme-package-config.js.map +1 -0
  17. package/dist/build/theme-stylesheet.d.ts +24 -0
  18. package/dist/build/theme-stylesheet.d.ts.map +1 -0
  19. package/dist/build/theme-stylesheet.js +24 -0
  20. package/dist/build/theme-stylesheet.js.map +1 -0
  21. package/dist/build-theme-css.d.ts +12 -0
  22. package/dist/build-theme-css.d.ts.map +1 -0
  23. package/dist/build-theme-css.js +102 -0
  24. package/dist/build-theme-css.js.map +1 -0
  25. package/dist/check-theme.d.ts +12 -0
  26. package/dist/check-theme.d.ts.map +1 -0
  27. package/dist/check-theme.js +46 -0
  28. package/dist/check-theme.js.map +1 -0
  29. package/dist/cli/command.d.ts +9 -0
  30. package/dist/cli/command.d.ts.map +1 -0
  31. package/dist/cli/command.js +19 -0
  32. package/dist/cli/command.js.map +1 -0
  33. package/dist/create-cn.d.ts +18 -0
  34. package/dist/create-cn.d.ts.map +1 -0
  35. package/dist/create-cn.js +17 -0
  36. package/dist/create-cn.js.map +1 -0
  37. package/dist/define-theme.d.ts +8 -0
  38. package/dist/define-theme.d.ts.map +1 -0
  39. package/dist/define-theme.js +14 -0
  40. package/dist/define-theme.js.map +1 -0
  41. package/dist/extend-theme.d.ts +20 -0
  42. package/dist/extend-theme.d.ts.map +1 -0
  43. package/dist/extend-theme.js +20 -0
  44. package/dist/extend-theme.js.map +1 -0
  45. package/dist/index.d.ts +7 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +7 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/preflight.css +77 -0
  50. package/dist/react/index.d.ts +2 -0
  51. package/dist/react/index.d.ts.map +1 -0
  52. package/dist/react/index.js +2 -0
  53. package/dist/react/index.js.map +1 -0
  54. package/dist/react/use-is-mobile.d.ts +8 -0
  55. package/dist/react/use-is-mobile.d.ts.map +1 -0
  56. package/dist/react/use-is-mobile.js +24 -0
  57. package/dist/react/use-is-mobile.js.map +1 -0
  58. package/dist/theme-definition.d.ts +49 -0
  59. package/dist/theme-definition.d.ts.map +1 -0
  60. package/dist/theme-definition.js +45 -0
  61. package/dist/theme-definition.js.map +1 -0
  62. package/package.json +59 -4
  63. package/src/build/build-theme-package.ts +45 -0
  64. package/src/build/index.ts +9 -0
  65. package/src/build/theme-package-config.ts +46 -0
  66. package/src/build/theme-stylesheet.ts +43 -0
  67. package/src/build-theme-css.ts +106 -0
  68. package/src/check-theme.ts +62 -0
  69. package/src/cli/command.ts +33 -0
  70. package/src/create-cn.ts +31 -0
  71. package/src/define-theme.ts +15 -0
  72. package/src/extend-theme.ts +33 -0
  73. package/src/index.ts +16 -0
  74. package/src/preflight.css +77 -0
  75. package/src/react/index.ts +1 -0
  76. package/src/react/use-is-mobile.ts +29 -0
  77. package/src/theme-definition.ts +92 -0
@@ -0,0 +1,43 @@
1
+ import { copyFileSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { buildThemeCss } from "../build-theme-css.js";
5
+ import type { ThemeDefinition } from "../theme-definition.js";
6
+ import type { ThemePackageConfig } from "./theme-package-config.js";
7
+
8
+ export interface ThemeStylesheetInput {
9
+ theme: ThemeDefinition;
10
+ /** `@source` paths: relative to the written file, or absolute (a test compiling the sources in place). */
11
+ sources: readonly string[];
12
+ imports: readonly string[];
13
+ }
14
+
15
+ /**
16
+ * The text of a theme package's `theme.css`: the CSS imports, the component folders as `@source` (Tailwind resolves
17
+ * them relative to the file, so the app's Tailwind finds them inside `node_modules`), then the generated tokens.
18
+ */
19
+ export function buildThemeStylesheet({ theme, sources, imports }: ThemeStylesheetInput): string {
20
+ return [
21
+ ...imports.map((name) => `@import ${JSON.stringify(name)};`),
22
+ ...sources.map((source) => `@source ${JSON.stringify(source)};`),
23
+ "",
24
+ buildThemeCss(theme),
25
+ ].join("\n");
26
+ }
27
+
28
+ export interface WriteThemeStylesheetsOptions {
29
+ /** The theme package's folder; its `src/` holds the stylesheets to copy. */
30
+ packageDir: string;
31
+ /** Where the files go; the package's `dist/` by default. */
32
+ distDir?: string;
33
+ theme: ThemeDefinition;
34
+ config: ThemePackageConfig;
35
+ }
36
+
37
+ /** Writes `theme.css` and copies the declared stylesheets; returns the written file names. */
38
+ export function writeThemeStylesheets({ packageDir, distDir = join(packageDir, "dist"), theme, config }: WriteThemeStylesheetsOptions): string[] {
39
+ mkdirSync(distDir, { recursive: true });
40
+ writeFileSync(join(distDir, "theme.css"), buildThemeStylesheet({ theme, sources: config.sources, imports: config.imports }));
41
+ for (const name of config.stylesheets) copyFileSync(join(packageDir, "src", name), join(distDir, name));
42
+ return ["theme.css", ...config.stylesheets];
43
+ }
@@ -0,0 +1,106 @@
1
+ import { TOKEN_NAME, UNSAFE_VALUE } from "./check-theme.js";
2
+ import { isColorAlias, type ThemeDefinition } from "./theme-definition.js";
3
+
4
+ /** Light by default, and for an element or a page that asks for it. */
5
+ const LIGHT_SELECTOR = ':root, .light, [data-theme="light"]';
6
+ /** Dark by the system setting, unless the page asks for light. */
7
+ const SYSTEM_DARK_SELECTOR = ':root:not(.light, [data-theme="light"])';
8
+ /** Dark by choice: `next-themes` (`class="dark"`) or `@softure-ai/ui`'s theme switch (`data-theme="dark"`). */
9
+ const DARK_SELECTOR = '.dark, [data-theme="dark"]';
10
+
11
+ /**
12
+ * Tailwind's `dark:` variant on the same rule as the tokens: the system setting unless the page asks for light, or an
13
+ * explicit dark scope. Without it, `dark:` would follow only the system setting.
14
+ */
15
+ const DARK_VARIANT = `@custom-variant dark {
16
+ @media (prefers-color-scheme: dark) {
17
+ &:where(${SYSTEM_DARK_SELECTOR}, ${SYSTEM_DARK_SELECTOR} *) {
18
+ @slot;
19
+ }
20
+ }
21
+ &:where(.dark, .dark *, [data-theme="dark"], [data-theme="dark"] *) {
22
+ @slot;
23
+ }
24
+ }`;
25
+
26
+ function checkName(kind: string, name: string): string {
27
+ if (!TOKEN_NAME.test(name)) throw new Error(`buildThemeCss: ${kind} name "${name}" is not a lowercase kebab-case name`);
28
+ return name;
29
+ }
30
+
31
+ function checkValue(name: string, value: string): string {
32
+ if (value.trim() === "" || UNSAFE_VALUE.test(value)) {
33
+ throw new Error(`buildThemeCss: value of "${name}" is empty or holds one of ; { } < > or a line break`);
34
+ }
35
+ return value;
36
+ }
37
+
38
+ function writeBlock(selector: string, lines: string[], indent = ""): string {
39
+ return [`${indent}${selector} {`, ...lines.map((line) => `${indent} ${line}`), `${indent}}`].join("\n");
40
+ }
41
+
42
+ function writeSchemeLines(definition: ThemeDefinition, scheme: "light" | "dark"): string[] {
43
+ const colors = Object.entries(definition.colors).flatMap(([name, value]) => {
44
+ checkName("colour", name);
45
+ if (isColorAlias(value)) {
46
+ if (!(value.alias in definition.colors)) {
47
+ throw new Error(`buildThemeCss: colour "${name}" follows "${value.alias}", which the theme does not define`);
48
+ }
49
+ // An alias is a reference, so it follows its target's scheme without a dark line of its own.
50
+ return scheme === "light" ? [`--${name}: var(--${checkName("colour", value.alias)});`] : [];
51
+ }
52
+ return [`--${name}: ${checkValue(name, value[scheme])};`];
53
+ });
54
+ const shadows = Object.entries(definition.shadows).map(
55
+ ([name, value]) => `--shadow-${checkName("shadow", name)}: ${checkValue(`shadow-${name}`, value[scheme])};`,
56
+ );
57
+ return [...colors, ...shadows];
58
+ }
59
+
60
+ function writeThemeLines(definition: ThemeDefinition): string[] {
61
+ const lines: string[] = [];
62
+ if (definition.clearDefaultPalette) lines.push("--color-*: initial;");
63
+ for (const name of Object.keys(definition.colors)) lines.push(`--color-${name}: var(--${name});`);
64
+ for (const [family, stack] of Object.entries(definition.fonts)) {
65
+ lines.push(`--font-${checkName("font", family)}: ${checkValue(`font-${family}`, stack)};`);
66
+ }
67
+ for (const [name, style] of Object.entries(definition.textStyles)) {
68
+ const prefix = `--text-${checkName("text style", name)}`;
69
+ lines.push(`${prefix}: ${checkValue(prefix, style.fontSize)};`);
70
+ lines.push(`${prefix}--line-height: ${checkValue(prefix, style.lineHeight)};`);
71
+ lines.push(`${prefix}--font-weight: ${String(style.fontWeight)};`);
72
+ if (style.letterSpacing !== undefined) lines.push(`${prefix}--letter-spacing: ${checkValue(prefix, style.letterSpacing)};`);
73
+ }
74
+ for (const [step, value] of Object.entries(definition.radii)) {
75
+ lines.push(`--radius-${checkName("radius", step)}: ${checkValue(`radius-${step}`, value)};`);
76
+ }
77
+ // A themed shadow: the utility reads the scheme blocks' value, which the dark blocks switch.
78
+ for (const name of Object.keys(definition.shadows)) lines.push(`--shadow-${name}: var(--shadow-${name});`);
79
+ for (const [name, value] of Object.entries(definition.sizes)) {
80
+ lines.push(`--spacing-${checkName("size", name)}: ${checkValue(`spacing-${name}`, value)};`);
81
+ }
82
+ return lines;
83
+ }
84
+
85
+ /**
86
+ * The theme's CSS for Tailwind CSS v4: the tokens per scheme and the `@theme inline` block that turns them into
87
+ * utilities (`bg-background`, `text-page-title`, `rounded-xl`, `shadow-menu`, `w-sidebar-width`).
88
+ *
89
+ * Schemes: light on `:root` (and on `.light`, `[data-theme="light"]`); dark by the system setting unless the page asks
90
+ * for light, and on `.dark`, `[data-theme="dark"]`, so `next-themes` and `@softure-ai/ui`'s theme switch both work,
91
+ * and a section can take a fixed scheme; Tailwind's `dark:` variant follows the same rule. Throws on a name that is not kebab-case, an unsafe value, or an alias of an
92
+ * undefined colour.
93
+ */
94
+ export function buildThemeCss(definition: ThemeDefinition): string {
95
+ const light = writeSchemeLines(definition, "light");
96
+ const dark = writeSchemeLines(definition, "dark");
97
+ return [
98
+ `/* Theme "${definition.name.replace(/\*\//g, "")}", generated by @softure-ai/theme-kit (buildThemeCss). */`,
99
+ DARK_VARIANT,
100
+ writeBlock(LIGHT_SELECTOR, light),
101
+ `@media (prefers-color-scheme: dark) {\n${writeBlock(SYSTEM_DARK_SELECTOR, dark, " ")}\n}`,
102
+ writeBlock(DARK_SELECTOR, dark),
103
+ writeBlock("@theme inline", writeThemeLines(definition)),
104
+ "",
105
+ ].join("\n\n");
106
+ }
@@ -0,0 +1,62 @@
1
+ import { findMissingColorRoles, isColorAlias, type ThemeDefinition } from "./theme-definition.js";
2
+
3
+ /** A token name: lowercase kebab-case (`primary-hover`, `chart-1`). */
4
+ export const TOKEN_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
+ /** Characters that would end a CSS declaration or block, or open markup. */
6
+ export const UNSAFE_VALUE = /[;{}<>\r\n]/;
7
+
8
+ function findNameProblems(kind: string, names: readonly string[]): string[] {
9
+ return names.filter((name) => !TOKEN_NAME.test(name)).map((name) => `${kind} name "${name}" is not a lowercase kebab-case name`);
10
+ }
11
+
12
+ function findValueProblems(entries: readonly (readonly [string, string])[]): string[] {
13
+ return entries
14
+ .filter(([, value]) => value.trim() === "" || UNSAFE_VALUE.test(value))
15
+ .map(([name]) => `value of "${name}" is empty or holds one of ; { } < > or a line break`);
16
+ }
17
+
18
+ function listValues(definition: ThemeDefinition): (readonly [string, string])[] {
19
+ const colors = Object.entries(definition.colors).flatMap(([name, value]) =>
20
+ isColorAlias(value) ? [] : [[name, value.light] as const, [name, value.dark] as const],
21
+ );
22
+ const shadows = Object.entries(definition.shadows).flatMap(([name, value]) => [
23
+ [`shadow-${name}`, value.light] as const,
24
+ [`shadow-${name}`, value.dark] as const,
25
+ ]);
26
+ const fonts = Object.entries(definition.fonts).map(([family, stack]) => [`font-${family}`, stack] as const);
27
+ const textStyles = Object.entries(definition.textStyles).flatMap(([name, style]) =>
28
+ [style.fontSize, style.lineHeight, ...(style.letterSpacing === undefined ? [] : [style.letterSpacing])].map(
29
+ (value) => [`text-${name}`, value] as const,
30
+ ),
31
+ );
32
+ const radii = Object.entries(definition.radii).map(([step, value]) => [`radius-${step}`, value] as const);
33
+ const sizes = Object.entries(definition.sizes).map(([name, value]) => [`spacing-${name}`, value] as const);
34
+ return [...colors, ...shadows, ...fonts, ...textStyles, ...radii, ...sizes];
35
+ }
36
+
37
+ /**
38
+ * Every way a definition breaks the theme contract, as readable messages; empty when the theme is complete. Checks the
39
+ * shadcn colour roles, that every token name is kebab-case, that no value is empty or could break out of the CSS, and
40
+ * that every alias points at a colour of the same theme.
41
+ */
42
+ export function checkTheme(definition: ThemeDefinition): string[] {
43
+ const problems: string[] = [];
44
+ if (definition.name.trim() === "") problems.push("the theme has no name");
45
+ const missing = findMissingColorRoles(definition);
46
+ if (missing.length > 0) problems.push(`missing shadcn colour roles: ${missing.join(", ")}`);
47
+ problems.push(
48
+ ...findNameProblems("colour", Object.keys(definition.colors)),
49
+ ...findNameProblems("shadow", Object.keys(definition.shadows)),
50
+ ...findNameProblems("font", Object.keys(definition.fonts)),
51
+ ...findNameProblems("text style", Object.keys(definition.textStyles)),
52
+ ...findNameProblems("radius", Object.keys(definition.radii)),
53
+ ...findNameProblems("size", Object.keys(definition.sizes)),
54
+ ...findValueProblems(listValues(definition)),
55
+ );
56
+ for (const [name, value] of Object.entries(definition.colors)) {
57
+ if (isColorAlias(value) && !(value.alias in definition.colors)) {
58
+ problems.push(`colour "${name}" follows "${value.alias}", which the theme does not define`);
59
+ }
60
+ }
61
+ return problems;
62
+ }
@@ -0,0 +1,33 @@
1
+ import { resolve } from "node:path";
2
+
3
+ import { buildThemePackage } from "../build/build-theme-package.js";
4
+
5
+ export interface ThemeCssCommandInput {
6
+ argv: readonly string[];
7
+ cwd: string;
8
+ write?: (line: string) => void;
9
+ writeError?: (line: string) => void;
10
+ }
11
+
12
+ const USAGE = "Usage: softure-theme-css [packageDir] (default: the current folder, after tsc)";
13
+
14
+ /** `softure-theme-css [packageDir]`: builds a theme package's stylesheets; returns the exit code. */
15
+ export async function runThemeCssCommand({
16
+ argv,
17
+ cwd,
18
+ write = (line) => console.log(line),
19
+ writeError = (line) => console.error(line),
20
+ }: ThemeCssCommandInput): Promise<number> {
21
+ if (argv.length > 1 || argv[0] === "--help" || argv[0] === "-h") {
22
+ writeError(USAGE);
23
+ return argv.length > 1 ? 1 : 0;
24
+ }
25
+ const packageDir = resolve(cwd, argv[0] ?? ".");
26
+ const result = await buildThemePackage(packageDir);
27
+ if (!result.ok) {
28
+ writeError(`softure-theme-css: ${result.error}`);
29
+ return 1;
30
+ }
31
+ write(`softure-theme-css: ${result.files.join(", ")} in ${resolve(packageDir, "dist")}`);
32
+ return 0;
33
+ }
@@ -0,0 +1,31 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { extendTailwindMerge } from "tailwind-merge";
3
+
4
+ import type { ThemeDefinition } from "./theme-definition.js";
5
+
6
+ export type { ClassValue } from "clsx";
7
+
8
+ export interface CnOptions {
9
+ /** The theme's named text styles, so `text-body` (a size) and `text-muted-foreground` (a colour) both stay. */
10
+ textStyles?: readonly string[];
11
+ /** The theme's named shadows, so a later `shadow-card` replaces an earlier `shadow-menu`. */
12
+ shadows?: readonly string[];
13
+ }
14
+
15
+ export type Cn = (...inputs: ClassValue[]) => string;
16
+
17
+ /**
18
+ * shadcn/ui's `cn` for a theme: joins class names and lets a later Tailwind utility win over an earlier one of the
19
+ * same kind, knowing the theme's own text styles and shadows.
20
+ */
21
+ export function createCn({ textStyles = [], shadows = [] }: CnOptions = {}): Cn {
22
+ const mergeClasses = extendTailwindMerge({
23
+ extend: { theme: { text: [...textStyles], shadow: [...shadows] } },
24
+ });
25
+ return (...inputs) => mergeClasses(clsx(inputs));
26
+ }
27
+
28
+ /** `createCn` with the text styles and shadows a theme definition names. */
29
+ export function createThemeCn(definition: Pick<ThemeDefinition, "textStyles" | "shadows">): Cn {
30
+ return createCn({ textStyles: Object.keys(definition.textStyles), shadows: Object.keys(definition.shadows) });
31
+ }
@@ -0,0 +1,15 @@
1
+ import { checkTheme } from "./check-theme.js";
2
+ import type { ThemeDefinition } from "./theme-definition.js";
3
+
4
+ /**
5
+ * Declares a theme: checks it against the contract (`checkTheme`) and returns it unchanged, with its literal types, so
6
+ * a broken theme fails where it is written, at import time, not when its CSS is generated. Throws with every problem
7
+ * listed.
8
+ */
9
+ export function defineTheme<const T extends ThemeDefinition>(definition: T): T {
10
+ const problems = checkTheme(definition);
11
+ if (problems.length > 0) {
12
+ throw new Error(`defineTheme: theme "${definition.name}" breaks the theme contract:\n- ${problems.join("\n- ")}`);
13
+ }
14
+ return definition;
15
+ }
@@ -0,0 +1,33 @@
1
+ import { defineTheme } from "./define-theme.js";
2
+ import type { ThemeDefinition } from "./theme-definition.js";
3
+
4
+ /** What a variant changes: a new name, and per token group only the tokens it adds or replaces. */
5
+ export interface ThemeOverrides {
6
+ name: string;
7
+ colors?: ThemeDefinition["colors"];
8
+ shadows?: ThemeDefinition["shadows"];
9
+ fonts?: ThemeDefinition["fonts"];
10
+ textStyles?: ThemeDefinition["textStyles"];
11
+ radii?: ThemeDefinition["radii"];
12
+ sizes?: ThemeDefinition["sizes"];
13
+ clearDefaultPalette?: boolean;
14
+ }
15
+
16
+ /**
17
+ * A theme built on another: each token group of `overrides` is laid over the base's group (a token is replaced
18
+ * whole, never merged field by field), the rest is the base's. Used for a theme that differs from an existing one in
19
+ * a few tokens, and by an app that generates its own variant with `buildThemeCss`. The result is checked like
20
+ * `defineTheme`.
21
+ */
22
+ export function extendTheme(base: ThemeDefinition, overrides: ThemeOverrides): ThemeDefinition {
23
+ return defineTheme({
24
+ name: overrides.name,
25
+ colors: { ...base.colors, ...overrides.colors },
26
+ shadows: { ...base.shadows, ...overrides.shadows },
27
+ fonts: { ...base.fonts, ...overrides.fonts },
28
+ textStyles: { ...base.textStyles, ...overrides.textStyles },
29
+ radii: { ...base.radii, ...overrides.radii },
30
+ sizes: { ...base.sizes, ...overrides.sizes },
31
+ clearDefaultPalette: overrides.clearDefaultPalette ?? base.clearDefaultPalette,
32
+ });
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export {
2
+ SHADCN_COLOR_ROLES,
3
+ findMissingColorRoles,
4
+ isColorAlias,
5
+ type ColorAlias,
6
+ type ColorValue,
7
+ type SchemeValue,
8
+ type ShadcnColorRole,
9
+ type TextStyle,
10
+ type ThemeDefinition,
11
+ } from "./theme-definition.js";
12
+ export { checkTheme } from "./check-theme.js";
13
+ export { defineTheme } from "./define-theme.js";
14
+ export { extendTheme, type ThemeOverrides } from "./extend-theme.js";
15
+ export { buildThemeCss } from "./build-theme-css.js";
16
+ export { createCn, createThemeCn, type ClassValue, type Cn, type CnOptions } from "./create-cn.js";
@@ -0,0 +1,77 @@
1
+ /*
2
+ * Tailwind CSS v4's preflight, scoped to design-system elements: every shadcn/ui part carries `data-slot`, so a
3
+ * screen root with a `data-slot` brings the reset to everything inside it, while pages drawn by other packages (on
4
+ * `@softure-ai/ui`'s own base) keep theirs. Import it instead of Tailwind's `preflight.css` when the app mixes both;
5
+ * an app that is all design system can take Tailwind's full preflight instead.
6
+ *
7
+ * Reads `--border`, `--ring` and `--muted-foreground` from the theme. Also turns transitions and animations off under
8
+ * `prefers-reduced-motion`.
9
+ */
10
+ @layer base {
11
+ :where([data-slot], [data-slot] *),
12
+ :where([data-slot], [data-slot] *)::before,
13
+ :where([data-slot], [data-slot] *)::after,
14
+ :where([data-slot], [data-slot] *)::backdrop {
15
+ box-sizing: border-box;
16
+ margin: 0;
17
+ padding: 0;
18
+ border: 0 solid var(--border);
19
+ }
20
+
21
+ :where([data-slot], [data-slot] *) {
22
+ outline-color: color-mix(in oklab, var(--ring) 50%, transparent);
23
+ }
24
+
25
+ :where(h1, h2, h3, h4, h5, h6):where([data-slot], [data-slot] *) {
26
+ font-size: inherit;
27
+ font-weight: inherit;
28
+ }
29
+
30
+ :where(a):where([data-slot], [data-slot] *) {
31
+ color: inherit;
32
+ text-decoration: inherit;
33
+ }
34
+
35
+ :where(ol, ul, menu):where([data-slot], [data-slot] *) {
36
+ list-style: none;
37
+ }
38
+
39
+ :where(img, svg, video, canvas):where([data-slot], [data-slot] *) {
40
+ display: block;
41
+ vertical-align: middle;
42
+ }
43
+
44
+ :where(table):where([data-slot], [data-slot] *) {
45
+ border-collapse: collapse;
46
+ text-indent: 0;
47
+ }
48
+
49
+ :where(button, input, select, optgroup, textarea):where([data-slot], [data-slot] *) {
50
+ font: inherit;
51
+ letter-spacing: inherit;
52
+ color: inherit;
53
+ background-color: transparent;
54
+ border-radius: 0;
55
+ opacity: 1;
56
+ }
57
+
58
+ :where(button, [role="button"]):where([data-slot], [data-slot] *):not(:disabled) {
59
+ cursor: pointer;
60
+ }
61
+
62
+ :where(textarea):where([data-slot], [data-slot] *) {
63
+ resize: vertical;
64
+ }
65
+
66
+ :where(::placeholder):where([data-slot], [data-slot] *) {
67
+ opacity: 1;
68
+ color: var(--muted-foreground);
69
+ }
70
+
71
+ @media (prefers-reduced-motion: reduce) {
72
+ :where([data-slot], [data-slot] *) {
73
+ transition-duration: 0s !important;
74
+ animation-duration: 0s !important;
75
+ }
76
+ }
77
+ }
@@ -0,0 +1 @@
1
+ export { MOBILE_BREAKPOINT, useIsMobile } from "./use-is-mobile.js";
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+
3
+ /** The width below which shadcn/ui's sidebar turns into a sheet. */
4
+ export const MOBILE_BREAKPOINT = 768;
5
+
6
+ const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
7
+
8
+ function subscribeToViewport(onChange: () => void): () => void {
9
+ const query = window.matchMedia(MOBILE_QUERY);
10
+ query.addEventListener("change", onChange);
11
+ return () => query.removeEventListener("change", onChange);
12
+ }
13
+
14
+ function isMobileViewport(): boolean {
15
+ return window.matchMedia(MOBILE_QUERY).matches;
16
+ }
17
+
18
+ // The server renders the desktop layout; the browser switches on hydration.
19
+ function isMobileOnServer(): boolean {
20
+ return false;
21
+ }
22
+
23
+ /**
24
+ * Whether the viewport is narrower than `MOBILE_BREAKPOINT` (shadcn/ui's `useIsMobile`, read through
25
+ * `useSyncExternalStore` instead of a state set inside an effect).
26
+ */
27
+ export function useIsMobile(): boolean {
28
+ return React.useSyncExternalStore(subscribeToViewport, isMobileViewport, isMobileOnServer);
29
+ }
@@ -0,0 +1,92 @@
1
+ /** A colour or shadow with one value per colour scheme. */
2
+ export interface SchemeValue {
3
+ light: string;
4
+ dark: string;
5
+ }
6
+
7
+ /** A colour that follows another token of the same theme in both schemes (`chart-1` → `blue`). */
8
+ export interface ColorAlias {
9
+ alias: string;
10
+ }
11
+
12
+ export type ColorValue = SchemeValue | ColorAlias;
13
+
14
+ /** A named text style: `text-<name>` sets the size, line height, weight and tracking at once. */
15
+ export interface TextStyle {
16
+ fontSize: string;
17
+ lineHeight: string;
18
+ fontWeight: number;
19
+ letterSpacing?: string;
20
+ }
21
+
22
+ /**
23
+ * Everything a theme decides, as plain data. `buildThemeCss` turns it into the theme's CSS; a theme package keeps
24
+ * one definition and ships the generated file.
25
+ */
26
+ export interface ThemeDefinition {
27
+ /** The theme's name, written into the generated file's header. */
28
+ name: string;
29
+ /** Colour tokens by name (`background`, `primary`, `green-tag`), utilities `bg-<name>`, `text-<name>`. */
30
+ colors: Record<string, ColorValue>;
31
+ /** Shadows by name without the prefix (`menu` → `--shadow-menu`, `shadow-menu`). */
32
+ shadows: Record<string, SchemeValue>;
33
+ /** Font stacks by family (`sans` → `font-sans`). */
34
+ fonts: Record<string, string>;
35
+ /** Named text styles (`page-title` → `text-page-title`). */
36
+ textStyles: Record<string, TextStyle>;
37
+ /** Radii by step (`md` → `rounded-md`). */
38
+ radii: Record<string, string>;
39
+ /** Layout sizes as spacing values (`sidebar-width` → `w-sidebar-width`). */
40
+ sizes: Record<string, string>;
41
+ /** Clears Tailwind's default palette, so only the theme's colours exist as utilities. */
42
+ clearDefaultPalette: boolean;
43
+ }
44
+
45
+ /**
46
+ * The colour roles shadcn/ui components read. Every theme defines them, so a component written for one theme renders
47
+ * in another; a theme adds its own tokens on top.
48
+ */
49
+ export const SHADCN_COLOR_ROLES = [
50
+ "background",
51
+ "foreground",
52
+ "card",
53
+ "card-foreground",
54
+ "popover",
55
+ "popover-foreground",
56
+ "primary",
57
+ "primary-foreground",
58
+ "secondary",
59
+ "secondary-foreground",
60
+ "muted",
61
+ "muted-foreground",
62
+ "accent",
63
+ "accent-foreground",
64
+ "destructive",
65
+ "border",
66
+ "input",
67
+ "ring",
68
+ "chart-1",
69
+ "chart-2",
70
+ "chart-3",
71
+ "chart-4",
72
+ "chart-5",
73
+ "sidebar",
74
+ "sidebar-foreground",
75
+ "sidebar-primary",
76
+ "sidebar-primary-foreground",
77
+ "sidebar-accent",
78
+ "sidebar-accent-foreground",
79
+ "sidebar-border",
80
+ "sidebar-ring",
81
+ ] as const;
82
+
83
+ export type ShadcnColorRole = (typeof SHADCN_COLOR_ROLES)[number];
84
+
85
+ /** The shadcn colour roles a definition lacks, in contract order; empty when it is complete. */
86
+ export function findMissingColorRoles(definition: Pick<ThemeDefinition, "colors">): ShadcnColorRole[] {
87
+ return SHADCN_COLOR_ROLES.filter((role) => !(role in definition.colors));
88
+ }
89
+
90
+ export function isColorAlias(value: ColorValue): value is ColorAlias {
91
+ return "alias" in value;
92
+ }