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.
@@ -0,0 +1,176 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { browser } from '$app/environment';
4
+
5
+ // Runes
6
+ let mode = $state<'system' | 'light' | 'dark'>('system');
7
+
8
+ // Constants
9
+ const THEME_KEY = 'clipper-theme';
10
+
11
+ function getSystemMode() {
12
+ if (!browser) return 'light'; // Default for SSR
13
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
14
+ }
15
+
16
+ function applyTheme(newMode: typeof mode) {
17
+ if (!browser) return;
18
+
19
+ const html = document.documentElement;
20
+ const resolvedMode = newMode === 'system' ? getSystemMode() : newMode;
21
+
22
+ // Toggle classes based on resolved mode
23
+ html.classList.toggle('dark', resolvedMode === 'dark');
24
+ html.classList.toggle('light', resolvedMode === 'light');
25
+ }
26
+
27
+ function setMode(newMode: typeof mode) {
28
+ mode = newMode;
29
+ localStorage.setItem(THEME_KEY, newMode);
30
+ }
31
+
32
+ $effect(() => {
33
+ applyTheme(mode);
34
+ });
35
+
36
+ function handleIconClick(event: MouseEvent) {
37
+ const target = event.target as Element;
38
+ const icon = target.closest<HTMLElement>('.theme-icon');
39
+
40
+ if (!icon) return;
41
+
42
+ const newMode = icon.dataset.mode as typeof mode | undefined;
43
+ if (newMode) {
44
+ setMode(newMode);
45
+ }
46
+ }
47
+
48
+ onMount(() => {
49
+ // Recover state from localStorage
50
+ const stored = localStorage.getItem(THEME_KEY);
51
+
52
+ // Handle legacy theme storage
53
+ const legacyTheme = localStorage.getItem('theme');
54
+ if (legacyTheme) {
55
+ localStorage.removeItem('theme');
56
+ if (!stored && ['dark', 'light', 'system'].includes(legacyTheme)) {
57
+ setMode(legacyTheme as typeof mode);
58
+ return;
59
+ }
60
+ }
61
+
62
+ if (stored && ['dark', 'light', 'system'].includes(stored)) {
63
+ mode = stored as typeof mode;
64
+ }
65
+
66
+ // Listener for system preference changes
67
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
68
+ const handleChange = () => {
69
+ if (mode === 'system') {
70
+ applyTheme('system');
71
+ }
72
+ };
73
+
74
+ mediaQuery.addEventListener('change', handleChange);
75
+
76
+ return () => {
77
+ mediaQuery.removeEventListener('change', handleChange);
78
+ };
79
+ });
80
+ </script>
81
+
82
+ <button
83
+ class="theme-toggle"
84
+ aria-label={`Theme mode ${mode}. Click an icon to set theme.`}
85
+ title={`Theme: ${mode} (click icon to set)`}
86
+ onclick={handleIconClick}
87
+ aria-pressed={false}
88
+ >
89
+ <!--
90
+ The original component implemented the logic by checking which icon was clicked.
91
+ We maintain this behavior to match the original structure.
92
+ -->
93
+ <span class="theme-icon" data-mode="light" data-active={mode === 'light'} aria-hidden="true">
94
+ ☀️
95
+ </span>
96
+ <span class="theme-icon" data-mode="system" data-active={mode === 'system'} aria-hidden="true">
97
+ 🖥️
98
+ </span>
99
+ <span class="theme-icon" data-mode="dark" data-active={mode === 'dark'} aria-hidden="true">
100
+ 🌙
101
+ </span>
102
+ </button>
103
+
104
+ <style>
105
+ /* Theme toggle button */
106
+ .theme-toggle {
107
+ display: inline-flex;
108
+ align-items: center;
109
+ gap: 0.25rem;
110
+ background-color: var(--background);
111
+ border: 1px solid var(--border);
112
+ border-radius: 9999px;
113
+ cursor: pointer;
114
+ font-size: 1rem;
115
+ padding: 0.25rem 0.4rem;
116
+ transition:
117
+ opacity 0.2s ease,
118
+ border-color 0.2s ease;
119
+ color: inherit;
120
+ }
121
+
122
+ .theme-toggle:hover {
123
+ opacity: 0.9;
124
+ }
125
+
126
+ .theme-toggle:active {
127
+ opacity: 0.75;
128
+ }
129
+
130
+ .theme-icon {
131
+ align-items: center;
132
+ border-radius: 9999px;
133
+ display: inline-flex;
134
+ height: 1.8rem;
135
+ justify-content: center;
136
+ opacity: 0.55;
137
+ transition:
138
+ opacity 0.2s ease,
139
+ background-color 0.2s ease;
140
+ width: 1.8rem;
141
+ }
142
+
143
+ /* Use data-active attribute selector to match logic */
144
+ .theme-icon[data-active='true'] {
145
+ background-color: var(--muted);
146
+ cursor: default;
147
+ opacity: 1;
148
+ pointer-events: none;
149
+ }
150
+
151
+ /* Use global selector for hover state to match structure */
152
+ .theme-toggle:hover .theme-icon[data-active='false'] {
153
+ opacity: 0.72;
154
+ }
155
+
156
+ .theme-toggle:focus-visible {
157
+ outline: 2px solid var(--primary);
158
+ outline-offset: 2px;
159
+ }
160
+
161
+ .theme-icon {
162
+ font-size: 1em;
163
+ line-height: 1;
164
+ }
165
+
166
+ .theme-toggle:focus-visible .theme-icon[data-active='true'] {
167
+ box-shadow: inset 0 0 0 1px var(--border);
168
+ }
169
+
170
+ @media (prefers-reduced-motion: reduce) {
171
+ .theme-toggle,
172
+ .theme-icon {
173
+ transition: none;
174
+ }
175
+ }
176
+ </style>
@@ -0,0 +1,64 @@
1
+ <script lang="ts">
2
+ import './layout.css';
3
+ import favicon from '$lib/assets/favicon.svg';
4
+ import ThemeToggle from '$lib/components/ThemeToggle.svelte';
5
+
6
+ let { children } = $props();
7
+
8
+ const headerLinks = [
9
+ { href: '#overview', label: 'Overview' },
10
+ { href: '#defaults', label: 'Defaults' },
11
+ { href: '#lean', label: 'Less Classes' },
12
+ { href: '#components', label: 'Components' },
13
+ { href: '#colors', label: 'Colors' },
14
+ { href: '#starter', label: 'Starter' }
15
+ ];
16
+ </script>
17
+
18
+ <svelte:head>
19
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
20
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
21
+ <link
22
+ href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
23
+ rel="stylesheet"
24
+ />
25
+ <link rel="icon" href={favicon} />
26
+ </svelte:head>
27
+
28
+ <header
29
+ id="header"
30
+ class="header-sticky border-b [background:color-mix(in_oklch,var(--background)_88%,transparent)]"
31
+ >
32
+ <div class="row flex-wrap items-center justify-between py-4">
33
+ <a
34
+ href="#header"
35
+ class="font-heading text-2xl font-bold text-foreground no-underline"
36
+ aria-label="Clipper home">Clipper ⛵</a
37
+ >
38
+
39
+ <nav aria-label="Primary" class="order-3 basis-full sm:order-2 sm:basis-auto">
40
+ <ul class="m-0 row list-none flex-wrap p-0 whitespace-nowrap sm:gap-4">
41
+ {#each headerLinks as link (link.href)}
42
+ <li>
43
+ <a href={link.href} rel="external" class="text-muted-foreground no-underline hover:text-foreground">
44
+ {link.label}
45
+ </a>
46
+ </li>
47
+ {/each}
48
+ </ul>
49
+ </nav>
50
+
51
+ <div class="order-2 sm:order-3">
52
+ <ThemeToggle />
53
+ </div>
54
+ </div>
55
+ </header>
56
+ <main>
57
+ {@render children()}
58
+ </main>
59
+ <footer class="border-t bg-foreground py-base text-muted-foreground">
60
+ <div class="row justify-between">
61
+ <p>Built with Clipper primitives and token-driven theme scales.</p>
62
+ <p>Enjoy the simplicity!</p>
63
+ </div>
64
+ </footer>
@@ -0,0 +1,274 @@
1
+ <script lang="ts">
2
+ import variablesCss from "../lib/clipper/variables.css?raw";
3
+
4
+ const cardClass = "min-w-0 rounded-xl border border-border bg-muted p-5";
5
+ const codeBlockClass =
6
+ "min-w-0 max-w-full overflow-x-auto rounded-xl border border-border bg-background p-4 text-sm leading-relaxed text-muted-foreground";
7
+ const swatchClass = "rounded-xl border bg-muted p-3";
8
+ const swatchChipClass =
9
+ "h-14 w-full rounded-md border border-[color-mix(in_oklch,var(--foreground)_16%,transparent)]";
10
+
11
+ const defaultsCards = [
12
+ {
13
+ title: "Sections are layout containers",
14
+ text: "Sections already have page-width grid constraints, vertical padding, and row gaps. Usually an id plus semantic content is enough.",
15
+ code: `<section id="hero">\n <h1>Landing headline</h1>\n <p class="readable">Long copy stays readable without custom CSS.</p>\n</section>`,
16
+ },
17
+ {
18
+ title: "Divs are modern by default",
19
+ text: "In Clipper, div/nav/article/figure are flex-column with tokenized spacing. Add a .row class when you need horizontal layout.",
20
+ code: `<div>\n <h3>No flex classes needed</h3>\n <p>Children stack with preset spacing.</p>\n</div>\n\n<div class="row">\n <a href="#">Primary</a>\n <a href="#">Secondary</a>\n</div>`,
21
+ },
22
+ {
23
+ title: "Typography is predefined",
24
+ text: "Heading scale and heading font is fluent. Body font and text wrapping are configured in tokens + base rules.",
25
+ code: `<h1>Display headline from theme scale</h1>\n<h2>Section title with heading font</h2>\n<p>Paragraph text uses global body font and pretty wrapping.</p>`,
26
+ },
27
+ {
28
+ title: "Anchors are pre-styled",
29
+ text: "Plain <a> tags are styled from tokens in clipper base styles. Underline is controlled by --link-underline and --link-underline-hover.",
30
+ code: `/* variables.css */\n:root {\n --link-underline: none;\n --link-underline-hover: underline;\n}\n\n<p>Read the <a href="#starter">starter snippet</a> and <a href="#colors">token palette</a>.</p>`,
31
+ },
32
+ ];
33
+
34
+ const createSpacingExample = (level: string) => ({
35
+ title: level,
36
+ className: level,
37
+ code: `<div class="${level}">\n <span>Item</span>\n <span>Item</span>\n <span>Item</span>\n</div>`,
38
+ });
39
+
40
+ const spacingLevels = [
41
+ "gap-4xs",
42
+ "gap-3xs",
43
+ "gap-2xs",
44
+ "gap-xs",
45
+ "gap-sm",
46
+ "gap-base",
47
+ "gap-lg",
48
+ "gap-xl",
49
+ "gap-2xl",
50
+ "gap-3xl",
51
+ "gap-4xl",
52
+ ] as const;
53
+ const gapExamples = spacingLevels.map((level) => createSpacingExample(level));
54
+
55
+ const leanComparison = {
56
+ Clipper: `<section id="feature">\n <h2>Built with Clipper defaults</h2>\n <div>\n <p>Layout and rhythm come from base rules.</p>\n <a href="#">Action</a>\n </div>\n</section>`,
57
+ utilityHeavy: `<section id="feature" class="mx-auto max-w-6xl px-6 py-20">\n <h2 class="text-4xl font-bold leading-tight tracking-tight">Built manually</h2>\n <div class="mt-4 flex flex-col gap-4">\n <p class="max-w-prose text-base leading-7 text-slate-600">Layout and rhythm configured inline.</p>\n <a class="text-blue-600 hover:text-blue-700" href="#">Action</a>\n </div>\n</section>`,
58
+ };
59
+
60
+ const quickCopySnippet = `<section id="intro">
61
+ <h1>Hello Clipper</h1>
62
+ <p class="readable">Start semantic, then add only the few utilities you really need.</p>
63
+ <div class="row">
64
+ <a href="#">Primary action</a>
65
+ <a href="#">Secondary action</a>
66
+ </div>
67
+ </section>`;
68
+
69
+ const tokenRegex = /--(primary|secondary)-(\d+)\s*:\s*([^;]+);/g;
70
+ const palette = {
71
+ primary: [] as Array<{ name: string; shade: number; value: string }>,
72
+ secondary: [] as Array<{ name: string; shade: number; value: string }>,
73
+ };
74
+
75
+ const componentSnippet = `<article class="card">
76
+ <span class="badge">New</span>
77
+ <h3>Reusable primitives</h3>
78
+ <p>Use .card, .badge, and .btn for common UI patterns.</p>
79
+ <div class="row">
80
+ <a href="#" class="btn">Primary action</a>
81
+ <a href="#" class="btn btn-outline">Secondary action</a>
82
+ </div>
83
+ </article>`;
84
+
85
+ for (const match of variablesCss.matchAll(tokenRegex)) {
86
+ const family = match[1] as "primary" | "secondary";
87
+ const shade = Number(match[2]);
88
+ const value = match[3].trim();
89
+
90
+ palette[family].push({
91
+ name: `--${family}-${shade}`,
92
+ shade,
93
+ value,
94
+ });
95
+ }
96
+
97
+ palette.primary.sort((left, right) => left.shade - right.shade);
98
+ palette.secondary.sort((left, right) => left.shade - right.shade);
99
+
100
+ const paletteGroups = [
101
+ {
102
+ title: "Primary scale",
103
+ ariaLabel: "Primary color scale",
104
+ tokens: palette.primary,
105
+ },
106
+ {
107
+ title: "Secondary scale",
108
+ ariaLabel: "Secondary color scale",
109
+ tokens: palette.secondary,
110
+ },
111
+ ].filter((group) => group.tokens.length > 0);
112
+ </script>
113
+
114
+ <section class="bg-muted" id="overview">
115
+ <div class="font-heading text-sm font-semibold tracking-wide text-primary uppercase">Clipper demo</div>
116
+ <div class="readable">
117
+ <h1>How to use Clipper</h1>
118
+ <p>
119
+ Clipper is a simple tailwind-based framework for building pages fast without fighting CSS and adding too many
120
+ utility classes. It is designed for designers and developers alike: semantic markup by default, token-driven
121
+ styling, and just enough utilities to stay productive.
122
+ </p>
123
+ <p>
124
+ The basic page is just semantic HTML (header, footer, main) with <code>&lt;section&gt;</code> containers inside.
125
+ </p>
126
+ </div>
127
+ <div class="bg-primary-muted py-xs">
128
+ As default, elements directly below sections will respect the page max width (with a margin for smaller screens).
129
+ </div>
130
+ <div class="full-width bg-primary-muted py-xs">
131
+ <span class="page-width">
132
+ But breaking out of a layout is no problem with the <code>.full-width</code> class. And just add
133
+ <code>.page-width</code> on the child element to constrain it back to the page width.
134
+ </span>
135
+ </div>
136
+ <p class="readable">
137
+ Elements can have a <code>.readable</code> utility class applied, which sets a max-width for easier reading without needing
138
+ a custom class or wrapper.
139
+ </p>
140
+ </section>
141
+ <section id="defaults">
142
+ <div class="font-heading text-sm font-semibold tracking-wide text-primary uppercase">Built-in behavior</div>
143
+ <h2>What Clipper gives you out of the box</h2>
144
+ <div class="grid md:grid-cols-2">
145
+ {#each defaultsCards as card (card.title)}
146
+ <article class={cardClass}>
147
+ <h3>{card.title}</h3>
148
+ <p>{card.text}</p>
149
+ <pre class={codeBlockClass}><code>{card.code}</code></pre>
150
+ </article>
151
+ {/each}
152
+ </div>
153
+ <p class="readable">
154
+ Example: plain style <a href="#starter">Starter</a> and <a href="#colors">Color demo</a>; set link underline
155
+ behavior globally in <code>variables.css</code>.
156
+ </p>
157
+
158
+ <h3>Spacing utilities</h3>
159
+ <p class="readable">
160
+ Clipper includes spacing utilities (<code>gap-4xs</code> through <code>gap-4xl</code>) that apply consistent spacing
161
+ containers. They work on sections and regular elements, making it easy to control vertical and horizontal rhythm.
162
+ </p>
163
+
164
+ <p class="readable">
165
+ The default spacing for non-section elements is the <code>base</code> level, which is tokenized as
166
+ <code>var(--spacing-base)</code>. This means you can adjust the global spacing rhythm by changing a single token
167
+ value.
168
+ </p>
169
+
170
+ <p class="readable">
171
+ What makes this system powerful is that <strong>every spacing token is fluid</strong>. Using CSS
172
+ <code>clamp()</code>, spacing scales smoothly from mobile to desktop. Change viewport width by 100px and spacing
173
+ adjusts seamlessly — no breakpoints needed. The entire rhythm system breathes with the content, maintaining visual
174
+ harmony at every size.
175
+ </p>
176
+
177
+ <div>
178
+ <h4>All spacing scales</h4>
179
+ <div class="grid md:grid-cols-4">
180
+ {#each gapExamples as example (example.title)}
181
+ <article class={cardClass}>
182
+ <h5>{example.title}</h5>
183
+ <div class={example.className}>
184
+ <span class="card bg-background text-xs">Item</span>
185
+ <span class="card bg-background text-xs">Item</span>
186
+ <span class="card bg-background text-xs">Item</span>
187
+ </div>
188
+ </article>
189
+ {/each}
190
+ </div>
191
+ </div>
192
+ </section>
193
+
194
+ <section id="lean">
195
+ <div class="font-heading text-sm font-semibold tracking-wide text-primary uppercase">Less Tailwind clutter</div>
196
+ <h2>Same intent with fewer classes</h2>
197
+ <p class="readable">
198
+ Because Clipper already handles section width, spacing rhythm, flex stacking, and typography defaults, most sections
199
+ require only semantic HTML plus a few targeted utilities.
200
+ </p>
201
+ <div class="grid gap-4 md:grid-cols-2">
202
+ <article class={cardClass}>
203
+ <h3>Clipper-first markup</h3>
204
+ <pre class={codeBlockClass}><code>{leanComparison.Clipper}</code></pre>
205
+ </article>
206
+ <article class={cardClass}>
207
+ <h3>Utility-heavy equivalent</h3>
208
+ <pre class={codeBlockClass}><code>{leanComparison.utilityHeavy}</code></pre>
209
+ </article>
210
+ </div>
211
+ </section>
212
+
213
+ <section id="components">
214
+ <div class="font-heading text-sm font-semibold tracking-wide text-primary uppercase">Reusable components</div>
215
+ <h2>Component primitives in action</h2>
216
+ <p class="readable">
217
+ Clipper includes three generic reusable primitives in <code>components.css</code>:
218
+ <code>.btn</code>, <code>.card</code>, and <code>.badge</code>, compatible with dark mode, purely for "getting
219
+ started" convenience. They can be replaced by any UI framework or custom styles.
220
+ </p>
221
+
222
+ <div class="grid gap-4 md:grid-cols-2">
223
+ <article class="card">
224
+ <span class="badge">New</span>
225
+ <h3>Card + badge</h3>
226
+ <p>Cards provide a consistent container style, while badges highlight metadata or state.</p>
227
+ <div class="row">
228
+ <a href="#starter" class="btn btn-inline">View starter</a>
229
+ </div>
230
+ </article>
231
+
232
+ <article class="card">
233
+ <h3>Button primitive</h3>
234
+ <p>The button style keeps typography, color, hover, and focus states consistent.</p>
235
+ <div class="row flex-wrap">
236
+ <a href="#" class="btn">Primary action</a>
237
+ <a href="#" class="btn btn-outline">Secondary action</a>
238
+ <span class="badge">Accessible focus</span>
239
+ </div>
240
+ </article>
241
+ </div>
242
+
243
+ <pre class={codeBlockClass}><code>{componentSnippet}</code></pre>
244
+ </section>
245
+
246
+ <section id="colors">
247
+ <div class="font-heading text-sm font-semibold tracking-wide text-primary uppercase">Color demo</div>
248
+ <h2>Token-driven color scales</h2>
249
+ <p class="readable">
250
+ All colors can be easily configured with <code>variables.css</code>, with <i>easy-to-use</i> support for dark mode.
251
+ </p>
252
+ <h3>Color tokens parsed from variables.css</h3>
253
+ {#each paletteGroups as group (group.title)}
254
+ <div>
255
+ <h4>{group.title}</h4>
256
+ <div class="grid grid-cols-[repeat(auto-fit,minmax(120px,1fr))] gap-3" role="list" aria-label={group.ariaLabel}>
257
+ {#each group.tokens as token (token.name)}
258
+ <article class={swatchClass} role="listitem">
259
+ <div class={swatchChipClass} style={`background-color: var(${token.name});`} aria-hidden="true"></div>
260
+ <div class="mt-2 text-sm text-muted-foreground">
261
+ <span class="whitespace-nowrap">{token.name}</span>
262
+ </div>
263
+ </article>
264
+ {/each}
265
+ </div>
266
+ </div>
267
+ {/each}
268
+ </section>
269
+
270
+ <section id="starter">
271
+ <div class="font-heading text-sm font-semibold tracking-wide text-primary uppercase">Starter</div>
272
+ <h2>Copy this minimal page pattern</h2>
273
+ <pre class={codeBlockClass}><code>{quickCopySnippet}</code></pre>
274
+ </section>