clampography 2.0.0-beta.9 → 2.0.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.
package/src/forms.js ADDED
@@ -0,0 +1,298 @@
1
+ export default (options = {}) => {
2
+ const root = options.root || ":root";
3
+
4
+ // Helper to scope selectors safely
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
+ if (char === "," && depth === 0) {
15
+ parts.push(current.trim());
16
+ current = "";
17
+ } else {
18
+ current += char;
19
+ }
20
+ }
21
+ parts.push(current.trim());
22
+
23
+ return parts
24
+ .filter(Boolean)
25
+ .map((part) => {
26
+ if (part === ":root" || part === "body") return root;
27
+ return `${root} ${part}`;
28
+ })
29
+ .join(", ");
30
+ };
31
+
32
+ return {
33
+ // ── Buttons ──────────────────────────────────────────────────────────────
34
+ [scope(":where(button, [type='button'], [type='reset'], [type='submit'])")]: {
35
+ "display": "inline-flex",
36
+ "align-items": "center",
37
+ "justify-content": "center",
38
+ "gap": "0.375em",
39
+ "padding": "var(--clampography-spacing-xs) var(--clampography-spacing-sm)",
40
+ "background-color": "var(--clampography-surface)",
41
+ "color": "var(--clampography-text)",
42
+ "border": "1px solid var(--clampography-border)",
43
+ "border-radius": "0.375rem",
44
+ "font-weight": "500",
45
+ "white-space": "nowrap",
46
+ "transition-property": "background-color, border-color, color, box-shadow",
47
+ "transition-duration": "150ms",
48
+ },
49
+
50
+ [scope(":where(button, [type='button'], [type='reset'], [type='submit']):hover")]: {
51
+ "background-color": "var(--clampography-background)",
52
+ "border-color": "var(--clampography-primary)",
53
+ },
54
+
55
+ [scope(":where(button, [type='button'], [type='submit']).primary, [type='submit']")]: {
56
+ "background-color": "var(--clampography-primary)",
57
+ "color": "var(--clampography-background)",
58
+ "border-color": "var(--clampography-primary)",
59
+ },
60
+
61
+ [scope(":where(button, [type='button'], [type='submit']).primary:hover, [type='submit']:hover")]: {
62
+ "filter": "brightness(1.1)",
63
+ },
64
+
65
+ // ── Text Inputs & Textarea ────────────────────────────────────────────────
66
+ [scope(":where(input:not([type='checkbox'], [type='radio'], [type='range'], [type='color']), textarea, select)")]: {
67
+ "display": "block",
68
+ "width": "100%",
69
+ "padding": "var(--clampography-spacing-xs) var(--clampography-spacing-sm)",
70
+ "background-color": "var(--clampography-background)",
71
+ "color": "var(--clampography-text)",
72
+ "border": "1px solid var(--clampography-border)",
73
+ "border-radius": "0.375rem",
74
+ "transition-property": "border-color, box-shadow",
75
+ "transition-duration": "150ms",
76
+ },
77
+
78
+ [scope(":where(input:not([type='checkbox'], [type='radio'], [type='range'], [type='color']), textarea, select):focus")]: {
79
+ "outline": "none",
80
+ "border-color": "var(--clampography-primary)",
81
+ "box-shadow": "0 0 0 3px color-mix(in oklab, var(--clampography-primary) 20%, transparent)",
82
+ },
83
+
84
+ [scope(":where(input, textarea, select):disabled")]: {
85
+ "opacity": "0.5",
86
+ "cursor": "not-allowed",
87
+ },
88
+
89
+ [scope(":where(input, textarea, select)[readonly]")]: {
90
+ "background-color": "color-mix(in oklab, var(--clampography-surface) 50%, transparent)",
91
+ "cursor": "default",
92
+ },
93
+
94
+ [scope(":where(input, textarea, select):user-invalid")]: {
95
+ "border-color": "var(--clampography-error)",
96
+ },
97
+
98
+ [scope(":where(input, textarea, select):user-invalid:focus")]: {
99
+ "box-shadow": "0 0 0 3px color-mix(in oklab, var(--clampography-error) 20%, transparent)",
100
+ },
101
+
102
+ [scope("[type='search']::-webkit-search-cancel-button, [type='search']::-webkit-search-decoration")]: {
103
+ "-webkit-appearance": "none",
104
+ "appearance": "none",
105
+ },
106
+
107
+ [scope("[type='number']::-webkit-inner-spin-button, [type='number']::-webkit-outer-spin-button")]: {
108
+ "height": "auto",
109
+ },
110
+
111
+ [scope(":where(input, textarea, select)::placeholder")]: {
112
+ "color": "var(--clampography-muted)",
113
+ },
114
+
115
+ // ── Select ────────────────────────────────────────────────────────────────
116
+ [scope("select")]: {
117
+ "appearance": "none",
118
+ "background-image": `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E")`,
119
+ "background-position": "inline-end 0.5rem center",
120
+ "background-repeat": "no-repeat",
121
+ "background-size": "1.5em 1.5em",
122
+ "padding-inline-end": "2.5rem",
123
+ },
124
+
125
+ // ── File Input ────────────────────────────────────────────────────────────
126
+ [scope("[type='file']")]: {
127
+ "padding": "0",
128
+ "background-color": "transparent",
129
+ "border": "none",
130
+ "cursor": "pointer",
131
+ },
132
+
133
+ [scope("[type='file']::file-selector-button")]: {
134
+ "display": "inline-flex",
135
+ "align-items": "center",
136
+ "padding": "var(--clampography-spacing-xs) var(--clampography-spacing-sm)",
137
+ "margin-inline-end": "var(--clampography-spacing-sm)",
138
+ "background-color": "var(--clampography-surface)",
139
+ "color": "var(--clampography-text)",
140
+ "border": "1px solid var(--clampography-border)",
141
+ "border-radius": "0.375rem",
142
+ "font-family": "inherit",
143
+ "font-size": "inherit",
144
+ "cursor": "pointer",
145
+ "transition-property": "background-color, border-color",
146
+ "transition-duration": "150ms",
147
+ },
148
+
149
+ [scope("[type='file']:hover::file-selector-button")]: {
150
+ "background-color": "var(--clampography-background)",
151
+ "border-color": "var(--clampography-primary)",
152
+ },
153
+
154
+ // ── Checkbox & Radio ──────────────────────────────────────────────────────
155
+ [scope("[type='checkbox'], [type='radio']")]: {
156
+ "width": "1em",
157
+ "height": "1em",
158
+ "accent-color": "var(--clampography-primary)",
159
+ "vertical-align": "middle",
160
+ "cursor": "pointer",
161
+ },
162
+
163
+ [scope("[type='checkbox']:focus-visible, [type='radio']:focus-visible")]: {
164
+ "outline": "2px solid var(--clampography-primary)",
165
+ "outline-offset": "2px",
166
+ },
167
+
168
+ // ── Range ────────────────────────────────────────────────────────────────
169
+ [scope("[type='range']")]: {
170
+ "accent-color": "var(--clampography-primary)",
171
+ "width": "100%",
172
+ "cursor": "pointer",
173
+ },
174
+
175
+ // ── Color Picker ──────────────────────────────────────────────────────────
176
+ [scope("[type='color']")]: {
177
+ "padding": "0.125rem",
178
+ "width": "2.5rem",
179
+ "height": "2.5rem",
180
+ "border": "1px solid var(--clampography-border)",
181
+ "border-radius": "0.375rem",
182
+ "background-color": "var(--clampography-background)",
183
+ "cursor": "pointer",
184
+ },
185
+
186
+ // ── Fieldset & Legend ────────────────────────────────────────────────────
187
+ [scope("fieldset")]: {
188
+ "border": "1px solid var(--clampography-border)",
189
+ "border-radius": "0.5rem",
190
+ "background-color": "var(--clampography-surface)",
191
+ },
192
+
193
+ [scope("legend")]: {
194
+ "color": "var(--clampography-heading)",
195
+ },
196
+
197
+ // ── Label ────────────────────────────────────────────────────────────────
198
+ [scope("label")]: {
199
+ "color": "var(--clampography-text)",
200
+ },
201
+
202
+ // ── Output ───────────────────────────────────────────────────────────────
203
+ [scope("output")]: {
204
+ "color": "var(--clampography-primary)",
205
+ "font-weight": "600",
206
+ },
207
+
208
+ // ── Progress ──────────────────────────────────────────────────────────────
209
+ [scope("progress")]: {
210
+ "-webkit-appearance": "none",
211
+ "appearance": "none",
212
+ "width": "100%",
213
+ "height": "1em",
214
+ "background": "transparent",
215
+ },
216
+
217
+ // WebKit progress track
218
+ [scope("progress::-webkit-progress-bar")]: {
219
+ "background": "color-mix(in oklab, var(--clampography-text) 20%, transparent)",
220
+ },
221
+
222
+ // WebKit progress value
223
+ [scope("progress::-webkit-progress-value")]: {
224
+ "background": "var(--clampography-success)",
225
+ },
226
+
227
+ // Firefox progress value
228
+ [scope("progress::-moz-progress-bar")]: {
229
+ "background": "var(--clampography-success)",
230
+ },
231
+
232
+ // ── Meter ─────────────────────────────────────────────────────────────────
233
+ // Custom styling for <meter> (accent-color does not work on meter)
234
+ [scope("meter")]: {
235
+ "-webkit-appearance": "none",
236
+ "appearance": "none",
237
+ "width": "100%",
238
+ "height": "1em",
239
+ "background": "transparent",
240
+ },
241
+
242
+ // Firefox track (restored via Firefox-only feature query)
243
+ // @supports (-moz-appearance: none) is ignored by all WebKit/Blink browsers
244
+ "@supports (-moz-appearance: none)": {
245
+ [scope("progress")]: {
246
+ "background": "color-mix(in oklab, var(--clampography-text) 20%, transparent)",
247
+ },
248
+ [scope("meter")]: {
249
+ "background": "color-mix(in oklab, var(--clampography-text) 20%, transparent)",
250
+ },
251
+ },
252
+
253
+ // Re-establish height context for WebKit shadow DOM
254
+ // appearance:none breaks Chrome's flex layout; inner elements can't resolve height:100%
255
+ // without a concrete parent height set here.
256
+ // display:flex + align-items:stretch forces the child bar to fill the full height
257
+ // without top-anchoring it the way display:block would.
258
+ [scope("meter::-webkit-meter-inner-element")]: {
259
+ "display": "flex",
260
+ "align-items": "stretch",
261
+ "height": "1em",
262
+ },
263
+
264
+ // WebKit inner track
265
+ [scope("meter::-webkit-meter-bar")]: {
266
+ "background": "color-mix(in oklab, var(--clampography-text) 20%, transparent)",
267
+ "height": "100%",
268
+ },
269
+
270
+ // 1. Optimum (Success)
271
+ [scope("meter::-webkit-meter-optimum-value")]: {
272
+ "background": "var(--clampography-success)",
273
+ "height": "100%",
274
+ },
275
+ [scope("meter:-moz-meter-optimum::-moz-meter-bar")]: {
276
+ "background": "var(--clampography-success)",
277
+ },
278
+
279
+ // 2. Sub-optimum (Warning)
280
+ [scope("meter::-webkit-meter-suboptimum-value")]: {
281
+ "background": "var(--clampography-warning)",
282
+ "height": "100%",
283
+ },
284
+ [scope("meter:-moz-meter-sub-optimum::-moz-meter-bar")]: {
285
+ "background": "var(--clampography-warning)",
286
+ },
287
+
288
+ // 3. Even less good (Error)
289
+ [scope("meter::-webkit-meter-even-less-good-value")]: {
290
+ "background": "var(--clampography-error)",
291
+ "height": "100%",
292
+ },
293
+ [scope("meter:-moz-meter-sub-sub-optimum::-moz-meter-bar")]: {
294
+ "background": "var(--clampography-error)",
295
+ },
296
+
297
+ };
298
+ };
package/src/index.js CHANGED
@@ -2,108 +2,266 @@ import plugin from "tailwindcss/plugin";
2
2
  import { themes as builtInThemes } from "./themes.js";
3
3
  import baseStyles from "./base.js";
4
4
  import extraStyles from "./extra.js";
5
+ import formsStyles from "./forms.js";
6
+ import kbdStyles from "./kbd.js";
7
+ import printStyles from "./print.js";
8
+
9
+
5
10
 
6
11
  /**
7
12
  * Helper to resolve boolean options from CSS configuration.
8
13
  * CSS values often come as strings ("true"/"false"), which are both truthy in JS.
9
14
  */
10
15
  const resolveBool = (value, defaultValue) => {
11
- if (value === "false" || value === false) return false;
12
- if (value === "true" || value === true) return true;
16
+ if (
17
+ value === "false" || value === false || value === "no" || value === "none"
18
+ ) return false;
19
+ if (value === "true" || value === true || value === "yes") return true;
13
20
  return defaultValue;
14
21
  };
15
22
 
16
23
  /**
17
24
  * Main plugin function.
18
25
  */
19
- export default plugin.withOptions((options = {}) => {
20
- return ({ addBase }) => {
21
- // 1. Load Base and Extra styles
22
- // We use the helper to correctly parse "false" string from CSS
23
- const includeBase = resolveBool(options.base, true); // Default: true
24
- const includeExtra = resolveBool(options.extra, false); // Default: false
25
-
26
- includeBase && addBase(baseStyles);
27
- includeExtra && addBase(extraStyles);
28
-
29
- // 2. Parse themes configuration
30
- let configThemes = options.themes;
31
- let themesToInclude = [];
32
- let defaultThemeName = null;
33
- let prefersDarkTheme = false;
34
- let rootSelector = options.root ?? ":root";
35
-
36
- // Normalize input to an array of strings
37
- let rawThemeList = [];
38
-
39
- if (typeof configThemes === "string") {
40
- if (configThemes.trim() === "all") {
41
- // Special case: themes: all
42
- rawThemeList = Object.keys(builtInThemes);
43
- } else if (configThemes.trim() === "false") {
44
- // Explicitly disabled themes
45
- rawThemeList = [];
46
- } else {
47
- rawThemeList = configThemes.split(",");
48
- }
49
- } else if (Array.isArray(configThemes)) {
50
- rawThemeList = configThemes;
51
- } else {
52
- // Default behavior: NO themes loaded automatically.
53
- // User must specify themes to load them.
54
- rawThemeList = [];
55
- }
56
-
57
- // 3. Process the list and look for flags (--default, --prefersdark)
58
- rawThemeList.forEach((rawItem) => {
59
- let themeName = rawItem.trim();
60
-
61
- // Ignore empty entries
62
- if (!themeName) return;
63
-
64
- // Check for --default flag
65
- if (themeName.includes("--default")) {
66
- themeName = themeName.replace("--default", "").trim();
67
- defaultThemeName = themeName;
68
- }
69
-
70
- // Check for --prefersdark flag
71
- if (themeName.toLowerCase().includes("--prefersdark")) {
72
- themeName = themeName.replace(/--prefersdark/i, "").trim();
73
- prefersDarkTheme = themeName;
74
- }
75
-
76
- // Check if theme exists in the database
77
- if (builtInThemes[themeName]) {
78
- themesToInclude.push(themeName);
79
- }
80
- });
81
-
82
- // If list is empty after filtering, stop here
83
- if (
84
- themesToInclude.length === 0 && !defaultThemeName && !prefersDarkTheme
85
- ) return;
86
-
87
- // 4. Generate CSS
88
- const themeStyles = {};
89
-
90
- // A. Default theme (:root)
91
- if (defaultThemeName && builtInThemes[defaultThemeName]) {
92
- themeStyles[rootSelector] = builtInThemes[defaultThemeName];
93
- }
94
-
95
- // B. Theme for prefers-color-scheme: dark
96
- if (prefersDarkTheme && builtInThemes[prefersDarkTheme]) {
97
- themeStyles["@media (prefers-color-scheme: dark)"] = {
98
- [rootSelector]: builtInThemes[prefersDarkTheme],
26
+ export default plugin.withOptions(
27
+ (() => {
28
+ let firstRun = true; // Track first run for logging
29
+
30
+ return (options = {}) => {
31
+ return ({ addBase }) => {
32
+ // Extract logs option (default: true)
33
+ const showLogs = resolveBool(options.logs, true);
34
+
35
+ // Show startup log only once
36
+ if (showLogs && firstRun) {
37
+ console.log(`🍀 Clampography loaded successfully`);
38
+ firstRun = false;
39
+ }
40
+
41
+ // 1. Load Base and Extra styles
42
+ // We use the helper to correctly parse "false" string from CSS
43
+ const includeBase = resolveBool(options.base, true); // Default: true
44
+ const includeExtra = resolveBool(options.extra, false); // Default: false
45
+ const includeForms = resolveBool(options.forms, false); // Default: false
46
+ const includeKbd = resolveBool(options.kbd, false); // Default: false
47
+ const includePrint = resolveBool(options.print, false); // Default: false
48
+
49
+ // Extract fluid bounds for clampography math engine
50
+ const fluidMin = parseInt(options["fluid-min"] || options.fluidMin || "320");
51
+ const fluidMax = parseInt(options["fluid-max"] || options.fluidMax || "1280");
52
+
53
+ // Extract scaleMode: 'viewport' (default, uses vw) or 'container' (uses cqi)
54
+ const scaleMode = options["scale-mode"] || options.scaleMode || "viewport";
55
+
56
+ // Extract typography scope option (default: global)
57
+ const typography = options.typography || "global";
58
+
59
+ // Pass options to the style functions to enable scoping
60
+ includeBase && addBase(baseStyles({ ...options, fluidMin, fluidMax, scaleMode, typography }));
61
+ includeExtra && addBase(extraStyles({ ...options, typography }));
62
+ includeForms && addBase(formsStyles({ ...options, typography }));
63
+ includeKbd && addBase(kbdStyles({ ...options, typography }));
64
+ includePrint && addBase(printStyles({ ...options, typography }));
65
+
66
+ // 2. Parse themes configuration
67
+ let configThemes = options.themes;
68
+ let themesToInclude = [];
69
+ let defaultThemeName = null;
70
+ let prefersDarkTheme = false;
71
+ let rootSelector = options.root || ":root";
72
+ let isAllThemes = false; // Track if user specified "all"
73
+
74
+ // Normalize input to an array of strings
75
+ let rawThemeList = [];
76
+ if (typeof configThemes === "string") {
77
+ if (["all", "true", "yes"].includes(configThemes.trim())) {
78
+ // Special case: themes: all
79
+ isAllThemes = true;
80
+ rawThemeList = Object.keys(builtInThemes);
81
+ } else if (["false", "none", "no"].includes(configThemes.trim())) {
82
+ // Explicitly disabled themes
83
+ rawThemeList = [];
84
+ } else {
85
+ rawThemeList = configThemes.split(",");
86
+ }
87
+ } else if (Array.isArray(configThemes)) {
88
+ rawThemeList = configThemes;
89
+ } else {
90
+ // Default behavior: NO themes loaded automatically.
91
+ // User must specify themes to load them.
92
+ rawThemeList = [];
93
+ }
94
+
95
+ // 3. Process the list and look for flags (--default, --prefersdark)
96
+ rawThemeList.forEach((rawItem) => {
97
+ let themeName = rawItem.trim();
98
+
99
+ // Ignore empty entries
100
+ if (!themeName) return;
101
+
102
+ // Check for --default flag
103
+ if (themeName.includes("--default")) {
104
+ themeName = themeName.replace("--default", "").trim();
105
+ defaultThemeName = themeName;
106
+ }
107
+
108
+ // Check for --prefersdark flag
109
+ if (themeName.toLowerCase().includes("--prefersdark")) {
110
+ themeName = themeName.replace(/--prefersdark/i, "").trim();
111
+ prefersDarkTheme = themeName;
112
+ }
113
+
114
+ // Check if theme exists in the database
115
+ if (builtInThemes[themeName]) {
116
+ themesToInclude.push(themeName);
117
+ }
118
+ });
119
+
120
+ // 4. Auto-configure defaults for "themes: all"
121
+ if (isAllThemes) {
122
+ if (!defaultThemeName) {
123
+ if (themesToInclude.includes("light")) {
124
+ defaultThemeName = "light";
125
+ } else if (themesToInclude.length > 0) {
126
+ defaultThemeName = themesToInclude[0];
127
+ }
128
+ }
129
+ if (!prefersDarkTheme && themesToInclude.includes("dark")) {
130
+ prefersDarkTheme = "dark";
131
+ }
132
+ }
133
+
134
+ // LOGGING: Built-in themes summary
135
+ if (showLogs) {
136
+ const explicitlyDisabled = ["false", "no", "none"].includes(
137
+ String(options.themes).trim(),
138
+ );
139
+
140
+ if (themesToInclude.length > 0) {
141
+ const themesList = themesToInclude.map((theme) => {
142
+ const flags = [];
143
+ if (theme === defaultThemeName) flags.push("default");
144
+ if (theme === prefersDarkTheme) flags.push("prefersdark");
145
+
146
+ const flagStr = flags.length > 0 ? ` (${flags.join(", ")})` : "";
147
+ return `${theme}${flagStr}`;
148
+ });
149
+
150
+ console.log(
151
+ `🍀 Clampography: Loaded ${themesToInclude.length} built-in themes: ${
152
+ themesList.join(", ")
153
+ }`,
154
+ );
155
+ } else if (!explicitlyDisabled) {
156
+ console.info("ℹ️ Clampography: No built-in themes loaded.");
157
+ }
158
+ }
159
+
160
+ // Final check before generating CSS
161
+ if (
162
+ themesToInclude.length === 0 && !defaultThemeName && !prefersDarkTheme
163
+ ) return;
164
+
165
+ // 5. Generate CSS
166
+ const themeStyles = {};
167
+
168
+ // A. Default theme - uses :where() for lower specificity
169
+ // Helper to combine root and data-theme
170
+ // If root is ":root", we use "html[data-theme...]" for backwards compatibility.
171
+ // If root is custom (e.g. "#morda"), we generate "#morda[data-theme...]"
172
+ const getThemeSelector = (themeName) => {
173
+ if (rootSelector === ":root") {
174
+ return `html[data-theme="${themeName}"], [data-theme="${themeName}"]`;
175
+ }
176
+ // For custom root, attach data-theme directly to it
177
+ // AND allow a nested data-theme inside it (optional, but good for nesting)
178
+ return `${rootSelector}[data-theme="${themeName}"], ${rootSelector} [data-theme="${themeName}"]`;
179
+ };
180
+
181
+ // A. Default theme
182
+ if (defaultThemeName && builtInThemes[defaultThemeName]) {
183
+ // Default variables applied to the root element itself without data-theme attribute
184
+ // uses :where() for lower specificity so it can be overridden
185
+ const defaultSelector = `:where(${rootSelector})`;
186
+
187
+ // Also apply if explicitly selected
188
+ const explicitSelector = getThemeSelector(defaultThemeName);
189
+
190
+ themeStyles[`${defaultSelector}, ${explicitSelector}`] =
191
+ builtInThemes[defaultThemeName];
192
+ }
193
+
194
+ // B. Theme for prefers-color-scheme: dark
195
+ if (prefersDarkTheme && builtInThemes[prefersDarkTheme]) {
196
+ themeStyles["@media (prefers-color-scheme: dark)"] = {
197
+ [rootSelector]: builtInThemes[prefersDarkTheme],
198
+ };
199
+ }
200
+
201
+ // C. All themes available via [data-theme] attribute
202
+ themesToInclude.forEach((themeName) => {
203
+ if (themeName === defaultThemeName) return;
204
+ const selector = getThemeSelector(themeName);
205
+ themeStyles[selector] = builtInThemes[themeName];
206
+ });
207
+
208
+ // D. Override media query with data-theme selectors
209
+ if (prefersDarkTheme) {
210
+ themeStyles["@media (prefers-color-scheme: dark)"] = {
211
+ ...themeStyles["@media (prefers-color-scheme: dark)"],
212
+ };
213
+
214
+ themesToInclude.forEach((themeName) => {
215
+ if (themeName === prefersDarkTheme) return;
216
+ const selector = getThemeSelector(themeName);
217
+
218
+ if (!themeStyles["@media (prefers-color-scheme: dark)"][selector]) {
219
+ themeStyles["@media (prefers-color-scheme: dark)"][selector] =
220
+ builtInThemes[themeName];
221
+ }
222
+ });
223
+ }
224
+
225
+ addBase(themeStyles);
99
226
  };
100
- }
227
+ };
228
+ })(),
229
+ // Theme extension - enables utilities like bg-surface, text-heading, etc.
230
+ (options = {}) => {
231
+ // ✅ Extract prefix option (default: "clampography")
232
+ // This prefix is ONLY used for Tailwind utility classes (e.g., bg-clampography-primary)
233
+ // CSS variables remain unchanged (always --clampography-*)
234
+ const prefixEnabled = resolveBool(options.prefix, true);
235
+ const prefix = prefixEnabled
236
+ ? (typeof options.prefix === "string" &&
237
+ !["false", "no", "none", "true"].includes(options.prefix)
238
+ ? options.prefix
239
+ : "clampography")
240
+ : "";
101
241
 
102
- // C. Scoped styles [data-theme="..."]
103
- themesToInclude.forEach((themeName) => {
104
- themeStyles[`[data-theme="${themeName}"]`] = builtInThemes[themeName];
105
- });
242
+ // Helper to add prefix with separator
243
+ const addPrefix = (name) => prefix ? `${prefix}-${name}` : name;
106
244
 
107
- addBase(themeStyles);
108
- };
109
- });
245
+ return {
246
+ theme: {
247
+ extend: {
248
+ colors: {
249
+ [addPrefix("background")]: "var(--clampography-background)",
250
+ [addPrefix("border")]: "var(--clampography-border)",
251
+ [addPrefix("error")]: "var(--clampography-error)",
252
+ [addPrefix("heading")]: "var(--clampography-heading)",
253
+ [addPrefix("info")]: "var(--clampography-info)",
254
+ [addPrefix("link")]: "var(--clampography-link)",
255
+ [addPrefix("muted")]: "var(--clampography-muted)",
256
+ [addPrefix("primary")]: "var(--clampography-primary)",
257
+ [addPrefix("secondary")]: "var(--clampography-secondary)",
258
+ [addPrefix("success")]: "var(--clampography-success)",
259
+ [addPrefix("surface")]: "var(--clampography-surface)",
260
+ [addPrefix("text")]: "var(--clampography-text)",
261
+ [addPrefix("warning")]: "var(--clampography-warning)",
262
+ },
263
+ },
264
+ },
265
+ };
266
+ },
267
+ );