fractalstyler2 0.7.1 → 0.8.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 +4 -3
- package/dist/components/ModeToggle.svelte +53 -0
- package/dist/components/ModeToggle.svelte.d.ts +7 -0
- package/dist/components/ThemePicker.svelte +175 -0
- package/dist/components/ThemePicker.svelte.d.ts +21 -0
- package/dist/css/fractalstyler.css +895 -8
- package/dist/css/fractalstyler.min.css +2 -2
- package/dist/index.d.ts +4 -7
- package/dist/index.js +4 -14
- package/dist/presets.core.d.ts +34 -4
- package/dist/presets.core.js +84 -13
- package/dist/presets.svelte.d.ts +19 -4
- package/dist/presets.svelte.js +38 -6
- package/dist/styles/_00_themes.sass +944 -1
- package/dist/styles/_03_containers.sass +15 -0
- package/dist/styles/_04_layouts.sass +1 -1
- package/dist/themes.d.ts +3 -1
- package/dist/themes.js +77 -42
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/registry.json +296 -0
- package/skills/fractal-styler/SKILL.md +9 -1
- package/skills/fractal-styler/references/fractals.md +38 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ the generators — the literal ladder and the responsive seam — before compili
|
|
|
28
28
|
Tokens (L0) ──► Dimensions (L1) ──► Containers (L2) ──► Layouts (L3) ──► Shells (L4) ──► Visuals (L5)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
1. **Tokens (L0)**: 31 semantic colour tokens,
|
|
31
|
+
1. **Tokens (L0)**: 31 semantic colour tokens, 76 paired themes, and fluid Utopia typography/space scales.
|
|
32
32
|
2. **Dimensions (L1)**: 17 space families (`.gap-sm`, `.pad-md`, `.marg-xs`), negative margins (`.marg--sm`), literal pixel utilities (`.w-120`, `.radius-8`), and `-mob` / `-desk` bands.
|
|
33
33
|
3. **Containers (L2)**: Flexible flow primitives (`.box`, `.row`, `.grid`) with strict physical X/Y axis alignment (`.xcenter`, `.ycenter`, `.xbetween`).
|
|
34
34
|
4. **Layouts (L3)**: Gridding Golden Rules ($3\to1$, $4\to2\to1$, $6\to3\to2\to1$), `.card-grid`, reading measure `.prose`, and scroll-snap `.reel`.
|
|
@@ -89,7 +89,7 @@ In your root layout (`src/routes/+layout.svelte`):
|
|
|
89
89
|
</script>
|
|
90
90
|
|
|
91
91
|
<svelte:head>
|
|
92
|
-
{@html `<script>${getPresetScript()}
|
|
92
|
+
{@html `<script>${getPresetScript()}<\/script>`}
|
|
93
93
|
</svelte:head>
|
|
94
94
|
|
|
95
95
|
{@render children()}
|
|
@@ -132,7 +132,7 @@ pnpm registry
|
|
|
132
132
|
| Chapter | Document | Scope & Contents |
|
|
133
133
|
|:---|:---|:---|
|
|
134
134
|
| **01** | [**`01-introduction.md`**](./docs/01-introduction.md) | Philosophy, $L0 \to L5$ mental model, and design invariants. |
|
|
135
|
-
| **02** | [**`02-getting-started.md`**](./docs/02-getting-started.md) | Installation (CSS or SASS), CLI init, SvelteKit setup, and the
|
|
135
|
+
| **02** | [**`02-getting-started.md`**](./docs/02-getting-started.md) | Installation (CSS or SASS), CLI init, SvelteKit setup, and the 76 built-in themes. |
|
|
136
136
|
| **03** | [**`03-structure.md`**](./docs/03-structure.md) | Numbered physical scale (`_00` through `_08`) and cascade order. |
|
|
137
137
|
| **04** | [**`04-tokens.md`**](./docs/04-tokens.md) | 30 semantic colors, fluid Utopia scales, and dark/light modes. |
|
|
138
138
|
| **05** | [**`05-dimensions.md`**](./docs/05-dimensions.md) | 17 space families, the literal px ladder, and `-mob`/`-desk` bands. |
|
|
@@ -144,6 +144,7 @@ pnpm registry
|
|
|
144
144
|
| **11** | [**`11-mcp-server.md`**](./docs/11-mcp-server.md) | Model Context Protocol server tools and agent connection configs. |
|
|
145
145
|
| **12** | [**`12-agent-plugin.md`**](./docs/12-agent-plugin.md) | `agent-plugins.org` spec, the bundled `fractal-styler` skill. |
|
|
146
146
|
| **13** | [**`13-cookbook.md`**](./docs/13-cookbook.md) | The Zero-SASS Recipe Gallery: real-world UI patterns composed in HTML. |
|
|
147
|
+
| **14** | [**`14-api.md`**](./docs/14-api.md) | Complete API reference: entry points, runtime functions, reactive state, and components. |
|
|
147
148
|
| **Registry** | [**`REGISTRY.md`**](./REGISTRY.md) | Comprehensive, grepable class dictionary and token table. |
|
|
148
149
|
|
|
149
150
|
---
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { presets, initPresets, toggleMode, toggleThemeMode } from '../presets.svelte.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `palettes` — when a theme is applied, swap to its light/dark twin instead
|
|
7
|
+
* of only flipping the mode. Off by default: plain mode is the common case,
|
|
8
|
+
* and most palettes have no twin to swap to.
|
|
9
|
+
*/
|
|
10
|
+
let { class: className = '', palettes = false }: { class?: string; palettes?: boolean } =
|
|
11
|
+
$props();
|
|
12
|
+
|
|
13
|
+
onMount(() => initPresets());
|
|
14
|
+
|
|
15
|
+
const dark = $derived(presets.mode === 'dark');
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
class="button ghost {className}"
|
|
21
|
+
aria-pressed={dark}
|
|
22
|
+
aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
23
|
+
title={dark ? 'Light mode' : 'Dark mode'}
|
|
24
|
+
onclick={() => (palettes ? toggleThemeMode() : toggleMode())}
|
|
25
|
+
>
|
|
26
|
+
<svg width="16" height="16" viewBox="0 0 19 19" aria-hidden="true">
|
|
27
|
+
{#if dark}
|
|
28
|
+
<!-- Sun: filled core, eight rays -->
|
|
29
|
+
<circle cx="9.5" cy="9.5" r="3.75" fill="currentColor" />
|
|
30
|
+
{#each [0, 45, 90, 135, 180, 225, 270, 315] as angle}
|
|
31
|
+
<line
|
|
32
|
+
x1="9.5"
|
|
33
|
+
y1="1.75"
|
|
34
|
+
x2="9.5"
|
|
35
|
+
y2="3.5"
|
|
36
|
+
stroke="currentColor"
|
|
37
|
+
stroke-width="1.5"
|
|
38
|
+
stroke-linecap="round"
|
|
39
|
+
transform="rotate({angle} 9.5 9.5)"
|
|
40
|
+
/>
|
|
41
|
+
{/each}
|
|
42
|
+
{:else}
|
|
43
|
+
<!-- Moon: one circle bitten by another, as a single path -->
|
|
44
|
+
<path
|
|
45
|
+
d="M13.5 11.9A6.25 6.25 0 0 1 7.1 5.5a6.25 6.25 0 1 0 6.4 6.4Z"
|
|
46
|
+
fill="currentColor"
|
|
47
|
+
stroke="currentColor"
|
|
48
|
+
stroke-width="1.5"
|
|
49
|
+
stroke-linejoin="round"
|
|
50
|
+
/>
|
|
51
|
+
{/if}
|
|
52
|
+
</svg>
|
|
53
|
+
</button>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, type Snippet } from 'svelte';
|
|
3
|
+
import { presets, initPresets, setTheme, themes } from '../presets.svelte.js';
|
|
4
|
+
import ModeToggle from './ModeToggle.svelte';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A popover palette picker over all built-in themes.
|
|
8
|
+
*
|
|
9
|
+
* Every theme card wears its own theme class, so the swatches inside it are
|
|
10
|
+
* that palette rendering itself — not a hardcoded preview. Composed entirely
|
|
11
|
+
* from registry classes; there is no stylesheet behind this component.
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
class?: string;
|
|
15
|
+
/** Start the mode filter on one side. Users can still switch it. */
|
|
16
|
+
filter?: 'all' | 'light' | 'dark';
|
|
17
|
+
/** Show the light/dark toggle inside the panel. */
|
|
18
|
+
showModeToggle?: boolean;
|
|
19
|
+
/** Replace the default palette-glyph trigger. */
|
|
20
|
+
trigger?: Snippet;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
class: className = '',
|
|
26
|
+
filter = 'all',
|
|
27
|
+
showModeToggle = true,
|
|
28
|
+
trigger,
|
|
29
|
+
...rest
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
let open = $state(false);
|
|
33
|
+
// `filter` seeds the initial value only; the user drives it after that.
|
|
34
|
+
// svelte-ignore state_referenced_locally
|
|
35
|
+
let mode = $state<'all' | 'light' | 'dark'>(filter);
|
|
36
|
+
let query = $state('');
|
|
37
|
+
let root = $state<HTMLDivElement | null>(null);
|
|
38
|
+
|
|
39
|
+
onMount(() => {
|
|
40
|
+
initPresets();
|
|
41
|
+
const onClick = (e: MouseEvent) => {
|
|
42
|
+
if (open && root && !root.contains(e.target as Node)) open = false;
|
|
43
|
+
};
|
|
44
|
+
const onKey = (e: KeyboardEvent) => {
|
|
45
|
+
if (e.key === 'Escape' && open) open = false;
|
|
46
|
+
};
|
|
47
|
+
window.addEventListener('click', onClick);
|
|
48
|
+
window.addEventListener('keydown', onKey);
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener('click', onClick);
|
|
51
|
+
window.removeEventListener('keydown', onKey);
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// theme-nordic-frost-light -> Nordic Frost Light
|
|
56
|
+
const title = (id: string) =>
|
|
57
|
+
id
|
|
58
|
+
.replace(/^theme-/, '')
|
|
59
|
+
.split('-')
|
|
60
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
61
|
+
.join(' ');
|
|
62
|
+
|
|
63
|
+
const shown = $derived.by(() => {
|
|
64
|
+
const byMode = mode === 'all' ? themes : themes.filter((t) => t.mode === mode);
|
|
65
|
+
const q = query.trim().toLowerCase();
|
|
66
|
+
return q ? byMode.filter((t) => t.id.toLowerCase().includes(q)) : byMode;
|
|
67
|
+
});
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div class="relative {className}" bind:this={root} {...rest}>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
class="button ghost"
|
|
74
|
+
aria-expanded={open}
|
|
75
|
+
aria-haspopup="true"
|
|
76
|
+
aria-label="Choose a palette"
|
|
77
|
+
title="Palette"
|
|
78
|
+
onclick={() => (open = !open)}
|
|
79
|
+
>
|
|
80
|
+
{#if trigger}
|
|
81
|
+
{@render trigger()}
|
|
82
|
+
{:else}
|
|
83
|
+
<svg width="16" height="16" viewBox="0 0 19 19" aria-hidden="true">
|
|
84
|
+
<circle cx="9.5" cy="9.5" r="7.75" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
85
|
+
<circle cx="9.5" cy="5.75" r="1.5" fill="currentColor" />
|
|
86
|
+
<circle cx="13.25" cy="9.5" r="1.5" fill="currentColor" />
|
|
87
|
+
<circle cx="6" cy="8" r="1.5" fill="currentColor" />
|
|
88
|
+
<circle cx="8" cy="13" r="1.5" fill="currentColor" />
|
|
89
|
+
</svg>
|
|
90
|
+
{/if}
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
<div class="popover box gap-8 pad-8 w-320" class:open>
|
|
94
|
+
<div class="row ycenter xbetween gap-8">
|
|
95
|
+
<div class="row gap-2" role="group" aria-label="Filter palettes by mode">
|
|
96
|
+
{#each ['all', 'light', 'dark'] as m (m)}
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
class="button ghost text-xs"
|
|
100
|
+
class:active={mode === m}
|
|
101
|
+
aria-pressed={mode === m}
|
|
102
|
+
onclick={() => (mode = m as typeof mode)}
|
|
103
|
+
>
|
|
104
|
+
{m}
|
|
105
|
+
</button>
|
|
106
|
+
{/each}
|
|
107
|
+
</div>
|
|
108
|
+
{#if showModeToggle}
|
|
109
|
+
<ModeToggle />
|
|
110
|
+
{/if}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<input
|
|
114
|
+
class="input text-xs"
|
|
115
|
+
type="search"
|
|
116
|
+
placeholder="Search palettes"
|
|
117
|
+
aria-label="Search palettes"
|
|
118
|
+
bind:value={query}
|
|
119
|
+
/>
|
|
120
|
+
|
|
121
|
+
<!-- One column on purpose: .grid-3 would collapse to 1 at this width
|
|
122
|
+
anyway (gridding golden rule), so a row card reads better. -->
|
|
123
|
+
<div class="box gap-2 scroll-y h-256">
|
|
124
|
+
{#each shown as theme (theme.id)}
|
|
125
|
+
{@const active = presets.theme === theme.id}
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
class="{theme.id} row ycenter gap-4 pad-4 bg border"
|
|
129
|
+
aria-pressed={active}
|
|
130
|
+
title={title(theme.id)}
|
|
131
|
+
onclick={() => setTheme(theme.id)}
|
|
132
|
+
>
|
|
133
|
+
<span class="row gap-1 ycenter shrink-0">
|
|
134
|
+
<span class="surface border-subtle w-12 h-12"></span>
|
|
135
|
+
<span class="raised border-subtle w-12 h-12"></span>
|
|
136
|
+
<span class="panel border-subtle w-12 h-12"></span>
|
|
137
|
+
<span class="text-theme row ycenter">
|
|
138
|
+
<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
|
|
139
|
+
<circle cx="6" cy="6" r="5" fill="currentColor" />
|
|
140
|
+
</svg>
|
|
141
|
+
</span>
|
|
142
|
+
</span>
|
|
143
|
+
<span class="box xleft grow min0">
|
|
144
|
+
<span class="text-xs text-primary truncate wfull">{title(theme.id)}</span>
|
|
145
|
+
</span>
|
|
146
|
+
{#if active}
|
|
147
|
+
<svg
|
|
148
|
+
width="12"
|
|
149
|
+
height="12"
|
|
150
|
+
viewBox="0 0 12 12"
|
|
151
|
+
aria-hidden="true"
|
|
152
|
+
class="text-theme shrink-0"
|
|
153
|
+
>
|
|
154
|
+
<path
|
|
155
|
+
d="M2.5 6.5 5 9l4.5-5.5"
|
|
156
|
+
fill="none"
|
|
157
|
+
stroke="currentColor"
|
|
158
|
+
stroke-width="1.75"
|
|
159
|
+
stroke-linecap="round"
|
|
160
|
+
stroke-linejoin="round"
|
|
161
|
+
/>
|
|
162
|
+
</svg>
|
|
163
|
+
{/if}
|
|
164
|
+
</button>
|
|
165
|
+
{/each}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div class="row ycenter xbetween gap-8">
|
|
169
|
+
<span class="text-xs text-muted">{shown.length} {shown.length === 1 ? 'palette' : 'palettes'}</span>
|
|
170
|
+
<button type="button" class="button ghost text-xs" onclick={() => setTheme(null)}>
|
|
171
|
+
Reset
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* A popover palette picker over all built-in themes.
|
|
4
|
+
*
|
|
5
|
+
* Every theme card wears its own theme class, so the swatches inside it are
|
|
6
|
+
* that palette rendering itself — not a hardcoded preview. Composed entirely
|
|
7
|
+
* from registry classes; there is no stylesheet behind this component.
|
|
8
|
+
*/
|
|
9
|
+
interface Props {
|
|
10
|
+
class?: string;
|
|
11
|
+
/** Start the mode filter on one side. Users can still switch it. */
|
|
12
|
+
filter?: 'all' | 'light' | 'dark';
|
|
13
|
+
/** Show the light/dark toggle inside the panel. */
|
|
14
|
+
showModeToggle?: boolean;
|
|
15
|
+
/** Replace the default palette-glyph trigger. */
|
|
16
|
+
trigger?: Snippet;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
declare const ThemePicker: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type ThemePicker = ReturnType<typeof ThemePicker>;
|
|
21
|
+
export default ThemePicker;
|