@sproutsocial/seeds-theme 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 (42) hide show
  1. package/README.md +259 -0
  2. package/dist/css-tokens.json +134 -0
  3. package/dist/legacy-dark.css +437 -0
  4. package/dist/legacy-light.css +437 -0
  5. package/dist/legacy.css +659 -0
  6. package/dist/schema.json +11798 -0
  7. package/dist/shadcn-dark.css +2 -0
  8. package/dist/shadcn-light.css +2 -0
  9. package/dist/shadcn.css +2 -0
  10. package/dist/shadcn.registry.json +77 -0
  11. package/dist/source/shadcn-dark.tokens.json +169 -0
  12. package/dist/source/shadcn-light.tokens.json +169 -0
  13. package/dist/source/theme-dark.tokens.json +1945 -0
  14. package/dist/source/theme-light.tokens.json +1945 -0
  15. package/dist/styled-components/index.cjs +1648 -0
  16. package/dist/styled-components/index.d.ts +3654 -0
  17. package/dist/styled-components/index.js +1641 -0
  18. package/dist/styled-components/index.ts +5285 -0
  19. package/dist/tailwind.css +40 -0
  20. package/dist/theme-dark.css +388 -0
  21. package/dist/theme-dark.d.ts +420 -0
  22. package/dist/theme-dark.js +401 -0
  23. package/dist/theme-dark.json +712 -0
  24. package/dist/theme-light.css +388 -0
  25. package/dist/theme-light.d.ts +420 -0
  26. package/dist/theme-light.js +400 -0
  27. package/dist/theme-light.json +712 -0
  28. package/dist/theme.css +611 -0
  29. package/mode-authoring.md +54 -0
  30. package/package.json +96 -0
  31. package/src/cli.js +42 -0
  32. package/src/compiler.d.ts +9 -0
  33. package/src/compiler.js +533 -0
  34. package/src/config.d.ts +39 -0
  35. package/src/config.js +280 -0
  36. package/src/contract.js +351 -0
  37. package/src/extension-schema.js +88 -0
  38. package/src/extension.d.ts +18 -0
  39. package/src/extension.js +81 -0
  40. package/src/index.d.ts +90 -0
  41. package/src/index.js +9 -0
  42. package/src/primitives.js +98 -0
@@ -0,0 +1,18 @@
1
+ import type { DtcgTokenTree, ThemeMode } from "./index.js";
2
+
3
+ /** Validate sparse light/dark token files and write only product CSS overrides. */
4
+ export function buildThemeExtension(
5
+ definition: {
6
+ name: string;
7
+ extends: "seeds";
8
+ common?: DtcgTokenTree;
9
+ modes: Record<ThemeMode, DtcgTokenTree>;
10
+ },
11
+ options: { outputFile: string }
12
+ ): Promise<string>;
13
+
14
+ export function composeSeedsTheme(definition: {
15
+ name: string;
16
+ common?: DtcgTokenTree;
17
+ modes?: Record<ThemeMode, DtcgTokenTree>;
18
+ }): Promise<{ name: string; modes: Record<ThemeMode, DtcgTokenTree> }>;
@@ -0,0 +1,81 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import StyleDictionary from "style-dictionary";
4
+ import {
5
+ composeTheme,
6
+ defineTheme,
7
+ defineThemeExtension,
8
+ selectorsForTheme,
9
+ } from "./contract.js";
10
+ import { compileExtensionCss } from "./compiler.js";
11
+ import { loadSeedsPrimitives } from "./primitives.js";
12
+
13
+ async function seedsTheme() {
14
+ const modes = Object.fromEntries(
15
+ await Promise.all(
16
+ ["light", "dark"].map(async (mode) => [
17
+ mode,
18
+ JSON.parse(
19
+ await fs.readFile(
20
+ new URL(
21
+ `../dist/source/theme-${mode}.tokens.json`,
22
+ import.meta.url
23
+ ),
24
+ "utf8"
25
+ )
26
+ ),
27
+ ])
28
+ )
29
+ );
30
+ return defineTheme({
31
+ name: "seeds",
32
+ common: await loadSeedsPrimitives(),
33
+ modes,
34
+ });
35
+ }
36
+
37
+ /** Native Style Dictionary inheritance, validated against the published Seeds tokens. */
38
+ export async function composeSeedsTheme({
39
+ name,
40
+ common = {},
41
+ modes = { light: {}, dark: {} },
42
+ }) {
43
+ const seeds = await seedsTheme();
44
+ const extension = defineThemeExtension({
45
+ name,
46
+ common,
47
+ modes,
48
+ extensionRoots: ["product"],
49
+ });
50
+ // Reject misspelled paths, changed types, missing references and cycles before merging.
51
+ const theme = composeTheme(seeds, extension);
52
+ const baseline = composeTheme(seeds);
53
+ for (const mode of ["light", "dark"]) {
54
+ const dictionary = new StyleDictionary({
55
+ tokens: baseline.modes[mode],
56
+ usesDtcg: true,
57
+ log: { verbosity: "silent" },
58
+ });
59
+ const shared = await dictionary.extend({ tokens: common });
60
+ theme.modes[mode] = (await shared.extend({ tokens: modes[mode] })).tokens;
61
+ }
62
+ return theme;
63
+ }
64
+
65
+ /** Emit only differences; import Seeds' CSS once for all themes and both modes. */
66
+ export async function buildThemeExtension(definition, { outputFile }) {
67
+ if (definition.extends !== "seeds")
68
+ throw new Error('Theme extensions must declare extends: "seeds".');
69
+ if (definition.name === "seeds")
70
+ throw new Error("An extension must have its own theme name.");
71
+ const baseline = composeTheme(await seedsTheme());
72
+ const product = await composeSeedsTheme(definition);
73
+ const css = await compileExtensionCss(
74
+ product,
75
+ baseline,
76
+ selectorsForTheme(definition.name)
77
+ );
78
+ await fs.mkdir(path.dirname(outputFile), { recursive: true });
79
+ await fs.writeFile(outputFile, css);
80
+ return outputFile;
81
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,90 @@
1
+ export type DtcgToken = {
2
+ $value: unknown;
3
+ $type?: string;
4
+ $description?: string;
5
+ [metadata: `$${string}`]: unknown;
6
+ };
7
+
8
+ export type DtcgTokenTree = {
9
+ [name: string]: DtcgTokenTree | DtcgToken | unknown;
10
+ };
11
+
12
+ export type ThemeMode = "light" | "dark";
13
+
14
+ export type ThemeDefinition = {
15
+ kind: "theme";
16
+ name: string;
17
+ common: DtcgTokenTree;
18
+ modes: Record<ThemeMode, DtcgTokenTree>;
19
+ };
20
+
21
+ export type ThemeExtension = {
22
+ kind: "extension";
23
+ name: string;
24
+ common: DtcgTokenTree;
25
+ modes: Record<ThemeMode, DtcgTokenTree>;
26
+ extensionRoots: string[];
27
+ };
28
+
29
+ export type ComposedTheme = {
30
+ name: string;
31
+ modes: Record<ThemeMode, DtcgTokenTree>;
32
+ };
33
+
34
+ export function defineTheme(input: {
35
+ name: string;
36
+ common?: DtcgTokenTree;
37
+ modes: Record<ThemeMode, DtcgTokenTree>;
38
+ }): ThemeDefinition;
39
+
40
+ export function defineThemeExtension(input: {
41
+ name: string;
42
+ common?: DtcgTokenTree;
43
+ modes: Record<ThemeMode, DtcgTokenTree>;
44
+ extensionRoots?: string[];
45
+ }): ThemeExtension;
46
+
47
+ export function composeTheme(
48
+ baseTheme: ThemeDefinition,
49
+ ...extensions: ThemeExtension[]
50
+ ): ComposedTheme;
51
+
52
+ /**
53
+ * The CSS selectors a theme's generated stylesheets are emitted under.
54
+ *
55
+ * `base` applies in every color mode and carries the complete token set;
56
+ * `dark` is layered over it and carries only the declarations that differ.
57
+ * `darkVariant` is the Tailwind `@custom-variant dark` condition and is
58
+ * deliberately theme-agnostic.
59
+ */
60
+ export type ThemeSelectors = {
61
+ base: string;
62
+ dark: string;
63
+ darkVariant: string;
64
+ /** Wrap dark token declarations in this media query for CSS-only hosts. */
65
+ darkMedia?: string;
66
+ };
67
+
68
+ export function assertThemeSelectors(
69
+ selectors: unknown
70
+ ): asserts selectors is ThemeSelectors;
71
+
72
+ export function selectorsForTheme(
73
+ name: string,
74
+ options?: {
75
+ /** Theme whose defaults apply to every brand; its selectors have lower specificity than product overrides. */
76
+ defaultTheme?: string;
77
+ mode?: "class" | "attribute" | "media";
78
+ darkVariant?: string;
79
+ }
80
+ ): ThemeSelectors;
81
+
82
+ export const themeModes: readonly ThemeMode[];
83
+
84
+ export {
85
+ buildProductTheme,
86
+ defineProductTheme,
87
+ type ProductThemeConfig,
88
+ type ProductThemeInput,
89
+ type ProductThemeTokenSource,
90
+ } from "./config.js";
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export {
2
+ assertThemeSelectors,
3
+ composeTheme,
4
+ defineTheme,
5
+ defineThemeExtension,
6
+ selectorsForTheme,
7
+ themeModes,
8
+ } from "./contract.js";
9
+ export { buildProductTheme, defineProductTheme } from "./config.js";
@@ -0,0 +1,98 @@
1
+ // Adapt the existing packages' public exports for Style Dictionary. No primitive
2
+ // values are authored or shipped again by seeds-theme. CSS names are matched
3
+ // against each package's actual stylesheet, including historical double hyphens.
4
+ import fs from "node:fs/promises";
5
+ import { createRequire } from "node:module";
6
+ const require = createRequire(import.meta.url);
7
+
8
+ const packages = [
9
+ "color",
10
+ "space",
11
+ "border",
12
+ "typography",
13
+ "depth",
14
+ "motion",
15
+ "networkcolor",
16
+ ];
17
+ export const primitiveRoots = new Set([
18
+ "color",
19
+ "space",
20
+ "border",
21
+ "typography",
22
+ "elevation",
23
+ "motion",
24
+ "networkColor",
25
+ ]);
26
+
27
+ function camel(parts) {
28
+ return parts
29
+ .map((part, index) =>
30
+ index
31
+ ? part[0].toUpperCase() + part.slice(1).toLowerCase()
32
+ : part.toLowerCase()
33
+ )
34
+ .join("");
35
+ }
36
+
37
+ function tokenPath(name) {
38
+ const parts = name.toLowerCase().split("_");
39
+ if (name.startsWith("COLOR_"))
40
+ return ["color", camel(parts.slice(1, -1)), parts.at(-1)];
41
+ if (name.startsWith("NETWORK_COLOR_"))
42
+ return ["networkColor", parts.slice(2).join("-")];
43
+ if (name.startsWith("TYPOGRAPHY_FAMILY"))
44
+ return ["typography", "family", parts.slice(2).join("-") || "family"];
45
+ if (name.startsWith("TYPOGRAPHY_FONT_SIZE_"))
46
+ return ["typography", "fontSize", parts.at(-1)];
47
+ if (name.startsWith("TYPOGRAPHY_LINE_HEIGHT_"))
48
+ return ["typography", "lineHeight", parts.at(-1)];
49
+ if (name.startsWith("TYPOGRAPHY_WEIGHT_"))
50
+ return ["typography", "weight", parts.slice(2).join(" ")];
51
+ return parts;
52
+ }
53
+
54
+ function tokenType(root, group) {
55
+ if (root === "color" || root === "networkColor") return "color";
56
+ if (root === "motion") return group === "duration" ? "duration" : "string";
57
+ if (root === "elevation") return "string";
58
+ if (root === "typography" && group === "family") return "fontFamily";
59
+ if (root === "typography" && group === "weight") return "fontWeight";
60
+ return "dimension";
61
+ }
62
+
63
+ export async function loadSeedsPrimitives() {
64
+ const tree = {};
65
+ for (const suffix of packages) {
66
+ const name = `@sproutsocial/seeds-${suffix}`;
67
+ const module = await import(name);
68
+ const values = module.default ?? module;
69
+ const css = await fs.readFile(
70
+ require.resolve(`${name}/dist/seeds-${suffix}.css`),
71
+ "utf8"
72
+ );
73
+ const normalize = (name) => name.replace(/[-_]/g, "").toLowerCase();
74
+ const cssNames = new Map(
75
+ [...css.matchAll(/(--[a-zA-Z0-9-]+)\s*:/g)].map(([, name]) => [
76
+ normalize(name),
77
+ name,
78
+ ])
79
+ );
80
+ for (const [key, value] of Object.entries(values)) {
81
+ // Composite typography sizes are resolved via their scalar fontSize and
82
+ // lineHeight tokens. They are not a second theme typography scale.
83
+ if (typeof value !== "string" && typeof value !== "number") continue;
84
+ const keys = tokenPath(key);
85
+ let node = tree;
86
+ for (const segment of keys.slice(0, -1)) node = node[segment] ??= {};
87
+ const cssName = cssNames.get(normalize(key));
88
+ if (!cssName)
89
+ throw new Error(`${name}: missing published CSS name for ${key}`);
90
+ node[keys.at(-1)] = {
91
+ $type: tokenType(keys[0], keys[1]),
92
+ $value: value,
93
+ $extensions: { "com.sproutsocial.theme": { cssName } },
94
+ };
95
+ }
96
+ }
97
+ return tree;
98
+ }