@adea-ai/ui 0.22.2 → 0.23.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 +6 -3
- package/dist/components/account-drawer.d.ts +3 -3
- package/dist/components/account-drawer.js +119 -19
- package/dist/components/on-screen-controls.d.ts +2 -1
- package/dist/components/on-screen-controls.js +158 -46
- package/dist/components/scene-settings.d.ts +3 -3
- package/dist/components/scene-settings.js +247 -28
- package/dist/components/theme-provider.d.ts +27 -5
- package/dist/components/theme-provider.js +96 -7
- package/dist/components/theme-toggle.d.ts +4 -4
- package/dist/components/theme-toggle.js +62 -18
- package/dist/components/ui/badge.d.ts +4 -3
- package/dist/components/ui/badge.js +31 -30
- package/dist/components/ui/button.d.ts +4 -3
- package/dist/components/ui/button.js +49 -32
- package/dist/components/ui/card.d.ts +3 -3
- package/dist/components/ui/card.js +25 -6
- package/dist/components/ui/dialog.d.ts +13 -13
- package/dist/components/ui/dialog.js +94 -26
- package/dist/components/ui/drawer.d.ts +23 -15
- package/dist/components/ui/drawer.js +109 -64
- package/dist/components/ui/dropdown-menu.d.ts +28 -22
- package/dist/components/ui/dropdown-menu.js +165 -36
- package/dist/components/ui/empty.d.ts +7 -6
- package/dist/components/ui/empty.js +78 -26
- package/dist/components/ui/field.d.ts +15 -14
- package/dist/components/ui/field.js +177 -55
- package/dist/components/ui/input.d.ts +2 -2
- package/dist/components/ui/input.js +20 -5
- package/dist/components/ui/label.d.ts +2 -2
- package/dist/components/ui/label.js +15 -5
- package/dist/components/ui/radio-group.d.ts +4 -4
- package/dist/components/ui/radio-group.js +36 -9
- package/dist/components/ui/separator.d.ts +6 -2
- package/dist/components/ui/separator.js +18 -6
- package/dist/components/ui/skeleton.d.ts +2 -1
- package/dist/components/ui/skeleton.js +15 -4
- package/dist/components/ui/spinner.d.ts +4 -3
- package/dist/components/ui/spinner.js +15 -5
- package/dist/components/ui/switch.d.ts +6 -3
- package/dist/components/ui/switch.js +57 -6
- package/dist/components/ui/tabs.d.ts +6 -5
- package/dist/components/ui/tabs.js +52 -24
- package/dist/components/ui/toggle-group.d.ts +10 -8
- package/dist/components/ui/toggle-group.js +91 -20
- package/dist/components/ui/toggle.d.ts +4 -2
- package/dist/components/ui/toggle.js +38 -23
- package/dist/components/ui/tooltip.d.ts +13 -6
- package/dist/components/ui/tooltip.js +51 -14
- package/dist/components/version-dialog.d.ts +2 -2
- package/dist/components/version-dialog.js +322 -129
- package/dist/components/workspace-brand.d.ts +1 -1
- package/dist/components/workspace-brand.js +12 -3
- package/dist/components/workspace-logo.d.ts +2 -2
- package/dist/components/workspace-logo.js +15 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +20 -19
- package/dist/lib/utils.js +7 -4
- package/dist/lib/version-notes.js +14 -29
- package/package.json +18 -16
- package/src/components/account-drawer.tsx +48 -67
- package/src/components/on-screen-controls.tsx +55 -62
- package/src/components/scene-settings.tsx +97 -100
- package/src/components/theme-provider.tsx +122 -22
- package/src/components/theme-toggle.tsx +29 -54
- package/src/components/ui/badge.tsx +14 -23
- package/src/components/ui/button.tsx +9 -11
- package/src/components/ui/card.tsx +9 -6
- package/src/components/ui/dialog.tsx +58 -55
- package/src/components/ui/drawer.tsx +84 -125
- package/src/components/ui/dropdown-menu.tsx +130 -153
- package/src/components/ui/empty.tsx +29 -26
- package/src/components/ui/field.tsx +83 -87
- package/src/components/ui/input.tsx +8 -8
- package/src/components/ui/label.tsx +6 -7
- package/src/components/ui/radio-group.tsx +22 -19
- package/src/components/ui/separator.tsx +16 -9
- package/src/components/ui/skeleton.tsx +6 -3
- package/src/components/ui/spinner.tsx +9 -5
- package/src/components/ui/switch.tsx +47 -20
- package/src/components/ui/tabs.tsx +31 -30
- package/src/components/ui/toggle-group.tsx +65 -54
- package/src/components/ui/toggle.tsx +9 -12
- package/src/components/ui/tooltip.tsx +41 -36
- package/src/components/version-dialog.tsx +144 -144
- package/src/components/workspace-brand.tsx +5 -5
- package/src/components/workspace-logo.tsx +9 -8
- package/src/index.tsx +7 -1
- package/src/styles/base.css +637 -0
- package/src/styles/globals.css +1 -1
- package/tsconfig.json +2 -1
- package/components.json +0 -19
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { Box, Camera, Focus, Grid3X3, UserRoundPen } from 'lucide-solid'
|
|
2
|
+
import { createEffect, createSignal, Show, type JSX } from 'solid-js'
|
|
3
|
+
import { Portal } from 'solid-js/web'
|
|
2
4
|
|
|
3
|
-
import { createPortal } from 'react-dom'
|
|
4
|
-
import { useEffect, useState, type ReactNode } from 'react'
|
|
5
|
-
import { Box, Camera, Focus, Grid3X3, UserRoundPen } from 'lucide-react'
|
|
6
5
|
import { AccountDrawer } from './account-drawer'
|
|
7
6
|
import { Button } from '#components/ui/button'
|
|
8
7
|
|
|
@@ -22,7 +21,7 @@ export type SceneSettingsProps = {
|
|
|
22
21
|
accountLabel?: string
|
|
23
22
|
accountAuthenticated?: boolean
|
|
24
23
|
accountBusy?: boolean
|
|
25
|
-
accountMusicControl?:
|
|
24
|
+
accountMusicControl?: JSX.Element
|
|
26
25
|
onAccountSignIn?: () => void
|
|
27
26
|
onAccountSignOut?: () => void
|
|
28
27
|
showAccountDrawer?: boolean
|
|
@@ -37,59 +36,39 @@ export type SceneSettingsProps = {
|
|
|
37
36
|
onSceneEditorChange?: (value: boolean) => void
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
function usePortalTarget(targetId
|
|
41
|
-
const [target, setTarget] =
|
|
39
|
+
function usePortalTarget(targetId: () => string | undefined) {
|
|
40
|
+
const [target, setTarget] = createSignal<HTMLElement | null>(null)
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
createEffect(() => {
|
|
43
|
+
const id = targetId()
|
|
44
|
+
setTarget(id ? document.getElementById(id) : null)
|
|
45
|
+
})
|
|
46
46
|
|
|
47
47
|
return target
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function renderInTarget(content:
|
|
51
|
-
return target ?
|
|
50
|
+
function renderInTarget(content: JSX.Element, target: HTMLElement | null) {
|
|
51
|
+
return target ? <Portal mount={target}>{content}</Portal> : content
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** Compact scene controls shared by the HQ shell and other scene hosts. */
|
|
55
|
-
export function SceneSettings({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
onCharacterDesignerChange,
|
|
61
|
-
characterDesignerTargetId,
|
|
62
|
-
onOpenRoomDesigner,
|
|
63
|
-
accountTargetId,
|
|
64
|
-
accountLabel,
|
|
65
|
-
accountAuthenticated,
|
|
66
|
-
accountBusy,
|
|
67
|
-
accountMusicControl,
|
|
68
|
-
onAccountSignIn,
|
|
69
|
-
onAccountSignOut,
|
|
70
|
-
showAccountDrawer = true,
|
|
71
|
-
cameraTargetId,
|
|
72
|
-
roomDesignerTargetId,
|
|
73
|
-
sceneEditorTargetId,
|
|
74
|
-
sceneEditorEnabled,
|
|
75
|
-
onSceneEditorChange,
|
|
76
|
-
}: SceneSettingsProps) {
|
|
77
|
-
const cameraTarget = usePortalTarget(cameraTargetId)
|
|
78
|
-
const characterDesignerTarget = usePortalTarget(characterDesignerTargetId)
|
|
79
|
-
const roomDesignerTarget = usePortalTarget(roomDesignerTargetId)
|
|
80
|
-
const sceneEditorTarget = usePortalTarget(sceneEditorTargetId)
|
|
55
|
+
export function SceneSettings(props: SceneSettingsProps) {
|
|
56
|
+
const cameraTarget = usePortalTarget(() => props.cameraTargetId)
|
|
57
|
+
const characterDesignerTarget = usePortalTarget(() => props.characterDesignerTargetId)
|
|
58
|
+
const roomDesignerTarget = usePortalTarget(() => props.roomDesignerTargetId)
|
|
59
|
+
const sceneEditorTarget = usePortalTarget(() => props.sceneEditorTargetId)
|
|
81
60
|
|
|
82
61
|
const cameraControl = (
|
|
83
|
-
<div
|
|
62
|
+
<div class="workspace-camera-control" role="group" aria-label="Camera view">
|
|
84
63
|
<Button
|
|
85
64
|
type="button"
|
|
86
65
|
size="sm"
|
|
87
|
-
variant={cameraViewMode === 'perspective' ? 'default' : 'outline'}
|
|
88
|
-
|
|
66
|
+
variant={props.cameraViewMode === 'perspective' ? 'default' : 'outline'}
|
|
67
|
+
class="workspace-camera-button"
|
|
89
68
|
aria-label="Perspective camera"
|
|
90
69
|
title="Perspective camera"
|
|
91
|
-
aria-pressed={cameraViewMode === 'perspective'}
|
|
92
|
-
onClick={() => onCameraViewModeChange('perspective')}
|
|
70
|
+
aria-pressed={props.cameraViewMode === 'perspective'}
|
|
71
|
+
onClick={() => props.onCameraViewModeChange('perspective')}
|
|
93
72
|
>
|
|
94
73
|
<Camera aria-hidden="true" />
|
|
95
74
|
Perspective
|
|
@@ -97,12 +76,12 @@ export function SceneSettings({
|
|
|
97
76
|
<Button
|
|
98
77
|
type="button"
|
|
99
78
|
size="sm"
|
|
100
|
-
variant={cameraViewMode === 'orthographic' ? 'default' : 'outline'}
|
|
101
|
-
|
|
79
|
+
variant={props.cameraViewMode === 'orthographic' ? 'default' : 'outline'}
|
|
80
|
+
class="workspace-camera-button"
|
|
102
81
|
aria-label="Top-down camera"
|
|
103
82
|
title="Top-down camera"
|
|
104
|
-
aria-pressed={cameraViewMode === 'orthographic'}
|
|
105
|
-
onClick={() => onCameraViewModeChange('orthographic')}
|
|
83
|
+
aria-pressed={props.cameraViewMode === 'orthographic'}
|
|
84
|
+
onClick={() => props.onCameraViewModeChange('orthographic')}
|
|
106
85
|
>
|
|
107
86
|
<Focus aria-hidden="true" />
|
|
108
87
|
Top-down
|
|
@@ -113,16 +92,20 @@ export function SceneSettings({
|
|
|
113
92
|
const characterDesignerControl = (
|
|
114
93
|
<Button
|
|
115
94
|
type="button"
|
|
116
|
-
variant={characterDesignerEnabled ? 'default' : 'outline'}
|
|
95
|
+
variant={props.characterDesignerEnabled ? 'default' : 'outline'}
|
|
117
96
|
size="sm"
|
|
118
97
|
aria-haspopup="dialog"
|
|
119
|
-
aria-label={
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
98
|
+
aria-label={
|
|
99
|
+
props.characterDesignerEnabled ? 'Close character designer' : 'Open character designer'
|
|
100
|
+
}
|
|
101
|
+
title={
|
|
102
|
+
props.characterDesignerEnabled ? 'Close character designer' : 'Open character designer'
|
|
103
|
+
}
|
|
104
|
+
aria-pressed={props.characterDesignerEnabled}
|
|
105
|
+
onClick={() => props.onCharacterDesignerChange?.(!props.characterDesignerEnabled)}
|
|
123
106
|
>
|
|
124
|
-
<UserRoundPen
|
|
125
|
-
<span
|
|
107
|
+
<UserRoundPen class="size-4" aria-hidden="true" />
|
|
108
|
+
<span class="workspace-character-designer-label">Character</span>
|
|
126
109
|
</Button>
|
|
127
110
|
)
|
|
128
111
|
|
|
@@ -134,69 +117,83 @@ export function SceneSettings({
|
|
|
134
117
|
aria-haspopup="dialog"
|
|
135
118
|
aria-label="Open room designer"
|
|
136
119
|
title="Open room designer"
|
|
137
|
-
onClick={onOpenRoomDesigner}
|
|
120
|
+
onClick={() => props.onOpenRoomDesigner?.()}
|
|
138
121
|
>
|
|
139
|
-
<Grid3X3
|
|
140
|
-
<span
|
|
122
|
+
<Grid3X3 class="size-4" aria-hidden="true" />
|
|
123
|
+
<span class="workspace-room-designer-label">Room designer</span>
|
|
141
124
|
</Button>
|
|
142
125
|
)
|
|
143
126
|
|
|
144
127
|
const sceneEditorControl = (
|
|
145
128
|
<Button
|
|
146
129
|
type="button"
|
|
147
|
-
variant={sceneEditorEnabled ? 'default' : 'outline'}
|
|
130
|
+
variant={props.sceneEditorEnabled ? 'default' : 'outline'}
|
|
148
131
|
size="sm"
|
|
149
132
|
aria-haspopup="dialog"
|
|
150
|
-
aria-label={sceneEditorEnabled ? 'Close scene editor' : 'Open scene editor'}
|
|
151
|
-
title={sceneEditorEnabled ? 'Close scene editor' : 'Open scene editor'}
|
|
152
|
-
aria-pressed={sceneEditorEnabled}
|
|
153
|
-
onClick={() => onSceneEditorChange?.(!sceneEditorEnabled)}
|
|
133
|
+
aria-label={props.sceneEditorEnabled ? 'Close scene editor' : 'Open scene editor'}
|
|
134
|
+
title={props.sceneEditorEnabled ? 'Close scene editor' : 'Open scene editor'}
|
|
135
|
+
aria-pressed={props.sceneEditorEnabled}
|
|
136
|
+
onClick={() => props.onSceneEditorChange?.(!props.sceneEditorEnabled)}
|
|
154
137
|
>
|
|
155
|
-
<Box
|
|
156
|
-
<span
|
|
138
|
+
<Box class="size-4" aria-hidden="true" />
|
|
139
|
+
<span class="workspace-scene-editor-label">Scene editor</span>
|
|
157
140
|
</Button>
|
|
158
141
|
)
|
|
159
142
|
|
|
143
|
+
const showCharacterDesignerControl = () =>
|
|
144
|
+
props.characterDesignerEnabled != null &&
|
|
145
|
+
!props.characterDesignerEnabled &&
|
|
146
|
+
Boolean(props.onCharacterDesignerChange)
|
|
147
|
+
|
|
148
|
+
const showRoomDesignerControl = () =>
|
|
149
|
+
Boolean(props.onOpenRoomDesigner) && props.cameraViewMode === 'orthographic'
|
|
150
|
+
|
|
151
|
+
const showSceneEditorControl = () =>
|
|
152
|
+
props.sceneEditorEnabled != null &&
|
|
153
|
+
!props.sceneEditorEnabled &&
|
|
154
|
+
Boolean(props.onSceneEditorChange) &&
|
|
155
|
+
props.cameraViewMode === 'perspective'
|
|
156
|
+
|
|
160
157
|
return (
|
|
161
158
|
<>
|
|
162
|
-
{showAccountDrawer
|
|
159
|
+
<Show when={props.showAccountDrawer ?? true}>
|
|
163
160
|
<AccountDrawer
|
|
164
|
-
accountLabel={accountLabel}
|
|
165
|
-
authenticated={accountAuthenticated}
|
|
166
|
-
busy={accountBusy}
|
|
167
|
-
musicControl={accountMusicControl}
|
|
168
|
-
triggerTargetId={accountTargetId}
|
|
169
|
-
onSignIn={onAccountSignIn}
|
|
170
|
-
onSignOut={onAccountSignOut}
|
|
161
|
+
accountLabel={props.accountLabel}
|
|
162
|
+
authenticated={props.accountAuthenticated}
|
|
163
|
+
busy={props.accountBusy}
|
|
164
|
+
musicControl={props.accountMusicControl}
|
|
165
|
+
triggerTargetId={props.accountTargetId}
|
|
166
|
+
onSignIn={props.onAccountSignIn}
|
|
167
|
+
onSignOut={props.onAccountSignOut}
|
|
171
168
|
/>
|
|
172
|
-
|
|
173
|
-
{allowCameraViewModeChange
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<div
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
169
|
+
</Show>
|
|
170
|
+
<Show when={props.allowCameraViewModeChange ?? true}>
|
|
171
|
+
{renderInTarget(cameraControl, cameraTarget())}
|
|
172
|
+
</Show>
|
|
173
|
+
<Show when={showCharacterDesignerControl()}>
|
|
174
|
+
<Show
|
|
175
|
+
when={characterDesignerTarget()}
|
|
176
|
+
fallback={<div class="fixed right-4 top-16 z-40">{characterDesignerControl}</div>}
|
|
177
|
+
>
|
|
178
|
+
{(target) => <Portal mount={target()}>{characterDesignerControl}</Portal>}
|
|
179
|
+
</Show>
|
|
180
|
+
</Show>
|
|
181
|
+
<Show when={showRoomDesignerControl()}>
|
|
182
|
+
<Show
|
|
183
|
+
when={roomDesignerTarget()}
|
|
184
|
+
fallback={<div class="fixed right-4 top-16 z-40">{roomDesignerControl}</div>}
|
|
185
|
+
>
|
|
186
|
+
{(target) => <Portal mount={target()}>{roomDesignerControl}</Portal>}
|
|
187
|
+
</Show>
|
|
188
|
+
</Show>
|
|
189
|
+
<Show when={showSceneEditorControl()}>
|
|
190
|
+
<Show
|
|
191
|
+
when={sceneEditorTarget()}
|
|
192
|
+
fallback={<div class="fixed right-4 top-16 z-40">{sceneEditorControl}</div>}
|
|
193
|
+
>
|
|
194
|
+
{(target) => <Portal mount={target()}>{sceneEditorControl}</Portal>}
|
|
195
|
+
</Show>
|
|
196
|
+
</Show>
|
|
200
197
|
</>
|
|
201
198
|
)
|
|
202
199
|
}
|
|
@@ -1,30 +1,130 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createEffect,
|
|
4
|
+
createSignal,
|
|
5
|
+
onCleanup,
|
|
6
|
+
useContext,
|
|
7
|
+
type Accessor,
|
|
8
|
+
type JSX,
|
|
9
|
+
type ParentProps,
|
|
10
|
+
} from 'solid-js'
|
|
2
11
|
|
|
3
|
-
|
|
4
|
-
|
|
12
|
+
export type Theme = 'light' | 'dark' | 'system'
|
|
13
|
+
export type ResolvedTheme = 'light' | 'dark'
|
|
5
14
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
type CompatibleNextThemesProviderProps = ComponentProps<typeof NextThemesProvider> & {
|
|
11
|
-
children?: ReactNode
|
|
15
|
+
type ThemeContextValue = {
|
|
16
|
+
theme: Accessor<Theme>
|
|
17
|
+
resolvedTheme: Accessor<ResolvedTheme>
|
|
18
|
+
setTheme: (theme: Theme) => void
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
const STORAGE_KEY = 'theme'
|
|
22
|
+
const DARK_QUERY = '(prefers-color-scheme: dark)'
|
|
23
|
+
|
|
24
|
+
const ThemeContext = createContext<ThemeContextValue>()
|
|
25
|
+
|
|
26
|
+
function readStoredTheme(): Theme {
|
|
27
|
+
if (typeof window === 'undefined') return 'system'
|
|
28
|
+
try {
|
|
29
|
+
const stored = window.localStorage.getItem(STORAGE_KEY)
|
|
30
|
+
return stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system'
|
|
31
|
+
} catch {
|
|
32
|
+
return 'system'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function systemPrefersDark(): boolean {
|
|
37
|
+
return typeof window !== 'undefined' && window.matchMedia(DARK_QUERY).matches
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Applies the resolved theme to the document and suppresses transitions for the
|
|
42
|
+
* frame that carries the change, so a theme switch cannot animate page-wide.
|
|
43
|
+
*/
|
|
44
|
+
function applyTheme(theme: ResolvedTheme, disableTransitionOnChange: boolean): void {
|
|
45
|
+
if (typeof document === 'undefined') return
|
|
46
|
+
|
|
47
|
+
const root = document.documentElement
|
|
48
|
+
let restoreTransitions: (() => void) | undefined
|
|
49
|
+
|
|
50
|
+
if (disableTransitionOnChange) {
|
|
51
|
+
const style = document.createElement('style')
|
|
52
|
+
style.appendChild(document.createTextNode('*{transition:none !important}'))
|
|
53
|
+
document.head.appendChild(style)
|
|
54
|
+
void style.offsetHeight
|
|
55
|
+
restoreTransitions = () => style.remove()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
root.classList.toggle('dark', theme === 'dark')
|
|
59
|
+
root.style.colorScheme = theme
|
|
60
|
+
|
|
61
|
+
if (restoreTransitions) {
|
|
62
|
+
requestAnimationFrame(() => requestAnimationFrame(() => restoreTransitions?.()))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type ThemeProviderProps = ParentProps<{
|
|
67
|
+
defaultTheme?: Theme
|
|
68
|
+
disableTransitionOnChange?: boolean
|
|
69
|
+
}>
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The shared Solid theme provider. It owns the same contract the previous
|
|
73
|
+
* provider exposed to the token layer: a `dark` class on the document element
|
|
74
|
+
* plus a `color-scheme` style, persisted in `localStorage` and defaulting to
|
|
75
|
+
* the system preference.
|
|
76
|
+
*/
|
|
77
|
+
export function ThemeProvider(props: ThemeProviderProps) {
|
|
78
|
+
const [theme, setThemeSignal] = createSignal<Theme>(props.defaultTheme ?? readStoredTheme())
|
|
79
|
+
const [prefersDark, setPrefersDark] = createSignal(systemPrefersDark())
|
|
80
|
+
|
|
81
|
+
const resolvedTheme = (): ResolvedTheme => {
|
|
82
|
+
const current = theme()
|
|
83
|
+
if (current === 'system') return prefersDark() ? 'dark' : 'light'
|
|
84
|
+
return current
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof window !== 'undefined') {
|
|
88
|
+
const media = window.matchMedia(DARK_QUERY)
|
|
89
|
+
const onChange = (event: MediaQueryListEvent) => setPrefersDark(event.matches)
|
|
90
|
+
media.addEventListener('change', onChange)
|
|
91
|
+
onCleanup(() => media.removeEventListener('change', onChange))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
createEffect(() => {
|
|
95
|
+
applyTheme(resolvedTheme(), props.disableTransitionOnChange ?? true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const setTheme = (next: Theme) => {
|
|
99
|
+
setThemeSignal(next)
|
|
100
|
+
try {
|
|
101
|
+
window.localStorage.setItem(STORAGE_KEY, next)
|
|
102
|
+
} catch {
|
|
103
|
+
// A blocked storage API must not break the in-memory theme.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
17
106
|
|
|
18
|
-
/** Shared theme provider used by each Next app in the workspace. */
|
|
19
|
-
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
20
107
|
return (
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
enableSystem
|
|
25
|
-
disableTransitionOnChange
|
|
26
|
-
>
|
|
27
|
-
{children}
|
|
28
|
-
</CompatibleNextThemesProvider>
|
|
108
|
+
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
|
|
109
|
+
{props.children}
|
|
110
|
+
</ThemeContext.Provider>
|
|
29
111
|
)
|
|
30
112
|
}
|
|
113
|
+
|
|
114
|
+
export function useTheme(): ThemeContextValue {
|
|
115
|
+
const context = useContext(ThemeContext)
|
|
116
|
+
if (!context) {
|
|
117
|
+
throw new Error('useTheme must be used within a ThemeProvider.')
|
|
118
|
+
}
|
|
119
|
+
return context
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Pre-paint theme restore for server-rendered documents. Rendered in the
|
|
124
|
+
* document head, it applies the stored or system theme before first paint so
|
|
125
|
+
* hydration never shows the wrong palette.
|
|
126
|
+
*/
|
|
127
|
+
export function ThemeScript(): JSX.Element {
|
|
128
|
+
const script = `(function(){try{var t=localStorage.getItem('${STORAGE_KEY}')||'system';var d=t==='dark'||(t==='system'&&window.matchMedia('${DARK_QUERY}').matches);var r=document.documentElement;r.classList.toggle('dark',d);r.style.colorScheme=d?'dark':'light'}catch(e){}})();`
|
|
129
|
+
return <script innerHTML={script} />
|
|
130
|
+
}
|
|
@@ -1,73 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
import { Monitor, Moon, Sun } from 'lucide-solid'
|
|
2
|
+
import { For } from 'solid-js'
|
|
2
3
|
|
|
3
|
-
import { useSyncExternalStore } from 'react'
|
|
4
|
-
import { Monitor, Moon, Sun } from 'lucide-react'
|
|
5
|
-
import { useTheme } from 'next-themes'
|
|
6
4
|
import { Button } from '#components/ui/button'
|
|
7
5
|
import { cn } from '#lib/utils'
|
|
6
|
+
import { useTheme, type Theme } from './theme-provider'
|
|
7
|
+
|
|
8
|
+
const THEMES: ReadonlyArray<{ value: Theme; label: string; icon: typeof Sun }> = [
|
|
9
|
+
{ value: 'system', label: 'System theme', icon: Monitor },
|
|
10
|
+
{ value: 'light', label: 'Light theme', icon: Sun },
|
|
11
|
+
{ value: 'dark', label: 'Dark theme', icon: Moon },
|
|
12
|
+
]
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
|
-
* Compact sun/moon theme selector. Wired to the
|
|
15
|
+
* Compact sun/moon theme selector. Wired to the shared theme provider so the
|
|
11
16
|
* choice persists across the app (localStorage) and applies to every scene.
|
|
12
17
|
*/
|
|
13
|
-
export function ThemeToggle(
|
|
18
|
+
export function ThemeToggle(props: { class?: string }) {
|
|
14
19
|
const { theme, setTheme } = useTheme()
|
|
15
|
-
const mounted = useSyncExternalStore(
|
|
16
|
-
() => () => undefined,
|
|
17
|
-
() => true,
|
|
18
|
-
() => false
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
// next-themes resolves the saved/system theme after mount. Use the stable
|
|
22
|
-
// light presentation for both SSR and the first client render so the
|
|
23
|
-
// aria/class attributes cannot mismatch during hydration.
|
|
24
|
-
const selectedTheme = mounted ? theme : 'system'
|
|
25
20
|
|
|
26
21
|
return (
|
|
27
22
|
<div
|
|
28
|
-
|
|
23
|
+
class={cn(
|
|
29
24
|
'inline-flex items-center gap-2 rounded-full border border-input bg-background p-2',
|
|
30
|
-
|
|
25
|
+
props.class
|
|
31
26
|
)}
|
|
32
27
|
role="radiogroup"
|
|
33
28
|
aria-label="Theme"
|
|
34
29
|
>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
aria-label="Light theme"
|
|
52
|
-
variant={selectedTheme === 'light' ? 'default' : 'ghost'}
|
|
53
|
-
size="icon-sm"
|
|
54
|
-
onClick={() => setTheme('light')}
|
|
55
|
-
className="rounded-full"
|
|
56
|
-
>
|
|
57
|
-
<Sun className="size-4" aria-hidden="true" />
|
|
58
|
-
</Button>
|
|
59
|
-
<Button
|
|
60
|
-
type="button"
|
|
61
|
-
role="radio"
|
|
62
|
-
aria-checked={selectedTheme === 'dark'}
|
|
63
|
-
aria-label="Dark theme"
|
|
64
|
-
variant={selectedTheme === 'dark' ? 'default' : 'ghost'}
|
|
65
|
-
size="icon-sm"
|
|
66
|
-
onClick={() => setTheme('dark')}
|
|
67
|
-
className="rounded-full"
|
|
68
|
-
>
|
|
69
|
-
<Moon className="size-4" aria-hidden="true" />
|
|
70
|
-
</Button>
|
|
30
|
+
<For each={THEMES}>
|
|
31
|
+
{(option) => (
|
|
32
|
+
<Button
|
|
33
|
+
type="button"
|
|
34
|
+
role="radio"
|
|
35
|
+
aria-checked={theme() === option.value}
|
|
36
|
+
aria-label={option.label}
|
|
37
|
+
variant={theme() === option.value ? 'default' : 'ghost'}
|
|
38
|
+
size="icon-sm"
|
|
39
|
+
onClick={() => setTheme(option.value)}
|
|
40
|
+
class="rounded-full"
|
|
41
|
+
>
|
|
42
|
+
<option.icon class="size-4" aria-hidden="true" />
|
|
43
|
+
</Button>
|
|
44
|
+
)}
|
|
45
|
+
</For>
|
|
71
46
|
</div>
|
|
72
47
|
)
|
|
73
48
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { mergeProps } from '@base-ui/react/merge-props'
|
|
2
|
-
import { useRender } from '@base-ui/react/use-render'
|
|
3
1
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import { splitProps, type ComponentProps } from 'solid-js'
|
|
4
3
|
|
|
5
4
|
import { cn } from '#lib/utils'
|
|
6
5
|
|
|
@@ -24,26 +23,18 @@ const badgeVariants = cva(
|
|
|
24
23
|
}
|
|
25
24
|
)
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
props
|
|
40
|
-
),
|
|
41
|
-
render,
|
|
42
|
-
state: {
|
|
43
|
-
slot: 'badge',
|
|
44
|
-
variant,
|
|
45
|
-
},
|
|
46
|
-
})
|
|
26
|
+
type BadgeProps = ComponentProps<'span'> & VariantProps<typeof badgeVariants>
|
|
27
|
+
|
|
28
|
+
function Badge(props: BadgeProps) {
|
|
29
|
+
const [local, rest] = splitProps(props, ['class', 'variant'])
|
|
30
|
+
return (
|
|
31
|
+
<span
|
|
32
|
+
data-slot="badge"
|
|
33
|
+
data-variant={local.variant ?? 'default'}
|
|
34
|
+
class={cn(badgeVariants({ variant: local.variant }), local.class)}
|
|
35
|
+
{...rest}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
47
38
|
}
|
|
48
39
|
|
|
49
|
-
export { Badge, badgeVariants }
|
|
40
|
+
export { Badge, badgeVariants, type BadgeProps }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Button as ButtonPrimitive } from '@base-ui/react/button'
|
|
2
1
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import { splitProps, type ComponentProps } from 'solid-js'
|
|
3
3
|
|
|
4
4
|
import { cn } from '#lib/utils'
|
|
5
5
|
|
|
@@ -42,19 +42,17 @@ const buttonVariants = cva(
|
|
|
42
42
|
}
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...props
|
|
50
|
-
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
45
|
+
type ButtonProps = ComponentProps<'button'> & VariantProps<typeof buttonVariants>
|
|
46
|
+
|
|
47
|
+
function Button(props: ButtonProps) {
|
|
48
|
+
const [local, rest] = splitProps(props, ['class', 'variant', 'size'])
|
|
51
49
|
return (
|
|
52
|
-
<
|
|
50
|
+
<button
|
|
53
51
|
data-slot="button"
|
|
54
|
-
|
|
55
|
-
{...
|
|
52
|
+
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
|
|
53
|
+
{...rest}
|
|
56
54
|
/>
|
|
57
55
|
)
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
export { Button, buttonVariants }
|
|
58
|
+
export { Button, buttonVariants, type ButtonProps }
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { splitProps, type ComponentProps } from 'solid-js'
|
|
2
|
+
|
|
2
3
|
import { cn } from '#lib/utils'
|
|
3
4
|
|
|
4
|
-
function Card(
|
|
5
|
+
function Card(props: ComponentProps<'div'>) {
|
|
6
|
+
const [local, rest] = splitProps(props, ['class'])
|
|
5
7
|
return (
|
|
6
8
|
<div
|
|
7
9
|
data-slot="card"
|
|
8
|
-
|
|
9
|
-
{...
|
|
10
|
+
class={cn('rounded-xl border bg-card text-card-foreground shadow-sm', local.class)}
|
|
11
|
+
{...rest}
|
|
10
12
|
/>
|
|
11
13
|
)
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
function CardContent(
|
|
15
|
-
|
|
16
|
+
function CardContent(props: ComponentProps<'div'>) {
|
|
17
|
+
const [local, rest] = splitProps(props, ['class'])
|
|
18
|
+
return <div data-slot="card-content" class={cn('p-4', local.class)} {...rest} />
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export { Card, CardContent }
|