@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
|
+
}
|