@adea-ai/ui 0.71.1 → 0.71.3
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/dist/components/composites/appearance-editor/appearance-editor.js +1 -1
- package/dist/components/composites/appearance-editor/appearance-editor.js.map +1 -1
- package/dist/components/composites/stat/stat.d.ts.map +1 -1
- package/dist/components/composites/stat/stat.js +8 -8
- package/dist/components/composites/stat/stat.js.map +1 -1
- package/dist/components/composites/update-dialog/update-dialog.d.ts +7 -0
- package/dist/components/composites/update-dialog/update-dialog.d.ts.map +1 -1
- package/dist/components/composites/update-dialog/update-dialog.js +71 -26
- package/dist/components/composites/update-dialog/update-dialog.js.map +1 -1
- package/dist/components/conversation/internal/chat-composer-shell.js +1 -1
- package/dist/components/conversation/internal/chat-composer-shell.js.map +1 -1
- package/dist/components/conversation/message-composer.d.ts.map +1 -1
- package/dist/components/conversation/message-composer.js +1 -1
- package/dist/components/conversation/message-composer.js.map +1 -1
- package/dist/components/ui/alert/alert.d.ts +3 -1
- package/dist/components/ui/alert/alert.d.ts.map +1 -1
- package/dist/components/ui/alert/alert.js +18 -8
- package/dist/components/ui/alert/alert.js.map +1 -1
- package/dist/components/ui/badge/badge.js +4 -4
- package/dist/components/ui/badge/badge.js.map +1 -1
- package/dist/components/ui/context-menu/context-menu.d.ts.map +1 -1
- package/dist/components/ui/context-menu/context-menu.js +2 -2
- package/dist/components/ui/context-menu/context-menu.js.map +1 -1
- package/dist/components/ui/dialog/dialog.d.ts +17 -1
- package/dist/components/ui/dialog/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog/dialog.js +120 -2
- package/dist/components/ui/dialog/dialog.js.map +1 -1
- package/dist/components/ui/diff-block/diff-block.js +2 -2
- package/dist/components/ui/diff-block/diff-block.js.map +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts +3 -2
- package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu.js +5 -4
- package/dist/components/ui/dropdown-menu/dropdown-menu.js.map +1 -1
- package/dist/components/ui/entity-icon/entity-icon.d.ts +3 -3
- package/dist/components/ui/entity-icon/entity-icon.js +7 -7
- package/dist/components/ui/entity-icon/entity-icon.js.map +1 -1
- package/dist/components/ui/field/field.js +1 -1
- package/dist/components/ui/field/field.js.map +1 -1
- package/dist/components/ui/menubar/menubar.d.ts.map +1 -1
- package/dist/components/ui/menubar/menubar.js +2 -2
- package/dist/components/ui/menubar/menubar.js.map +1 -1
- package/dist/components/ui/modal-dialog/modal-dialog.d.ts +12 -2
- package/dist/components/ui/modal-dialog/modal-dialog.d.ts.map +1 -1
- package/dist/components/ui/modal-dialog/modal-dialog.js +44 -27
- package/dist/components/ui/modal-dialog/modal-dialog.js.map +1 -1
- package/dist/components/ui/status-chip/status-chip.js +4 -4
- package/dist/components/ui/status-chip/status-chip.js.map +1 -1
- package/dist/lib/overlay.d.ts +2 -0
- package/dist/lib/overlay.d.ts.map +1 -1
- package/dist/lib/overlay.js +3 -1
- package/dist/lib/overlay.js.map +1 -1
- package/dist/lib/themes.d.ts +7 -0
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +22 -1
- package/dist/lib/themes.js.map +1 -1
- package/dist/lib/tokens.d.ts.map +1 -1
- package/dist/lib/tokens.js +4 -4
- package/dist/lib/tokens.js.map +1 -1
- package/dist/r/src/components/composites/appearance-editor/appearance-editor.tsx +1 -1
- package/dist/r/src/components/composites/stat/stat.tsx +5 -8
- package/dist/r/src/components/composites/update-dialog/update-dialog.tsx +98 -26
- package/dist/r/src/components/conversation/internal/chat-composer-shell.tsx +1 -1
- package/dist/r/src/components/conversation/message-composer.tsx +3 -1
- package/dist/r/src/components/ui/alert/alert.tsx +20 -7
- package/dist/r/src/components/ui/badge/badge.tsx +4 -4
- package/dist/r/src/components/ui/context-menu/context-menu.tsx +2 -2
- package/dist/r/src/components/ui/dialog/dialog.tsx +204 -2
- package/dist/r/src/components/ui/diff-block/diff-block.tsx +4 -4
- package/dist/r/src/components/ui/dropdown-menu/dropdown-menu.tsx +5 -4
- package/dist/r/src/components/ui/entity-icon/entity-icon.tsx +7 -7
- package/dist/r/src/components/ui/field/field.tsx +1 -1
- package/dist/r/src/components/ui/menubar/menubar.tsx +2 -2
- package/dist/r/src/components/ui/modal-dialog/modal-dialog.tsx +24 -2
- package/dist/r/src/components/ui/status-chip/status-chip.tsx +4 -4
- package/dist/r/src/lib/overlay.ts +4 -0
- package/dist/r/src/lib/themes.ts +29 -0
- package/dist/r/src/lib/tokens.ts +19 -4
- package/dist/r/src/styles/theme.css +10 -10
- package/package.json +2 -2
- package/src/components/composites/appearance-editor/appearance-editor.tsx +1 -1
- package/src/components/composites/stat/stat.tsx +5 -8
- package/src/components/composites/update-dialog/update-dialog.tsx +98 -26
- package/src/components/conversation/internal/chat-composer-shell.tsx +1 -1
- package/src/components/conversation/message-composer.tsx +3 -1
- package/src/components/ui/alert/alert.tsx +20 -7
- package/src/components/ui/badge/badge.tsx +4 -4
- package/src/components/ui/context-menu/context-menu.tsx +2 -2
- package/src/components/ui/dialog/dialog.tsx +204 -2
- package/src/components/ui/diff-block/diff-block.tsx +4 -4
- package/src/components/ui/dropdown-menu/dropdown-menu.tsx +5 -4
- package/src/components/ui/entity-icon/entity-icon.tsx +7 -7
- package/src/components/ui/field/field.tsx +1 -1
- package/src/components/ui/menubar/menubar.tsx +2 -2
- package/src/components/ui/modal-dialog/modal-dialog.tsx +24 -2
- package/src/components/ui/status-chip/status-chip.tsx +4 -4
- package/src/lib/overlay.ts +4 -0
- package/src/lib/themes.ts +29 -0
- package/src/lib/tokens.ts +19 -4
- package/src/styles/theme.css +10 -10
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createEffect,
|
|
3
|
+
createMemo,
|
|
4
|
+
createSignal,
|
|
5
|
+
onCleanup,
|
|
6
|
+
onMount,
|
|
7
|
+
Show,
|
|
8
|
+
splitProps,
|
|
9
|
+
} from 'solid-js'
|
|
10
|
+
import type { Accessor } from 'solid-js'
|
|
2
11
|
import { Check, Download, ExternalLink, LoaderCircle, RefreshCw, Sparkles } from 'lucide-solid'
|
|
3
12
|
import { Badge } from '../../ui/badge'
|
|
4
13
|
import { Button } from '../../ui/button'
|
|
@@ -12,6 +21,7 @@ import {
|
|
|
12
21
|
DialogTitle,
|
|
13
22
|
DialogTrigger,
|
|
14
23
|
} from '../../ui/dialog'
|
|
24
|
+
import { createDialogFocusRestoration } from '../../ui/dialog/dialog'
|
|
15
25
|
import { Progress } from '../../ui/progress'
|
|
16
26
|
import { cn } from '#lib/utils'
|
|
17
27
|
import { formatBytes, formatReleaseDate, plainTextFromMarkdown } from '#lib/version-notes'
|
|
@@ -131,6 +141,12 @@ export type UpdateDialogProps = {
|
|
|
131
141
|
*/
|
|
132
142
|
defaultOpen?: boolean
|
|
133
143
|
onOpenChange?: (open: boolean) => void
|
|
144
|
+
/**
|
|
145
|
+
* Supplies a stable external control to focus when a controlled dialog closes.
|
|
146
|
+
* Useful when a menu or other layer closes before this dialog opens; when omitted,
|
|
147
|
+
* the element focused immediately before the dialog opens is used when available.
|
|
148
|
+
*/
|
|
149
|
+
restoreFocusRef?: Accessor<HTMLElement | undefined>
|
|
134
150
|
/** Render only the panel, for a caller that provides its own dialog. */
|
|
135
151
|
class?: string
|
|
136
152
|
}
|
|
@@ -162,6 +178,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
162
178
|
'open',
|
|
163
179
|
'defaultOpen',
|
|
164
180
|
'onOpenChange',
|
|
181
|
+
'restoreFocusRef',
|
|
165
182
|
'class',
|
|
166
183
|
])
|
|
167
184
|
|
|
@@ -172,23 +189,41 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
172
189
|
local.onOpenChange?.(next)
|
|
173
190
|
}
|
|
174
191
|
|
|
192
|
+
const focusRestoration = createDialogFocusRestoration({
|
|
193
|
+
open,
|
|
194
|
+
restoreFocusRef: local.restoreFocusRef,
|
|
195
|
+
})
|
|
196
|
+
|
|
175
197
|
const appName = () => local.appName ?? 'the application'
|
|
176
198
|
const desktop = () => local.adapter.isDesktopRuntime()
|
|
177
199
|
const [state, setState] = createSignal<UpdateState | null>(null)
|
|
178
200
|
const [busy, setBusy] = createSignal(false)
|
|
179
201
|
const [error, setError] = createSignal('')
|
|
202
|
+
let lifecycleEpoch = 0
|
|
203
|
+
let activeOpenEpoch: number | undefined
|
|
204
|
+
let disposed = false
|
|
205
|
+
|
|
206
|
+
onCleanup(() => {
|
|
207
|
+
disposed = true
|
|
208
|
+
activeOpenEpoch = undefined
|
|
209
|
+
lifecycleEpoch += 1
|
|
210
|
+
})
|
|
180
211
|
|
|
181
|
-
const loadStatus = async () => {
|
|
182
|
-
if (!desktop()) return
|
|
212
|
+
const loadStatus = async (isCurrent: () => boolean = () => !disposed) => {
|
|
213
|
+
if (!desktop() || !isCurrent()) return
|
|
183
214
|
try {
|
|
184
|
-
|
|
215
|
+
const status = await local.adapter.getStatus()
|
|
216
|
+
if (isCurrent()) setState(status)
|
|
185
217
|
} catch (caught) {
|
|
186
|
-
setError(errorMessage(caught, 'Version status is unavailable'))
|
|
218
|
+
if (isCurrent()) setError(errorMessage(caught, 'Version status is unavailable'))
|
|
187
219
|
}
|
|
188
220
|
}
|
|
189
221
|
|
|
190
222
|
onMount(() => {
|
|
191
|
-
if (desktop()
|
|
223
|
+
if (desktop() && !open()) {
|
|
224
|
+
const epoch = lifecycleEpoch
|
|
225
|
+
void loadStatus(() => !disposed && lifecycleEpoch === epoch)
|
|
226
|
+
}
|
|
192
227
|
})
|
|
193
228
|
|
|
194
229
|
const check = async () => {
|
|
@@ -196,15 +231,25 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
196
231
|
setError(local.desktopOnlyMessage ?? `Update checks are available from the desktop app.`)
|
|
197
232
|
return
|
|
198
233
|
}
|
|
234
|
+
const epoch = activeOpenEpoch
|
|
235
|
+
const isCurrent = () =>
|
|
236
|
+
!disposed &&
|
|
237
|
+
epoch !== undefined &&
|
|
238
|
+
activeOpenEpoch === epoch &&
|
|
239
|
+
lifecycleEpoch === epoch &&
|
|
240
|
+
open()
|
|
241
|
+
if (!isCurrent()) return
|
|
199
242
|
setBusy(true)
|
|
200
243
|
setError('')
|
|
201
244
|
try {
|
|
202
|
-
|
|
245
|
+
const checked = await local.adapter.check()
|
|
246
|
+
if (isCurrent()) setState(checked)
|
|
203
247
|
} catch (caught) {
|
|
248
|
+
if (!isCurrent()) return
|
|
204
249
|
setError(errorMessage(caught, 'Could not check for updates'))
|
|
205
|
-
await loadStatus()
|
|
250
|
+
await loadStatus(isCurrent)
|
|
206
251
|
} finally {
|
|
207
|
-
setBusy(false)
|
|
252
|
+
if (isCurrent()) setBusy(false)
|
|
208
253
|
}
|
|
209
254
|
}
|
|
210
255
|
|
|
@@ -214,35 +259,57 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
214
259
|
* disposed scope.
|
|
215
260
|
*/
|
|
216
261
|
createEffect(() => {
|
|
217
|
-
if (!open() || !desktop())
|
|
262
|
+
if (!open() || !desktop()) {
|
|
263
|
+
if (activeOpenEpoch !== undefined) {
|
|
264
|
+
activeOpenEpoch = undefined
|
|
265
|
+
lifecycleEpoch += 1
|
|
266
|
+
}
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
const epoch = ++lifecycleEpoch
|
|
270
|
+
activeOpenEpoch = epoch
|
|
218
271
|
let active = true
|
|
272
|
+
const isCurrent = () =>
|
|
273
|
+
active && !disposed && activeOpenEpoch === epoch && lifecycleEpoch === epoch && open()
|
|
274
|
+
onCleanup(() => {
|
|
275
|
+
active = false
|
|
276
|
+
if (activeOpenEpoch === epoch) {
|
|
277
|
+
activeOpenEpoch = undefined
|
|
278
|
+
lifecycleEpoch += 1
|
|
279
|
+
}
|
|
280
|
+
})
|
|
219
281
|
setError('')
|
|
220
282
|
setBusy(true)
|
|
221
283
|
void (async () => {
|
|
222
284
|
try {
|
|
223
285
|
const current = await local.adapter.getStatus()
|
|
224
|
-
if (!
|
|
286
|
+
if (!isCurrent()) return
|
|
225
287
|
setState(current)
|
|
226
288
|
const checked = await local.adapter.check()
|
|
227
|
-
if (
|
|
289
|
+
if (isCurrent()) setState(checked)
|
|
228
290
|
} catch (caught) {
|
|
229
|
-
if (
|
|
291
|
+
if (isCurrent()) setError(errorMessage(caught, 'Could not check for updates'))
|
|
230
292
|
} finally {
|
|
231
|
-
if (
|
|
293
|
+
if (isCurrent()) setBusy(false)
|
|
232
294
|
}
|
|
233
295
|
})()
|
|
234
|
-
return () => {
|
|
235
|
-
active = false
|
|
236
|
-
}
|
|
237
296
|
})
|
|
238
297
|
|
|
239
298
|
const install = async () => {
|
|
240
299
|
const version = state()?.availableVersion
|
|
241
|
-
|
|
300
|
+
const epoch = activeOpenEpoch
|
|
301
|
+
const isCurrent = () =>
|
|
302
|
+
!disposed &&
|
|
303
|
+
epoch !== undefined &&
|
|
304
|
+
activeOpenEpoch === epoch &&
|
|
305
|
+
lifecycleEpoch === epoch &&
|
|
306
|
+
open()
|
|
307
|
+
if (!version || !isCurrent()) return
|
|
242
308
|
setBusy(true)
|
|
243
309
|
setError('')
|
|
244
310
|
try {
|
|
245
311
|
const next = await local.adapter.install(version)
|
|
312
|
+
if (!isCurrent()) return
|
|
246
313
|
setState(next)
|
|
247
314
|
// A refused install answers with a normal payload whose phase is `failed`.
|
|
248
315
|
// Without this the click looked like nothing happened.
|
|
@@ -250,10 +317,11 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
250
317
|
setError(errorMessage(next.error, 'Update installation failed'))
|
|
251
318
|
}
|
|
252
319
|
} catch (caught) {
|
|
320
|
+
if (!isCurrent()) return
|
|
253
321
|
setError(errorMessage(caught, 'Update installation failed'))
|
|
254
|
-
await loadStatus()
|
|
322
|
+
await loadStatus(isCurrent)
|
|
255
323
|
} finally {
|
|
256
|
-
setBusy(false)
|
|
324
|
+
if (isCurrent()) setBusy(false)
|
|
257
325
|
}
|
|
258
326
|
}
|
|
259
327
|
|
|
@@ -308,7 +376,11 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
308
376
|
</DialogTrigger>
|
|
309
377
|
</Show>
|
|
310
378
|
|
|
311
|
-
<DialogContent
|
|
379
|
+
<DialogContent
|
|
380
|
+
class={cn('max-w-3xl', local.class)}
|
|
381
|
+
onOpenAutoFocus={focusRestoration.onOpenAutoFocus}
|
|
382
|
+
onCloseAutoFocus={focusRestoration.onCloseAutoFocus}
|
|
383
|
+
>
|
|
312
384
|
<DialogHeader>
|
|
313
385
|
<div class="flex items-center gap-3">
|
|
314
386
|
<span class="flex size-10 items-center justify-center rounded-xl bg-primary text-primary-foreground">
|
|
@@ -432,15 +504,15 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
432
504
|
</Show>
|
|
433
505
|
|
|
434
506
|
<Show when={state()?.phase === 'current'}>
|
|
435
|
-
<p class="flex items-center gap-2 text-sm
|
|
436
|
-
<Check class="size-4" aria-hidden="true" />
|
|
507
|
+
<p class="text-foreground flex items-center gap-2 text-sm" role="status">
|
|
508
|
+
<Check class="size-4 text-success" aria-hidden="true" />
|
|
437
509
|
{appName()} is up to date.
|
|
438
510
|
</p>
|
|
439
511
|
</Show>
|
|
440
512
|
|
|
441
513
|
<Show when={state()?.phase === 'installed'}>
|
|
442
|
-
<p class="flex items-center gap-2 text-sm
|
|
443
|
-
<Check class="size-4" aria-hidden="true" />
|
|
514
|
+
<p class="text-foreground flex items-center gap-2 text-sm" role="status">
|
|
515
|
+
<Check class="size-4 text-success" aria-hidden="true" />
|
|
444
516
|
<Show when={state()?.restartRequired} fallback="The update is applied.">
|
|
445
517
|
Restart {appName()} to finish the update.
|
|
446
518
|
</Show>
|
|
@@ -449,7 +521,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
|
|
|
449
521
|
|
|
450
522
|
<Show when={error()}>
|
|
451
523
|
<p
|
|
452
|
-
class="rounded-lg border border-destructive/35 bg-destructive-subtle px-3 py-2 text-sm
|
|
524
|
+
class="text-foreground rounded-lg border border-destructive/35 bg-destructive-subtle px-3 py-2 text-sm"
|
|
453
525
|
role="alert"
|
|
454
526
|
>
|
|
455
527
|
{error()}
|
|
@@ -237,7 +237,7 @@ export function ChatComposerShell<Extra extends object>(props: ChatComposerShell
|
|
|
237
237
|
</Show>
|
|
238
238
|
<Show when={error()}>
|
|
239
239
|
{(message) => (
|
|
240
|
-
<p role="alert" class="text-
|
|
240
|
+
<p role="alert" class="text-foreground mb-1 border-s-2 border-destructive ps-2 text-xs">
|
|
241
241
|
{message()}
|
|
242
242
|
</p>
|
|
243
243
|
)}
|
|
@@ -197,7 +197,9 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
197
197
|
field the user is typing in. */}
|
|
198
198
|
<div aria-live="polite" class="min-h-0">
|
|
199
199
|
<Show when={error()}>
|
|
200
|
-
{(message) =>
|
|
200
|
+
{(message) => (
|
|
201
|
+
<p class="text-foreground border-s-2 border-destructive ps-2 text-xs">{message()}</p>
|
|
202
|
+
)}
|
|
201
203
|
</Show>
|
|
202
204
|
</div>
|
|
203
205
|
</form>
|
|
@@ -14,7 +14,9 @@ import { cn } from '#lib/utils'
|
|
|
14
14
|
*
|
|
15
15
|
* The glyph follows the tone rather than being passed in, so an alert cannot
|
|
16
16
|
* be given a green checkmark on a destructive fill. A caller that genuinely
|
|
17
|
-
* needs a different icon passes `icon`; `icon={null}` removes it.
|
|
17
|
+
* needs a different icon passes `icon`; `icon={null}` removes it. Text stays on
|
|
18
|
+
* the body foreground over the tinted status surface, while the glyph and edge
|
|
19
|
+
* carry the status hue.
|
|
18
20
|
*/
|
|
19
21
|
const alertVariants = cva(
|
|
20
22
|
'relative grid w-full grid-cols-[auto_1fr] items-start gap-x-3 gap-y-1 rounded-lg border px-3 py-2.5 text-sm',
|
|
@@ -22,10 +24,10 @@ const alertVariants = cva(
|
|
|
22
24
|
variants: {
|
|
23
25
|
variant: {
|
|
24
26
|
default: 'bg-card text-card-foreground border-border',
|
|
25
|
-
destructive: 'bg-destructive-subtle text-
|
|
26
|
-
success: 'bg-success-subtle text-
|
|
27
|
-
warning: 'bg-warning-subtle text-
|
|
28
|
-
info: 'bg-info-subtle text-
|
|
27
|
+
destructive: 'bg-destructive-subtle text-foreground border-destructive/30',
|
|
28
|
+
success: 'bg-success-subtle text-foreground border-success/30',
|
|
29
|
+
warning: 'bg-warning-subtle text-foreground border-warning/30',
|
|
30
|
+
info: 'bg-info-subtle text-foreground border-info/30',
|
|
29
31
|
},
|
|
30
32
|
},
|
|
31
33
|
defaultVariants: { variant: 'default' },
|
|
@@ -40,6 +42,14 @@ const alertIcons = {
|
|
|
40
42
|
info: Info,
|
|
41
43
|
} as const
|
|
42
44
|
|
|
45
|
+
const alertIconTones = {
|
|
46
|
+
default: 'text-muted-foreground',
|
|
47
|
+
destructive: 'text-destructive',
|
|
48
|
+
success: 'text-success',
|
|
49
|
+
warning: 'text-warning',
|
|
50
|
+
info: 'text-info',
|
|
51
|
+
} as const
|
|
52
|
+
|
|
43
53
|
type AlertVariantProps = VariantProps<typeof alertVariants>
|
|
44
54
|
|
|
45
55
|
export type AlertProps = ComponentProps<'div'> &
|
|
@@ -61,7 +71,10 @@ export function Alert(props: AlertProps) {
|
|
|
61
71
|
{...rest}
|
|
62
72
|
>
|
|
63
73
|
{local.icon === null ? null : (
|
|
64
|
-
<span
|
|
74
|
+
<span
|
|
75
|
+
data-slot="alert-icon"
|
|
76
|
+
class={cn('row-span-2 pt-0.5 [&_svg]:size-4', alertIconTones[tone()])}
|
|
77
|
+
>
|
|
65
78
|
{local.icon ?? <Dynamic component={alertIcons[tone()]} />}
|
|
66
79
|
</span>
|
|
67
80
|
)}
|
|
@@ -86,7 +99,7 @@ export function AlertDescription(props: ComponentProps<'div'>) {
|
|
|
86
99
|
return (
|
|
87
100
|
<div
|
|
88
101
|
data-slot="alert-description"
|
|
89
|
-
class={cn('col-start-2 text-sm
|
|
102
|
+
class={cn('col-start-2 text-sm [&_p]:leading-relaxed', local.class)}
|
|
90
103
|
{...rest}
|
|
91
104
|
/>
|
|
92
105
|
)
|
|
@@ -20,10 +20,10 @@ export const badgeVariants = cva(
|
|
|
20
20
|
secondary: 'bg-secondary text-secondary-foreground',
|
|
21
21
|
outline: 'border-border text-foreground',
|
|
22
22
|
/** Tinted status fills, for counts and states rather than actions. */
|
|
23
|
-
success: 'bg-success-subtle text-
|
|
24
|
-
warning: 'bg-warning-subtle text-
|
|
25
|
-
destructive: 'bg-destructive-subtle text-
|
|
26
|
-
info: 'bg-info-subtle text-
|
|
23
|
+
success: 'bg-success-subtle text-foreground',
|
|
24
|
+
warning: 'bg-warning-subtle text-foreground',
|
|
25
|
+
destructive: 'bg-destructive-subtle text-foreground',
|
|
26
|
+
info: 'bg-info-subtle text-foreground',
|
|
27
27
|
subtle: 'bg-primary-subtle text-primary',
|
|
28
28
|
},
|
|
29
29
|
size: {
|
|
@@ -4,6 +4,7 @@ import type { ComponentProps } from 'solid-js'
|
|
|
4
4
|
import { splitProps } from 'solid-js'
|
|
5
5
|
import {
|
|
6
6
|
menuContentPadding,
|
|
7
|
+
destructiveMenuItem,
|
|
7
8
|
menuItem,
|
|
8
9
|
menuLabel,
|
|
9
10
|
menuSeparator,
|
|
@@ -65,8 +66,7 @@ export function ContextMenuItem(
|
|
|
65
66
|
class={cn(
|
|
66
67
|
menuItem,
|
|
67
68
|
{
|
|
68
|
-
|
|
69
|
-
local.variant === 'destructive',
|
|
69
|
+
[destructiveMenuItem]: local.variant === 'destructive',
|
|
70
70
|
},
|
|
71
71
|
local.class
|
|
72
72
|
)}
|
|
@@ -1,10 +1,212 @@
|
|
|
1
1
|
import { Dialog as KobalteDialog } from '@kobalte/core/dialog'
|
|
2
2
|
import { X } from 'lucide-solid'
|
|
3
|
-
import type { ComponentProps, JSX } from 'solid-js'
|
|
4
|
-
import { Show, splitProps } from 'solid-js'
|
|
3
|
+
import type { Accessor, ComponentProps, JSX } from 'solid-js'
|
|
4
|
+
import { createRenderEffect, onCleanup, Show, splitProps } from 'solid-js'
|
|
5
5
|
import { overlayMotion, overlayPositioner, overlayScrim, overlaySurface } from '#lib/overlay'
|
|
6
6
|
import { cn } from '#lib/utils'
|
|
7
7
|
|
|
8
|
+
type FocusCycle = {
|
|
9
|
+
content: HTMLElement
|
|
10
|
+
opener: HTMLElement | null
|
|
11
|
+
openerEligible: boolean
|
|
12
|
+
closed: boolean
|
|
13
|
+
focusMovedOutside: boolean
|
|
14
|
+
closeAutoFocusScheduled: boolean
|
|
15
|
+
frame?: number
|
|
16
|
+
fallbackTimer?: number
|
|
17
|
+
observer?: MutationObserver
|
|
18
|
+
onFocusIn: (event: FocusEvent) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function canRestoreFocus(target: HTMLElement): boolean {
|
|
22
|
+
return (
|
|
23
|
+
target.isConnected &&
|
|
24
|
+
target !== document.body &&
|
|
25
|
+
target !== document.documentElement &&
|
|
26
|
+
!target.closest('[inert], [aria-hidden="true"]') &&
|
|
27
|
+
!target.matches(':disabled') &&
|
|
28
|
+
target.getClientRects().length > 0
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasOtherOverlay(content: HTMLElement, opener: HTMLElement): boolean {
|
|
33
|
+
return [...document.querySelectorAll<HTMLElement>('[role="dialog"], [role="alertdialog"]')].some(
|
|
34
|
+
(overlay) =>
|
|
35
|
+
overlay !== content &&
|
|
36
|
+
!content.contains(overlay) &&
|
|
37
|
+
!overlay.contains(opener) &&
|
|
38
|
+
!overlay.closest('[aria-hidden="true"], [inert]') &&
|
|
39
|
+
overlay.getClientRects().length > 0
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type DialogFocusRestorationOptions = {
|
|
44
|
+
open: Accessor<boolean>
|
|
45
|
+
restoreFocusRef?: Accessor<HTMLElement | undefined>
|
|
46
|
+
onOpenAutoFocus?: ComponentProps<typeof KobalteDialog.Content>['onOpenAutoFocus']
|
|
47
|
+
onCloseAutoFocus?: ComponentProps<typeof KobalteDialog.Content>['onCloseAutoFocus']
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds guarded opener capture and restoration to a Kobalte dialog content.
|
|
52
|
+
* Composed surfaces use this internal helper; it is omitted from the public
|
|
53
|
+
* dialog barrel so the restoration policy stays with the dialog components.
|
|
54
|
+
*/
|
|
55
|
+
export function createDialogFocusRestoration({
|
|
56
|
+
open,
|
|
57
|
+
restoreFocusRef,
|
|
58
|
+
onOpenAutoFocus: callerOnOpenAutoFocus,
|
|
59
|
+
onCloseAutoFocus: callerOnCloseAutoFocus,
|
|
60
|
+
}: DialogFocusRestorationOptions) {
|
|
61
|
+
const focusCycles = new WeakMap<HTMLElement, FocusCycle>()
|
|
62
|
+
let activeFocusCycle: FocusCycle | undefined
|
|
63
|
+
let pendingOpener: HTMLElement | null = null
|
|
64
|
+
let pendingOpenerEligible = false
|
|
65
|
+
|
|
66
|
+
createRenderEffect(() => {
|
|
67
|
+
if (!open() || typeof document === 'undefined') {
|
|
68
|
+
pendingOpener = null
|
|
69
|
+
pendingOpenerEligible = false
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
const active = restoreFocusRef?.() ?? document.activeElement
|
|
73
|
+
pendingOpener = active instanceof HTMLElement ? active : null
|
|
74
|
+
pendingOpenerEligible = active instanceof HTMLElement && canRestoreFocus(active)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const finishFocusCycle = (cycle: FocusCycle) => {
|
|
78
|
+
if (cycle.frame !== undefined) window.cancelAnimationFrame(cycle.frame)
|
|
79
|
+
if (cycle.fallbackTimer !== undefined) window.clearTimeout(cycle.fallbackTimer)
|
|
80
|
+
cycle.observer?.disconnect()
|
|
81
|
+
document.removeEventListener('focusin', cycle.onFocusIn)
|
|
82
|
+
if (activeFocusCycle === cycle) activeFocusCycle = undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const scheduleFocusRestore = (cycle: FocusCycle) => {
|
|
86
|
+
if (cycle.frame !== undefined) return
|
|
87
|
+
cycle.frame = window.requestAnimationFrame(() => {
|
|
88
|
+
cycle.frame = undefined
|
|
89
|
+
const opener = cycle.opener
|
|
90
|
+
const active = document.activeElement
|
|
91
|
+
const focusIsAvailable =
|
|
92
|
+
active === document.body ||
|
|
93
|
+
active === document.documentElement ||
|
|
94
|
+
active === cycle.content ||
|
|
95
|
+
(active instanceof Node && cycle.content.contains(active))
|
|
96
|
+
if (
|
|
97
|
+
activeFocusCycle !== cycle ||
|
|
98
|
+
!cycle.closed ||
|
|
99
|
+
cycle.focusMovedOutside ||
|
|
100
|
+
!opener ||
|
|
101
|
+
!opener.isConnected ||
|
|
102
|
+
opener.matches(':disabled') ||
|
|
103
|
+
opener.getClientRects().length === 0 ||
|
|
104
|
+
!focusIsAvailable ||
|
|
105
|
+
hasOtherOverlay(cycle.content, opener)
|
|
106
|
+
) {
|
|
107
|
+
finishFocusCycle(cycle)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Inert and aria-hidden are removed by the dialog's containment cleanup.
|
|
112
|
+
// Wait for that cleanup before returning focus to the original control.
|
|
113
|
+
if (opener.closest('[inert], [aria-hidden="true"]')) return
|
|
114
|
+
if (cycle.openerEligible && canRestoreFocus(opener)) {
|
|
115
|
+
opener.focus({ preventScroll: true })
|
|
116
|
+
}
|
|
117
|
+
finishFocusCycle(cycle)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const onOpenAutoFocus = (event: Event) => {
|
|
122
|
+
const element = event.currentTarget
|
|
123
|
+
if (element instanceof HTMLElement) {
|
|
124
|
+
if (activeFocusCycle) finishFocusCycle(activeFocusCycle)
|
|
125
|
+
|
|
126
|
+
const openerWasCaptured = pendingOpener !== null
|
|
127
|
+
const opener = pendingOpener ?? document.activeElement
|
|
128
|
+
const openerEligible = openerWasCaptured
|
|
129
|
+
? pendingOpenerEligible
|
|
130
|
+
: opener instanceof HTMLElement && canRestoreFocus(opener)
|
|
131
|
+
pendingOpener = null
|
|
132
|
+
pendingOpenerEligible = false
|
|
133
|
+
const cycle: FocusCycle = {
|
|
134
|
+
content: element,
|
|
135
|
+
opener: opener instanceof HTMLElement ? opener : null,
|
|
136
|
+
openerEligible,
|
|
137
|
+
closed: false,
|
|
138
|
+
focusMovedOutside: false,
|
|
139
|
+
closeAutoFocusScheduled: false,
|
|
140
|
+
onFocusIn: (focusEvent) => {
|
|
141
|
+
const target = focusEvent.target
|
|
142
|
+
if (
|
|
143
|
+
!(target instanceof HTMLElement) ||
|
|
144
|
+
target === document.body ||
|
|
145
|
+
target === document.documentElement ||
|
|
146
|
+
target === element ||
|
|
147
|
+
element.contains(target) ||
|
|
148
|
+
target === (opener instanceof HTMLElement ? opener : null)
|
|
149
|
+
) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
cycle.focusMovedOutside = true
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
focusCycles.set(element, cycle)
|
|
156
|
+
activeFocusCycle = cycle
|
|
157
|
+
document.addEventListener('focusin', cycle.onFocusIn)
|
|
158
|
+
}
|
|
159
|
+
callerOnOpenAutoFocus?.(event)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const onCloseAutoFocus = (event: Event) => {
|
|
163
|
+
const element = event.currentTarget
|
|
164
|
+
const cycle = element instanceof HTMLElement ? focusCycles.get(element) : undefined
|
|
165
|
+
callerOnCloseAutoFocus?.(event)
|
|
166
|
+
|
|
167
|
+
if (!cycle) return
|
|
168
|
+
cycle.closed = true
|
|
169
|
+
|
|
170
|
+
// A caller that prevents Kobalte's close autofocus owns the focus decision.
|
|
171
|
+
if (event.defaultPrevented) {
|
|
172
|
+
finishFocusCycle(cycle)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (cycle.fallbackTimer !== undefined) {
|
|
177
|
+
window.clearTimeout(cycle.fallbackTimer)
|
|
178
|
+
cycle.fallbackTimer = undefined
|
|
179
|
+
}
|
|
180
|
+
cycle.closeAutoFocusScheduled = true
|
|
181
|
+
cycle.observer = new MutationObserver(() => scheduleFocusRestore(cycle))
|
|
182
|
+
cycle.observer.observe(document.body, {
|
|
183
|
+
attributes: true,
|
|
184
|
+
attributeFilter: ['aria-hidden', 'inert'],
|
|
185
|
+
childList: true,
|
|
186
|
+
subtree: true,
|
|
187
|
+
})
|
|
188
|
+
scheduleFocusRestore(cycle)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onCleanup(() => {
|
|
192
|
+
const cycle = activeFocusCycle
|
|
193
|
+
if (!cycle) return
|
|
194
|
+
cycle.closed = true
|
|
195
|
+
|
|
196
|
+
// Kobalte dispatches unmount autofocus from a timer. This fallback only
|
|
197
|
+
// releases the scoped focus listener if the content did not dispatch it.
|
|
198
|
+
queueMicrotask(() => {
|
|
199
|
+
if (cycle.closeAutoFocusScheduled) return
|
|
200
|
+
cycle.fallbackTimer = window.setTimeout(() => {
|
|
201
|
+
cycle.fallbackTimer = undefined
|
|
202
|
+
if (!cycle.closeAutoFocusScheduled) finishFocusCycle(cycle)
|
|
203
|
+
}, 0)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
return { onOpenAutoFocus, onCloseAutoFocus }
|
|
208
|
+
}
|
|
209
|
+
|
|
8
210
|
/**
|
|
9
211
|
* Dialog.
|
|
10
212
|
*
|
|
@@ -205,8 +205,8 @@ export function DiffBlock(props: DiffBlockProps) {
|
|
|
205
205
|
{/* The counts are text, so the summary survives greyscale and a reader
|
|
206
206
|
who cannot separate the two line tones still gets the shape of it. */}
|
|
207
207
|
<span class="shrink-0 font-mono text-2xs text-muted-foreground">
|
|
208
|
-
<span class="text-
|
|
209
|
-
<span class="text-
|
|
208
|
+
<span class="text-foreground">+{stats().added}</span>{' '}
|
|
209
|
+
<span class="text-foreground">−{stats().removed}</span>
|
|
210
210
|
</span>
|
|
211
211
|
</span>
|
|
212
212
|
{local.actions}
|
|
@@ -278,8 +278,8 @@ export function DiffSummary(props: { patch: string; class?: string; onSelect?: (
|
|
|
278
278
|
>
|
|
279
279
|
<span class="truncate text-foreground">{name()}</span>
|
|
280
280
|
<span class="shrink-0 text-2xs">
|
|
281
|
-
<span class="text-
|
|
282
|
-
<span class="text-
|
|
281
|
+
<span class="text-foreground">+{stats().added}</span>{' '}
|
|
282
|
+
<span class="text-foreground">−{stats().removed}</span>
|
|
283
283
|
</span>
|
|
284
284
|
</button>
|
|
285
285
|
)
|
|
@@ -4,6 +4,7 @@ import type { ComponentProps } from 'solid-js'
|
|
|
4
4
|
import { splitProps } from 'solid-js'
|
|
5
5
|
import {
|
|
6
6
|
menuContentPadding,
|
|
7
|
+
destructiveMenuItem,
|
|
7
8
|
menuItem,
|
|
8
9
|
menuLabel,
|
|
9
10
|
menuSeparator,
|
|
@@ -25,8 +26,9 @@ import { cn } from '#lib/utils'
|
|
|
25
26
|
* highlight, Escape to close and return focus — which is exactly what a
|
|
26
27
|
* hand-rolled `div` with click handlers gets wrong.
|
|
27
28
|
*
|
|
28
|
-
* The `destructive` variant
|
|
29
|
-
*
|
|
29
|
+
* The `destructive` variant marks the row with a status-coloured edge and
|
|
30
|
+
* highlighted tint while keeping its label on the body foreground. Pairing it
|
|
31
|
+
* with an AlertDialog is the caller's job.
|
|
30
32
|
*/
|
|
31
33
|
export function DropdownMenu(props: ComponentProps<typeof KobalteDropdownMenu>) {
|
|
32
34
|
return <KobalteDropdownMenu {...props} />
|
|
@@ -83,8 +85,7 @@ export function DropdownMenuItem(
|
|
|
83
85
|
class={cn(
|
|
84
86
|
menuItem,
|
|
85
87
|
{
|
|
86
|
-
|
|
87
|
-
local.variant === 'destructive',
|
|
88
|
+
[destructiveMenuItem]: local.variant === 'destructive',
|
|
88
89
|
},
|
|
89
90
|
local.class
|
|
90
91
|
)}
|
|
@@ -17,9 +17,9 @@ import { cn } from '#lib/utils'
|
|
|
17
17
|
* 1. **A glyph is always present.** A brand with no logo gets a monogram, not an
|
|
18
18
|
* empty box. `fallback` takes the name and returns the glyph, so a caller can
|
|
19
19
|
* choose initials, a domain letter, or a kind icon.
|
|
20
|
-
* 2. **The tile owns its own contrast.** `tone` tints the background
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* 2. **The tile owns its own contrast.** `tone` tints the background while
|
|
21
|
+
* monogram text keeps the body foreground; vector glyphs retain the status
|
|
22
|
+
* colour. A caller never picks a colour pair or ships a glyph that disappears.
|
|
23
23
|
* 3. **Size is on the control ladder**, not arbitrary. A tile in a rail and a
|
|
24
24
|
* tile in a detail header are the same two or three sizes every other
|
|
25
25
|
* interactive element uses.
|
|
@@ -51,10 +51,10 @@ export const entityIconVariants = cva(
|
|
|
51
51
|
primary: 'border-transparent bg-primary-subtle text-primary',
|
|
52
52
|
/** Bare: no fill and no edge, for a tile that already sits on a surface. */
|
|
53
53
|
bare: 'border-transparent bg-transparent text-foreground',
|
|
54
|
-
success: 'border-transparent bg-success-subtle text-success',
|
|
55
|
-
warning: 'border-transparent bg-warning-subtle text-warning',
|
|
56
|
-
danger: 'border-transparent bg-destructive-subtle text-destructive',
|
|
57
|
-
info: 'border-transparent bg-info-subtle text-info',
|
|
54
|
+
success: 'border-transparent bg-success-subtle text-foreground [&_svg]:text-success',
|
|
55
|
+
warning: 'border-transparent bg-warning-subtle text-foreground [&_svg]:text-warning',
|
|
56
|
+
danger: 'border-transparent bg-destructive-subtle text-foreground [&_svg]:text-destructive',
|
|
57
|
+
info: 'border-transparent bg-info-subtle text-foreground [&_svg]:text-info',
|
|
58
58
|
},
|
|
59
59
|
},
|
|
60
60
|
defaultVariants: { shape: 'rounded', size: 'md', tone: 'neutral' },
|
|
@@ -44,7 +44,7 @@ export function FieldDescription(props: ComponentProps<typeof KobalteTextField.D
|
|
|
44
44
|
export function FieldError(props: ComponentProps<typeof KobalteTextField.ErrorMessage>) {
|
|
45
45
|
const [local, rest] = splitProps(props, ['class'])
|
|
46
46
|
return (
|
|
47
|
-
<KobalteTextField.ErrorMessage class={cn('text-
|
|
47
|
+
<KobalteTextField.ErrorMessage class={cn('text-foreground text-sm', local.class)} {...rest} />
|
|
48
48
|
)
|
|
49
49
|
}
|
|
50
50
|
|