@adea-ai/ui 0.101.1 → 0.103.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 +27 -11
- package/dist/components/composites/appearance-editor/appearance-choices.d.ts +7 -0
- package/dist/components/composites/appearance-editor/appearance-choices.d.ts.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-choices.js +44 -3
- package/dist/components/composites/appearance-editor/appearance-choices.js.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-editor.d.ts.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-editor.js +7 -3
- package/dist/components/composites/appearance-editor/appearance-editor.js.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-types.d.ts +8 -0
- package/dist/components/composites/appearance-editor/appearance-types.d.ts.map +1 -1
- package/dist/components/composites/update-dialog/update-dialog.d.ts +15 -2
- package/dist/components/composites/update-dialog/update-dialog.d.ts.map +1 -1
- package/dist/components/composites/update-dialog/update-dialog.js +80 -16
- package/dist/components/composites/update-dialog/update-dialog.js.map +1 -1
- package/dist/components/theme/appearance-panel.d.ts +2 -1
- package/dist/components/theme/appearance-panel.d.ts.map +1 -1
- package/dist/components/theme/appearance-panel.js +62 -22
- package/dist/components/theme/appearance-panel.js.map +1 -1
- package/dist/components/theme/theme-provider.d.ts +13 -2
- package/dist/components/theme/theme-provider.d.ts.map +1 -1
- package/dist/components/theme/theme-provider.js +7 -4
- package/dist/components/theme/theme-provider.js.map +1 -1
- package/dist/components/ui/board/board.d.ts +39 -2
- package/dist/components/ui/board/board.d.ts.map +1 -1
- package/dist/components/ui/board/board.js +230 -92
- package/dist/components/ui/board/board.js.map +1 -1
- package/dist/components/ui/board/index.d.ts +1 -1
- package/dist/components/ui/board/index.d.ts.map +1 -1
- package/dist/components/ui/board/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/lib/theme-accents.d.ts +9 -0
- package/dist/lib/theme-accents.d.ts.map +1 -0
- package/dist/lib/theme-accents.js +41 -0
- package/dist/lib/theme-accents.js.map +1 -0
- package/dist/lib/themes.d.ts +69 -5
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +100 -7
- package/dist/lib/themes.js.map +1 -1
- package/dist/r/appearance-editor.json +1 -1
- package/dist/r/board.json +2 -0
- package/dist/r/lib.json +6 -1
- package/dist/r/registry.json +10 -2
- package/dist/r/src/components/composites/appearance-editor/appearance-choices.tsx +41 -4
- package/dist/r/src/components/composites/appearance-editor/appearance-editor.tsx +12 -6
- package/dist/r/src/components/composites/appearance-editor/appearance-types.ts +8 -0
- package/dist/r/src/components/composites/update-dialog/update-dialog.tsx +90 -10
- package/dist/r/src/components/theme/appearance-panel.tsx +60 -29
- package/dist/r/src/components/theme/theme-provider.tsx +31 -7
- package/dist/r/src/components/ui/board/board.tsx +231 -77
- package/dist/r/src/components/ui/board/index.ts +2 -0
- package/dist/r/src/lib/theme-accents.ts +44 -0
- package/dist/r/src/lib/themes.ts +116 -5
- package/dist/r/src/styles/base.css +11 -0
- package/dist/r/theming.json +1 -0
- package/package.json +2 -2
- package/registry.json +10 -2
- package/src/components/composites/appearance-editor/appearance-choices.tsx +41 -4
- package/src/components/composites/appearance-editor/appearance-editor.tsx +12 -6
- package/src/components/composites/appearance-editor/appearance-types.ts +8 -0
- package/src/components/composites/update-dialog/update-dialog.tsx +90 -10
- package/src/components/theme/appearance-panel.tsx +60 -29
- package/src/components/theme/theme-provider.tsx +31 -7
- package/src/components/ui/board/board.tsx +231 -77
- package/src/components/ui/board/index.ts +2 -0
- package/src/index.ts +7 -0
- package/src/lib/theme-accents.ts +44 -0
- package/src/lib/themes.ts +116 -5
- package/src/styles/base.css +11 -0
package/registry.json
CHANGED
|
@@ -152,6 +152,8 @@
|
|
|
152
152
|
"description": "",
|
|
153
153
|
"dependencies": [],
|
|
154
154
|
"registryDependencies": [
|
|
155
|
+
"@adea-ai/ui/badge",
|
|
156
|
+
"@adea-ai/ui/typography",
|
|
155
157
|
"lib"
|
|
156
158
|
],
|
|
157
159
|
"files": [
|
|
@@ -1903,7 +1905,7 @@
|
|
|
1903
1905
|
"title": "Appearance Editor",
|
|
1904
1906
|
"description": "",
|
|
1905
1907
|
"dependencies": [
|
|
1906
|
-
"@adea-ai/themes@^0.
|
|
1908
|
+
"@adea-ai/themes@^0.9.5",
|
|
1907
1909
|
"@kobalte/core@^0.13.14",
|
|
1908
1910
|
"lucide-solid@^1.48.0"
|
|
1909
1911
|
],
|
|
@@ -2349,6 +2351,7 @@
|
|
|
2349
2351
|
"title": "Theming",
|
|
2350
2352
|
"description": "",
|
|
2351
2353
|
"dependencies": [
|
|
2354
|
+
"@adea-ai/themes@^0.9.5",
|
|
2352
2355
|
"lucide-solid@^1.48.0"
|
|
2353
2356
|
],
|
|
2354
2357
|
"registryDependencies": [
|
|
@@ -2456,7 +2459,7 @@
|
|
|
2456
2459
|
"title": "Class-name and token helpers",
|
|
2457
2460
|
"description": "The private helpers the components import: the class-name merge, the variant recipes, the token manifest and the theme bridge. Installed automatically as a peer of any component that needs it.",
|
|
2458
2461
|
"dependencies": [
|
|
2459
|
-
"@adea-ai/themes@^0.
|
|
2462
|
+
"@adea-ai/themes@^0.9.5",
|
|
2460
2463
|
"class-variance-authority@^0.7.1",
|
|
2461
2464
|
"cn@^0.4.0"
|
|
2462
2465
|
],
|
|
@@ -2492,6 +2495,11 @@
|
|
|
2492
2495
|
"type": "registry:lib",
|
|
2493
2496
|
"target": "lib/overlay.ts"
|
|
2494
2497
|
},
|
|
2498
|
+
{
|
|
2499
|
+
"path": "src/lib/theme-accents.ts",
|
|
2500
|
+
"type": "registry:lib",
|
|
2501
|
+
"target": "lib/theme-accents.ts"
|
|
2502
|
+
},
|
|
2495
2503
|
{
|
|
2496
2504
|
"path": "src/lib/themes.ts",
|
|
2497
2505
|
"type": "registry:lib",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// the app port's manual roving focus; it does not replace the donor hierarchy.
|
|
26
26
|
import { RadioGroup as Radio } from '@kobalte/core/radio-group'
|
|
27
27
|
import { For, Show } from 'solid-js'
|
|
28
|
+
import { isThemeAccentId } from '#lib/theme-accents'
|
|
28
29
|
import type { AppearanceEditorProps } from './appearance-types'
|
|
29
30
|
import { ThemeMiniature, ThemeMiniatureSplit } from './theme-preview'
|
|
30
31
|
|
|
@@ -82,16 +83,30 @@ export function ModeChoices(props: AppearanceEditorProps) {
|
|
|
82
83
|
)
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Whether the draft accent is the host's custom colour: not the theme default,
|
|
88
|
+
* not a preset, and not a theme accent id. A theme accent id counts as known even
|
|
89
|
+
* when the current pair cannot offer it — it is a stored role, not a colour the
|
|
90
|
+
* user typed, and showing it in the custom field would invite editing `ansi-blue`.
|
|
91
|
+
*/
|
|
92
|
+
export function isCustomAccent(props: AppearanceEditorProps): boolean {
|
|
93
|
+
const accent = props.draft.accent
|
|
94
|
+
return (
|
|
95
|
+
accent !== 'theme' &&
|
|
96
|
+
!props.accentOptions.some((option) => option.id === accent) &&
|
|
97
|
+
!isThemeAccentId(accent)
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
85
101
|
export function AccentChoices(props: AppearanceEditorProps) {
|
|
86
102
|
// The resolved appearance decides which side of a preset pair — and which
|
|
87
103
|
// theme accent — the swatches paint, so the row previews each choice as the
|
|
88
104
|
// user will see it.
|
|
89
105
|
const resolvedAppearance = () =>
|
|
90
106
|
props.draft.mode === 'system' ? props.resolvedAppearance : props.draft.mode
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const accentSelection = () => (custom() ? 'custom' : props.draft.accent)
|
|
107
|
+
// The accents the previewed theme pair carries, offered after the presets.
|
|
108
|
+
const themeAccents = () => props.themeAccentOptions ?? []
|
|
109
|
+
const accentSelection = () => (isCustomAccent(props) ? 'custom' : props.draft.accent)
|
|
95
110
|
return (
|
|
96
111
|
<Radio
|
|
97
112
|
value={accentSelection()}
|
|
@@ -152,6 +167,28 @@ export function AccentChoices(props: AppearanceEditorProps) {
|
|
|
152
167
|
</Radio.Item>
|
|
153
168
|
)}
|
|
154
169
|
</For>
|
|
170
|
+
<For each={themeAccents()}>
|
|
171
|
+
{(option) => (
|
|
172
|
+
<Radio.Item
|
|
173
|
+
value={option.id}
|
|
174
|
+
data-theme-accent={option.id}
|
|
175
|
+
class="rounded-full focus-within:ring-3 focus-within:ring-ring/50"
|
|
176
|
+
>
|
|
177
|
+
<Radio.ItemInput />
|
|
178
|
+
<Radio.ItemLabel class="block size-7 cursor-pointer rounded-full border border-dashed p-0.5 data-[checked]:border-solid data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
|
|
179
|
+
<span
|
|
180
|
+
class="block size-full rounded-full"
|
|
181
|
+
aria-hidden="true"
|
|
182
|
+
style={{
|
|
183
|
+
'background-color':
|
|
184
|
+
resolvedAppearance() === 'light' ? option.light : option.dark,
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
<span class="sr-only">{`Theme ${option.label.toLowerCase()}`}</span>
|
|
188
|
+
</Radio.ItemLabel>
|
|
189
|
+
</Radio.Item>
|
|
190
|
+
)}
|
|
191
|
+
</For>
|
|
155
192
|
</div>
|
|
156
193
|
<Radio.Item value="custom">
|
|
157
194
|
<Radio.ItemInput />
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
PopoverTrigger,
|
|
47
47
|
} from '../../ui/popover/popover'
|
|
48
48
|
import type { AppearanceEditorProps, AppearancePopoverProps } from './appearance-types'
|
|
49
|
-
import { ModeChoices, AccentChoices, GlassChoices } from './appearance-choices'
|
|
49
|
+
import { ModeChoices, AccentChoices, GlassChoices, isCustomAccent } from './appearance-choices'
|
|
50
50
|
import { SettingsRow, TerminalRow, ThemeRow } from './appearance-rows'
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -60,13 +60,19 @@ import { SettingsRow, TerminalRow, ThemeRow } from './appearance-rows'
|
|
|
60
60
|
export function AppearanceEditor(props: AppearanceEditorProps) {
|
|
61
61
|
const errorId = createUniqueId()
|
|
62
62
|
const saveReasonId = createUniqueId()
|
|
63
|
-
const custom = () =>
|
|
64
|
-
props.draft.accent !== 'theme' &&
|
|
65
|
-
!props.accentOptions.some((option) => option.id === props.draft.accent)
|
|
63
|
+
const custom = () => isCustomAccent(props)
|
|
66
64
|
const accentDescription = () => {
|
|
67
65
|
if (props.draft.accent === 'theme') return "Theme default · Uses the palette's intended color."
|
|
68
|
-
const
|
|
69
|
-
return `${
|
|
66
|
+
const preset = props.accentOptions.find((option) => option.id === props.draft.accent)
|
|
67
|
+
if (preset) return `${preset.label} · Controls, glyphs, selections, code, and activity.`
|
|
68
|
+
if (custom()) return 'Custom · Controls, glyphs, selections, code, and activity.'
|
|
69
|
+
const offered = props.themeAccentOptions?.find((option) => option.id === props.draft.accent)
|
|
70
|
+
// A stored theme accent survives a theme switch by id; when the new pair
|
|
71
|
+
// cannot offer that slot the host falls back to the theme's own primary,
|
|
72
|
+
// and the description says so rather than naming a colour not on screen.
|
|
73
|
+
return offered
|
|
74
|
+
? `Theme ${offered.label.toLowerCase()} · The theme's own color, on controls and activity.`
|
|
75
|
+
: "Theme default · This theme does not carry that accent, so it uses the palette's intended color."
|
|
70
76
|
}
|
|
71
77
|
const glassDescription = () =>
|
|
72
78
|
props.draft.surface === 'opaque'
|
|
@@ -25,6 +25,14 @@ export type AppearanceEditorProps = {
|
|
|
25
25
|
resolvedAppearance: 'light' | 'dark'
|
|
26
26
|
themes: readonly AdeaThemeRecord[]
|
|
27
27
|
accentOptions: readonly AccentPreset[]
|
|
28
|
+
/**
|
|
29
|
+
* The accents the previewed light/dark pair carries itself, offered after
|
|
30
|
+
* `accentOptions` — `themeAccentsFor(lightThemeId, darkThemeId)` from
|
|
31
|
+
* `@adea-ai/ui`. Supplied by the host because computing them measures through
|
|
32
|
+
* the catalogue's normalization, which the editor does not load. Omitted, the
|
|
33
|
+
* row offers the presets only.
|
|
34
|
+
*/
|
|
35
|
+
themeAccentOptions?: readonly AccentPreset[]
|
|
28
36
|
onChange: (patch: Partial<AppearanceDraft>) => void
|
|
29
37
|
onSave: () => void
|
|
30
38
|
onCancel: () => void
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
Show,
|
|
8
8
|
splitProps,
|
|
9
9
|
} from 'solid-js'
|
|
10
|
-
import type { Accessor } from 'solid-js'
|
|
10
|
+
import type { Accessor, JSX } from 'solid-js'
|
|
11
11
|
import {
|
|
12
12
|
Check,
|
|
13
13
|
CircleStop,
|
|
@@ -148,6 +148,13 @@ export type UpdateAdapter = Readonly<{
|
|
|
148
148
|
isDesktopRuntime(): boolean
|
|
149
149
|
}>
|
|
150
150
|
|
|
151
|
+
type ChannelControlActions = Readonly<{
|
|
152
|
+
/** Disabled while the app is checking, downloading, installing, or saving a channel. */
|
|
153
|
+
disabled: Accessor<boolean>
|
|
154
|
+
/** Persist a channel change, then check that channel without retaining a stale offer. */
|
|
155
|
+
recheck(beforeCheck?: () => Promise<void>): Promise<void>
|
|
156
|
+
}>
|
|
157
|
+
|
|
151
158
|
export type UpdateDialogProps = {
|
|
152
159
|
adapter: UpdateAdapter
|
|
153
160
|
/** The application's name, used in the title and the copy. */
|
|
@@ -160,6 +167,12 @@ export type UpdateDialogProps = {
|
|
|
160
167
|
fallbackVersion?: string
|
|
161
168
|
/** Copy for the adapter's non-desktop case. */
|
|
162
169
|
desktopOnlyMessage?: string
|
|
170
|
+
/**
|
|
171
|
+
* Optional host-owned release-channel control, rendered in the version status
|
|
172
|
+
* card. The host owns its accessible label and calls `recheck` after a selection;
|
|
173
|
+
* the optional callback persists that selection before the dialog checks it.
|
|
174
|
+
*/
|
|
175
|
+
channelControl?: (actions: ChannelControlActions) => JSX.Element
|
|
163
176
|
/** A controlled open state. Omit for the trigger to manage its own. */
|
|
164
177
|
open?: boolean
|
|
165
178
|
/**
|
|
@@ -220,6 +233,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
220
233
|
'changelog',
|
|
221
234
|
'fallbackVersion',
|
|
222
235
|
'desktopOnlyMessage',
|
|
236
|
+
'channelControl',
|
|
223
237
|
'open',
|
|
224
238
|
'defaultOpen',
|
|
225
239
|
'onOpenChange',
|
|
@@ -248,6 +262,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
248
262
|
const [cancelPending, setCancelPending] = createSignal(false)
|
|
249
263
|
let lifecycleEpoch = 0
|
|
250
264
|
let activeOpenEpoch: number | undefined
|
|
265
|
+
let channelCheckUnconfirmed = false
|
|
251
266
|
let disposed = false
|
|
252
267
|
|
|
253
268
|
onCleanup(() => {
|
|
@@ -263,12 +278,15 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
263
278
|
const previous = state()
|
|
264
279
|
const hasActionableSnapshot = (candidate: UpdateState | null) =>
|
|
265
280
|
candidate?.phase === 'available' || isBusy(candidate)
|
|
281
|
+
const ignoresUnconfirmedChannelSnapshot =
|
|
282
|
+
channelCheckUnconfirmed && hasActionableSnapshot(status)
|
|
266
283
|
const preservesSnapshot =
|
|
267
284
|
previous?.phase === 'available'
|
|
268
285
|
? status.phase === 'available' || isBusy(status)
|
|
269
286
|
: isBusy(previous) && isBusy(status)
|
|
270
287
|
if (
|
|
271
288
|
isCurrent() &&
|
|
289
|
+
!ignoresUnconfirmedChannelSnapshot &&
|
|
272
290
|
!(checkFailed() && hasActionableSnapshot(previous) && !preservesSnapshot)
|
|
273
291
|
) {
|
|
274
292
|
setState(status)
|
|
@@ -291,10 +309,10 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
291
309
|
setError(message)
|
|
292
310
|
}
|
|
293
311
|
|
|
294
|
-
const applyCheckResult = (next: UpdateState) => {
|
|
312
|
+
const applyCheckResult = (next: UpdateState, preserveOnFailure = true) => {
|
|
295
313
|
if (next.phase === 'failed') {
|
|
296
314
|
const current = state()
|
|
297
|
-
if (!current) setState(next)
|
|
315
|
+
if (!current || !preserveOnFailure) setState(next)
|
|
298
316
|
failCheck(next.error, 'Could not check for updates')
|
|
299
317
|
return
|
|
300
318
|
}
|
|
@@ -303,7 +321,10 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
303
321
|
setError('')
|
|
304
322
|
}
|
|
305
323
|
|
|
306
|
-
const check = async (
|
|
324
|
+
const check = async (
|
|
325
|
+
beforeCheck?: () => Promise<void>,
|
|
326
|
+
clearStaleOffer = beforeCheck !== undefined
|
|
327
|
+
) => {
|
|
307
328
|
if (!desktop()) {
|
|
308
329
|
setError(local.desktopOnlyMessage ?? `Update checks are available from the desktop app.`)
|
|
309
330
|
return
|
|
@@ -316,16 +337,43 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
316
337
|
lifecycleEpoch === epoch &&
|
|
317
338
|
open()
|
|
318
339
|
if (!isCurrent()) return
|
|
340
|
+
if (clearStaleOffer && working()) return
|
|
341
|
+
if (clearStaleOffer) channelCheckUnconfirmed = true
|
|
342
|
+
const discardStaleOffer = clearStaleOffer || channelCheckUnconfirmed
|
|
319
343
|
setBusy(true)
|
|
320
344
|
setError('')
|
|
321
345
|
setCheckFailed(false)
|
|
346
|
+
if (discardStaleOffer) {
|
|
347
|
+
const current = state()
|
|
348
|
+
if (current) setState({ phase: 'checking', currentVersion: current.currentVersion })
|
|
349
|
+
}
|
|
350
|
+
let channelSaved = beforeCheck === undefined
|
|
322
351
|
try {
|
|
352
|
+
await beforeCheck?.()
|
|
353
|
+
channelSaved = true
|
|
354
|
+
if (!isCurrent()) return
|
|
323
355
|
const checked = await local.adapter.check()
|
|
324
|
-
if (isCurrent())
|
|
356
|
+
if (isCurrent()) {
|
|
357
|
+
applyCheckResult(checked, !discardStaleOffer)
|
|
358
|
+
if (discardStaleOffer && checked.phase !== 'failed') channelCheckUnconfirmed = false
|
|
359
|
+
}
|
|
325
360
|
} catch (caught) {
|
|
326
361
|
if (isCurrent()) {
|
|
327
|
-
|
|
328
|
-
|
|
362
|
+
if (discardStaleOffer) {
|
|
363
|
+
const fallback = channelSaved
|
|
364
|
+
? 'Could not check for updates'
|
|
365
|
+
: 'Could not save the update channel'
|
|
366
|
+
const message = errorMessage(caught, fallback)
|
|
367
|
+
setState({
|
|
368
|
+
phase: 'failed',
|
|
369
|
+
currentVersion: state()?.currentVersion ?? '',
|
|
370
|
+
error: message,
|
|
371
|
+
})
|
|
372
|
+
failCheck(message, fallback)
|
|
373
|
+
} else {
|
|
374
|
+
failCheck(caught, 'Could not check for updates')
|
|
375
|
+
await loadStatus(isCurrent)
|
|
376
|
+
}
|
|
329
377
|
}
|
|
330
378
|
} finally {
|
|
331
379
|
if (isCurrent()) setBusy(false)
|
|
@@ -366,11 +414,31 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
366
414
|
try {
|
|
367
415
|
const current = await local.adapter.getStatus()
|
|
368
416
|
if (!isCurrent()) return
|
|
369
|
-
|
|
417
|
+
const discardStaleOffer = channelCheckUnconfirmed
|
|
418
|
+
setState(
|
|
419
|
+
discardStaleOffer
|
|
420
|
+
? { phase: 'checking', currentVersion: current.currentVersion }
|
|
421
|
+
: current
|
|
422
|
+
)
|
|
370
423
|
const checked = await local.adapter.check()
|
|
371
|
-
if (isCurrent())
|
|
424
|
+
if (isCurrent()) {
|
|
425
|
+
applyCheckResult(checked, !discardStaleOffer)
|
|
426
|
+
if (discardStaleOffer && checked.phase !== 'failed') channelCheckUnconfirmed = false
|
|
427
|
+
}
|
|
372
428
|
} catch (caught) {
|
|
373
|
-
if (isCurrent())
|
|
429
|
+
if (isCurrent()) {
|
|
430
|
+
if (channelCheckUnconfirmed) {
|
|
431
|
+
const message = errorMessage(caught, 'Could not check for updates')
|
|
432
|
+
setState({
|
|
433
|
+
phase: 'failed',
|
|
434
|
+
currentVersion: state()?.currentVersion ?? '',
|
|
435
|
+
error: message,
|
|
436
|
+
})
|
|
437
|
+
failCheck(message, 'Could not check for updates')
|
|
438
|
+
} else {
|
|
439
|
+
failCheck(caught, 'Could not check for updates')
|
|
440
|
+
}
|
|
441
|
+
}
|
|
374
442
|
} finally {
|
|
375
443
|
if (isCurrent()) setBusy(false)
|
|
376
444
|
}
|
|
@@ -497,6 +565,8 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
497
565
|
return value ? plainTextFromMarkdown(value) : ''
|
|
498
566
|
}
|
|
499
567
|
const working = () => busy() || cancelPending() || isBusy(state())
|
|
568
|
+
const channelControlDisabled = () => !desktop() || working()
|
|
569
|
+
const recheckChannel = (beforeCheck?: () => Promise<void>) => check(beforeCheck, true)
|
|
500
570
|
const isCurrent = () => state()?.phase === 'current' && !error() && !checkFailed() && !working()
|
|
501
571
|
|
|
502
572
|
const triggerLabel = () => {
|
|
@@ -644,6 +714,16 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
644
714
|
</Show>
|
|
645
715
|
</div>
|
|
646
716
|
</div>
|
|
717
|
+
<Show when={local.channelControl}>
|
|
718
|
+
{(channelControl) => (
|
|
719
|
+
<div class="mt-4 border-t border-border pt-4">
|
|
720
|
+
{channelControl()({
|
|
721
|
+
disabled: channelControlDisabled,
|
|
722
|
+
recheck: recheckChannel,
|
|
723
|
+
})}
|
|
724
|
+
</div>
|
|
725
|
+
)}
|
|
726
|
+
</Show>
|
|
647
727
|
</section>
|
|
648
728
|
|
|
649
729
|
<Show
|
|
@@ -24,7 +24,8 @@ import { Heading } from '../ui/typography'
|
|
|
24
24
|
* 2. **Themes** — one for each appearance. Both are shown at once, because the
|
|
25
25
|
* choice is per-appearance and hiding the other half makes it feel like one
|
|
26
26
|
* setting that keeps changing.
|
|
27
|
-
* 3. **Accent** — adea's presets,
|
|
27
|
+
* 3. **Accent** — adea's presets, then the accents the selected theme pair
|
|
28
|
+
* carries itself, layered on whichever theme is active.
|
|
28
29
|
* 4. **Font** — the interface face.
|
|
29
30
|
*
|
|
30
31
|
* The panel reads and writes the provider's preference and keeps no state of its
|
|
@@ -37,7 +38,7 @@ const DARK_THEMES = themesForAppearance('dark')
|
|
|
37
38
|
|
|
38
39
|
export function AppearancePanel(props: AppearancePanelProps) {
|
|
39
40
|
const [local, rest] = splitProps(props, ['class'])
|
|
40
|
-
const { selection, setSelection, resolvedAppearance } = useTheme()
|
|
41
|
+
const { selection, setSelection, resolvedAppearance, themeAccents } = useTheme()
|
|
41
42
|
|
|
42
43
|
return (
|
|
43
44
|
<div data-slot="appearance-panel" class={cn('flex flex-col gap-8', local.class)} {...rest}>
|
|
@@ -95,38 +96,38 @@ export function AppearancePanel(props: AppearancePanelProps) {
|
|
|
95
96
|
</Heading>
|
|
96
97
|
<p class="max-w-prose text-sm text-muted-foreground">
|
|
97
98
|
Colours the interactive roles — the primary button, the focus ring, a selected row — on
|
|
98
|
-
top of the theme above
|
|
99
|
-
none of them can make a label
|
|
99
|
+
top of the theme above — one of the presets, or a colour the theme carries itself. Every
|
|
100
|
+
option is measured against the surface it lands on, so none of them can make a label
|
|
101
|
+
unreadable.
|
|
100
102
|
</p>
|
|
101
103
|
</header>
|
|
102
104
|
<div role="radiogroup" aria-label="Accent" class="flex flex-wrap gap-2">
|
|
103
105
|
<For each={accentPresets}>
|
|
104
106
|
{(preset) => (
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
</Button>
|
|
107
|
+
<AccentOption
|
|
108
|
+
id={preset.id}
|
|
109
|
+
label={preset.label}
|
|
110
|
+
checked={selection().accent === preset.id}
|
|
111
|
+
swatch={
|
|
112
|
+
preset.id === 'theme'
|
|
113
|
+
? 'var(--primary)'
|
|
114
|
+
: `var(--accent-${preset.id}, ${preset.dark})`
|
|
115
|
+
}
|
|
116
|
+
onSelect={() => setSelection({ accent: preset.id })}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
</For>
|
|
120
|
+
{/* The theme's own accents follow the presets. Their values are the
|
|
121
|
+
theme pair's, so the swatch shows the side that is active now. */}
|
|
122
|
+
<For each={themeAccents()}>
|
|
123
|
+
{(accent) => (
|
|
124
|
+
<AccentOption
|
|
125
|
+
id={accent.id}
|
|
126
|
+
label={`Theme ${accent.label.toLowerCase()}`}
|
|
127
|
+
checked={selection().accent === accent.id}
|
|
128
|
+
swatch={resolvedAppearance() === 'dark' ? accent.dark : accent.light}
|
|
129
|
+
onSelect={() => setSelection({ accent: accent.id })}
|
|
130
|
+
/>
|
|
130
131
|
)}
|
|
131
132
|
</For>
|
|
132
133
|
</div>
|
|
@@ -163,6 +164,36 @@ export function AppearancePanel(props: AppearancePanelProps) {
|
|
|
163
164
|
)
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
function AccentOption(props: {
|
|
168
|
+
id: string
|
|
169
|
+
label: string
|
|
170
|
+
checked: boolean
|
|
171
|
+
swatch: string
|
|
172
|
+
onSelect: () => void
|
|
173
|
+
}) {
|
|
174
|
+
return (
|
|
175
|
+
<Button
|
|
176
|
+
as="button"
|
|
177
|
+
type="button"
|
|
178
|
+
variant="outline"
|
|
179
|
+
size="sm"
|
|
180
|
+
role="radio"
|
|
181
|
+
data-accent-option={props.id}
|
|
182
|
+
aria-checked={props.checked}
|
|
183
|
+
aria-label={props.label}
|
|
184
|
+
class={cn({ 'border-ring ring-3 ring-primary-subtle': props.checked })}
|
|
185
|
+
onClick={() => props.onSelect()}
|
|
186
|
+
>
|
|
187
|
+
<span
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
class="size-3 rounded-full border border-border"
|
|
190
|
+
style={{ 'background-color': props.swatch }}
|
|
191
|
+
/>
|
|
192
|
+
{props.label}
|
|
193
|
+
</Button>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
166
197
|
function ThemeSection(props: {
|
|
167
198
|
title: string
|
|
168
199
|
description: string
|
|
@@ -9,11 +9,14 @@ import {
|
|
|
9
9
|
useContext,
|
|
10
10
|
} from 'solid-js'
|
|
11
11
|
import { accentPresets, fontOptions } from '#lib/tokens'
|
|
12
|
+
import type { AccentPreset } from '@adea-ai/themes'
|
|
12
13
|
import {
|
|
13
14
|
accentVariables,
|
|
14
15
|
builtinThemes,
|
|
15
16
|
defaultDarkThemeId,
|
|
16
17
|
defaultLightThemeId,
|
|
18
|
+
isThemeAccentId,
|
|
19
|
+
themeAccentsFor,
|
|
17
20
|
themeById,
|
|
18
21
|
themeCssVariables,
|
|
19
22
|
type ThemeAppearance,
|
|
@@ -33,7 +36,8 @@ import {
|
|
|
33
36
|
* - **appearance** — light, dark, or follow the system.
|
|
34
37
|
* - **a theme per appearance** — so a user can run Catppuccin Mocha by night and
|
|
35
38
|
* Solarized Light by day without re-choosing when the system flips.
|
|
36
|
-
* - **an accent** — adea's six presets,
|
|
39
|
+
* - **an accent** — adea's six presets, or one of the accents the selected theme
|
|
40
|
+
* pair carries itself (`ansi-blue`…), which re-colour the interactive roles on
|
|
37
41
|
* top of whichever theme is active.
|
|
38
42
|
* - **a font** — the interface face.
|
|
39
43
|
*
|
|
@@ -57,7 +61,11 @@ export type ThemeSelection = {
|
|
|
57
61
|
lightThemeId: string
|
|
58
62
|
/** The variant to use when the resolved appearance is dark. */
|
|
59
63
|
darkThemeId: string
|
|
60
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* An `accentPresets` id, a theme accent id from `themeAccents()` (`ansi-blue`…),
|
|
66
|
+
* or `theme` for the variant's own primary. A theme accent the current pair does
|
|
67
|
+
* not offer falls back to the variant's own primary.
|
|
68
|
+
*/
|
|
61
69
|
accent: string
|
|
62
70
|
/** A `fontOptions` id, or `space-grotesk` for the default. */
|
|
63
71
|
font: string
|
|
@@ -111,6 +119,11 @@ export type ThemeContextValue = {
|
|
|
111
119
|
setSelection: (patch: Partial<ThemeSelection>) => void
|
|
112
120
|
/** The catalogue, so a picker does not import it separately. */
|
|
113
121
|
themes: readonly ThemeVariant[]
|
|
122
|
+
/**
|
|
123
|
+
* The accents the selected light/dark pair carries, offered beside the presets.
|
|
124
|
+
* Follows the selection: a different theme pair may offer different slots.
|
|
125
|
+
*/
|
|
126
|
+
themeAccents: () => readonly AccentPreset[]
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
const ThemeContext = createContext<ThemeContextValue>()
|
|
@@ -157,9 +170,15 @@ function readSelection(storageKey: string): ThemeSelection {
|
|
|
157
170
|
themeById(parsed.darkThemeId ?? '')?.appearance === 'dark'
|
|
158
171
|
? (parsed.darkThemeId as string)
|
|
159
172
|
: defaultDarkThemeId,
|
|
160
|
-
accent
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
// A theme accent is kept whenever it names a slot, even one the stored pair
|
|
174
|
+
// cannot offer: the id is role-shaped so it survives a theme switch, and an
|
|
175
|
+
// unoffered slot already resolves to the theme's own primary when applied.
|
|
176
|
+
accent:
|
|
177
|
+
typeof parsed.accent === 'string' &&
|
|
178
|
+
(accentPresets.some((preset) => preset.id === parsed.accent) ||
|
|
179
|
+
isThemeAccentId(parsed.accent))
|
|
180
|
+
? parsed.accent
|
|
181
|
+
: defaultThemeSelection.accent,
|
|
163
182
|
font: fontOptions.some((option) => option.id === parsed.font)
|
|
164
183
|
? (parsed.font as string)
|
|
165
184
|
: defaultThemeSelection.font,
|
|
@@ -246,7 +265,7 @@ export function ThemeProvider(props: ThemeProviderProps) {
|
|
|
246
265
|
// them meant the attribute was set, the rules were present, and nothing
|
|
247
266
|
// changed on screen.
|
|
248
267
|
const properties = { ...themeCssVariables(active) }
|
|
249
|
-
const accent = accentVariables(selection().accent, resolvedAppearance())
|
|
268
|
+
const accent = accentVariables(selection().accent, resolvedAppearance(), selection())
|
|
250
269
|
if (accent) Object.assign(properties, accent)
|
|
251
270
|
|
|
252
271
|
for (const [name, value] of Object.entries(properties)) {
|
|
@@ -258,7 +277,7 @@ export function ThemeProvider(props: ThemeProviderProps) {
|
|
|
258
277
|
root.dataset['appearance'] = resolvedAppearance()
|
|
259
278
|
root.style.colorScheme = resolvedAppearance()
|
|
260
279
|
|
|
261
|
-
if (
|
|
280
|
+
if (!accent) root.removeAttribute('data-accent')
|
|
262
281
|
else root.dataset['accent'] = selection().accent
|
|
263
282
|
|
|
264
283
|
if (selection().font === 'space-grotesk') root.removeAttribute('data-font')
|
|
@@ -272,11 +291,16 @@ export function ThemeProvider(props: ThemeProviderProps) {
|
|
|
272
291
|
else root.dataset['density'] = resolvedDensity(selection())
|
|
273
292
|
})
|
|
274
293
|
|
|
294
|
+
const themeAccents = createMemo(() =>
|
|
295
|
+
themeAccentsFor(selection().lightThemeId, selection().darkThemeId)
|
|
296
|
+
)
|
|
297
|
+
|
|
275
298
|
const value: ThemeContextValue = {
|
|
276
299
|
selection,
|
|
277
300
|
resolvedAppearance,
|
|
278
301
|
variant,
|
|
279
302
|
themes: builtinThemes,
|
|
303
|
+
themeAccents,
|
|
280
304
|
setSelection(patch) {
|
|
281
305
|
const next = { ...selection(), ...patch }
|
|
282
306
|
setSelectionState(next)
|