clampography 2.0.0-beta.26 → 2.0.0-beta.28

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.
package/package.json CHANGED
@@ -24,9 +24,8 @@
24
24
  "src",
25
25
  "css"
26
26
  ],
27
- "unpkg": "css/clampography.min.css",
28
- "jsdelivr": "css/clampography.min.css",
29
27
  "homepage": "https://next.dav.one/clampography/",
28
+ "jsdelivr": "css/clampography.min.css",
30
29
  "keywords": [
31
30
  "alternative",
32
31
  "blog",
@@ -61,12 +60,12 @@
61
60
  "url": "git+https://github.com/Avaray/clampography.git"
62
61
  },
63
62
  "scripts": {
64
- "build": "bun src/convert.js",
65
- "test": "bun test",
66
- "dev:css": "bunx @tailwindcss/cli -i ./dev/input.css -o ./dev/output.css --watch",
67
- "dev:server": "bun run dev/server.js",
68
- "dev": "bun run dev/server.js"
63
+ "build": "bun src/convert.js && bun src/export-figma.js",
64
+ "dev": "bun run dev/server.js",
65
+ "test": "bun test"
69
66
  },
67
+ "sideEffects": false,
70
68
  "type": "module",
71
- "version": "2.0.0-beta.26"
69
+ "unpkg": "css/clampography.min.css",
70
+ "version": "2.0.0-beta.28"
72
71
  }
package/src/base.js CHANGED
@@ -21,34 +21,91 @@ 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
- // ROOT CONFIGURATION
36
- [root]: {
37
- "--spacing-xs": "clamp(0.25rem, 1.25vw, 0.75rem)",
38
- "--spacing-sm": "clamp(0.375rem, -0.0625rem + 2.1875vw, 1.25rem)",
39
- "--spacing-md": "clamp(0.5rem, 2.5vw, 1.5rem)",
40
- "--spacing-lg": "clamp(0.75rem, -0.125rem + 4.375vw, 2.5rem)",
41
- "--spacing-xl": "clamp(1rem, 5vw, 3rem)",
42
- "--list-indent": "clamp(1.5rem, 1.25rem + 1.25vw, 2rem)",
60
+ // ROOT CONFIGURATION (CSS variables)
61
+ // Uses :where() for zero specificity so user overrides always win regardless of layer/source order
62
+ [`:where(${root})`]: {
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),
43
70
  "--scroll-offset": "5rem",
44
71
  "--font-family-base":
45
- "system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif",
72
+ "Inter, system-ui, -apple-system, 'Segoe UI Variable Display', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif",
46
73
  "--font-family-mono":
47
- "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
48
-
49
- // Body styles applied to the root container
50
- "font-family": "var(--font-family-base)",
51
- "font-size": "clamp(1rem, 0.95rem + 0.25vw, 1.125rem)",
74
+ "ui-monospace, 'Cascadia Code', 'Cascadia Mono', 'Segoe UI Mono', 'Ubuntu Mono', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
75
+
76
+ // HEADINGS FLUID TYPOGRAPHY
77
+ // Matches Tailwind CSS sizes: sm (min) to 2xl (max)
78
+ // Override any of these in :root to customize individual headings.
79
+ "--clampography-h1-size": makeFluid(1.875, 4),
80
+ "--clampography-h2-size": makeFluid(1.25, 3),
81
+ "--clampography-h3-size": makeFluid(1.125, 2.25),
82
+ "--clampography-h4-size": makeFluid(1, 1.5),
83
+ "--clampography-h5-size": "1rem",
84
+ "--clampography-h6-size": "0.875rem",
85
+
86
+ // Global heading scale multiplier (default: 1 = no scaling).
87
+ // Override in :root to proportionally scale all headings at once.
88
+ // Example: :root { --clampography-heading-scale: 0.85; }
89
+ "--clampography-heading-scale": "1",
90
+
91
+ // Individual heading scales (default to global scale)
92
+ "--clampography-h1-scale": "var(--clampography-heading-scale)",
93
+ "--clampography-h2-scale": "var(--clampography-heading-scale)",
94
+ "--clampography-h3-scale": "var(--clampography-heading-scale)",
95
+ "--clampography-h4-scale": "var(--clampography-heading-scale)",
96
+ "--clampography-h5-scale": "var(--clampography-heading-scale)",
97
+ "--clampography-h6-scale": "var(--clampography-heading-scale)",
98
+ },
99
+
100
+ // BODY STYLES (Typography baseline)
101
+ // Note: font-family is intentionally NOT set here.
102
+ // It is applied in extra.js with user-font priority via --font-sans.
103
+ [(() => {
104
+ const typographyPrefix = options.typography && options.typography !== "global" ? ` ${options.typography}` : "";
105
+ const bodyBase = root === ":root" ? "body" : root;
106
+ return typographyPrefix ? `${bodyBase}${typographyPrefix}` : bodyBase;
107
+ })()]: {
108
+ "font-size": makeFluid(0.875, 1.125),
52
109
  "line-height": "1.75",
53
110
  "text-rendering": "optimizeLegibility",
54
111
  "-webkit-font-smoothing": "antialiased",
@@ -63,7 +120,7 @@ export default (options = {}) => {
63
120
  },
64
121
 
65
122
  [scope("h1")]: {
66
- "font-size": "clamp(2.25rem, 1.95rem + 1.5vw, 3rem)",
123
+ "font-size": "calc(var(--clampography-h1-size) * var(--clampography-h1-scale))",
67
124
  "line-height": "1.1111",
68
125
  "font-weight": "800",
69
126
  "margin-top": "0",
@@ -71,7 +128,7 @@ export default (options = {}) => {
71
128
  },
72
129
 
73
130
  [scope("h2")]: {
74
- "font-size": "clamp(1.5rem, 1.35rem + 0.75vw, 1.875rem)",
131
+ "font-size": "calc(var(--clampography-h2-size) * var(--clampography-h2-scale))",
75
132
  "line-height": "1.3333",
76
133
  "font-weight": "700",
77
134
  "margin-top": "var(--spacing-xl)",
@@ -79,28 +136,28 @@ export default (options = {}) => {
79
136
  },
80
137
 
81
138
  [scope("h3")]: {
82
- "font-size": "clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem)",
139
+ "font-size": "calc(var(--clampography-h3-size) * var(--clampography-h3-scale))",
83
140
  "line-height": "1.5",
84
141
  "margin-top": "var(--spacing-lg)",
85
142
  "margin-bottom": "var(--spacing-sm)",
86
143
  },
87
144
 
88
145
  [scope("h4")]: {
89
- "font-size": "clamp(1rem, 0.975rem + 0.125vw, 1.125rem)",
146
+ "font-size": "calc(var(--clampography-h4-size) * var(--clampography-h4-scale))",
90
147
  "line-height": "1.5",
91
148
  "margin-top": "var(--spacing-lg)",
92
149
  "margin-bottom": "var(--spacing-sm)",
93
150
  },
94
151
 
95
152
  [scope("h5")]: {
96
- "font-size": "1rem",
153
+ "font-size": "calc(var(--clampography-h5-size) * var(--clampography-h5-scale))",
97
154
  "line-height": "1.5",
98
155
  "margin-top": "var(--spacing-md)",
99
156
  "margin-bottom": "var(--spacing-xs)",
100
157
  },
101
158
 
102
159
  [scope("h6")]: {
103
- "font-size": "0.875rem",
160
+ "font-size": "calc(var(--clampography-h6-size) * var(--clampography-h6-scale))",
104
161
  "line-height": "1.5",
105
162
  "margin-top": "var(--spacing-md)",
106
163
  "margin-bottom": "var(--spacing-xs)",
@@ -175,6 +232,8 @@ export default (options = {}) => {
175
232
  [scope(":where(code, kbd, samp)")]: {
176
233
  "font-family": "var(--font-family-mono)",
177
234
  "font-size": "0.875em",
235
+ "-webkit-font-smoothing": "auto",
236
+ "-moz-osx-font-smoothing": "auto",
178
237
  },
179
238
 
180
239
  [scope("kbd")]: {
@@ -358,6 +417,8 @@ export default (options = {}) => {
358
417
  "font-family": "var(--font-family-mono)",
359
418
  "line-height": "1.6",
360
419
  "overflow-x": "auto",
420
+ "-webkit-font-smoothing": "auto",
421
+ "-moz-osx-font-smoothing": "auto",
361
422
  },
362
423
 
363
424
  [scope("pre code")]: {
@@ -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,20 +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 {
34
- // --- Basic Coloring ---
35
- [root]: {
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
+
46
+ // --- Basic Coloring & Font (with user-font priority) ---
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)",
54
+ // --font-sans is Tailwind v4's way to expose the user's font choice.
55
+ // If the user sets a font in their project, it wins. If not, fallback to clampography's system stack.
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",
38
63
  },
39
64
 
40
65
  [scope(":where(h1, h2, h3, h4, h5, h6)")]: {
@@ -147,7 +172,7 @@ export default (options = {}) => {
147
172
 
148
173
  // Deleted Text
149
174
  [scope("del")]: {
150
- "text-decoration-color": "var(--clampography-secondary)",
175
+ "text-decoration-color": "var(--clampography-error)",
151
176
  "text-decoration-thickness": "2px",
152
177
  },
153
178
 
@@ -167,5 +192,66 @@ export default (options = {}) => {
167
192
  "border-bottom-width": "1px",
168
193
  "padding-bottom": "var(--spacing-sm)",
169
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
+ },
170
256
  };
171
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
+ };