@rokkit/app 1.0.5 → 1.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/app",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "description": "App-level controls for Rokkit applications - theme management and UI chrome",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,10 +37,10 @@
37
37
  "build": "echo 'No build step needed for source-only package'"
38
38
  },
39
39
  "dependencies": {
40
- "@rokkit/actions": "1.0.5",
41
- "@rokkit/core": "1.0.5",
42
- "@rokkit/states": "1.0.5",
43
- "@rokkit/ui": "1.0.5"
40
+ "@rokkit/actions": "1.1.1",
41
+ "@rokkit/core": "1.1.1",
42
+ "@rokkit/states": "1.1.1",
43
+ "@rokkit/ui": "1.1.1"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "svelte": "^5.0.0"
@@ -7,6 +7,7 @@
7
7
  import { ColorModeManager, type ColorMode } from '../utils/color-mode.svelte.js'
8
8
 
9
9
  let {
10
+ variant = 'triad',
10
11
  modes = ['system', 'light', 'dark'],
11
12
  includeSystem = true,
12
13
  icons: userIcons,
@@ -20,9 +21,18 @@
20
21
  }: ThemeSwitcherToggleProps = $props()
21
22
 
22
23
  const icons = $derived({ ...defaultThemeSwitcherIcons, ...userIcons })
23
- const effectiveModes = $derived(includeSystem ? modes : modes.filter((m) => m !== 'system'))
24
+
25
+ // `single` and `pair` exclude system; `triad` honors `modes` + `includeSystem`.
26
+ const effectiveModes = $derived.by<ColorMode[]>(() => {
27
+ if (variant === 'single' || variant === 'pair') return ['light', 'dark']
28
+ return includeSystem ? modes : modes.filter((m) => m !== 'system')
29
+ })
30
+
24
31
  const options = $derived(buildThemeSwitcherOptions(icons, effectiveModes, userLabels))
25
32
 
33
+ // `single` → Toggle's button variant (cycles); `pair` / `triad` → group.
34
+ const toggleVariant = $derived<'button' | 'group'>(variant === 'single' ? 'button' : 'group')
35
+
26
36
  const manager = new ColorModeManager(vibe)
27
37
  let value = $derived(manager.mode)
28
38
 
@@ -38,6 +48,7 @@
38
48
  </script>
39
49
 
40
50
  <Toggle
51
+ variant={toggleVariant}
41
52
  {options}
42
53
  {value}
43
54
  onchange={handleChange}
@@ -63,11 +63,31 @@ export function buildThemeSwitcherOptions(
63
63
  // Component Props
64
64
  // =============================================================================
65
65
 
66
+ /**
67
+ * Visual variant.
68
+ * - `'single'` — one icon-only button that cycles between light and dark on click.
69
+ * System mode is not part of the cycle. The icon shows the mode you'd switch *to*.
70
+ * - `'pair'` — two buttons: light and dark. Equivalent to `includeSystem={false}`
71
+ * for the legacy triad UI.
72
+ * - `'triad'` — three buttons: system, light, dark. Default.
73
+ */
74
+ export type ThemeSwitcherVariant = 'single' | 'pair' | 'triad'
75
+
66
76
  export interface ThemeSwitcherToggleProps {
67
- /** Which modes to show. Default: ['system', 'light', 'dark'] */
77
+ /** Visual variant. Default: 'triad'. */
78
+ variant?: ThemeSwitcherVariant
79
+
80
+ /**
81
+ * Which modes to show. Only honored for `variant: 'triad'`.
82
+ * Default: `['system', 'light', 'dark']`.
83
+ */
68
84
  modes?: ColorMode[]
69
85
 
70
- /** Whether to include system mode. Default: true. Shortcut for filtering 'system' from modes. */
86
+ /**
87
+ * Whether to include system mode. Default: true. Shortcut for filtering 'system'
88
+ * from modes. Only honored for `variant: 'triad'`. For `'pair'` and `'single'`
89
+ * system is never included.
90
+ */
71
91
  includeSystem?: boolean
72
92
 
73
93
  /** Override icons per mode */
@@ -76,7 +96,7 @@ export interface ThemeSwitcherToggleProps {
76
96
  /** Override labels per mode. Merged over messages.mode */
77
97
  labels?: ThemeSwitcherLabels
78
98
 
79
- /** Show text labels alongside icons. Default: false */
99
+ /** Show text labels alongside icons. Ignored for `variant: 'single'`. Default: false */
80
100
  showLabels?: boolean
81
101
 
82
102
  /** Size variant. Default: 'sm' */
@@ -88,7 +108,7 @@ export interface ThemeSwitcherToggleProps {
88
108
  /** Additional CSS classes */
89
109
  class?: string
90
110
 
91
- /** Custom snippet for rendering toggle items */
111
+ /** Custom snippet for rendering toggle items (only honored for `'pair'` / `'triad'`) */
92
112
  item?: ToggleItemSnippet
93
113
 
94
114
  /** Called when mode changes */
@@ -42,8 +42,21 @@ export class ColorModeManager {
42
42
  constructor(target: ThemeTarget, initialMode: ColorMode = 'system') {
43
43
  this.#target = target
44
44
  this.#mode = initialMode
45
- this.#resolved = resolveMode(initialMode)
46
- this.#target.mode = this.#resolved
45
+ // Don't override target.mode at construction. If the target already
46
+ // has a meaningful light/dark value (loaded from storage, set by the
47
+ // flash-prevention init script, or persisted by a prior session),
48
+ // adopt it as the resolved state. Only fall back to deriving from
49
+ // `initialMode` when the target has no value to read.
50
+ //
51
+ // Without this guard, every mount of a consumer like
52
+ // `<ThemeSwitcherToggle/>` flipped target.mode to OS preference,
53
+ // causing user-visible mode changes on navigation between routes.
54
+ if (target.mode === 'light' || target.mode === 'dark') {
55
+ this.#resolved = target.mode
56
+ } else {
57
+ this.#resolved = resolveMode(initialMode)
58
+ this.#target.mode = this.#resolved
59
+ }
47
60
  }
48
61
 
49
62
  get mode(): ColorMode {