clampography 2.0.0-beta.3 → 2.0.0-beta.30

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