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 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, 41 themes, and fluid Utopia typography/space scales.
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()}</script>`}
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 41 built-in themes. |
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,7 @@
1
+ type $$ComponentProps = {
2
+ class?: string;
3
+ palettes?: boolean;
4
+ };
5
+ declare const ModeToggle: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ModeToggle = ReturnType<typeof ModeToggle>;
7
+ export default ModeToggle;
@@ -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;