clampography 2.0.0-beta.1 → 2.0.0-beta.11

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/convert.js ADDED
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Converts base.css to base.js format with preserved comments
5
+ * https://claude.ai/chat/90dedd98-3e31-4831-a620-c35d6523b836
6
+ */
7
+
8
+ import { readFileSync, writeFileSync } from "fs";
9
+ import { resolve } from "path";
10
+
11
+ // Configuration
12
+ const INPUT_FILE = "base.css";
13
+ const OUTPUT_FILE = "base.js";
14
+
15
+ /**
16
+ * Parse CSS content into structured data
17
+ */
18
+ function parseCSS(css) {
19
+ const result = {};
20
+ const lines = css.split("\n");
21
+ let currentContext = result;
22
+ let contextStack = [result];
23
+ let currentSelector = null;
24
+ let inAtLayer = false;
25
+ let buffer = "";
26
+ let commentBuffer = [];
27
+
28
+ for (let i = 0; i < lines.length; i++) {
29
+ const line = lines[i].trim();
30
+
31
+ // Skip empty lines
32
+ if (!line) {
33
+ if (commentBuffer.length > 0) {
34
+ commentBuffer.push("");
35
+ }
36
+ continue;
37
+ }
38
+
39
+ // Capture comments
40
+ if (line.startsWith("/*")) {
41
+ const commentLines = [line];
42
+ while (i < lines.length && !lines[i].includes("*/")) {
43
+ i++;
44
+ commentLines.push(lines[i].trim());
45
+ }
46
+ commentBuffer.push(...commentLines);
47
+ continue;
48
+ }
49
+
50
+ // Handle @layer
51
+ if (line.startsWith("@layer")) {
52
+ const match = line.match(/@layer\s+(\w+)/);
53
+ if (match) {
54
+ inAtLayer = true;
55
+ currentContext["@layer base"] = {};
56
+ contextStack.push(currentContext["@layer base"]);
57
+ currentContext = currentContext["@layer base"];
58
+ }
59
+ continue;
60
+ }
61
+
62
+ // Handle opening brace (new rule)
63
+ if (line.includes("{") && !line.includes("}")) {
64
+ const selector = line.replace("{", "").trim();
65
+
66
+ // Store comments before selector
67
+ if (commentBuffer.length > 0) {
68
+ currentContext[`__comment_${Object.keys(currentContext).length}`] =
69
+ commentBuffer.join("\n");
70
+ commentBuffer = [];
71
+ }
72
+
73
+ currentContext[selector] = {};
74
+ contextStack.push(currentContext[selector]);
75
+ currentContext = currentContext[selector];
76
+ currentSelector = selector;
77
+ continue;
78
+ }
79
+
80
+ // Handle closing brace
81
+ if (line === "}") {
82
+ contextStack.pop();
83
+ currentContext = contextStack[contextStack.length - 1];
84
+ currentSelector = null;
85
+ continue;
86
+ }
87
+
88
+ // Handle properties
89
+ if (line.includes(":") && currentSelector) {
90
+ let [prop, ...valueParts] = line.split(":");
91
+ let value = valueParts.join(":").trim();
92
+
93
+ // Remove semicolon and inline comments
94
+ const inlineComment = value.match(/\/\*.*?\*\//);
95
+ if (inlineComment) {
96
+ value = value.replace(inlineComment[0], "").trim();
97
+ }
98
+ value = value.replace(";", "").trim();
99
+
100
+ prop = prop.trim();
101
+
102
+ // Store inline comment separately if exists
103
+ if (inlineComment && inlineComment[0]) {
104
+ currentContext[`__inline_comment_${prop}`] = inlineComment[0];
105
+ }
106
+
107
+ currentContext[prop] = value;
108
+ }
109
+ }
110
+
111
+ return result;
112
+ }
113
+
114
+ /**
115
+ * Convert parsed CSS to JS object string
116
+ */
117
+ function toJSObject(obj, indent = 0) {
118
+ const spaces = " ".repeat(indent);
119
+ const lines = [];
120
+
121
+ for (const [key, value] of Object.entries(obj)) {
122
+ // Handle comments
123
+ if (key.startsWith("__comment_")) {
124
+ const comment = value.split("\n").map((line) => `${spaces}${line}`).join(
125
+ "\n",
126
+ );
127
+ lines.push(comment);
128
+ continue;
129
+ }
130
+
131
+ if (key.startsWith("__inline_comment_")) {
132
+ continue; // Skip inline comments in first pass
133
+ }
134
+
135
+ if (typeof value === "object" && !Array.isArray(value)) {
136
+ // It's a nested object (selector)
137
+ lines.push(`${spaces}"${key}": {`);
138
+ lines.push(toJSObject(value, indent + 1));
139
+ lines.push(`${spaces}},`);
140
+ } else {
141
+ // It's a property
142
+ const formattedValue = value.includes('"') || value.includes("'")
143
+ ? `"${value.replace(/"/g, '\\"')}"`
144
+ : `"${value}"`;
145
+
146
+ // Check for inline comment
147
+ const inlineCommentKey = `__inline_comment_${key}`;
148
+ const inlineComment = obj[inlineCommentKey];
149
+ const comment = inlineComment ? ` ${inlineComment}` : "";
150
+
151
+ lines.push(`${spaces}"${key}": ${formattedValue},${comment}`);
152
+ }
153
+ }
154
+
155
+ return lines.join("\n");
156
+ }
157
+
158
+ /**
159
+ * Main conversion function
160
+ */
161
+ function convertCSStoJS(cssContent) {
162
+ const parsed = parseCSS(cssContent);
163
+ const jsContent = `export default {\n${
164
+ toJSObject(parsed["@layer base"] || parsed, 1)
165
+ }\n};\n`;
166
+ return jsContent;
167
+ }
168
+
169
+ // Main execution
170
+ try {
171
+ console.log(`📖 Reading ${INPUT_FILE}...`);
172
+ const cssContent = readFileSync(resolve(INPUT_FILE), "utf-8");
173
+
174
+ console.log("⚙️ Converting CSS to JS...");
175
+ const jsContent = convertCSStoJS(cssContent);
176
+
177
+ console.log(`💾 Writing ${OUTPUT_FILE}...`);
178
+ writeFileSync(resolve(OUTPUT_FILE), jsContent, "utf-8");
179
+
180
+ console.log("✅ Conversion complete!");
181
+ console.log(`📄 Output: ${OUTPUT_FILE}`);
182
+ } catch (error) {
183
+ console.error("❌ Error:", error.message);
184
+ process.exit(1);
185
+ }
package/src/extra.js CHANGED
@@ -1,34 +1,170 @@
1
+ /**
2
+ * Extra opinionated styles and coloring.
3
+ * Applies colors to the structural base elements.
4
+ */
5
+
1
6
  export default {
7
+ // --- Basic Coloring (Applying theme variables) ---
8
+ "body": {
9
+ "background-color": "oklch(var(--clampography-background))",
10
+ "color": "oklch(var(--clampography-text))",
11
+ },
12
+
13
+ ":where(h1, h2, h3, h4, h5, h6)": {
14
+ "color": "oklch(var(--clampography-heading))",
15
+ },
16
+
17
+ "a": {
18
+ "color": "oklch(var(--clampography-link))",
19
+ },
20
+
21
+ // Lists
22
+ "ul > li::before": {
23
+ "background-color": "oklch(var(--clampography-primary))", // Bullet points
24
+ },
25
+
26
+ "ol > li::before": {
27
+ "color": "oklch(var(--clampography-secondary))", // Numbers
28
+ },
29
+
30
+ // Inline Code
31
+ ":where(code, kbd, samp)": {
32
+ "background-color": "oklch(var(--clampography-surface))",
33
+ "color": "oklch(var(--clampography-heading))",
34
+ "border": "1px solid oklch(var(--clampography-border))",
35
+ "border-radius": "0.25rem",
36
+ "padding": "0.125rem var(--spacing-xs)",
37
+ },
38
+
39
+ // Keyboard input - vertical alignment
40
+ "kbd": {
41
+ transform: "translateY(-0.15em)",
42
+ },
43
+
44
+ // Preformatted Code Blocks
45
+ "pre": {
46
+ "background-color": "oklch(var(--clampography-surface))",
47
+ "border": "1px solid oklch(var(--clampography-border))",
48
+ "border-radius": "0.375rem",
49
+ "padding": "1rem",
50
+ },
51
+
52
+ // Tables
53
+ "table": {
54
+ "padding": "var(--spacing-sm)",
55
+ "border": "1px solid oklch(var(--clampography-border))",
56
+ },
57
+
58
+ "th": {
59
+ "color": "oklch(var(--clampography-heading))",
60
+ },
61
+
62
+ "th, td": {
63
+ "border": "1px solid oklch(var(--clampography-border))",
64
+ },
65
+
66
+ "thead th": {
67
+ "border-bottom-width": "2px",
68
+ },
69
+
70
+ // Zebra striping for table rows
71
+ "tbody tr:nth-child(even)": {
72
+ "background-color": "oklch(var(--clampography-surface))",
73
+ },
74
+
75
+ // Captions & Muted text
76
+ "caption, figcaption, .muted": {
77
+ "color": "oklch(var(--clampography-muted))",
78
+ },
79
+
80
+ // Horizontal Rule (Thematic)
2
81
  "hr": {
3
82
  "height": "1px",
4
83
  "border-width": "0",
5
- "margin-top": "3rem", /* my-12 (12 * 0.25rem) */
6
- "margin-bottom": "3rem", /* my-12 */
7
- "background-color": "var(--clampography-secondary)",
84
+ "margin-top": "3rem",
85
+ "margin-bottom": "3rem",
86
+ "background-color": "oklch(var(--clampography-border))",
8
87
  },
9
88
 
89
+ // --- Opinionated Extras ---
90
+
91
+ // Styled Blockquote
10
92
  "blockquote": {
11
93
  "border-left-width": "4px",
12
- "border-color": "var(--clampography-primary)",
13
- "padding-left": "1rem", /* pl-4 */
14
- "padding-top": "0.5rem", /* py-2 */
15
- "padding-bottom": "0.5rem", /* py-2 */
16
- "padding-right": "0.5rem", /* pr-2 */
94
+ "border-left-color": "oklch(var(--clampography-primary))",
95
+ "background-color": "oklch(var(--clampography-surface))",
96
+ "padding": "1rem",
97
+ "border-radius": "0.25rem",
98
+ "font-style": "italic",
99
+ "color": "oklch(var(--clampography-heading))",
17
100
  },
18
101
 
102
+ // Styled Links (Enhanced)
19
103
  "a": {
20
- "color": "var(--clampography-link)",
21
- "font-weight": "700", /* font-bold */
22
- "letter-spacing": "0.025em", /* tracking-wide */
104
+ "font-weight": "700",
105
+ "letter-spacing": "0.025em",
23
106
  "text-decoration-line": "underline",
24
- "text-decoration-thickness": "2px", /* decoration-2 */
25
- "text-underline-offset": "4px", /* underline-offset-4 */
107
+ "text-decoration-thickness": "2px",
108
+ "text-underline-offset": "4px",
26
109
  "transition-property": "color, text-decoration-color",
27
- "transition-duration": "150ms", /* duration-150 */
110
+ "transition-duration": "150ms",
28
111
  },
29
112
 
30
113
  "a:hover": {
31
- // Używamy zmiennej zdefiniowanej w themes.js (bez prefixu --color-)
32
- "text-decoration-color": "var(--clampography-primary)",
114
+ "text-decoration-color": "oklch(var(--clampography-primary))",
115
+ },
116
+
117
+ // Mark
118
+ "mark": {
119
+ "background-color": "oklch(var(--clampography-primary))",
120
+ "color": "oklch(var(--clampography-background))",
121
+ "padding": "0.125rem var(--spacing-xs)",
122
+ "border-radius": "0.25rem",
123
+ },
124
+
125
+ // Deleted Text
126
+ "del": {
127
+ "text-decoration-color": "oklch(var(--clampography-secondary))",
128
+ "text-decoration-thickness": "2px",
129
+ },
130
+
131
+ // Buttons - All types
132
+ ":where(button, [type='button'], [type='reset'], [type='submit'])": {
133
+ "padding": "var(--spacing-xs) var(--spacing-sm)",
134
+ "border": "1px solid oklch(var(--clampography-border))",
135
+ "border-radius": "0.375rem", // ← Rounded corners
136
+ },
137
+
138
+ // Inputs - All types
139
+ ":where(input:not([type='checkbox'], [type='radio']), textarea, select)": {
140
+ "padding": "var(--spacing-xs) var(--spacing-sm)",
141
+ "border": "1px solid oklch(var(--clampography-border))",
142
+ "border-radius": "0.375rem", // ← Rounded corners
143
+ },
144
+
145
+ // Fieldset
146
+ "fieldset": {
147
+ "border": "1px solid oklch(var(--clampography-border))",
148
+ "border-radius": "0.375rem",
149
+ },
150
+
151
+ "legend": {
152
+ "color": "oklch(var(--clampography-heading))",
153
+ },
154
+
155
+ // Details
156
+ "details": {
157
+ "border": "1px solid oklch(var(--clampography-border))",
158
+ "border-radius": "0.375rem",
159
+ "padding": "0.5rem",
160
+ },
161
+
162
+ "summary": {
163
+ "color": "oklch(var(--clampography-heading))",
164
+ },
165
+
166
+ "details[open] > summary": {
167
+ "border-bottom": "1px solid oklch(var(--clampography-border))",
168
+ "padding-bottom": "0.5rem",
33
169
  },
34
170
  };
package/src/index.js CHANGED
@@ -3,99 +3,133 @@ import { themes as builtInThemes } from "./themes.js";
3
3
  import baseStyles from "./base.js";
4
4
  import extraStyles from "./extra.js";
5
5
 
6
+ /**
7
+ * Helper to resolve boolean options from CSS configuration.
8
+ * CSS values often come as strings ("true"/"false"), which are both truthy in JS.
9
+ */
10
+ const resolveBool = (value, defaultValue) => {
11
+ if (value === "false" || value === false) return false;
12
+ if (value === "true" || value === true) return true;
13
+ return defaultValue;
14
+ };
15
+
6
16
  /**
7
17
  * Main plugin function.
8
18
  */
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 = [];
19
+ export default plugin.withOptions(
20
+ (options = {}) => {
21
+ return ({ addBase }) => {
22
+ // 1. Load Base and Extra styles
23
+ // We use the helper to correctly parse "false" string from CSS
24
+ const includeBase = resolveBool(options.base, true); // Default: true
25
+ const includeExtra = resolveBool(options.extra, false); // Default: false
26
+
27
+ includeBase && addBase(baseStyles);
28
+ includeExtra && addBase(extraStyles);
29
+
30
+ // 2. Parse themes configuration
31
+ let configThemes = options.themes;
32
+ let themesToInclude = [];
33
+ let defaultThemeName = null;
34
+ let prefersDarkTheme = false;
35
+ let rootSelector = options.root ?? ":root";
36
+
37
+ // Normalize input to an array of strings
38
+ let rawThemeList = [];
39
+
40
+ if (typeof configThemes === "string") {
41
+ if (["all", "true", "yes"].includes(configThemes.trim())) {
42
+ // Special case: themes: all
43
+ rawThemeList = Object.keys(builtInThemes);
44
+ } else if (["false", "none", "no"].includes(configThemes.trim())) {
45
+ // Explicitly disabled themes
46
+ rawThemeList = [];
47
+ } else {
48
+ rawThemeList = configThemes.split(",");
49
+ }
50
+ } else if (Array.isArray(configThemes)) {
51
+ rawThemeList = configThemes;
37
52
  } 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;
53
+ // Default behavior: NO themes loaded automatically.
54
+ // User must specify themes to load them.
55
+ rawThemeList = [];
62
56
  }
63
57
 
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;
58
+ // 3. Process the list and look for flags (--default, --prefersdark)
59
+ rawThemeList.forEach((rawItem) => {
60
+ let themeName = rawItem.trim();
61
+
62
+ // Ignore empty entries
63
+ if (!themeName) return;
64
+
65
+ // Check for --default flag
66
+ if (themeName.includes("--default")) {
67
+ themeName = themeName.replace("--default", "").trim();
68
+ defaultThemeName = themeName;
69
+ }
70
+
71
+ // Check for --prefersdark flag
72
+ if (themeName.toLowerCase().includes("--prefersdark")) {
73
+ themeName = themeName.replace(/--prefersdark/i, "").trim();
74
+ prefersDarkTheme = themeName;
75
+ }
76
+
77
+ // Check if theme exists in the database
78
+ if (builtInThemes[themeName]) {
79
+ themesToInclude.push(themeName);
80
+ }
81
+ });
82
+
83
+ // If list is empty after filtering, stop here
84
+ if (
85
+ themesToInclude.length === 0 && !defaultThemeName && !prefersDarkTheme
86
+ ) return;
87
+
88
+ // 4. Generate CSS
89
+ const themeStyles = {};
90
+
91
+ // A. Default theme (:root)
92
+ if (defaultThemeName && builtInThemes[defaultThemeName]) {
93
+ themeStyles[rootSelector] = builtInThemes[defaultThemeName];
68
94
  }
69
95
 
70
- // Check if theme exists in the database
71
- if (builtInThemes[themeName]) {
72
- themesToInclude.push(themeName);
96
+ // B. Theme for prefers-color-scheme: dark
97
+ if (prefersDarkTheme && builtInThemes[prefersDarkTheme]) {
98
+ themeStyles["@media (prefers-color-scheme: dark)"] = {
99
+ [rootSelector]: builtInThemes[prefersDarkTheme],
100
+ };
73
101
  }
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],
91
- };
92
- }
93
-
94
- // C. Scoped styles [data-theme="..."]
95
- themesToInclude.forEach((themeName) => {
96
- themeStyles[`[data-theme="${themeName}"]`] = builtInThemes[themeName];
97
- });
98
-
99
- addBase(themeStyles);
100
- };
101
- });
102
+
103
+ // C. Scoped styles [data-theme="..."]
104
+ themesToInclude.forEach((themeName) => {
105
+ themeStyles[`[data-theme="${themeName}"]`] = builtInThemes[themeName];
106
+ });
107
+
108
+ addBase(themeStyles);
109
+ };
110
+ },
111
+ // Theme extension - enables utilities like bg-surface, text-heading, etc.
112
+ (options = {}) => {
113
+ return {
114
+ theme: {
115
+ extend: {
116
+ colors: {
117
+ background: "oklch(var(--clampography-background) / <alpha-value>)",
118
+ border: "oklch(var(--clampography-border) / <alpha-value>)",
119
+ error: "oklch(var(--clampography-error) / <alpha-value>)",
120
+ heading: "oklch(var(--clampography-heading) / <alpha-value>)",
121
+ info: "oklch(var(--clampography-info) / <alpha-value>)",
122
+ link: "oklch(var(--clampography-link) / <alpha-value>)",
123
+ muted: "oklch(var(--clampography-muted) / <alpha-value>)",
124
+ primary: "oklch(var(--clampography-primary) / <alpha-value>)",
125
+ secondary: "oklch(var(--clampography-secondary) / <alpha-value>)",
126
+ success: "oklch(var(--clampography-success) / <alpha-value>)",
127
+ surface: "oklch(var(--clampography-surface) / <alpha-value>)",
128
+ text: "oklch(var(--clampography-text) / <alpha-value>)",
129
+ warning: "oklch(var(--clampography-warning) / <alpha-value>)",
130
+ },
131
+ },
132
+ },
133
+ };
134
+ },
135
+ );