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 +154 -0
- package/bin/cli.js +299 -0
- package/clipper/clipper.css +416 -0
- package/clipper/components.css +42 -0
- package/clipper/variables.css +156 -0
- package/package.json +38 -0
- package/templates/astro/src/components/ThemeToggle.astro +184 -0
- package/templates/astro/src/layouts/Body.astro +21 -0
- package/templates/astro/src/layouts/Demo.astro +331 -0
- package/templates/astro/src/layouts/Footer.astro +1 -0
- package/templates/astro/src/layouts/Header.astro +1 -0
- package/templates/astro/src/layouts/Html.astro +28 -0
- package/templates/astro/src/layouts/Markdown.astro +43 -0
- package/templates/astro/src/pages/index.astro +8 -0
- package/templates/sveltekit/src/lib/components/ThemeToggle.svelte +176 -0
- package/templates/sveltekit/src/routes/+layout.svelte +64 -0
- package/templates/sveltekit/src/routes/+page.svelte +274 -0
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);
|