@softure-ai/theme-kit 0.0.0-stage → 0.1.1

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 +18 -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,9 @@
1
+ export interface ThemeCssCommandInput {
2
+ argv: readonly string[];
3
+ cwd: string;
4
+ write?: (line: string) => void;
5
+ writeError?: (line: string) => void;
6
+ }
7
+ /** `softure-theme-css [packageDir]`: builds a theme package's stylesheets; returns the exit code. */
8
+ export declare function runThemeCssCommand({ argv, cwd, write, writeError, }: ThemeCssCommandInput): Promise<number>;
9
+ //# sourceMappingURL=command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/cli/command.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAID,qGAAqG;AACrG,wBAAsB,kBAAkB,CAAC,EACvC,IAAI,EACJ,GAAG,EACH,KAAmC,EACnC,UAA0C,GAC3C,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAaxC"}
@@ -0,0 +1,19 @@
1
+ import { resolve } from "node:path";
2
+ import { buildThemePackage } from "../build/build-theme-package.js";
3
+ const USAGE = "Usage: softure-theme-css [packageDir] (default: the current folder, after tsc)";
4
+ /** `softure-theme-css [packageDir]`: builds a theme package's stylesheets; returns the exit code. */
5
+ export async function runThemeCssCommand({ argv, cwd, write = (line) => console.log(line), writeError = (line) => console.error(line), }) {
6
+ if (argv.length > 1 || argv[0] === "--help" || argv[0] === "-h") {
7
+ writeError(USAGE);
8
+ return argv.length > 1 ? 1 : 0;
9
+ }
10
+ const packageDir = resolve(cwd, argv[0] ?? ".");
11
+ const result = await buildThemePackage(packageDir);
12
+ if (!result.ok) {
13
+ writeError(`softure-theme-css: ${result.error}`);
14
+ return 1;
15
+ }
16
+ write(`softure-theme-css: ${result.files.join(", ")} in ${resolve(packageDir, "dist")}`);
17
+ return 0;
18
+ }
19
+ //# sourceMappingURL=command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command.js","sourceRoot":"","sources":["../../src/cli/command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AASpE,MAAM,KAAK,GAAG,kFAAkF,CAAC;AAEjG,qGAAqG;AACrG,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,EACvC,IAAI,EACJ,GAAG,EACH,KAAK,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EACnC,UAAU,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GACrB;IACrB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAChE,UAAU,CAAC,KAAK,CAAC,CAAC;QAClB,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,UAAU,CAAC,CAAC;IACnD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,UAAU,CAAC,sBAAsB,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QACjD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,KAAK,CAAC,sBAAsB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACzF,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type ClassValue } from "clsx";
2
+ import type { ThemeDefinition } from "./theme-definition.js";
3
+ export type { ClassValue } from "clsx";
4
+ export interface CnOptions {
5
+ /** The theme's named text styles, so `text-body` (a size) and `text-muted-foreground` (a colour) both stay. */
6
+ textStyles?: readonly string[];
7
+ /** The theme's named shadows, so a later `shadow-card` replaces an earlier `shadow-menu`. */
8
+ shadows?: readonly string[];
9
+ }
10
+ export type Cn = (...inputs: ClassValue[]) => string;
11
+ /**
12
+ * shadcn/ui's `cn` for a theme: joins class names and lets a later Tailwind utility win over an earlier one of the
13
+ * same kind, knowing the theme's own text styles and shadows.
14
+ */
15
+ export declare function createCn({ textStyles, shadows }?: CnOptions): Cn;
16
+ /** `createCn` with the text styles and shadows a theme definition names. */
17
+ export declare function createThemeCn(definition: Pick<ThemeDefinition, "textStyles" | "shadows">): Cn;
18
+ //# sourceMappingURL=create-cn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-cn.d.ts","sourceRoot":"","sources":["../src/create-cn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAG7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEvC,MAAM,WAAW,SAAS;IACxB,+GAA+G;IAC/G,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,6FAA6F;IAC7F,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,KAAK,MAAM,CAAC;AAErD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EAAE,UAAe,EAAE,OAAY,EAAE,GAAE,SAAc,GAAG,EAAE,CAK9E;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,YAAY,GAAG,SAAS,CAAC,GAAG,EAAE,CAE7F"}
@@ -0,0 +1,17 @@
1
+ import { clsx } from "clsx";
2
+ import { extendTailwindMerge } from "tailwind-merge";
3
+ /**
4
+ * shadcn/ui's `cn` for a theme: joins class names and lets a later Tailwind utility win over an earlier one of the
5
+ * same kind, knowing the theme's own text styles and shadows.
6
+ */
7
+ export function createCn({ textStyles = [], shadows = [] } = {}) {
8
+ const mergeClasses = extendTailwindMerge({
9
+ extend: { theme: { text: [...textStyles], shadow: [...shadows] } },
10
+ });
11
+ return (...inputs) => mergeClasses(clsx(inputs));
12
+ }
13
+ /** `createCn` with the text styles and shadows a theme definition names. */
14
+ export function createThemeCn(definition) {
15
+ return createCn({ textStyles: Object.keys(definition.textStyles), shadows: Object.keys(definition.shadows) });
16
+ }
17
+ //# sourceMappingURL=create-cn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-cn.js","sourceRoot":"","sources":["../src/create-cn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,MAAM,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAerD;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,UAAU,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,KAAgB,EAAE;IACxE,MAAM,YAAY,GAAG,mBAAmB,CAAC;QACvC,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,EAAE,EAAE;KACnE,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,MAAM,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;AACnD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAAC,UAA2D;IACvF,OAAO,QAAQ,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AAChH,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { ThemeDefinition } from "./theme-definition.js";
2
+ /**
3
+ * Declares a theme: checks it against the contract (`checkTheme`) and returns it unchanged, with its literal types, so
4
+ * a broken theme fails where it is written, at import time, not when its CSS is generated. Throws with every problem
5
+ * listed.
6
+ */
7
+ export declare function defineTheme<const T extends ThemeDefinition>(definition: T): T;
8
+ //# sourceMappingURL=define-theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-theme.d.ts","sourceRoot":"","sources":["../src/define-theme.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,CAAC,SAAS,eAAe,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,CAM7E"}
@@ -0,0 +1,14 @@
1
+ import { checkTheme } from "./check-theme.js";
2
+ /**
3
+ * Declares a theme: checks it against the contract (`checkTheme`) and returns it unchanged, with its literal types, so
4
+ * a broken theme fails where it is written, at import time, not when its CSS is generated. Throws with every problem
5
+ * listed.
6
+ */
7
+ export function defineTheme(definition) {
8
+ const problems = checkTheme(definition);
9
+ if (problems.length > 0) {
10
+ throw new Error(`defineTheme: theme "${definition.name}" breaks the theme contract:\n- ${problems.join("\n- ")}`);
11
+ }
12
+ return definition;
13
+ }
14
+ //# sourceMappingURL=define-theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-theme.js","sourceRoot":"","sources":["../src/define-theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAkC,UAAa;IACxE,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,uBAAuB,UAAU,CAAC,IAAI,mCAAmC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACpH,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { ThemeDefinition } from "./theme-definition.js";
2
+ /** What a variant changes: a new name, and per token group only the tokens it adds or replaces. */
3
+ export interface ThemeOverrides {
4
+ name: string;
5
+ colors?: ThemeDefinition["colors"];
6
+ shadows?: ThemeDefinition["shadows"];
7
+ fonts?: ThemeDefinition["fonts"];
8
+ textStyles?: ThemeDefinition["textStyles"];
9
+ radii?: ThemeDefinition["radii"];
10
+ sizes?: ThemeDefinition["sizes"];
11
+ clearDefaultPalette?: boolean;
12
+ }
13
+ /**
14
+ * A theme built on another: each token group of `overrides` is laid over the base's group (a token is replaced
15
+ * whole, never merged field by field), the rest is the base's. Used for a theme that differs from an existing one in
16
+ * a few tokens, and by an app that generates its own variant with `buildThemeCss`. The result is checked like
17
+ * `defineTheme`.
18
+ */
19
+ export declare function extendTheme(base: ThemeDefinition, overrides: ThemeOverrides): ThemeDefinition;
20
+ //# sourceMappingURL=extend-theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extend-theme.d.ts","sourceRoot":"","sources":["../src/extend-theme.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,mGAAmG;AACnG,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IACjC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,cAAc,GAAG,eAAe,CAW7F"}
@@ -0,0 +1,20 @@
1
+ import { defineTheme } from "./define-theme.js";
2
+ /**
3
+ * A theme built on another: each token group of `overrides` is laid over the base's group (a token is replaced
4
+ * whole, never merged field by field), the rest is the base's. Used for a theme that differs from an existing one in
5
+ * a few tokens, and by an app that generates its own variant with `buildThemeCss`. The result is checked like
6
+ * `defineTheme`.
7
+ */
8
+ export function extendTheme(base, overrides) {
9
+ return defineTheme({
10
+ name: overrides.name,
11
+ colors: { ...base.colors, ...overrides.colors },
12
+ shadows: { ...base.shadows, ...overrides.shadows },
13
+ fonts: { ...base.fonts, ...overrides.fonts },
14
+ textStyles: { ...base.textStyles, ...overrides.textStyles },
15
+ radii: { ...base.radii, ...overrides.radii },
16
+ sizes: { ...base.sizes, ...overrides.sizes },
17
+ clearDefaultPalette: overrides.clearDefaultPalette ?? base.clearDefaultPalette,
18
+ });
19
+ }
20
+ //# sourceMappingURL=extend-theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extend-theme.js","sourceRoot":"","sources":["../src/extend-theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAehD;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAqB,EAAE,SAAyB;IAC1E,OAAO,WAAW,CAAC;QACjB,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,MAAM,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,EAAE;QAC/C,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE;QAClD,KAAK,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,SAAS,CAAC,KAAK,EAAE;QAC5C,UAAU,EAAE,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,SAAS,CAAC,UAAU,EAAE;QAC3D,KAAK,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,SAAS,CAAC,KAAK,EAAE;QAC5C,KAAK,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,SAAS,CAAC,KAAK,EAAE;QAC5C,mBAAmB,EAAE,SAAS,CAAC,mBAAmB,IAAI,IAAI,CAAC,mBAAmB;KAC/E,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { SHADCN_COLOR_ROLES, findMissingColorRoles, isColorAlias, type ColorAlias, type ColorValue, type SchemeValue, type ShadcnColorRole, type TextStyle, type ThemeDefinition, } from "./theme-definition.js";
2
+ export { checkTheme } from "./check-theme.js";
3
+ export { defineTheme } from "./define-theme.js";
4
+ export { extendTheme, type ThemeOverrides } from "./extend-theme.js";
5
+ export { buildThemeCss } from "./build-theme-css.js";
6
+ export { createCn, createThemeCn, type ClassValue, type Cn, type CnOptions } from "./create-cn.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,eAAe,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { SHADCN_COLOR_ROLES, findMissingColorRoles, isColorAlias, } from "./theme-definition.js";
2
+ export { checkTheme } from "./check-theme.js";
3
+ export { defineTheme } from "./define-theme.js";
4
+ export { extendTheme } from "./extend-theme.js";
5
+ export { buildThemeCss } from "./build-theme-css.js";
6
+ export { createCn, createThemeCn } from "./create-cn.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,YAAY,GAOb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAuB,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAA4C,MAAM,gBAAgB,CAAC"}
@@ -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,2 @@
1
+ export { MOBILE_BREAKPOINT, useIsMobile } from "./use-is-mobile.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { MOBILE_BREAKPOINT, useIsMobile } from "./use-is-mobile.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /** The width below which shadcn/ui's sidebar turns into a sheet. */
2
+ export declare const MOBILE_BREAKPOINT = 768;
3
+ /**
4
+ * Whether the viewport is narrower than `MOBILE_BREAKPOINT` (shadcn/ui's `useIsMobile`, read through
5
+ * `useSyncExternalStore` instead of a state set inside an effect).
6
+ */
7
+ export declare function useIsMobile(): boolean;
8
+ //# sourceMappingURL=use-is-mobile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-is-mobile.d.ts","sourceRoot":"","sources":["../../src/react/use-is-mobile.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAmBrC;;;GAGG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAErC"}
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+ /** The width below which shadcn/ui's sidebar turns into a sheet. */
3
+ export const MOBILE_BREAKPOINT = 768;
4
+ const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
5
+ function subscribeToViewport(onChange) {
6
+ const query = window.matchMedia(MOBILE_QUERY);
7
+ query.addEventListener("change", onChange);
8
+ return () => query.removeEventListener("change", onChange);
9
+ }
10
+ function isMobileViewport() {
11
+ return window.matchMedia(MOBILE_QUERY).matches;
12
+ }
13
+ // The server renders the desktop layout; the browser switches on hydration.
14
+ function isMobileOnServer() {
15
+ return false;
16
+ }
17
+ /**
18
+ * Whether the viewport is narrower than `MOBILE_BREAKPOINT` (shadcn/ui's `useIsMobile`, read through
19
+ * `useSyncExternalStore` instead of a state set inside an effect).
20
+ */
21
+ export function useIsMobile() {
22
+ return React.useSyncExternalStore(subscribeToViewport, isMobileViewport, isMobileOnServer);
23
+ }
24
+ //# sourceMappingURL=use-is-mobile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-is-mobile.js","sourceRoot":"","sources":["../../src/react/use-is-mobile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,oEAAoE;AACpE,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC,MAAM,YAAY,GAAG,eAAe,iBAAiB,GAAG,CAAC,KAAK,CAAC;AAE/D,SAAS,mBAAmB,CAAC,QAAoB;IAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IAC9C,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC3C,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,gBAAgB;IACvB,OAAO,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC;AACjD,CAAC;AAED,4EAA4E;AAC5E,SAAS,gBAAgB;IACvB,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,KAAK,CAAC,oBAAoB,CAAC,mBAAmB,EAAE,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;AAC7F,CAAC"}
@@ -0,0 +1,49 @@
1
+ /** A colour or shadow with one value per colour scheme. */
2
+ export interface SchemeValue {
3
+ light: string;
4
+ dark: string;
5
+ }
6
+ /** A colour that follows another token of the same theme in both schemes (`chart-1` → `blue`). */
7
+ export interface ColorAlias {
8
+ alias: string;
9
+ }
10
+ export type ColorValue = SchemeValue | ColorAlias;
11
+ /** A named text style: `text-<name>` sets the size, line height, weight and tracking at once. */
12
+ export interface TextStyle {
13
+ fontSize: string;
14
+ lineHeight: string;
15
+ fontWeight: number;
16
+ letterSpacing?: string;
17
+ }
18
+ /**
19
+ * Everything a theme decides, as plain data. `buildThemeCss` turns it into the theme's CSS; a theme package keeps
20
+ * one definition and ships the generated file.
21
+ */
22
+ export interface ThemeDefinition {
23
+ /** The theme's name, written into the generated file's header. */
24
+ name: string;
25
+ /** Colour tokens by name (`background`, `primary`, `green-tag`), utilities `bg-<name>`, `text-<name>`. */
26
+ colors: Record<string, ColorValue>;
27
+ /** Shadows by name without the prefix (`menu` → `--shadow-menu`, `shadow-menu`). */
28
+ shadows: Record<string, SchemeValue>;
29
+ /** Font stacks by family (`sans` → `font-sans`). */
30
+ fonts: Record<string, string>;
31
+ /** Named text styles (`page-title` → `text-page-title`). */
32
+ textStyles: Record<string, TextStyle>;
33
+ /** Radii by step (`md` → `rounded-md`). */
34
+ radii: Record<string, string>;
35
+ /** Layout sizes as spacing values (`sidebar-width` → `w-sidebar-width`). */
36
+ sizes: Record<string, string>;
37
+ /** Clears Tailwind's default palette, so only the theme's colours exist as utilities. */
38
+ clearDefaultPalette: boolean;
39
+ }
40
+ /**
41
+ * The colour roles shadcn/ui components read. Every theme defines them, so a component written for one theme renders
42
+ * in another; a theme adds its own tokens on top.
43
+ */
44
+ export declare const SHADCN_COLOR_ROLES: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "primary", "primary-foreground", "secondary", "secondary-foreground", "muted", "muted-foreground", "accent", "accent-foreground", "destructive", "border", "input", "ring", "chart-1", "chart-2", "chart-3", "chart-4", "chart-5", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring"];
45
+ export type ShadcnColorRole = (typeof SHADCN_COLOR_ROLES)[number];
46
+ /** The shadcn colour roles a definition lacks, in contract order; empty when it is complete. */
47
+ export declare function findMissingColorRoles(definition: Pick<ThemeDefinition, "colors">): ShadcnColorRole[];
48
+ export declare function isColorAlias(value: ColorValue): value is ColorAlias;
49
+ //# sourceMappingURL=theme-definition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-definition.d.ts","sourceRoot":"","sources":["../src/theme-definition.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,kGAAkG;AAClG,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,UAAU,CAAC;AAElD,iGAAiG;AACjG,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,0GAA0G;IAC1G,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACrC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACtC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,yFAAyF;IACzF,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,yeAgCrB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,gGAAgG;AAChG,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,GAAG,eAAe,EAAE,CAEpG;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,IAAI,UAAU,CAEnE"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The colour roles shadcn/ui components read. Every theme defines them, so a component written for one theme renders
3
+ * in another; a theme adds its own tokens on top.
4
+ */
5
+ export const SHADCN_COLOR_ROLES = [
6
+ "background",
7
+ "foreground",
8
+ "card",
9
+ "card-foreground",
10
+ "popover",
11
+ "popover-foreground",
12
+ "primary",
13
+ "primary-foreground",
14
+ "secondary",
15
+ "secondary-foreground",
16
+ "muted",
17
+ "muted-foreground",
18
+ "accent",
19
+ "accent-foreground",
20
+ "destructive",
21
+ "border",
22
+ "input",
23
+ "ring",
24
+ "chart-1",
25
+ "chart-2",
26
+ "chart-3",
27
+ "chart-4",
28
+ "chart-5",
29
+ "sidebar",
30
+ "sidebar-foreground",
31
+ "sidebar-primary",
32
+ "sidebar-primary-foreground",
33
+ "sidebar-accent",
34
+ "sidebar-accent-foreground",
35
+ "sidebar-border",
36
+ "sidebar-ring",
37
+ ];
38
+ /** The shadcn colour roles a definition lacks, in contract order; empty when it is complete. */
39
+ export function findMissingColorRoles(definition) {
40
+ return SHADCN_COLOR_ROLES.filter((role) => !(role in definition.colors));
41
+ }
42
+ export function isColorAlias(value) {
43
+ return "alias" in value;
44
+ }
45
+ //# sourceMappingURL=theme-definition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-definition.js","sourceRoot":"","sources":["../src/theme-definition.ts"],"names":[],"mappings":"AA4CA;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,YAAY;IACZ,YAAY;IACZ,MAAM;IACN,iBAAiB;IACjB,SAAS;IACT,oBAAoB;IACpB,SAAS;IACT,oBAAoB;IACpB,WAAW;IACX,sBAAsB;IACtB,OAAO;IACP,kBAAkB;IAClB,QAAQ;IACR,mBAAmB;IACnB,aAAa;IACb,QAAQ;IACR,OAAO;IACP,MAAM;IACN,SAAS;IACT,SAAS;IACT,SAAS;IACT,SAAS;IACT,SAAS;IACT,SAAS;IACT,oBAAoB;IACpB,iBAAiB;IACjB,4BAA4B;IAC5B,gBAAgB;IAChB,2BAA2B;IAC3B,gBAAgB;IAChB,cAAc;CACN,CAAC;AAIX,gGAAgG;AAChG,MAAM,UAAU,qBAAqB,CAAC,UAA2C;IAC/E,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAiB;IAC5C,OAAO,OAAO,IAAI,KAAK,CAAC;AAC1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@softure-ai/theme-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "SOFTURE AI theme kit: the theme contract (defineTheme), the theme CSS generator and build command, cn and the scoped preflight every shadcn/ui theme package shares.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "*.css"
9
+ ],
10
+ "engines": {
11
+ "node": ">=22"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/SOFTURE/AI.git",
16
+ "directory": "themes/theme-kit"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "dist",
21
+ "CHANGELOG.md",
22
+ "src",
23
+ "!src/**/*.test.ts",
24
+ "!src/**/*.test.tsx"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "provenance": true
29
+ },
30
+ "bin": {
31
+ "softure-theme-css": "./bin/softure-theme-css.mjs"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "@softure-ai/source": "./src/index.ts",
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ },
39
+ "./react": {
40
+ "@softure-ai/source": "./src/react/index.ts",
41
+ "types": "./dist/react/index.d.ts",
42
+ "default": "./dist/react/index.js"
43
+ },
44
+ "./build": {
45
+ "@softure-ai/source": "./src/build/index.ts",
46
+ "types": "./dist/build/index.d.ts",
47
+ "default": "./dist/build/index.js"
48
+ },
49
+ "./preflight.css": "./dist/preflight.css"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-css.mjs"
53
+ },
54
+ "dependencies": {
55
+ "clsx": "^2.1.1",
56
+ "tailwind-merge": "^3.7.0"
57
+ },
58
+ "peerDependencies": {
59
+ "react": "^19.0.0"
60
+ }
61
+ }
@@ -0,0 +1,45 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ import { checkTheme } from "../check-theme.js";
6
+ import type { ThemeDefinition } from "../theme-definition.js";
7
+ import { parseThemePackageConfig } from "./theme-package-config.js";
8
+ import { writeThemeStylesheets } from "./theme-stylesheet.js";
9
+
10
+ export type BuildResult = { ok: true; files: string[] } | { ok: false; error: string };
11
+
12
+ const DEFINITION_GROUPS = ["colors", "shadows", "fonts", "textStyles", "radii", "sizes"] as const;
13
+
14
+ /** A first, structural narrowing of the `theme` export; `checkTheme` then checks the values. */
15
+ function isThemeDefinition(value: unknown): value is ThemeDefinition {
16
+ if (typeof value !== "object" || value === null) return false;
17
+ const record = value as Record<string, unknown>;
18
+ return (
19
+ typeof record.name === "string" &&
20
+ typeof record.clearDefaultPalette === "boolean" &&
21
+ DEFINITION_GROUPS.every((group) => typeof record[group] === "object" && record[group] !== null)
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Builds a theme package's stylesheets after `tsc`: reads `softureTheme` from its `package.json` and the `theme`
27
+ * export of the built `dist/tokens.js`, checks the theme, writes `dist/theme.css` and copies the stylesheets.
28
+ */
29
+ export async function buildThemePackage(packageDir: string): Promise<BuildResult> {
30
+ const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")) as Record<string, unknown>;
31
+ const parsed = parseThemePackageConfig(manifest.softureTheme);
32
+ if (!parsed.ok) return parsed;
33
+ const tokensPath = join(packageDir, "dist", "tokens.js");
34
+ let tokens: Record<string, unknown>;
35
+ try {
36
+ tokens = (await import(pathToFileURL(tokensPath).href)) as Record<string, unknown>;
37
+ } catch (error) {
38
+ return { ok: false, error: `cannot load ${tokensPath} (run tsc first): ${error instanceof Error ? error.message : String(error)}` };
39
+ }
40
+ const theme = tokens.theme;
41
+ if (!isThemeDefinition(theme)) return { ok: false, error: `${tokensPath} does not export \`theme\` as a theme definition` };
42
+ const problems = checkTheme(theme);
43
+ if (problems.length > 0) return { ok: false, error: `theme "${theme.name}" breaks the theme contract: ${problems.join("; ")}` };
44
+ return { ok: true, files: writeThemeStylesheets({ packageDir, theme, config: parsed.config }) };
45
+ }
@@ -0,0 +1,9 @@
1
+ // Node-only: the code that builds a theme package's stylesheets (`softure-theme-css`). Not for the browser bundle.
2
+ export { buildThemePackage, type BuildResult } from "./build-theme-package.js";
3
+ export { parseThemePackageConfig, type ConfigResult, type ThemePackageConfig } from "./theme-package-config.js";
4
+ export {
5
+ buildThemeStylesheet,
6
+ writeThemeStylesheets,
7
+ type ThemeStylesheetInput,
8
+ type WriteThemeStylesheetsOptions,
9
+ } from "./theme-stylesheet.js";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * How a theme package ships its stylesheet, declared under `softureTheme` in its `package.json`. Data, not a script:
3
+ * every theme builds with the same `softure-theme-css` command, and the repository test reads the same declaration.
4
+ */
5
+ export interface ThemePackageConfig {
6
+ /** Folders of components, relative to `theme.css` (`./ui`), that the app's Tailwind scans through `@source`. */
7
+ sources: string[];
8
+ /** CSS packages the components need, imported at the top of `theme.css` (`tw-animate-css`). */
9
+ imports: string[];
10
+ /** Further plain stylesheets in `src/`, copied to `dist/` as they are (`sft-bridge.css`). */
11
+ stylesheets: string[];
12
+ }
13
+
14
+ export type ConfigResult = { ok: true; config: ThemePackageConfig } | { ok: false; error: string };
15
+
16
+ const SOURCE_PATH = /^\.\/[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
17
+ const STYLESHEET_NAME = /^[a-z0-9-]+\.css$/;
18
+ const IMPORT_NAME = /^[@a-z0-9][@a-z0-9._/-]*$/;
19
+
20
+ function readList(record: Record<string, unknown>, key: keyof ThemePackageConfig, pattern: RegExp, hint: string): string[] | string {
21
+ const value = record[key] ?? [];
22
+ if (!Array.isArray(value)) return `"${key}" is not a list`;
23
+ const items: string[] = [];
24
+ for (const item of value as unknown[]) {
25
+ if (typeof item !== "string" || !pattern.test(item)) return `"${key}" holds ${JSON.stringify(item)}, which is not ${hint}`;
26
+ items.push(item);
27
+ }
28
+ return items;
29
+ }
30
+
31
+ /** Narrows the `softureTheme` value of a `package.json` to a config, or says what is wrong with it. */
32
+ export function parseThemePackageConfig(value: unknown): ConfigResult {
33
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
34
+ return { ok: false, error: "softureTheme: missing, or not an object" };
35
+ }
36
+ const record = value as Record<string, unknown>;
37
+ const unknownKeys = Object.keys(record).filter((key) => !["sources", "imports", "stylesheets"].includes(key));
38
+ if (unknownKeys.length > 0) return { ok: false, error: `softureTheme: unknown keys ${unknownKeys.join(", ")}` };
39
+ const sources = readList(record, "sources", SOURCE_PATH, "a relative folder like ./ui");
40
+ const imports = readList(record, "imports", IMPORT_NAME, "a package name");
41
+ const stylesheets = readList(record, "stylesheets", STYLESHEET_NAME, "a .css file name in src/");
42
+ for (const result of [sources, imports, stylesheets]) {
43
+ if (typeof result === "string") return { ok: false, error: `softureTheme: ${result}` };
44
+ }
45
+ return { ok: true, config: { sources: sources as string[], imports: imports as string[], stylesheets: stylesheets as string[] } };
46
+ }