clampography 2.0.0-beta.27 → 2.0.0-beta.29

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.
@@ -0,0 +1,112 @@
1
+ {
2
+ "dark": {
3
+ "background": {
4
+ "value": "oklch(10% 0 0)",
5
+ "type": "color"
6
+ },
7
+ "border": {
8
+ "value": "oklch(31% 0.03 254)",
9
+ "type": "color"
10
+ },
11
+ "error": {
12
+ "value": "oklch(63% 0.22 27)",
13
+ "type": "color"
14
+ },
15
+ "heading": {
16
+ "value": "oklch(98% 0.003 264)",
17
+ "type": "color"
18
+ },
19
+ "info": {
20
+ "value": "oklch(72% 0.17 254)",
21
+ "type": "color"
22
+ },
23
+ "link": {
24
+ "value": "oklch(72% 0.17 254)",
25
+ "type": "color"
26
+ },
27
+ "muted": {
28
+ "value": "oklch(68% 0.024 254)",
29
+ "type": "color"
30
+ },
31
+ "primary": {
32
+ "value": "oklch(63% 0.258 262)",
33
+ "type": "color"
34
+ },
35
+ "secondary": {
36
+ "value": "oklch(63% 0.25 300)",
37
+ "type": "color"
38
+ },
39
+ "success": {
40
+ "value": "oklch(65% 0.17 165)",
41
+ "type": "color"
42
+ },
43
+ "surface": {
44
+ "value": "oklch(12% 0 0)",
45
+ "type": "color"
46
+ },
47
+ "text": {
48
+ "value": "oklch(95% 0 0)",
49
+ "type": "color"
50
+ },
51
+ "warning": {
52
+ "value": "oklch(72% 0.17 65)",
53
+ "type": "color"
54
+ }
55
+ },
56
+ "light": {
57
+ "background": {
58
+ "value": "oklch(100% 0 0)",
59
+ "type": "color"
60
+ },
61
+ "border": {
62
+ "value": "oklch(92% 0.003 264)",
63
+ "type": "color"
64
+ },
65
+ "error": {
66
+ "value": "oklch(63% 0.22 27)",
67
+ "type": "color"
68
+ },
69
+ "heading": {
70
+ "value": "oklch(15% 0.02 264)",
71
+ "type": "color"
72
+ },
73
+ "info": {
74
+ "value": "oklch(63% 0.258 262)",
75
+ "type": "color"
76
+ },
77
+ "link": {
78
+ "value": "oklch(43% 0.19 264)",
79
+ "type": "color"
80
+ },
81
+ "muted": {
82
+ "value": "oklch(52% 0.014 258)",
83
+ "type": "color"
84
+ },
85
+ "primary": {
86
+ "value": "oklch(63% 0.258 262)",
87
+ "type": "color"
88
+ },
89
+ "secondary": {
90
+ "value": "oklch(55% 0.28 300)",
91
+ "type": "color"
92
+ },
93
+ "success": {
94
+ "value": "oklch(65% 0.17 165)",
95
+ "type": "color"
96
+ },
97
+ "surface": {
98
+ "value": "oklch(96% 0.003 264)",
99
+ "type": "color"
100
+ },
101
+ "text": {
102
+ "value": "oklch(31% 0.02 257)",
103
+ "type": "color"
104
+ },
105
+ "warning": {
106
+ "value": "oklch(72% 0.17 65)",
107
+ "type": "color"
108
+ }
109
+ },
110
+ "themes": {},
111
+ "themesList": {}
112
+ }
package/css/theme.css CHANGED
@@ -1,6 +1,6 @@
1
1
  @layer base {
2
2
 
3
- :where(:root), [data-theme="light"] {
3
+ :where(:root) {
4
4
  color-scheme: light;
5
5
  --clampography-background: oklch(100% 0 0);
6
6
  --clampography-border: oklch(92% 0.003 264);
@@ -35,40 +35,6 @@
35
35
  --clampography-text: oklch(95% 0 0);
36
36
  --clampography-warning: oklch(72% 0.17 65);
37
37
  }
38
-
39
- [data-theme="dark"] {
40
- color-scheme: dark;
41
- --clampography-background: oklch(10% 0 0);
42
- --clampography-border: oklch(31% 0.03 254);
43
- --clampography-error: oklch(63% 0.22 27);
44
- --clampography-heading: oklch(98% 0.003 264);
45
- --clampography-info: oklch(72% 0.17 254);
46
- --clampography-link: oklch(72% 0.17 254);
47
- --clampography-muted: oklch(68% 0.024 254);
48
- --clampography-primary: oklch(63% 0.258 262);
49
- --clampography-secondary: oklch(63% 0.25 300);
50
- --clampography-success: oklch(65% 0.17 165);
51
- --clampography-surface: oklch(12% 0 0);
52
- --clampography-text: oklch(95% 0 0);
53
- --clampography-warning: oklch(72% 0.17 65);
54
- }
55
-
56
- [data-theme="light"] {
57
- color-scheme: light;
58
- --clampography-background: oklch(100% 0 0);
59
- --clampography-border: oklch(92% 0.003 264);
60
- --clampography-error: oklch(63% 0.22 27);
61
- --clampography-heading: oklch(15% 0.02 264);
62
- --clampography-info: oklch(63% 0.258 262);
63
- --clampography-link: oklch(43% 0.19 264);
64
- --clampography-muted: oklch(52% 0.014 258);
65
- --clampography-primary: oklch(63% 0.258 262);
66
- --clampography-secondary: oklch(55% 0.28 300);
67
- --clampography-success: oklch(65% 0.17 165);
68
- --clampography-surface: oklch(96% 0.003 264);
69
- --clampography-text: oklch(31% 0.02 257);
70
- --clampography-warning: oklch(72% 0.17 65);
71
- }
72
38
  }
73
39
 
74
40
  [data-theme="dark"] {
@@ -87,4 +53,21 @@
87
53
  --clampography-text: oklch(95% 0 0);
88
54
  --clampography-warning: oklch(72% 0.17 65);
89
55
  }
56
+
57
+ [data-theme="light"] {
58
+ color-scheme: light;
59
+ --clampography-background: oklch(100% 0 0);
60
+ --clampography-border: oklch(92% 0.003 264);
61
+ --clampography-error: oklch(63% 0.22 27);
62
+ --clampography-heading: oklch(15% 0.02 264);
63
+ --clampography-info: oklch(63% 0.258 262);
64
+ --clampography-link: oklch(43% 0.19 264);
65
+ --clampography-muted: oklch(52% 0.014 258);
66
+ --clampography-primary: oklch(63% 0.258 262);
67
+ --clampography-secondary: oklch(55% 0.28 300);
68
+ --clampography-success: oklch(65% 0.17 165);
69
+ --clampography-surface: oklch(96% 0.003 264);
70
+ --clampography-text: oklch(31% 0.02 257);
71
+ --clampography-warning: oklch(72% 0.17 65);
72
+ }
90
73
  }
package/css/theme.min.css CHANGED
@@ -1 +1 @@
1
- @layer base{:where(:root), [data-theme="light"]{color-scheme:light;--clampography-background:oklch(100% 0 0);--clampography-border:oklch(92% 0.003 264);--clampography-error:oklch(63% 0.22 27);--clampography-heading:oklch(15% 0.02 264);--clampography-info:oklch(63% 0.258 262);--clampography-link:oklch(43% 0.19 264);--clampography-muted:oklch(52% 0.014 258);--clampography-primary:oklch(63% 0.258 262);--clampography-secondary:oklch(55% 0.28 300);--clampography-success:oklch(65% 0.17 165);--clampography-surface:oklch(96% 0.003 264);--clampography-text:oklch(31% 0.02 257);--clampography-warning:oklch(72% 0.17 65)}@media (prefers-color-scheme: dark){:root{color-scheme:dark;--clampography-background:oklch(10% 0 0);--clampography-border:oklch(31% 0.03 254);--clampography-error:oklch(63% 0.22 27);--clampography-heading:oklch(98% 0.003 264);--clampography-info:oklch(72% 0.17 254);--clampography-link:oklch(72% 0.17 254);--clampography-muted:oklch(68% 0.024 254);--clampography-primary:oklch(63% 0.258 262);--clampography-secondary:oklch(63% 0.25 300);--clampography-success:oklch(65% 0.17 165);--clampography-surface:oklch(12% 0 0);--clampography-text:oklch(95% 0 0);--clampography-warning:oklch(72% 0.17 65)}[data-theme="dark"]{color-scheme:dark;--clampography-background:oklch(10% 0 0);--clampography-border:oklch(31% 0.03 254);--clampography-error:oklch(63% 0.22 27);--clampography-heading:oklch(98% 0.003 264);--clampography-info:oklch(72% 0.17 254);--clampography-link:oklch(72% 0.17 254);--clampography-muted:oklch(68% 0.024 254);--clampography-primary:oklch(63% 0.258 262);--clampography-secondary:oklch(63% 0.25 300);--clampography-success:oklch(65% 0.17 165);--clampography-surface:oklch(12% 0 0);--clampography-text:oklch(95% 0 0);--clampography-warning:oklch(72% 0.17 65)}[data-theme="light"]{color-scheme:light;--clampography-background:oklch(100% 0 0);--clampography-border:oklch(92% 0.003 264);--clampography-error:oklch(63% 0.22 27);--clampography-heading:oklch(15% 0.02 264);--clampography-info:oklch(63% 0.258 262);--clampography-link:oklch(43% 0.19 264);--clampography-muted:oklch(52% 0.014 258);--clampography-primary:oklch(63% 0.258 262);--clampography-secondary:oklch(55% 0.28 300);--clampography-success:oklch(65% 0.17 165);--clampography-surface:oklch(96% 0.003 264);--clampography-text:oklch(31% 0.02 257);--clampography-warning:oklch(72% 0.17 65)}}[data-theme="dark"]{color-scheme:dark;--clampography-background:oklch(10% 0 0);--clampography-border:oklch(31% 0.03 254);--clampography-error:oklch(63% 0.22 27);--clampography-heading:oklch(98% 0.003 264);--clampography-info:oklch(72% 0.17 254);--clampography-link:oklch(72% 0.17 254);--clampography-muted:oklch(68% 0.024 254);--clampography-primary:oklch(63% 0.258 262);--clampography-secondary:oklch(63% 0.25 300);--clampography-success:oklch(65% 0.17 165);--clampography-surface:oklch(12% 0 0);--clampography-text:oklch(95% 0 0);--clampography-warning:oklch(72% 0.17 65)}}
1
+ @layer base{:where(:root){color-scheme:light;--clampography-background:oklch(100% 0 0);--clampography-border:oklch(92% 0.003 264);--clampography-error:oklch(63% 0.22 27);--clampography-heading:oklch(15% 0.02 264);--clampography-info:oklch(63% 0.258 262);--clampography-link:oklch(43% 0.19 264);--clampography-muted:oklch(52% 0.014 258);--clampography-primary:oklch(63% 0.258 262);--clampography-secondary:oklch(55% 0.28 300);--clampography-success:oklch(65% 0.17 165);--clampography-surface:oklch(96% 0.003 264);--clampography-text:oklch(31% 0.02 257);--clampography-warning:oklch(72% 0.17 65)}@media (prefers-color-scheme: dark){:root{color-scheme:dark;--clampography-background:oklch(10% 0 0);--clampography-border:oklch(31% 0.03 254);--clampography-error:oklch(63% 0.22 27);--clampography-heading:oklch(98% 0.003 264);--clampography-info:oklch(72% 0.17 254);--clampography-link:oklch(72% 0.17 254);--clampography-muted:oklch(68% 0.024 254);--clampography-primary:oklch(63% 0.258 262);--clampography-secondary:oklch(63% 0.25 300);--clampography-success:oklch(65% 0.17 165);--clampography-surface:oklch(12% 0 0);--clampography-text:oklch(95% 0 0);--clampography-warning:oklch(72% 0.17 65)}}[data-theme="dark"]{color-scheme:dark;--clampography-background:oklch(10% 0 0);--clampography-border:oklch(31% 0.03 254);--clampography-error:oklch(63% 0.22 27);--clampography-heading:oklch(98% 0.003 264);--clampography-info:oklch(72% 0.17 254);--clampography-link:oklch(72% 0.17 254);--clampography-muted:oklch(68% 0.024 254);--clampography-primary:oklch(63% 0.258 262);--clampography-secondary:oklch(63% 0.25 300);--clampography-success:oklch(65% 0.17 165);--clampography-surface:oklch(12% 0 0);--clampography-text:oklch(95% 0 0);--clampography-warning:oklch(72% 0.17 65)}[data-theme="light"]{color-scheme:light;--clampography-background:oklch(100% 0 0);--clampography-border:oklch(92% 0.003 264);--clampography-error:oklch(63% 0.22 27);--clampography-heading:oklch(15% 0.02 264);--clampography-info:oklch(63% 0.258 262);--clampography-link:oklch(43% 0.19 264);--clampography-muted:oklch(52% 0.014 258);--clampography-primary:oklch(63% 0.258 262);--clampography-secondary:oklch(55% 0.28 300);--clampography-success:oklch(65% 0.17 165);--clampography-surface:oklch(96% 0.003 264);--clampography-text:oklch(31% 0.02 257);--clampography-warning:oklch(72% 0.17 65)}}
package/package.json CHANGED
@@ -60,13 +60,12 @@
60
60
  "url": "git+https://github.com/Avaray/clampography.git"
61
61
  },
62
62
  "scripts": {
63
- "build": "bun src/convert.js",
63
+ "build": "bun src/convert.js && bun src/export-figma.js",
64
64
  "dev": "bun run dev/server.js",
65
- "dev:server": "bun run dev/server.js",
66
65
  "test": "bun test"
67
66
  },
68
67
  "sideEffects": false,
69
68
  "type": "module",
70
69
  "unpkg": "css/clampography.min.css",
71
- "version": "2.0.0-beta.27"
70
+ "version": "2.0.0-beta.29"
72
71
  }
package/src/base.js CHANGED
@@ -21,40 +21,118 @@ export default (options = {}) => {
21
21
  }
22
22
  parts.push(current.trim());
23
23
 
24
+ const typographyPrefix = options.typography && options.typography !== "global" ? ` ${options.typography}` : "";
25
+
24
26
  return parts
25
27
  .filter(Boolean) // Remove empty strings
26
28
  .map((part) => {
27
29
  if (part === ":root" || part === "body") return root;
30
+
31
+ // Apply typography scope isolation if configured
32
+ if (typographyPrefix) {
33
+ return `${root}${typographyPrefix} ${part}`;
34
+ }
35
+
28
36
  // Avoid double spacing
29
37
  return `${root} ${part}`;
30
38
  })
31
39
  .join(", ");
32
40
  };
33
41
 
42
+ // Fluid math engine: Generates mathematically perfect clamp() strings
43
+ // dynamically based on the configured min and max screen sizes.
44
+ const minScreenRem = (options.fluidMin || 320) / 16;
45
+ const maxScreenRem = (options.fluidMax || 1280) / 16;
46
+
47
+ const makeFluid = (minRem, maxRem) => {
48
+ // If min and max are the same, or if we have invalid screens, just return the static value
49
+ if (minRem === maxRem || minScreenRem >= maxScreenRem) return `${minRem}rem`;
50
+
51
+ const slope = (maxRem - minRem) / (maxScreenRem - minScreenRem);
52
+ const intersection = minRem - slope * minScreenRem;
53
+
54
+ const format = (num) => parseFloat(num.toFixed(4));
55
+
56
+ return `clamp(${minRem}rem, ${format(intersection)}rem + ${format(slope * 100)}vw, ${maxRem}rem)`;
57
+ };
58
+
34
59
  return {
35
60
  // ROOT CONFIGURATION (CSS variables)
36
61
  // Uses :where() for zero specificity so user overrides always win regardless of layer/source order
37
62
  [`:where(${root})`]: {
38
- "--spacing-xs": "clamp(0.25rem, 1.25vw, 0.75rem)",
39
- "--spacing-sm": "clamp(0.375rem, -0.0625rem + 2.1875vw, 1.25rem)",
40
- "--spacing-md": "clamp(0.5rem, 2.5vw, 1.5rem)",
41
- "--spacing-lg": "clamp(0.75rem, -0.125rem + 4.375vw, 2.5rem)",
42
- "--spacing-xl": "clamp(1rem, 5vw, 3rem)",
43
- "--list-indent": "clamp(1.5rem, 1.25rem + 1.25vw, 2rem)",
63
+ // FLUID SPACING SYSTEM
64
+ "--spacing-xs": makeFluid(0.25, 0.75),
65
+ "--spacing-sm": makeFluid(0.375, 1.25),
66
+ "--spacing-md": makeFluid(0.5, 1.5),
67
+ "--spacing-lg": makeFluid(0.75, 2.5),
68
+ "--spacing-xl": makeFluid(1, 3),
69
+ "--list-indent": makeFluid(1.5, 2),
44
70
  "--scroll-offset": "5rem",
45
71
  "--font-family-base":
46
72
  "Inter, system-ui, -apple-system, 'Segoe UI Variable Display', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif",
47
73
  "--font-family-mono":
48
74
  "ui-monospace, 'Cascadia Code', 'Cascadia Mono', 'Segoe UI Mono', 'Ubuntu Mono', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
49
75
 
50
- // Heading size tokens (30→54px, 22→30px, 18→24px, 16→20px, static)
51
- // Override any of these in :root to customize individual headings.
52
- "--clampography-h1-size": "clamp(1.875rem, 1.375rem + 2.5vw, 3.375rem)",
53
- "--clampography-h2-size": "clamp(1.375rem, 1.2rem + 0.85vw, 1.875rem)",
54
- "--clampography-h3-size": "clamp(1.125rem, 1rem + 0.625vw, 1.5rem)",
55
- "--clampography-h4-size": "clamp(1rem, 0.917rem + 0.42vw, 1.25rem)",
56
- "--clampography-h5-size": "1rem",
57
- "--clampography-h6-size": "0.875rem",
76
+ // FLUID TYPOGRAPHY ENGINE Viewport Bounds
77
+ // Unitless rem values derived from plugin options: fluid-min and fluid-max.
78
+ // e.g. 320px → 20rem, 1280px → 80rem
79
+ "--clampography-v-min": String(minScreenRem),
80
+ "--clampography-v-max": String(maxScreenRem),
81
+
82
+ // HEADINGS FLUID TYPOGRAPHY
83
+ // Each heading exposes --min and --max as unitless rem values.
84
+ // Override these in :root to change font-size bounds without rewriting the full clamp().
85
+ //
86
+ // Example — reduce H1 max size on large screens:
87
+ // :root { --clampography-h1-max: 3; }
88
+ //
89
+ // Example — increase H2 minimum size on mobile:
90
+ // :root { --clampography-h2-min: 1.5; }
91
+ //
92
+ // --clampography-h*-slope and --clampography-h*-base are computed automatically via CSS calc().
93
+ // --clampography-h*-size is the final fluid clamp() value used by the heading element.
94
+
95
+ // H1
96
+ "--clampography-h1-min": "1.875",
97
+ "--clampography-h1-max": "4",
98
+ "--clampography-h1-slope": "calc((var(--clampography-h1-max) - var(--clampography-h1-min)) / (var(--clampography-v-max) - var(--clampography-v-min)))",
99
+ "--clampography-h1-base": "calc(var(--clampography-h1-min) - var(--clampography-h1-slope) * var(--clampography-v-min))",
100
+ "--clampography-h1-size": "clamp(calc(var(--clampography-h1-min) * 1rem), calc(var(--clampography-h1-base) * 1rem + var(--clampography-h1-slope) * 100vw), calc(var(--clampography-h1-max) * 1rem))",
101
+
102
+ // H2
103
+ "--clampography-h2-min": "1.25",
104
+ "--clampography-h2-max": "3",
105
+ "--clampography-h2-slope": "calc((var(--clampography-h2-max) - var(--clampography-h2-min)) / (var(--clampography-v-max) - var(--clampography-v-min)))",
106
+ "--clampography-h2-base": "calc(var(--clampography-h2-min) - var(--clampography-h2-slope) * var(--clampography-v-min))",
107
+ "--clampography-h2-size": "clamp(calc(var(--clampography-h2-min) * 1rem), calc(var(--clampography-h2-base) * 1rem + var(--clampography-h2-slope) * 100vw), calc(var(--clampography-h2-max) * 1rem))",
108
+
109
+ // H3
110
+ "--clampography-h3-min": "1.125",
111
+ "--clampography-h3-max": "2.25",
112
+ "--clampography-h3-slope": "calc((var(--clampography-h3-max) - var(--clampography-h3-min)) / (var(--clampography-v-max) - var(--clampography-v-min)))",
113
+ "--clampography-h3-base": "calc(var(--clampography-h3-min) - var(--clampography-h3-slope) * var(--clampography-v-min))",
114
+ "--clampography-h3-size": "clamp(calc(var(--clampography-h3-min) * 1rem), calc(var(--clampography-h3-base) * 1rem + var(--clampography-h3-slope) * 100vw), calc(var(--clampography-h3-max) * 1rem))",
115
+
116
+ // H4
117
+ "--clampography-h4-min": "1",
118
+ "--clampography-h4-max": "1.5",
119
+ "--clampography-h4-slope": "calc((var(--clampography-h4-max) - var(--clampography-h4-min)) / (var(--clampography-v-max) - var(--clampography-v-min)))",
120
+ "--clampography-h4-base": "calc(var(--clampography-h4-min) - var(--clampography-h4-slope) * var(--clampography-v-min))",
121
+ "--clampography-h4-size": "clamp(calc(var(--clampography-h4-min) * 1rem), calc(var(--clampography-h4-base) * 1rem + var(--clampography-h4-slope) * 100vw), calc(var(--clampography-h4-max) * 1rem))",
122
+
123
+ // H5 — static by default (min === max); set --clampography-h5-max to a different value to make it fluid
124
+ "--clampography-h5-min": "1",
125
+ "--clampography-h5-max": "1",
126
+ "--clampography-h5-slope": "calc((var(--clampography-h5-max) - var(--clampography-h5-min)) / (var(--clampography-v-max) - var(--clampography-v-min)))",
127
+ "--clampography-h5-base": "calc(var(--clampography-h5-min) - var(--clampography-h5-slope) * var(--clampography-v-min))",
128
+ "--clampography-h5-size": "clamp(calc(var(--clampography-h5-min) * 1rem), calc(var(--clampography-h5-base) * 1rem + var(--clampography-h5-slope) * 100vw), calc(var(--clampography-h5-max) * 1rem))",
129
+
130
+ // H6 — static by default (min === max)
131
+ "--clampography-h6-min": "0.875",
132
+ "--clampography-h6-max": "0.875",
133
+ "--clampography-h6-slope": "calc((var(--clampography-h6-max) - var(--clampography-h6-min)) / (var(--clampography-v-max) - var(--clampography-v-min)))",
134
+ "--clampography-h6-base": "calc(var(--clampography-h6-min) - var(--clampography-h6-slope) * var(--clampography-v-min))",
135
+ "--clampography-h6-size": "clamp(calc(var(--clampography-h6-min) * 1rem), calc(var(--clampography-h6-base) * 1rem + var(--clampography-h6-slope) * 100vw), calc(var(--clampography-h6-max) * 1rem))",
58
136
 
59
137
  // Global heading scale multiplier (default: 1 = no scaling).
60
138
  // Override in :root to proportionally scale all headings at once.
@@ -73,8 +151,12 @@ export default (options = {}) => {
73
151
  // BODY STYLES (Typography baseline)
74
152
  // Note: font-family is intentionally NOT set here.
75
153
  // It is applied in extra.js with user-font priority via --font-sans.
76
- [root === ":root" ? "body" : root]: {
77
- "font-size": "clamp(1rem, 0.95rem + 0.25vw, 1.125rem)",
154
+ [(() => {
155
+ const typographyPrefix = options.typography && options.typography !== "global" ? ` ${options.typography}` : "";
156
+ const bodyBase = root === ":root" ? "body" : root;
157
+ return typographyPrefix ? `${bodyBase}${typographyPrefix}` : bodyBase;
158
+ })()]: {
159
+ "font-size": makeFluid(0.875, 1.125),
78
160
  "line-height": "1.75",
79
161
  "text-rendering": "optimizeLegibility",
80
162
  "-webkit-font-smoothing": "antialiased",
@@ -0,0 +1,43 @@
1
+ import fs from "fs";
2
+ import { resolve } from "path";
3
+ import * as themes from "./themes.js";
4
+
5
+ const OUTPUT_DIR = "css";
6
+ const OUTPUT_FILE = "figma-tokens.json";
7
+
8
+ function exportTokens() {
9
+ console.log("🎨 Starting Figma Design Tokens export...");
10
+
11
+ if (!fs.existsSync(OUTPUT_DIR)) {
12
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
13
+ }
14
+
15
+ const tokens = {};
16
+ let themeCount = 0;
17
+
18
+ for (const [themeName, themeData] of Object.entries(themes)) {
19
+ // Figma convention: nested objects by theme name
20
+ tokens[themeName] = {};
21
+ themeCount++;
22
+
23
+ for (const [key, value] of Object.entries(themeData)) {
24
+ // Skip non-color attributes like color-scheme
25
+ if (!key.startsWith("--clampography-")) continue;
26
+
27
+ // Clean up the key name for Figma (e.g., "--clampography-primary" -> "primary")
28
+ const colorName = key.replace("--clampography-", "");
29
+
30
+ tokens[themeName][colorName] = {
31
+ value: value,
32
+ type: "color"
33
+ };
34
+ }
35
+ }
36
+
37
+ const outputPath = resolve(OUTPUT_DIR, OUTPUT_FILE);
38
+ fs.writeFileSync(outputPath, JSON.stringify(tokens, null, 2), "utf-8");
39
+
40
+ console.log(`✅ Successfully exported ${themeCount} themes to ${OUTPUT_DIR}/${OUTPUT_FILE}!`);
41
+ }
42
+
43
+ exportTokens();
package/src/extra.js CHANGED
@@ -21,23 +21,45 @@ export default (options = {}) => {
21
21
  }
22
22
  parts.push(current.trim());
23
23
 
24
+ const typographyPrefix = options.typography && options.typography !== "global" ? ` ${options.typography}` : "";
25
+
24
26
  return parts
25
27
  .filter(Boolean)
26
28
  .map((part) => {
27
29
  if (part === ":root" || part === "body") return root;
30
+ if (typographyPrefix) {
31
+ return `${root}${typographyPrefix} ${part}`;
32
+ }
28
33
  return `${root} ${part}`;
29
34
  })
30
35
  .join(", ");
31
36
  };
32
37
 
33
38
  return {
39
+ // Transition duration token — drives smooth color changes when the theme changes.
40
+ // Scoped here (not in base.js) because transitions are an extra.js concern.
41
+ // Users can override: :root { --clampography-transition-duration: 0ms; } to disable.
42
+ [`:where(${root})`]: {
43
+ "--clampography-transition-duration": "200ms",
44
+ },
45
+
34
46
  // --- Basic Coloring & Font (with user-font priority) ---
35
- [root === ":root" ? "body" : root]: {
47
+ [(() => {
48
+ const typographyPrefix = options.typography && options.typography !== "global" ? ` ${options.typography}` : "";
49
+ const bodyBase = root === ":root" ? "body" : root;
50
+ return typographyPrefix ? `${bodyBase}${typographyPrefix}` : bodyBase;
51
+ })()]: {
36
52
  "background-color": "var(--clampography-background)",
37
53
  "color": "var(--clampography-text)",
38
54
  // --font-sans is Tailwind v4's way to expose the user's font choice.
39
55
  // If the user sets a font in their project, it wins. If not, fallback to clampography's system stack.
40
56
  "font-family": "var(--font-sans, var(--font-family-base))",
57
+ // Smooth theme transitions: all color CSS variables animate when data-theme changes.
58
+ // Duration is driven by the token set in base.js (default 200ms).
59
+ // Automatically disabled by the prefers-reduced-motion media query in base.js.
60
+ "transition-property": "color, background-color, border-color, text-decoration-color, fill, stroke",
61
+ "transition-duration": "var(--clampography-transition-duration, 200ms)",
62
+ "transition-timing-function": "ease",
41
63
  },
42
64
 
43
65
  [scope(":where(h1, h2, h3, h4, h5, h6)")]: {
@@ -150,7 +172,7 @@ export default (options = {}) => {
150
172
 
151
173
  // Deleted Text
152
174
  [scope("del")]: {
153
- "text-decoration-color": "var(--clampography-secondary)",
175
+ "text-decoration-color": "var(--clampography-error)",
154
176
  "text-decoration-thickness": "2px",
155
177
  },
156
178
 
@@ -170,5 +192,66 @@ export default (options = {}) => {
170
192
  "border-bottom-width": "1px",
171
193
  "padding-bottom": "var(--spacing-sm)",
172
194
  },
195
+
196
+ // ACCESSIBILITY: Disable all transitions for users who prefer reduced motion.
197
+ // Respects the OS-level "Reduce Motion" setting on macOS, Windows, iOS, and Android.
198
+ // Placed here (not in base.js) because it targets the transitions added above.
199
+ "@media (prefers-reduced-motion: reduce)": {
200
+ [(() => {
201
+ const typographyPrefix = options.typography && options.typography !== "global" ? ` ${options.typography}` : "";
202
+ const bodyBase = root === ":root" ? "body" : root;
203
+ return typographyPrefix ? `${bodyBase}${typographyPrefix}` : bodyBase;
204
+ })()]: {
205
+ "transition": "none",
206
+ "--clampography-transition-duration": "0ms",
207
+ },
208
+ },
209
+
210
+ // ACCESSIBILITY: High-contrast mode for users who need maximum legibility.
211
+ // Triggered automatically by the OS "Increase Contrast" setting on macOS,
212
+ // Windows High Contrast Mode, or Android's Accessibility settings.
213
+ "@media (prefers-contrast: more)": {
214
+ [(() => {
215
+ const typographyPrefix = options.typography && options.typography !== "global" ? ` ${options.typography}` : "";
216
+ const bodyBase = root === ":root" ? "body" : root;
217
+ return typographyPrefix ? `${bodyBase}${typographyPrefix}` : bodyBase;
218
+ })()]: {
219
+ // Override theme colors with absolute black/white for maximum legibility
220
+ "background-color": "white",
221
+ "color": "black",
222
+ },
223
+ [scope(":where(h1, h2, h3, h4, h5, h6)")]: {
224
+ "color": "black",
225
+ },
226
+ [scope("a")]: {
227
+ "color": "black",
228
+ "text-decoration": "underline",
229
+ "text-decoration-thickness": "2px",
230
+ "font-weight": "700",
231
+ },
232
+ [scope(":where(code:not(pre code), kbd, samp)")]: {
233
+ "background-color": "#f0f0f0",
234
+ "color": "black",
235
+ "border": "2px solid black",
236
+ },
237
+ [scope("pre")]: {
238
+ "background-color": "#f0f0f0",
239
+ "color": "black",
240
+ "border": "2px solid black",
241
+ },
242
+ [scope("blockquote")]: {
243
+ "background-color": "#f0f0f0",
244
+ "border-left-color": "black",
245
+ "border-left-width": "6px",
246
+ "color": "black",
247
+ },
248
+ [scope("th, td")]: {
249
+ "border": "2px solid black",
250
+ },
251
+ [scope("hr")]: {
252
+ "background-color": "black",
253
+ "height": "2px",
254
+ },
255
+ },
173
256
  };
174
257
  };
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ import baseStyles from "./base.js";
4
4
  import extraStyles from "./extra.js";
5
5
  import formsStyles from "./forms.js";
6
6
  import kbdStyles from "./kbd.js";
7
+ import printStyles from "./print.js";
7
8
 
8
9
  // Import version from package.json
9
10
  import { version } from "../package.json" with { type: "json" };
@@ -44,12 +45,21 @@ export default plugin.withOptions(
44
45
  const includeExtra = resolveBool(options.extra, false); // Default: false
45
46
  const includeForms = resolveBool(options.forms, false); // Default: false
46
47
  const includeKbd = resolveBool(options.kbd, false); // Default: false
48
+ const includePrint = resolveBool(options.print, false); // Default: false
49
+
50
+ // Extract fluid bounds for clampography math engine
51
+ const fluidMin = parseInt(options["fluid-min"] || options.fluidMin || "320");
52
+ const fluidMax = parseInt(options["fluid-max"] || options.fluidMax || "1280");
53
+
54
+ // Extract typography scope option (default: global)
55
+ const typography = options.typography || "global";
47
56
 
48
57
  // Pass options to the style functions to enable scoping
49
- includeBase && addBase(baseStyles(options));
50
- includeExtra && addBase(extraStyles(options));
51
- includeForms && addBase(formsStyles(options));
52
- includeKbd && addBase(kbdStyles(options));
58
+ includeBase && addBase(baseStyles({ ...options, fluidMin, fluidMax, typography }));
59
+ includeExtra && addBase(extraStyles({ ...options, typography }));
60
+ includeForms && addBase(formsStyles({ ...options, typography }));
61
+ includeKbd && addBase(kbdStyles({ ...options, typography }));
62
+ includePrint && addBase(printStyles({ ...options, typography }));
53
63
 
54
64
  // 2. Parse themes configuration
55
65
  let configThemes = options.themes;
package/src/print.js ADDED
@@ -0,0 +1,92 @@
1
+ export default (options = {}) => {
2
+ const root = options.root || ":root";
3
+
4
+ // Helper to scope selectors safely (ignoring commas inside parentheses)
5
+ const scope = (selector) => {
6
+ const parts = [];
7
+ let current = "";
8
+ let depth = 0;
9
+
10
+ for (let i = 0; i < selector.length; i++) {
11
+ const char = selector[i];
12
+ if (char === "(") depth++;
13
+ if (char === ")") depth--;
14
+
15
+ if (char === "," && depth === 0) {
16
+ parts.push(current.trim());
17
+ current = "";
18
+ } else {
19
+ current += char;
20
+ }
21
+ }
22
+ parts.push(current.trim());
23
+
24
+ const typographyPrefix = options.typography && options.typography !== "global" ? ` ${options.typography}` : "";
25
+
26
+ return parts
27
+ .filter(Boolean)
28
+ .map((part) => {
29
+ if (part === ":root" || part === "body") return root;
30
+ if (typographyPrefix) {
31
+ return `${root}${typographyPrefix} ${part}`;
32
+ }
33
+ return `${root} ${part}`;
34
+ })
35
+ .join(", ");
36
+ };
37
+
38
+ return {
39
+ // PRINT OPTIMIZATION: Force clean, ink-friendly output for printing and PDF export.
40
+ // Overrides all theme colors, removes backgrounds, and converts fluid vw units to
41
+ // static sizes so text renders correctly on physical paper (A4/Letter).
42
+ "@media print": {
43
+ [(() => {
44
+ const typographyPrefix = options.typography && options.typography !== "global" ? ` ${options.typography}` : "";
45
+ const bodyBase = root === ":root" ? "body" : root;
46
+ return typographyPrefix ? `${bodyBase}${typographyPrefix}` : bodyBase;
47
+ })()]: {
48
+ // Static sizes — vw-based clamp() is meaningless on paper
49
+ "font-size": "12pt",
50
+ "line-height": "1.6",
51
+ // Force black-on-white for max legibility and ink saving
52
+ "color": "black",
53
+ "background": "white",
54
+ // Disable all transitions
55
+ "transition": "none",
56
+ },
57
+ [scope(":where(h1, h2, h3, h4, h5, h6)")]: {
58
+ "color": "black",
59
+ "page-break-after": "avoid",
60
+ },
61
+ [scope("h1")]: { "font-size": "28pt" },
62
+ [scope("h2")]: { "font-size": "22pt" },
63
+ [scope("h3")]: { "font-size": "18pt" },
64
+ [scope("h4")]: { "font-size": "14pt" },
65
+ [scope("h5")]: { "font-size": "12pt" },
66
+ [scope("h6")]: { "font-size": "11pt" },
67
+ [scope("a")]: {
68
+ // Force readable link styling on paper
69
+ "color": "black",
70
+ "text-decoration": "underline",
71
+ },
72
+ [scope("pre, blockquote")]: {
73
+ // Avoid cutting code blocks and blockquotes across page breaks
74
+ "page-break-inside": "avoid",
75
+ "border": "1px solid #ccc",
76
+ "background": "#f5f5f5",
77
+ },
78
+ [scope("table")]: {
79
+ "page-break-inside": "avoid",
80
+ "border": "1px solid #ccc",
81
+ },
82
+ [scope("th, td")]: {
83
+ "border": "1px solid #ccc",
84
+ },
85
+ [scope("img, figure")]: {
86
+ // Prevent images from overflowing the page
87
+ "max-width": "100%",
88
+ "page-break-inside": "avoid",
89
+ },
90
+ },
91
+ };
92
+ };