@rokkit/app 1.0.0-next.128

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 ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@rokkit/app",
3
+ "version": "1.0.0-next.128",
4
+ "description": "App-level controls for Rokkit applications - theme management and UI chrome",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "svelte": "./src/index.ts",
10
+ "types": "./src/index.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./src/index.ts",
14
+ "svelte": "./src/index.ts",
15
+ "default": "./src/index.ts"
16
+ },
17
+ "./types": {
18
+ "types": "./src/types/index.ts",
19
+ "default": "./src/types/index.ts"
20
+ }
21
+ },
22
+ "files": [
23
+ "src"
24
+ ],
25
+ "scripts": {
26
+ "check": "svelte-check --tsconfig ./tsconfig.json",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "build": "echo 'No build step needed for source-only package'"
30
+ },
31
+ "dependencies": {
32
+ "@rokkit/core": "1.0.0-next.128",
33
+ "@rokkit/states": "1.0.0-next.128",
34
+ "@rokkit/ui": "1.0.0-next.128"
35
+ },
36
+ "peerDependencies": {
37
+ "svelte": "^5.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
41
+ "@testing-library/jest-dom": "^6.9.1",
42
+ "@testing-library/svelte": "^5.3.1",
43
+ "@testing-library/user-event": "^14.6.1",
44
+ "@vitest/browser": "^4.0.18",
45
+ "playwright": "^1.58.2",
46
+ "svelte": "^5.53.5",
47
+ "svelte-check": "^4.4.3",
48
+ "typescript": "^5.9.3",
49
+ "vite": "^7.3.1",
50
+ "vitest": "^4.0.18"
51
+ }
52
+ }
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+ import { vibe } from '@rokkit/states'
4
+ import { Toggle } from '@rokkit/ui'
5
+ import type { ThemeSwitcherToggleProps } from '../types/theme-switcher.js'
6
+ import {
7
+ defaultThemeSwitcherIcons,
8
+ buildThemeSwitcherOptions
9
+ } from '../types/theme-switcher.js'
10
+ import { ColorModeManager, type ColorMode } from '../utils/color-mode.svelte.js'
11
+
12
+ let {
13
+ modes = ['system', 'light', 'dark'],
14
+ includeSystem = true,
15
+ icons: userIcons,
16
+ labels: userLabels = {},
17
+ showLabels = false,
18
+ size = 'sm',
19
+ disabled = false,
20
+ class: className = '',
21
+ item: itemSnippet,
22
+ onchange
23
+ }: ThemeSwitcherToggleProps = $props()
24
+
25
+ const icons = $derived({ ...defaultThemeSwitcherIcons, ...userIcons })
26
+ const effectiveModes = $derived(includeSystem ? modes : modes.filter((m) => m !== 'system'))
27
+ const options = $derived(buildThemeSwitcherOptions(icons, effectiveModes, userLabels))
28
+
29
+ const manager = new ColorModeManager(vibe)
30
+ let value = $derived(manager.mode)
31
+
32
+ onMount(() => {
33
+ return manager.listen()
34
+ })
35
+
36
+ function handleChange(newValue: unknown) {
37
+ const mode = newValue as ColorMode
38
+ manager.mode = mode
39
+ onchange?.(mode)
40
+ }
41
+ </script>
42
+
43
+ <Toggle
44
+ {options}
45
+ {value}
46
+ onchange={handleChange}
47
+ {showLabels}
48
+ {size}
49
+ {disabled}
50
+ class={className}
51
+ item={itemSnippet}
52
+ />
@@ -0,0 +1 @@
1
+ export { default as ThemeSwitcherToggle } from './ThemeSwitcherToggle.svelte'
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ // Components
2
+ export { ThemeSwitcherToggle } from './components/index.js'
3
+
4
+ // Types
5
+ export * from './types/index.js'
6
+
7
+ // Utilities
8
+ export {
9
+ ColorModeManager,
10
+ resolveMode,
11
+ type ColorMode,
12
+ type ResolvedMode
13
+ } from './utils/color-mode.svelte.js'
@@ -0,0 +1 @@
1
+ export * from './theme-switcher.js'
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ThemeSwitcherToggle Types
3
+ *
4
+ * Types and defaults for the theme mode switcher component.
5
+ */
6
+
7
+ import type { ToggleItemSnippet } from '@rokkit/ui'
8
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
9
+ import { messages } from '@rokkit/states'
10
+ import type { ColorMode } from '../utils/color-mode.svelte.js'
11
+
12
+ // =============================================================================
13
+ // Icons
14
+ // =============================================================================
15
+
16
+ export interface ThemeSwitcherIcons {
17
+ system?: string
18
+ light?: string
19
+ dark?: string
20
+ }
21
+
22
+ export const defaultThemeSwitcherIcons: Required<ThemeSwitcherIcons> = {
23
+ system: DEFAULT_STATE_ICONS.mode.system,
24
+ light: DEFAULT_STATE_ICONS.mode.light,
25
+ dark: DEFAULT_STATE_ICONS.mode.dark
26
+ }
27
+
28
+ // =============================================================================
29
+ // Options
30
+ // =============================================================================
31
+
32
+ export interface ThemeSwitcherOption {
33
+ value: ColorMode
34
+ text: string
35
+ icon: string
36
+ [key: string]: unknown
37
+ }
38
+
39
+ export interface ThemeSwitcherLabels {
40
+ system?: string
41
+ light?: string
42
+ dark?: string
43
+ }
44
+
45
+ /**
46
+ * Build toggle options from icons, modes, and labels
47
+ */
48
+ export function buildThemeSwitcherOptions(
49
+ icons: Required<ThemeSwitcherIcons>,
50
+ modes: ColorMode[],
51
+ labels: ThemeSwitcherLabels = {}
52
+ ): ThemeSwitcherOption[] {
53
+ const mergedLabels = { ...messages.current.mode, ...labels }
54
+ const all: ThemeSwitcherOption[] = [
55
+ { value: 'system', text: mergedLabels.system, icon: icons.system },
56
+ { value: 'light', text: mergedLabels.light, icon: icons.light },
57
+ { value: 'dark', text: mergedLabels.dark, icon: icons.dark }
58
+ ]
59
+ return all.filter((o) => modes.includes(o.value))
60
+ }
61
+
62
+ // =============================================================================
63
+ // Component Props
64
+ // =============================================================================
65
+
66
+ export interface ThemeSwitcherToggleProps {
67
+ /** Which modes to show. Default: ['system', 'light', 'dark'] */
68
+ modes?: ColorMode[]
69
+
70
+ /** Whether to include system mode. Default: true. Shortcut for filtering 'system' from modes. */
71
+ includeSystem?: boolean
72
+
73
+ /** Override icons per mode */
74
+ icons?: ThemeSwitcherIcons
75
+
76
+ /** Override labels per mode. Merged over messages.current.mode */
77
+ labels?: ThemeSwitcherLabels
78
+
79
+ /** Show text labels alongside icons. Default: false */
80
+ showLabels?: boolean
81
+
82
+ /** Size variant. Default: 'sm' */
83
+ size?: 'sm' | 'md' | 'lg'
84
+
85
+ /** Disabled state */
86
+ disabled?: boolean
87
+
88
+ /** Additional CSS classes */
89
+ class?: string
90
+
91
+ /** Custom snippet for rendering toggle items */
92
+ item?: ToggleItemSnippet
93
+
94
+ /** Called when mode changes */
95
+ onchange?: (mode: ColorMode) => void
96
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Color Mode Manager
3
+ *
4
+ * Bridges three-mode UI (system/light/dark) with vibe's two-mode store (light/dark).
5
+ * Resolves 'system' to actual OS preference via matchMedia.
6
+ */
7
+
8
+ export type ColorMode = 'system' | 'light' | 'dark'
9
+ export type ResolvedMode = 'light' | 'dark'
10
+
11
+ /**
12
+ * Resolve a ColorMode to an actual light/dark value.
13
+ * When 'system', queries the OS preference via matchMedia.
14
+ */
15
+ export function resolveMode(mode: ColorMode): ResolvedMode {
16
+ if (mode === 'system') {
17
+ if (typeof window !== 'undefined') {
18
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
19
+ }
20
+ return 'dark'
21
+ }
22
+ return mode
23
+ }
24
+
25
+ /**
26
+ * Theme target interface — anything with a settable mode property.
27
+ * Compatible with the vibe singleton from @rokkit/states.
28
+ */
29
+ interface ThemeTarget {
30
+ mode: string
31
+ }
32
+
33
+ /**
34
+ * Reactive color mode manager that tracks system/light/dark,
35
+ * resolves to actual light/dark, and syncs to a theme target (e.g. vibe).
36
+ */
37
+ export class ColorModeManager {
38
+ #mode = $state<ColorMode>('system')
39
+ #resolved = $state<ResolvedMode>('dark')
40
+ #target: ThemeTarget
41
+
42
+ constructor(target: ThemeTarget, initialMode: ColorMode = 'system') {
43
+ this.#target = target
44
+ this.#mode = initialMode
45
+ this.#resolved = resolveMode(initialMode)
46
+ this.#target.mode = this.#resolved
47
+ }
48
+
49
+ get mode(): ColorMode {
50
+ return this.#mode
51
+ }
52
+
53
+ set mode(value: ColorMode) {
54
+ if (value === this.#mode) return
55
+ this.#mode = value
56
+ this.#resolved = resolveMode(value)
57
+ this.#target.mode = this.#resolved
58
+ }
59
+
60
+ get resolved(): ResolvedMode {
61
+ return this.#resolved
62
+ }
63
+
64
+ /**
65
+ * Start listening for OS preference changes.
66
+ * Returns a cleanup function to stop listening.
67
+ * Call in onMount or $effect.root.
68
+ */
69
+ listen(): () => void {
70
+ if (typeof window === 'undefined') return () => {}
71
+
72
+ const mq = window.matchMedia('(prefers-color-scheme: dark)')
73
+ const handler = () => {
74
+ if (this.#mode === 'system') {
75
+ this.#resolved = resolveMode('system')
76
+ this.#target.mode = this.#resolved
77
+ }
78
+ }
79
+ mq.addEventListener('change', handler)
80
+ return () => mq.removeEventListener('change', handler)
81
+ }
82
+ }