clampography 2.0.0-beta.8 → 2.0.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.
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,267 @@ 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
+ // Import version from package.json
10
+ import { version } from "../package.json" with { type: "json" };
5
11
 
6
12
  /**
7
13
  * Helper to resolve boolean options from CSS configuration.
8
14
  * CSS values often come as strings ("true"/"false"), which are both truthy in JS.
9
15
  */
10
16
  const resolveBool = (value, defaultValue) => {
11
- if (value === "false" || value === false) return false;
12
- if (value === "true" || value === true) return true;
17
+ if (
18
+ value === "false" || value === false || value === "no" || value === "none"
19
+ ) return false;
20
+ if (value === "true" || value === true || value === "yes") return true;
13
21
  return defaultValue;
14
22
  };
15
23
 
16
24
  /**
17
25
  * Main plugin function.
18
26
  */
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],
27
+ export default plugin.withOptions(
28
+ (() => {
29
+ let firstRun = true; // Track first run for logging
30
+
31
+ return (options = {}) => {
32
+ return ({ addBase }) => {
33
+ // Extract logs option (default: true)
34
+ const showLogs = resolveBool(options.logs, true);
35
+
36
+ // Show startup log only once
37
+ if (showLogs && firstRun) {
38
+ console.log(`🍀 Clampography v${version} loaded successfully`);
39
+ firstRun = false;
40
+ }
41
+
42
+ // 1. Load Base and Extra styles
43
+ // We use the helper to correctly parse "false" string from CSS
44
+ const includeBase = resolveBool(options.base, true); // Default: true
45
+ const includeExtra = resolveBool(options.extra, false); // Default: false
46
+ const includeForms = resolveBool(options.forms, false); // Default: false
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 scaleMode: 'viewport' (default, uses vw) or 'container' (uses cqi)
55
+ const scaleMode = options["scale-mode"] || options.scaleMode || "viewport";
56
+
57
+ // Extract typography scope option (default: global)
58
+ const typography = options.typography || "global";
59
+
60
+ // Pass options to the style functions to enable scoping
61
+ includeBase && addBase(baseStyles({ ...options, fluidMin, fluidMax, scaleMode, typography }));
62
+ includeExtra && addBase(extraStyles({ ...options, typography }));
63
+ includeForms && addBase(formsStyles({ ...options, typography }));
64
+ includeKbd && addBase(kbdStyles({ ...options, typography }));
65
+ includePrint && addBase(printStyles({ ...options, typography }));
66
+
67
+ // 2. Parse themes configuration
68
+ let configThemes = options.themes;
69
+ let themesToInclude = [];
70
+ let defaultThemeName = null;
71
+ let prefersDarkTheme = false;
72
+ let rootSelector = options.root || ":root";
73
+ let isAllThemes = false; // Track if user specified "all"
74
+
75
+ // Normalize input to an array of strings
76
+ let rawThemeList = [];
77
+ if (typeof configThemes === "string") {
78
+ if (["all", "true", "yes"].includes(configThemes.trim())) {
79
+ // Special case: themes: all
80
+ isAllThemes = true;
81
+ rawThemeList = Object.keys(builtInThemes);
82
+ } else if (["false", "none", "no"].includes(configThemes.trim())) {
83
+ // Explicitly disabled themes
84
+ rawThemeList = [];
85
+ } else {
86
+ rawThemeList = configThemes.split(",");
87
+ }
88
+ } else if (Array.isArray(configThemes)) {
89
+ rawThemeList = configThemes;
90
+ } else {
91
+ // Default behavior: NO themes loaded automatically.
92
+ // User must specify themes to load them.
93
+ rawThemeList = [];
94
+ }
95
+
96
+ // 3. Process the list and look for flags (--default, --prefersdark)
97
+ rawThemeList.forEach((rawItem) => {
98
+ let themeName = rawItem.trim();
99
+
100
+ // Ignore empty entries
101
+ if (!themeName) return;
102
+
103
+ // Check for --default flag
104
+ if (themeName.includes("--default")) {
105
+ themeName = themeName.replace("--default", "").trim();
106
+ defaultThemeName = themeName;
107
+ }
108
+
109
+ // Check for --prefersdark flag
110
+ if (themeName.toLowerCase().includes("--prefersdark")) {
111
+ themeName = themeName.replace(/--prefersdark/i, "").trim();
112
+ prefersDarkTheme = themeName;
113
+ }
114
+
115
+ // Check if theme exists in the database
116
+ if (builtInThemes[themeName]) {
117
+ themesToInclude.push(themeName);
118
+ }
119
+ });
120
+
121
+ // 4. Auto-configure defaults for "themes: all"
122
+ if (isAllThemes) {
123
+ if (!defaultThemeName) {
124
+ if (themesToInclude.includes("light")) {
125
+ defaultThemeName = "light";
126
+ } else if (themesToInclude.length > 0) {
127
+ defaultThemeName = themesToInclude[0];
128
+ }
129
+ }
130
+ if (!prefersDarkTheme && themesToInclude.includes("dark")) {
131
+ prefersDarkTheme = "dark";
132
+ }
133
+ }
134
+
135
+ // LOGGING: Built-in themes summary
136
+ if (showLogs) {
137
+ const explicitlyDisabled = ["false", "no", "none"].includes(
138
+ String(options.themes).trim(),
139
+ );
140
+
141
+ if (themesToInclude.length > 0) {
142
+ const themesList = themesToInclude.map((theme) => {
143
+ const flags = [];
144
+ if (theme === defaultThemeName) flags.push("default");
145
+ if (theme === prefersDarkTheme) flags.push("prefersdark");
146
+
147
+ const flagStr = flags.length > 0 ? ` (${flags.join(", ")})` : "";
148
+ return `${theme}${flagStr}`;
149
+ });
150
+
151
+ console.log(
152
+ `🍀 Clampography: Loaded ${themesToInclude.length} built-in themes: ${
153
+ themesList.join(", ")
154
+ }`,
155
+ );
156
+ } else if (!explicitlyDisabled) {
157
+ console.info("ℹ️ Clampography: No built-in themes loaded.");
158
+ }
159
+ }
160
+
161
+ // Final check before generating CSS
162
+ if (
163
+ themesToInclude.length === 0 && !defaultThemeName && !prefersDarkTheme
164
+ ) return;
165
+
166
+ // 5. Generate CSS
167
+ const themeStyles = {};
168
+
169
+ // A. Default theme - uses :where() for lower specificity
170
+ // Helper to combine root and data-theme
171
+ // If root is ":root", we use "html[data-theme...]" for backwards compatibility.
172
+ // If root is custom (e.g. "#morda"), we generate "#morda[data-theme...]"
173
+ const getThemeSelector = (themeName) => {
174
+ if (rootSelector === ":root") {
175
+ return `html[data-theme="${themeName}"], [data-theme="${themeName}"]`;
176
+ }
177
+ // For custom root, attach data-theme directly to it
178
+ // AND allow a nested data-theme inside it (optional, but good for nesting)
179
+ return `${rootSelector}[data-theme="${themeName}"], ${rootSelector} [data-theme="${themeName}"]`;
180
+ };
181
+
182
+ // A. Default theme
183
+ if (defaultThemeName && builtInThemes[defaultThemeName]) {
184
+ // Default variables applied to the root element itself without data-theme attribute
185
+ // uses :where() for lower specificity so it can be overridden
186
+ const defaultSelector = `:where(${rootSelector})`;
187
+
188
+ // Also apply if explicitly selected
189
+ const explicitSelector = getThemeSelector(defaultThemeName);
190
+
191
+ themeStyles[`${defaultSelector}, ${explicitSelector}`] =
192
+ builtInThemes[defaultThemeName];
193
+ }
194
+
195
+ // B. Theme for prefers-color-scheme: dark
196
+ if (prefersDarkTheme && builtInThemes[prefersDarkTheme]) {
197
+ themeStyles["@media (prefers-color-scheme: dark)"] = {
198
+ [rootSelector]: builtInThemes[prefersDarkTheme],
199
+ };
200
+ }
201
+
202
+ // C. All themes available via [data-theme] attribute
203
+ themesToInclude.forEach((themeName) => {
204
+ if (themeName === defaultThemeName) return;
205
+ const selector = getThemeSelector(themeName);
206
+ themeStyles[selector] = builtInThemes[themeName];
207
+ });
208
+
209
+ // D. Override media query with data-theme selectors
210
+ if (prefersDarkTheme) {
211
+ themeStyles["@media (prefers-color-scheme: dark)"] = {
212
+ ...themeStyles["@media (prefers-color-scheme: dark)"],
213
+ };
214
+
215
+ themesToInclude.forEach((themeName) => {
216
+ if (themeName === prefersDarkTheme) return;
217
+ const selector = getThemeSelector(themeName);
218
+
219
+ if (!themeStyles["@media (prefers-color-scheme: dark)"][selector]) {
220
+ themeStyles["@media (prefers-color-scheme: dark)"][selector] =
221
+ builtInThemes[themeName];
222
+ }
223
+ });
224
+ }
225
+
226
+ addBase(themeStyles);
99
227
  };
100
- }
228
+ };
229
+ })(),
230
+ // Theme extension - enables utilities like bg-surface, text-heading, etc.
231
+ (options = {}) => {
232
+ // ✅ Extract prefix option (default: "clampography")
233
+ // This prefix is ONLY used for Tailwind utility classes (e.g., bg-clampography-primary)
234
+ // CSS variables remain unchanged (always --clampography-*)
235
+ const prefixEnabled = resolveBool(options.prefix, true);
236
+ const prefix = prefixEnabled
237
+ ? (typeof options.prefix === "string" &&
238
+ !["false", "no", "none", "true"].includes(options.prefix)
239
+ ? options.prefix
240
+ : "clampography")
241
+ : "";
101
242
 
102
- // C. Scoped styles [data-theme="..."]
103
- themesToInclude.forEach((themeName) => {
104
- themeStyles[`[data-theme="${themeName}"]`] = builtInThemes[themeName];
105
- });
243
+ // Helper to add prefix with separator
244
+ const addPrefix = (name) => prefix ? `${prefix}-${name}` : name;
106
245
 
107
- addBase(themeStyles);
108
- };
109
- });
246
+ return {
247
+ theme: {
248
+ extend: {
249
+ colors: {
250
+ [addPrefix("background")]: "var(--clampography-background)",
251
+ [addPrefix("border")]: "var(--clampography-border)",
252
+ [addPrefix("error")]: "var(--clampography-error)",
253
+ [addPrefix("heading")]: "var(--clampography-heading)",
254
+ [addPrefix("info")]: "var(--clampography-info)",
255
+ [addPrefix("link")]: "var(--clampography-link)",
256
+ [addPrefix("muted")]: "var(--clampography-muted)",
257
+ [addPrefix("primary")]: "var(--clampography-primary)",
258
+ [addPrefix("secondary")]: "var(--clampography-secondary)",
259
+ [addPrefix("success")]: "var(--clampography-success)",
260
+ [addPrefix("surface")]: "var(--clampography-surface)",
261
+ [addPrefix("text")]: "var(--clampography-text)",
262
+ [addPrefix("warning")]: "var(--clampography-warning)",
263
+ },
264
+ },
265
+ },
266
+ };
267
+ },
268
+ );