@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.
Files changed (92) hide show
  1. package/README.md +6 -3
  2. package/dist/components/account-drawer.d.ts +3 -3
  3. package/dist/components/account-drawer.js +119 -19
  4. package/dist/components/on-screen-controls.d.ts +2 -1
  5. package/dist/components/on-screen-controls.js +158 -46
  6. package/dist/components/scene-settings.d.ts +3 -3
  7. package/dist/components/scene-settings.js +247 -28
  8. package/dist/components/theme-provider.d.ts +27 -5
  9. package/dist/components/theme-provider.js +96 -7
  10. package/dist/components/theme-toggle.d.ts +4 -4
  11. package/dist/components/theme-toggle.js +62 -18
  12. package/dist/components/ui/badge.d.ts +4 -3
  13. package/dist/components/ui/badge.js +31 -30
  14. package/dist/components/ui/button.d.ts +4 -3
  15. package/dist/components/ui/button.js +49 -32
  16. package/dist/components/ui/card.d.ts +3 -3
  17. package/dist/components/ui/card.js +25 -6
  18. package/dist/components/ui/dialog.d.ts +13 -13
  19. package/dist/components/ui/dialog.js +94 -26
  20. package/dist/components/ui/drawer.d.ts +23 -15
  21. package/dist/components/ui/drawer.js +109 -64
  22. package/dist/components/ui/dropdown-menu.d.ts +28 -22
  23. package/dist/components/ui/dropdown-menu.js +165 -36
  24. package/dist/components/ui/empty.d.ts +7 -6
  25. package/dist/components/ui/empty.js +78 -26
  26. package/dist/components/ui/field.d.ts +15 -14
  27. package/dist/components/ui/field.js +177 -55
  28. package/dist/components/ui/input.d.ts +2 -2
  29. package/dist/components/ui/input.js +20 -5
  30. package/dist/components/ui/label.d.ts +2 -2
  31. package/dist/components/ui/label.js +15 -5
  32. package/dist/components/ui/radio-group.d.ts +4 -4
  33. package/dist/components/ui/radio-group.js +36 -9
  34. package/dist/components/ui/separator.d.ts +6 -2
  35. package/dist/components/ui/separator.js +18 -6
  36. package/dist/components/ui/skeleton.d.ts +2 -1
  37. package/dist/components/ui/skeleton.js +15 -4
  38. package/dist/components/ui/spinner.d.ts +4 -3
  39. package/dist/components/ui/spinner.js +15 -5
  40. package/dist/components/ui/switch.d.ts +6 -3
  41. package/dist/components/ui/switch.js +57 -6
  42. package/dist/components/ui/tabs.d.ts +6 -5
  43. package/dist/components/ui/tabs.js +52 -24
  44. package/dist/components/ui/toggle-group.d.ts +10 -8
  45. package/dist/components/ui/toggle-group.js +91 -20
  46. package/dist/components/ui/toggle.d.ts +4 -2
  47. package/dist/components/ui/toggle.js +38 -23
  48. package/dist/components/ui/tooltip.d.ts +13 -6
  49. package/dist/components/ui/tooltip.js +51 -14
  50. package/dist/components/version-dialog.d.ts +2 -2
  51. package/dist/components/version-dialog.js +322 -129
  52. package/dist/components/workspace-brand.d.ts +1 -1
  53. package/dist/components/workspace-brand.js +12 -3
  54. package/dist/components/workspace-logo.d.ts +2 -2
  55. package/dist/components/workspace-logo.js +15 -3
  56. package/dist/index.d.ts +1 -1
  57. package/dist/index.js +20 -19
  58. package/dist/lib/utils.js +7 -4
  59. package/dist/lib/version-notes.js +14 -29
  60. package/package.json +18 -16
  61. package/src/components/account-drawer.tsx +48 -67
  62. package/src/components/on-screen-controls.tsx +55 -62
  63. package/src/components/scene-settings.tsx +97 -100
  64. package/src/components/theme-provider.tsx +122 -22
  65. package/src/components/theme-toggle.tsx +29 -54
  66. package/src/components/ui/badge.tsx +14 -23
  67. package/src/components/ui/button.tsx +9 -11
  68. package/src/components/ui/card.tsx +9 -6
  69. package/src/components/ui/dialog.tsx +58 -55
  70. package/src/components/ui/drawer.tsx +84 -125
  71. package/src/components/ui/dropdown-menu.tsx +130 -153
  72. package/src/components/ui/empty.tsx +29 -26
  73. package/src/components/ui/field.tsx +83 -87
  74. package/src/components/ui/input.tsx +8 -8
  75. package/src/components/ui/label.tsx +6 -7
  76. package/src/components/ui/radio-group.tsx +22 -19
  77. package/src/components/ui/separator.tsx +16 -9
  78. package/src/components/ui/skeleton.tsx +6 -3
  79. package/src/components/ui/spinner.tsx +9 -5
  80. package/src/components/ui/switch.tsx +47 -20
  81. package/src/components/ui/tabs.tsx +31 -30
  82. package/src/components/ui/toggle-group.tsx +65 -54
  83. package/src/components/ui/toggle.tsx +9 -12
  84. package/src/components/ui/tooltip.tsx +41 -36
  85. package/src/components/version-dialog.tsx +144 -144
  86. package/src/components/workspace-brand.tsx +5 -5
  87. package/src/components/workspace-logo.tsx +9 -8
  88. package/src/index.tsx +7 -1
  89. package/src/styles/base.css +637 -0
  90. package/src/styles/globals.css +1 -1
  91. package/tsconfig.json +2 -1
  92. package/components.json +0 -19
@@ -1,8 +1,7 @@
1
- 'use client'
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?: ReactNode
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?: string) {
41
- const [target, setTarget] = useState<HTMLElement | null>(null)
39
+ function usePortalTarget(targetId: () => string | undefined) {
40
+ const [target, setTarget] = createSignal<HTMLElement | null>(null)
42
41
 
43
- useEffect(() => {
44
- setTarget(targetId ? document.getElementById(targetId) : null)
45
- }, [targetId])
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: ReactNode, target: HTMLElement | null) {
51
- return target ? createPortal(content, target) : content
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
- cameraViewMode,
57
- onCameraViewModeChange,
58
- allowCameraViewModeChange = true,
59
- characterDesignerEnabled,
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 className="workspace-camera-control" role="group" aria-label="Camera view">
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
- className="workspace-camera-button"
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
- className="workspace-camera-button"
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={characterDesignerEnabled ? 'Close character designer' : 'Open character designer'}
120
- title={characterDesignerEnabled ? 'Close character designer' : 'Open character designer'}
121
- aria-pressed={characterDesignerEnabled}
122
- onClick={() => onCharacterDesignerChange?.(!characterDesignerEnabled)}
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 className="size-4" aria-hidden="true" />
125
- <span className="workspace-character-designer-label">Character</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 className="size-4" aria-hidden="true" />
140
- <span className="workspace-room-designer-label">Room designer</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 className="size-4" aria-hidden="true" />
156
- <span className="workspace-scene-editor-label">Scene editor</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
- ) : null}
173
- {allowCameraViewModeChange ? renderInTarget(cameraControl, cameraTarget) : null}
174
- {characterDesignerEnabled != null &&
175
- !characterDesignerEnabled &&
176
- onCharacterDesignerChange ? (
177
- characterDesignerTarget ? (
178
- createPortal(characterDesignerControl, characterDesignerTarget)
179
- ) : (
180
- <div className="fixed right-4 top-16 z-40">{characterDesignerControl}</div>
181
- )
182
- ) : null}
183
- {onOpenRoomDesigner && cameraViewMode === 'orthographic' ? (
184
- roomDesignerTarget ? (
185
- createPortal(roomDesignerControl, roomDesignerTarget)
186
- ) : (
187
- <div className="fixed right-4 top-16 z-40">{roomDesignerControl}</div>
188
- )
189
- ) : null}
190
- {sceneEditorEnabled != null &&
191
- !sceneEditorEnabled &&
192
- onSceneEditorChange &&
193
- cameraViewMode === 'perspective' ? (
194
- sceneEditorTarget ? (
195
- createPortal(sceneEditorControl, sceneEditorTarget)
196
- ) : (
197
- <div className="fixed right-4 top-16 z-40">{sceneEditorControl}</div>
198
- )
199
- ) : null}
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
- 'use client'
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
- import { ThemeProvider as NextThemesProvider } from 'next-themes'
4
- import type { ComponentProps, ReactElement, ReactNode } from 'react'
12
+ export type Theme = 'light' | 'dark' | 'system'
13
+ export type ResolvedTheme = 'light' | 'dark'
5
14
 
6
- // Bun 1.4 can resolve next-themes against a separate React type instance. The
7
- // runtime component is compatible, but TypeScript then drops `children` from
8
- // its JSX props. Keep the shared provider's public contract stable by
9
- // normalizing the third-party component's props at this boundary.
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 CompatibleNextThemesProvider = NextThemesProvider as unknown as (
15
- props: CompatibleNextThemesProviderProps
16
- ) => ReactElement
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
- <CompatibleNextThemesProvider
22
- attribute="class"
23
- defaultTheme="system"
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
- 'use client'
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 next-themes provider so 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({ className }: { className?: string }) {
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
- className={cn(
23
+ class={cn(
29
24
  'inline-flex items-center gap-2 rounded-full border border-input bg-background p-2',
30
- className
25
+ props.class
31
26
  )}
32
27
  role="radiogroup"
33
28
  aria-label="Theme"
34
29
  >
35
- <Button
36
- type="button"
37
- role="radio"
38
- aria-checked={selectedTheme === 'system'}
39
- aria-label="System theme"
40
- variant={selectedTheme === 'system' ? 'default' : 'ghost'}
41
- size="icon-sm"
42
- onClick={() => setTheme('system')}
43
- className="rounded-full"
44
- >
45
- <Monitor className="size-4" aria-hidden="true" />
46
- </Button>
47
- <Button
48
- type="button"
49
- role="radio"
50
- aria-checked={selectedTheme === 'light'}
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
- function Badge({
28
- className,
29
- variant = 'default',
30
- render,
31
- ...props
32
- }: useRender.ComponentProps<'span'> & VariantProps<typeof badgeVariants>) {
33
- return useRender({
34
- defaultTagName: 'span',
35
- props: mergeProps<'span'>(
36
- {
37
- className: cn(badgeVariants({ variant }), className),
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
- function Button({
46
- className,
47
- variant = 'default',
48
- size = 'default',
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
- <ButtonPrimitive
50
+ <button
53
51
  data-slot="button"
54
- className={cn(buttonVariants({ variant, size, className }))}
55
- {...props}
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 { HTMLAttributes } from 'react'
1
+ import { splitProps, type ComponentProps } from 'solid-js'
2
+
2
3
  import { cn } from '#lib/utils'
3
4
 
4
- function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
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
- className={cn('rounded-xl border bg-card text-card-foreground shadow-sm', className)}
9
- {...props}
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({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
15
- return <div data-slot="card-content" className={cn('p-4', className)} {...props} />
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 }