clipper-css 0.1.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/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # Clipper
2
+
3
+ Clipper is a simple Tailwind framework for building pages fast without fighting CSS. It is designed for designers and developers alike: semantic markup by default, token-driven styling, and just enough utilities to stay productive.
4
+
5
+ You can start with clean HTML and only add utilities when they actually help.
6
+
7
+ ## Quick start
8
+
9
+ The best way to install clipper is to run it in a freshly installed framework project with [Tailwind](https://tailwindcss.com/) installed. Clipper currently supports **Astro** and **SvelteKit**.
10
+
11
+ ### Astro
12
+
13
+ - [How to install Astro](https://docs.astro.build/en/guides/styling/)
14
+ - [How to install Tailwind for Astro](https://docs.astro.build/en/guides/styling/#tailwind)
15
+
16
+ ### SvelteKit
17
+
18
+ - [How to install SvelteKit](https://svelte.dev/docs/kit/creating-a-project)
19
+ - [How to install Tailwind for SvelteKit](https://svelte.dev/docs/cli/tailwind)
20
+
21
+ After installation, run this in your project folder:
22
+
23
+ ```sh
24
+ npx clipper-css
25
+ ```
26
+
27
+ The installation is user-friendly and won't overwrite anything without your permission. You can run it multiple times to update clipper to the latest version (will only overwrite `clipper.css` in that case).
28
+
29
+ After installing, the root page will display a demo of Clipper's features.
30
+
31
+ ## Core idea in one example
32
+
33
+ The section is the fundamental building block. Put these directly below `main`. The rest is pretty much self-explanatory.
34
+
35
+ ```html
36
+ <body>
37
+ <header class="header-sticky"></header>
38
+ <main>
39
+ <section id="intro">
40
+ <h1>Hello Clipper</h1>
41
+ <p class="readable">Start semantic, then add only the few utilities you really need.</p>
42
+ <div class="row">
43
+ <a href="/primary" class="btn">Primary action</a>
44
+ <a href="/secondary" class="btn btn-outline">Secondary action</a>
45
+ </div>
46
+ </section>
47
+ </main>
48
+ <footer></footer>
49
+ </body>
50
+ ```
51
+
52
+ ## Spacing (the fluent part)
53
+
54
+ Spacing is tokenized and fluid via `clamp()`. Use Clipper spacing utilities between `4xs` to `4xl` as normal tailwind classes. `base` is in the middle.
55
+
56
+ Example:
57
+
58
+ ```html
59
+ <div class="gap-sm">
60
+ <span>First item</span>
61
+ <span>Second item</span>
62
+ <span>Third item</span>
63
+ </div>
64
+ ```
65
+
66
+ Change spacing tokens in `variables.css` and rhythm updates everywhere.
67
+
68
+ ## Colors
69
+
70
+ Colors are also tokenized in `variables.css`, with semantic tokens so theme decisions stay centralized and dark mode works properly. Built-in tokens that can be used directly on the utility classes:
71
+
72
+ ### Base colors
73
+
74
+ ```
75
+ background
76
+ foreground
77
+ accent
78
+ accent-foreground
79
+ muted
80
+ muted-foreground
81
+ ```
82
+
83
+ ### Primary color
84
+
85
+ ```
86
+ primary (incl. 50-900)
87
+ primary-foreground
88
+ primary-hover
89
+ primary-muted
90
+ ```
91
+
92
+ ### Other
93
+
94
+ ```
95
+ link
96
+ link-hover
97
+ link-underline
98
+ link-underline-hover
99
+ border
100
+ ```
101
+
102
+ Example:
103
+
104
+ ```html
105
+ <span class="bg-accent-foreground">First item</span>
106
+ ```
107
+
108
+ ## Typography
109
+
110
+ Headings are semantic first (`h1`..`h5`).
111
+ If a heading needs a different visual size, apply the display class directly:
112
+
113
+ ```astro
114
+ <h3 class="h2">Semantically h3, visually h2</h3>
115
+ ```
116
+
117
+ Body text stays stable while header sizes (and spacing) scale fluidly.
118
+
119
+ ## List of utility classes
120
+
121
+ | Class name | Function |
122
+ | --------------- | --------------------------------------------- |
123
+ | `row` | Flex-row with sensible defaults |
124
+ | `readable` | Max-width for readable text |
125
+ | `full-width` | To break section children out of `page-width` |
126
+ | `page-width` | To restore `page-width` to inner content |
127
+ | `header-sticky` | Simple sticky header |
128
+
129
+ ## List of components
130
+
131
+ Clipper includes three generic reusable primitives, compatible with dark mode, purely for "getting started" convenience. They can be replaced by any UI framework or custom styles.
132
+
133
+ | Class name | Function |
134
+ | ----------------- | -------------------------------------- |
135
+ | `btn` | You guessed it! |
136
+ | `card` | You guessed that too |
137
+ | `badge` | You guessed right three times in a row |
138
+ | `btn btn-outline` | Outline button version |
139
+
140
+ ## Where to edit what
141
+
142
+ - `variables.css` → tokens (color, type, spacing)
143
+ - `components.css` → reusable components (`.btn`, `.card`, `.badge`)
144
+ - `clipper.css` → framework definitions - usually no need to change this file! Will be updated if `npx clipper-css` is run multiple times.
145
+
146
+ ## Design philosophy
147
+
148
+ Clipper is intentionally small and unobtrusive. Use Tailwind classes or a UI component framework whenever you need, Clipper won't stand in your way.
149
+
150
+ > If you can express it semantically, do that first. If you need control, use tokens/utilities. If it repeats, make it a component.
151
+
152
+ ## Get in touch
153
+
154
+ Please suggest fixes etc on Github. Improvements can surely be made.
package/bin/cli.js ADDED
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { globby } from "globby";
7
+ import prompts from "prompts";
8
+
9
+ // Use path helpers for ES modules
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ // ANSI color codes for terminal output
13
+ const colors = {
14
+ reset: "\x1b[0m",
15
+ bright: "\x1b[1m",
16
+ cyan: "\x1b[36m",
17
+ green: "\x1b[32m",
18
+ yellow: "\x1b[33m",
19
+ red: "\x1b[31m",
20
+ gray: "\x1b[90m",
21
+ };
22
+
23
+ const c = colors; // shorthand
24
+
25
+ // Helper to automatically reset colors after logging
26
+ function log(message, color = "") {
27
+ // Replace any internal ._reset patterns with nothing since we'll reset at the end
28
+ const cleaned = message.replace(/\x1b\[0m/g, "");
29
+ console.log(`${color}${cleaned}${c.reset}`);
30
+ }
31
+
32
+ async function main() {
33
+ const cwd = process.cwd();
34
+ const autoYes = process.argv.includes("-y") || process.argv.includes("--yes");
35
+
36
+ // Print header
37
+ log(`\n ✨ Clipper CSS Installer ✨\n`, `${c.cyan}${c.bright}`);
38
+
39
+ // Detect dev mode: if clipper/ source directory exists relative to bin/, we're in development
40
+ const devMode = await exists(path.resolve(__dirname, "..", "clipper"));
41
+
42
+ // 1. Detect project type
43
+ let type = null;
44
+
45
+ if ((await exists(path.join(cwd, "astro.config.mjs"))) || (await exists(path.join(cwd, "astro.config.ts")))) {
46
+ type = "astro";
47
+ } else if ((await exists(path.join(cwd, "svelte.config.js"))) || (await exists(path.join(cwd, "svelte.config.ts")))) {
48
+ type = "sveltekit";
49
+ } else if ((await exists(path.join(cwd, "next.config.js"))) || (await exists(path.join(cwd, "next.config.mjs")))) {
50
+ type = "next";
51
+ }
52
+
53
+ if (!type) {
54
+ log(`❌ No supported framework detected\n Supported: Astro, SvelteKit, Next.js`, `${c.red}${c.bright}`);
55
+ log(` Run this command at the root of a supported project.`, c.gray);
56
+ process.exit(1);
57
+ }
58
+
59
+ log(`✓ Detected project type: ${c.bright}${type}${c.reset}`, c.green);
60
+
61
+ // 2. Configuration for frameworks
62
+ const config = {
63
+ astro: {
64
+ clipperDest: "src/styles",
65
+ templateSrc: "astro",
66
+ },
67
+ sveltekit: {
68
+ clipperDest: "src/lib/clipper",
69
+ templateSrc: "sveltekit",
70
+ },
71
+ next: {
72
+ clipperDest: "src/clipper",
73
+ templateSrc: "next",
74
+ },
75
+ };
76
+
77
+ const selectedConfig = config[type];
78
+ const clipperSourceDir = path.resolve(__dirname, "..", "clipper");
79
+ const templatesDir = path.resolve(__dirname, "..", "templates");
80
+ const templateSourceDir = path.join(templatesDir, selectedConfig.templateSrc);
81
+
82
+ // 3. Scan for existing clipper.css
83
+ // Using globby with gitignore support to avoid manual ignore lists
84
+ // In dev mode, only ignore node_modules; in production, respect .gitignore
85
+ const existingClipperFiles = await globby("**/clipper.css", {
86
+ cwd,
87
+ ...(devMode ? { ignore: ["node_modules/**"] } : { gitignore: true }),
88
+ });
89
+
90
+ const existingClipperPath = existingClipperFiles.length > 0 ? existingClipperFiles[0] : null;
91
+
92
+ if (existingClipperPath) {
93
+ // --- FLOW 2: FOUND ---
94
+ log(`\n⚠️ Found existing configuration\n ${existingClipperPath}`, c.yellow);
95
+
96
+ const newClipperCssPath = path.join(clipperSourceDir, "clipper.css");
97
+
98
+ let oldContent = "";
99
+ try {
100
+ oldContent = await fs.readFile(path.join(cwd, existingClipperPath), "utf-8");
101
+ } catch (e) {
102
+ log(`Could not read existing file`, c.red);
103
+ }
104
+
105
+ const newContent = await fs.readFile(newClipperCssPath, "utf-8");
106
+
107
+ const oldVersion = parseVersion(oldContent);
108
+ const newVersion = parseVersion(newContent);
109
+ const isNewer = oldVersion && newVersion && oldVersion < newVersion;
110
+
111
+ log(` Current version: ${oldVersion || "unknown"}`);
112
+ log(` New version: ${newVersion || "unknown"}`);
113
+
114
+ let overwrite = isNewer;
115
+
116
+ if (isNewer && !autoYes) {
117
+ const response = await prompts({
118
+ type: "confirm",
119
+ name: "overwrite",
120
+ message: "Do you want to overwrite clipper.css with the latest version?",
121
+ initial: true,
122
+ });
123
+ overwrite = response.overwrite;
124
+ }
125
+
126
+ if (overwrite) {
127
+ await fs.copyFile(newClipperCssPath, path.join(cwd, existingClipperPath));
128
+ log(`✅ Updated clipper.css`, c.green);
129
+ } else {
130
+ log(`Skipping update.`, c.gray);
131
+ }
132
+ } else {
133
+ // --- FLOW 1: NOT FOUND ---
134
+ log(`\n✨ New setup detected`, c.cyan);
135
+
136
+ // Gather files to copy
137
+ const filesToCopy = [];
138
+
139
+ // Core Clipper Files
140
+ if (await exists(clipperSourceDir)) {
141
+ const coreFiles = await globby("**/*", {
142
+ cwd: clipperSourceDir,
143
+ ...(devMode ? { ignore: ["node_modules/**"] } : { gitignore: true }),
144
+ });
145
+ for (const f of coreFiles) {
146
+ filesToCopy.push({
147
+ src: path.join(clipperSourceDir, f),
148
+ dest: path.join(selectedConfig.clipperDest, f),
149
+ });
150
+ }
151
+ }
152
+
153
+ // Framework Template Files
154
+ if (await exists(templateSourceDir)) {
155
+ const templFiles = await globby("**/*", {
156
+ cwd: templateSourceDir,
157
+ ...(devMode ? { ignore: ["node_modules/**"] } : { gitignore: true }),
158
+ });
159
+ for (const f of templFiles) {
160
+ filesToCopy.push({
161
+ src: path.join(templateSourceDir, f),
162
+ dest: f, // relative to root
163
+ });
164
+ }
165
+ }
166
+
167
+ if (filesToCopy.length === 0) {
168
+ log(`No files found to copy.`, c.red);
169
+ process.exit(1);
170
+ }
171
+
172
+ log(`\nThe following files will be created/updated:`, c.gray);
173
+ filesToCopy.forEach((f) => log(` + ${f.dest}`, c.cyan));
174
+
175
+ let proceed = autoYes;
176
+ if (!autoYes) {
177
+ const response = await prompts({
178
+ type: "confirm",
179
+ name: "proceed",
180
+ message: "Proceed with installation?",
181
+ initial: true,
182
+ });
183
+ proceed = response.proceed;
184
+ }
185
+
186
+ if (!proceed) {
187
+ log(`Aborted.`, c.gray);
188
+ process.exit(0);
189
+ }
190
+
191
+ // Perform Copy
192
+ for (const f of filesToCopy) {
193
+ const absDest = path.join(cwd, f.dest);
194
+ await fs.mkdir(path.dirname(absDest), { recursive: true });
195
+ await fs.copyFile(f.src, absDest);
196
+ }
197
+
198
+ log(`✅ Files installed.`, c.green);
199
+
200
+ // Inject @import
201
+ const destDir = selectedConfig.clipperDest;
202
+ await injectImport(cwd, destDir, devMode);
203
+ }
204
+ }
205
+
206
+ // Helpers
207
+
208
+ /**
209
+ * Parses version from CSS file if present (e.g. v1.0.0 in comment blocks)
210
+ * @param {string} content
211
+ */
212
+ function parseVersion(content) {
213
+ const match = content.match(/v([\d\.]+)/);
214
+ return match ? match[1] : null;
215
+ }
216
+
217
+ /**
218
+ * Scans for tailwind imports and injects clipper import
219
+ * @param {string} cwd
220
+ * @param {string} clipperDestRelative
221
+ * @param {boolean} devMode
222
+ */
223
+ async function injectImport(cwd, clipperDestRelative, devMode) {
224
+ // Use .gitignore or custom ignore based on dev mode
225
+ const cssFiles = await globby("**/*.css", {
226
+ cwd,
227
+ ...(devMode ? { ignore: ["node_modules/**"] } : { gitignore: true }),
228
+ });
229
+
230
+ if (cssFiles.length === 0) {
231
+ log(`ℹ️ No CSS files found to inject import.`, c.gray);
232
+ return;
233
+ }
234
+
235
+ let patched = false;
236
+
237
+ for (const file of cssFiles) {
238
+ const absPath = path.join(cwd, file);
239
+ let content = await fs.readFile(absPath, "utf-8");
240
+
241
+ // Regex to match @import "tailwindcss" or 'tailwindcss' or similar
242
+ // Matches: @import "tailwindcss"; OR @import 'tailwindcss'
243
+ const tailwindImportRegex = /@import\s+['"]tailwindcss['"]\s*;?/i;
244
+ const match = content.match(tailwindImportRegex);
245
+
246
+ if (match) {
247
+ // Check if already imported
248
+ if (content.includes("clipper.css")) continue;
249
+
250
+ // Calculate relative path from this css file to the installed clipper.css
251
+ // clipperDestRelative is usually src/clipper
252
+ // file is usually src/app.css
253
+
254
+ const clipperCssAbsPath = path.join(cwd, clipperDestRelative, "clipper.css");
255
+ const cssFileDir = path.dirname(absPath);
256
+
257
+ let relPath = path.relative(cssFileDir, clipperCssAbsPath);
258
+
259
+ // Ensure "./" prefix if it's in the same directory or simple relative path
260
+ if (!relPath.startsWith(".")) {
261
+ relPath = "./" + relPath;
262
+ }
263
+ // Fix windows backslashes
264
+ relPath = relPath.replace(/\\/g, "/");
265
+
266
+ const injection = `\n@import '${relPath}';`;
267
+
268
+ // Insert after the match
269
+ const insertPos = match.index + match[0].length;
270
+ const newContent = content.slice(0, insertPos) + injection + content.slice(insertPos);
271
+
272
+ await fs.writeFile(absPath, newContent, "utf-8");
273
+ log(`✅ Injected import into ${file}`, c.green);
274
+ patched = true;
275
+ break;
276
+ }
277
+ }
278
+
279
+ if (!patched) {
280
+ log(
281
+ `ℹ️ Could not automatically inject CSS import.\n Please import ${clipperDestRelative}/clipper.css manually.`,
282
+ c.gray,
283
+ );
284
+ }
285
+ }
286
+
287
+ /**
288
+ * @param {string} p
289
+ */
290
+ async function exists(p) {
291
+ try {
292
+ await fs.access(p);
293
+ return true;
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+
299
+ main().catch(console.error);