fractalstyler2 0.7.0 → 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/dist/index.d.ts CHANGED
@@ -1,10 +1,5 @@
1
1
  import { version } from './version.js';
2
2
  export { version };
3
- export type Mode = 'light' | 'dark';
4
- /** Set the color mode by writing the [data-mode] marker on <html>. */
5
- export declare function setMode(mode: Mode): void;
6
- /** Toggle between light and dark, returning the new mode. */
7
- export declare function toggleMode(): Mode;
8
3
  export declare const breakpoints: {
9
4
  readonly sm: "640px";
10
5
  readonly md: "768px";
@@ -17,7 +12,7 @@ export declare const radiusSteps: readonly ["0", "2", "3", "4", "6", "8", "12",
17
12
  export declare const surfaceRoles: readonly ["bg", "surface", "raised", "panel", "footer", "popover", "dialog", "terminal", "input", "canvas"];
18
13
  export declare const inkRoles: readonly ["primary", "secondary", "muted", "inverse", "theme-color", "theme-color-alt"];
19
14
  export declare const tokens: {
20
- readonly version: "0.7.0";
15
+ readonly version: "0.8.0";
21
16
  readonly breakpoints: {
22
17
  readonly sm: "640px";
23
18
  readonly md: "768px";
@@ -36,7 +31,7 @@ export type TypographyStep = (typeof typographyScale)[number];
36
31
  export type RadiusStep = (typeof radiusSteps)[number];
37
32
  export type SurfaceRole = (typeof surfaceRoles)[number];
38
33
  export type InkRole = (typeof inkRoles)[number];
39
- export { presets, presetAxes, initPresets, setPreset, getPresetScript, setTheme, getTheme, toggleThemeMode, themes, themeIds, type ThemeMeta, type PresetAxis } from './presets.svelte.js';
34
+ export { presets, presetAxes, presetDefaults, initPresets, setPreset, getPreset, cyclePreset, onPresetChange, getPresetScript, setTheme, getTheme, twinTheme, toggleThemeMode, setMode, toggleMode, getMode, isDark, onModeChange, type Mode, themes, themeIds, type ThemeMeta, type PresetAxis } from './presets.svelte.js';
40
35
  export { default as AppShell } from './components/AppShell.svelte';
41
36
  export { default as PageShell } from './components/PageShell.svelte';
42
37
  export { default as PageSplit } from './components/PageSplit.svelte';
@@ -47,3 +42,5 @@ export { default as ShapePicker } from './components/ShapePicker.svelte';
47
42
  export { default as LayoutPicker } from './components/LayoutPicker.svelte';
48
43
  export { default as ColorPicker } from './components/ColorPicker.svelte';
49
44
  export { default as MotionPicker } from './components/MotionPicker.svelte';
45
+ export { default as ModeToggle } from './components/ModeToggle.svelte';
46
+ export { default as ThemePicker } from './components/ThemePicker.svelte';
package/dist/index.js CHANGED
@@ -9,19 +9,6 @@
9
9
  // framework-free equivalents live at 'fractalstyler2/presets'.
10
10
  import { version } from './version.js';
11
11
  export { version };
12
- /** Set the color mode by writing the [data-mode] marker on <html>. */
13
- export function setMode(mode) {
14
- if (typeof document === 'undefined')
15
- return;
16
- document.documentElement.setAttribute('data-mode', mode);
17
- }
18
- /** Toggle between light and dark, returning the new mode. */
19
- export function toggleMode() {
20
- const current = typeof document !== 'undefined' ? document.documentElement.getAttribute('data-mode') : null;
21
- const next = current === 'dark' ? 'light' : 'dark';
22
- setMode(next);
23
- return next;
24
- }
25
12
  // Design Token Metadata
26
13
  export const breakpoints = {
27
14
  sm: '640px',
@@ -93,7 +80,7 @@ export const tokens = {
93
80
  };
94
81
  // Preset runtime — fs2 owns application, persistence, and anti-flicker for
95
82
  // the four preset languages (layout / shape / color / motion).
96
- export { presets, presetAxes, initPresets, setPreset, getPresetScript, setTheme, getTheme, toggleThemeMode, themes, themeIds } from './presets.svelte.js';
83
+ export { presets, presetAxes, presetDefaults, initPresets, setPreset, getPreset, cyclePreset, onPresetChange, getPresetScript, setTheme, getTheme, twinTheme, toggleThemeMode, setMode, toggleMode, getMode, isDark, onModeChange, themes, themeIds } from './presets.svelte.js';
97
84
  // Shell components — the canonical markups, executable. Optional convenience
98
85
  // over the class contract, never a replacement for it: a CSS consumer copying
99
86
  // canonical-markups.md by hand gets identical output.
@@ -103,7 +90,10 @@ export { default as PageSplit } from './components/PageSplit.svelte';
103
90
  export { default as Hero } from './components/Hero.svelte';
104
91
  export { default as Accordion } from './components/Accordion.svelte';
105
92
  export { default as AccordionItem } from './components/AccordionItem.svelte';
93
+ // Preset pickers — one per axis, plus the two theming controls.
106
94
  export { default as ShapePicker } from './components/ShapePicker.svelte';
107
95
  export { default as LayoutPicker } from './components/LayoutPicker.svelte';
108
96
  export { default as ColorPicker } from './components/ColorPicker.svelte';
109
97
  export { default as MotionPicker } from './components/MotionPicker.svelte';
98
+ export { default as ModeToggle } from './components/ModeToggle.svelte';
99
+ export { default as ThemePicker } from './components/ThemePicker.svelte';
@@ -14,6 +14,7 @@ export declare const presetDefaults: {
14
14
  export type PresetAxis = keyof typeof presetAxes;
15
15
  export declare const STORAGE_KEY = "fs2.presets";
16
16
  export declare const THEME_KEY = "fs2.theme";
17
+ export declare const MODE_KEY = "fs2.mode";
17
18
  /** Current value of every axis. Read it; mutate through setPreset. */
18
19
  export declare const presetState: Record<PresetAxis, string>;
19
20
  type Listener = (axis: PresetAxis, value: string) => void;
@@ -35,8 +36,37 @@ export declare function getPresetScript(): string;
35
36
  export declare function setTheme(id: string | null): void;
36
37
  /** The active palette, or null when none is applied. */
37
38
  export declare function getTheme(): string | null;
38
- /** Swap to the light/dark counterpart of the active theme, if it has one. */
39
+ /**
40
+ * The same palette in the opposite mode — `theme-sun-light` <-> `theme-sun-dark`.
41
+ * Pairing is declared in `_00_themes.sass`, so it works for palettes whose
42
+ * names do not rhyme: `theme-catppuccin-mocha` <-> `theme-catppuccin-latte`.
43
+ * Null only when a palette genuinely has no counterpart.
44
+ */
45
+ export declare function twinTheme(id: string): string | null;
46
+ /**
47
+ * Swap to the light/dark counterpart of the active theme.
48
+ *
49
+ * With no theme applied this is just toggleMode(). With a theme that has a twin
50
+ * it swaps palettes. With a theme that has no twin it still flips the mode, but
51
+ * the palette's own colours outrank `[data-mode]` in the cascade, so nothing
52
+ * visibly changes — see docs/14-api.md.
53
+ *
54
+ * Returns the theme id that is active afterwards, or null when none is.
55
+ */
39
56
  export declare function toggleThemeMode(): string | null;
40
- /** Set the colour mode marker. Light/dark is a mode, not a preset axis. */
41
- export declare function setMode(mode: 'light' | 'dark'): void;
42
- export declare function toggleMode(): 'light' | 'dark';
57
+ export type Mode = 'light' | 'dark';
58
+ /**
59
+ * Set the colour mode. Persists, so it survives reload — light/dark is the most
60
+ * basic thing a user toggles and it has no business forgetting itself.
61
+ *
62
+ * Pass null to clear the choice and fall back to the OS preference.
63
+ */
64
+ export declare function setMode(mode: Mode | null): void;
65
+ /** The active mode. Falls back to the OS preference when nothing is chosen. */
66
+ export declare function getMode(): Mode;
67
+ /** True when the page is currently rendering dark. */
68
+ export declare function isDark(): boolean;
69
+ export declare function toggleMode(): Mode;
70
+ type ModeListener = (mode: Mode) => void;
71
+ /** Subscribe to mode changes — however they were made. Returns an unsubscribe. */
72
+ export declare function onModeChange(fn: ModeListener): () => void;
@@ -27,6 +27,7 @@ export const presetDefaults = {
27
27
  };
28
28
  export const STORAGE_KEY = 'fs2.presets';
29
29
  export const THEME_KEY = 'fs2.theme';
30
+ export const MODE_KEY = 'fs2.mode';
30
31
  /** Current value of every axis. Read it; mutate through setPreset. */
31
32
  export const presetState = { ...presetDefaults };
32
33
  const listeners = new Set();
@@ -73,6 +74,9 @@ export function initPresets() {
73
74
  const savedTheme = localStorage.getItem(THEME_KEY);
74
75
  if (savedTheme)
75
76
  setTheme(savedTheme);
77
+ const savedMode = localStorage.getItem(MODE_KEY);
78
+ if (savedMode === 'dark' || savedMode === 'light')
79
+ setMode(savedMode);
76
80
  }
77
81
  catch {
78
82
  /* storage unavailable — defaults stand */
@@ -121,7 +125,7 @@ export function cyclePreset(axis) {
121
125
  export function getPresetScript() {
122
126
  const axes = JSON.stringify(presetAxes).replaceAll('"', "'");
123
127
  const defs = JSON.stringify(presetDefaults).replaceAll('"', "'");
124
- return `(function(){try{var v=${axes};var d=${defs};var r=document.documentElement;var s=JSON.parse(localStorage.getItem('${STORAGE_KEY}')||'{}');for(var k in v){var val=s[k];if(val&&val!==d[k])r.setAttribute('data-'+k,val);}var t=localStorage.getItem('${THEME_KEY}');if(t){r.classList.add(t);r.setAttribute('data-mode',t.indexOf('-dark')>-1?'dark':'light');}}catch(e){}})();`;
128
+ return `(function(){try{var v=${axes};var d=${defs};var r=document.documentElement;var s=JSON.parse(localStorage.getItem('${STORAGE_KEY}')||'{}');for(var k in v){var val=s[k];if(val&&val!==d[k])r.setAttribute('data-'+k,val);}var t=localStorage.getItem('${THEME_KEY}');if(t){r.classList.add(t);r.setAttribute('data-mode',t.indexOf('-dark')>-1?'dark':'light');}var m=localStorage.getItem('${MODE_KEY}');if(m==='dark'||m==='light')r.setAttribute('data-mode',m);}catch(e){}})();`;
125
129
  }
126
130
  // ─── themes ────────────────────────────────────────────────────────────────
127
131
  // A theme is one class on <html>, always paired with its data-mode: the colour
@@ -135,26 +139,28 @@ export function setTheme(id) {
135
139
  for (const t of themes)
136
140
  root.classList.remove(t.id);
137
141
  if (!id) {
138
- root.removeAttribute('data-mode');
139
142
  try {
140
143
  localStorage.removeItem(THEME_KEY);
141
144
  }
142
145
  catch {
143
146
  /* storage unavailable */
144
147
  }
148
+ setMode(null);
145
149
  return;
146
150
  }
147
151
  const theme = themes.find((t) => t.id === id);
148
152
  if (!theme)
149
153
  return;
150
154
  root.classList.add(theme.id);
151
- root.setAttribute('data-mode', theme.mode);
152
155
  try {
153
156
  localStorage.setItem(THEME_KEY, theme.id);
154
157
  }
155
158
  catch {
156
159
  /* storage unavailable — the theme stays session-local */
157
160
  }
161
+ // A palette carries its own mode; route through setMode so it persists and
162
+ // notifies like any other mode change.
163
+ setMode(theme.mode);
158
164
  }
159
165
  /** The active palette, or null when none is applied. */
160
166
  export function getTheme() {
@@ -163,28 +169,93 @@ export function getTheme() {
163
169
  const root = document.documentElement;
164
170
  return themes.find((t) => root.classList.contains(t.id))?.id ?? null;
165
171
  }
166
- /** Swap to the light/dark counterpart of the active theme, if it has one. */
172
+ /**
173
+ * The same palette in the opposite mode — `theme-sun-light` <-> `theme-sun-dark`.
174
+ * Pairing is declared in `_00_themes.sass`, so it works for palettes whose
175
+ * names do not rhyme: `theme-catppuccin-mocha` <-> `theme-catppuccin-latte`.
176
+ * Null only when a palette genuinely has no counterpart.
177
+ */
178
+ export function twinTheme(id) {
179
+ return themes.find((t) => t.id === id)?.twin ?? null;
180
+ }
181
+ /**
182
+ * Swap to the light/dark counterpart of the active theme.
183
+ *
184
+ * With no theme applied this is just toggleMode(). With a theme that has a twin
185
+ * it swaps palettes. With a theme that has no twin it still flips the mode, but
186
+ * the palette's own colours outrank `[data-mode]` in the cascade, so nothing
187
+ * visibly changes — see docs/14-api.md.
188
+ *
189
+ * Returns the theme id that is active afterwards, or null when none is.
190
+ */
167
191
  export function toggleThemeMode() {
168
192
  const active = getTheme();
169
193
  if (!active) {
170
194
  toggleMode();
171
195
  return null;
172
196
  }
173
- const want = themes.find((t) => t.id === active).mode === 'dark' ? 'light' : 'dark';
174
- const twin = themes.find((t) => t.mode === want);
175
- if (twin)
176
- setTheme(twin.id);
177
- return twin?.id ?? null;
197
+ const twin = twinTheme(active);
198
+ if (twin) {
199
+ setTheme(twin);
200
+ return twin;
201
+ }
202
+ toggleMode();
203
+ return active;
178
204
  }
179
- /** Set the colour mode marker. Light/dark is a mode, not a preset axis. */
205
+ /**
206
+ * Set the colour mode. Persists, so it survives reload — light/dark is the most
207
+ * basic thing a user toggles and it has no business forgetting itself.
208
+ *
209
+ * Pass null to clear the choice and fall back to the OS preference.
210
+ */
180
211
  export function setMode(mode) {
181
212
  if (typeof document === 'undefined')
182
213
  return;
183
- document.documentElement.setAttribute('data-mode', mode);
214
+ const root = document.documentElement;
215
+ if (mode === null) {
216
+ root.removeAttribute('data-mode');
217
+ try {
218
+ localStorage.removeItem(MODE_KEY);
219
+ }
220
+ catch {
221
+ /* storage unavailable */
222
+ }
223
+ }
224
+ else {
225
+ root.setAttribute('data-mode', mode);
226
+ try {
227
+ localStorage.setItem(MODE_KEY, mode);
228
+ }
229
+ catch {
230
+ /* storage unavailable — the choice stays session-local */
231
+ }
232
+ }
233
+ for (const fn of modeListeners)
234
+ fn(getMode());
235
+ }
236
+ /** The active mode. Falls back to the OS preference when nothing is chosen. */
237
+ export function getMode() {
238
+ if (typeof document === 'undefined')
239
+ return 'light';
240
+ const attr = document.documentElement.getAttribute('data-mode');
241
+ if (attr === 'dark' || attr === 'light')
242
+ return attr;
243
+ return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches
244
+ ? 'dark'
245
+ : 'light';
246
+ }
247
+ /** True when the page is currently rendering dark. */
248
+ export function isDark() {
249
+ return getMode() === 'dark';
184
250
  }
185
251
  export function toggleMode() {
186
- const current = typeof document !== 'undefined' ? document.documentElement.getAttribute('data-mode') : null;
187
- const next = current === 'dark' ? 'light' : 'dark';
252
+ const next = getMode() === 'dark' ? 'light' : 'dark';
188
253
  setMode(next);
189
254
  return next;
190
255
  }
256
+ const modeListeners = new Set();
257
+ /** Subscribe to mode changes — however they were made. Returns an unsubscribe. */
258
+ export function onModeChange(fn) {
259
+ modeListeners.add(fn);
260
+ return () => modeListeners.delete(fn);
261
+ }
@@ -1,6 +1,21 @@
1
- import { type PresetAxis } from './presets.core.js';
2
- export { presetAxes, presetDefaults, getPreset, cyclePreset, getPresetScript, setMode, toggleMode, setTheme, getTheme, toggleThemeMode, themes, themeIds, type ThemeMeta, onPresetChange, type PresetAxis } from './presets.core.js';
3
- /** Reactive mirror of presetState, for pickers and any $derived reading it. */
4
- export declare const presets: Record<"color" | "layout" | "shape" | "motion", string>;
1
+ import { type Mode, type PresetAxis } from './presets.core.js';
2
+ export { presetAxes, presetDefaults, getPreset, getMode, isDark, getTheme, twinTheme, cyclePreset, getPresetScript, onModeChange, onPresetChange, themes, themeIds, type Mode, type ThemeMeta, type PresetAxis } from './presets.core.js';
3
+ /**
4
+ * Reactive mirror of the whole runtime — the four preset axes plus `mode` and
5
+ * `theme`. Read it straight in a template:
6
+ *
7
+ * {#if presets.mode === 'dark'} … {/if}
8
+ *
9
+ * It stays correct however the change was made: a picker, setTheme(), the OS
10
+ * preference at load, or another component entirely.
11
+ */
12
+ export declare const presets: Record<"color" | "layout" | "shape" | "motion", string> & {
13
+ mode: Mode;
14
+ theme: string | null;
15
+ };
5
16
  export declare function initPresets(): void;
6
17
  export declare function setPreset(axis: PresetAxis, value: string): void;
18
+ export declare function setMode(mode: Mode | null): void;
19
+ export declare function toggleMode(): Mode;
20
+ export declare function setTheme(id: string | null): void;
21
+ export declare function toggleThemeMode(): string | null;
@@ -3,19 +3,51 @@
3
3
  // The logic lives there so the vanilla CSS distribution and the Svelte package
4
4
  // share one implementation. All this adds is a reactive mirror of the state so
5
5
  // the bundled pickers re-render.
6
- import { presetAxes, presetDefaults, initPresets as coreInit, setPreset as coreSet, onPresetChange, presetState } from './presets.core.js';
7
- export { presetAxes, presetDefaults, getPreset, cyclePreset, getPresetScript, setMode, toggleMode, setTheme, getTheme, toggleThemeMode, themes, themeIds, onPresetChange } from './presets.core.js';
8
- /** Reactive mirror of presetState, for pickers and any $derived reading it. */
9
- export const presets = $state({ ...presetDefaults });
6
+ import { presetAxes, presetDefaults, initPresets as coreInit, setPreset as coreSet, setMode as coreSetMode, setTheme as coreSetTheme, toggleMode as coreToggleMode, toggleThemeMode as coreToggleThemeMode, onPresetChange, onModeChange, getMode, getTheme, presetState } from './presets.core.js';
7
+ // Re-exported unchanged. Everything that MUTATES state is wrapped below so the
8
+ // reactive mirror stays in step; everything else passes straight through.
9
+ export { presetAxes, presetDefaults, getPreset, getMode, isDark, getTheme, twinTheme, cyclePreset, getPresetScript, onModeChange, onPresetChange, themes, themeIds } from './presets.core.js';
10
+ /**
11
+ * Reactive mirror of the whole runtime — the four preset axes plus `mode` and
12
+ * `theme`. Read it straight in a template:
13
+ *
14
+ * {#if presets.mode === 'dark'} … {/if}
15
+ *
16
+ * It stays correct however the change was made: a picker, setTheme(), the OS
17
+ * preference at load, or another component entirely.
18
+ */
19
+ export const presets = $state({ ...presetDefaults, mode: 'light', theme: null });
10
20
  // Keep the mirror in step with the core, however the core was driven.
11
21
  onPresetChange((axis, value) => {
12
22
  presets[axis] = value;
13
23
  });
14
- export function initPresets() {
15
- coreInit();
24
+ onModeChange((mode) => {
25
+ presets.mode = mode;
26
+ presets.theme = getTheme();
27
+ });
28
+ function sync() {
16
29
  for (const axis of Object.keys(presetAxes))
17
30
  presets[axis] = presetState[axis];
31
+ presets.mode = getMode();
32
+ presets.theme = getTheme();
33
+ }
34
+ export function initPresets() {
35
+ coreInit();
36
+ sync();
18
37
  }
19
38
  export function setPreset(axis, value) {
20
39
  coreSet(axis, value);
21
40
  }
41
+ export function setMode(mode) {
42
+ coreSetMode(mode);
43
+ }
44
+ export function toggleMode() {
45
+ const next = coreToggleMode();
46
+ return next;
47
+ }
48
+ export function setTheme(id) {
49
+ coreSetTheme(id);
50
+ }
51
+ export function toggleThemeMode() {
52
+ return coreToggleThemeMode();
53
+ }