@adea-ai/ui 0.96.0 → 0.97.1

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 (49) hide show
  1. package/dist/components/composites/about-dialog/about-dialog.d.ts +17 -0
  2. package/dist/components/composites/about-dialog/about-dialog.d.ts.map +1 -0
  3. package/dist/components/composites/about-dialog/about-dialog.js +167 -0
  4. package/dist/components/composites/about-dialog/about-dialog.js.map +1 -0
  5. package/dist/components/composites/about-dialog/index.d.ts +3 -0
  6. package/dist/components/composites/about-dialog/index.d.ts.map +1 -0
  7. package/dist/components/composites/about-dialog/index.js +2 -0
  8. package/dist/components/composites/account-menu/account-menu.d.ts +20 -1
  9. package/dist/components/composites/account-menu/account-menu.d.ts.map +1 -1
  10. package/dist/components/composites/account-menu/account-menu.js +103 -50
  11. package/dist/components/composites/account-menu/account-menu.js.map +1 -1
  12. package/dist/components/composites/account-menu/index.d.ts +1 -1
  13. package/dist/components/composites/account-menu/index.d.ts.map +1 -1
  14. package/dist/components/composites/account-menu/index.js +2 -2
  15. package/dist/components/composites/help-center/help-center.d.ts +19 -0
  16. package/dist/components/composites/help-center/help-center.d.ts.map +1 -0
  17. package/dist/components/composites/help-center/help-center.js +135 -0
  18. package/dist/components/composites/help-center/help-center.js.map +1 -0
  19. package/dist/components/composites/help-center/index.d.ts +3 -0
  20. package/dist/components/composites/help-center/index.d.ts.map +1 -0
  21. package/dist/components/composites/help-center/index.js +2 -0
  22. package/dist/components/composites/update-dialog/update-dialog.d.ts +11 -1
  23. package/dist/components/composites/update-dialog/update-dialog.d.ts.map +1 -1
  24. package/dist/components/composites/update-dialog/update-dialog.js +209 -78
  25. package/dist/components/composites/update-dialog/update-dialog.js.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -2
  29. package/dist/r/about-dialog.json +26 -0
  30. package/dist/r/help-center.json +28 -0
  31. package/dist/r/registry.json +55 -0
  32. package/dist/r/src/components/composites/about-dialog/about-dialog.tsx +161 -0
  33. package/dist/r/src/components/composites/about-dialog/index.ts +2 -0
  34. package/dist/r/src/components/composites/account-menu/account-menu.tsx +76 -13
  35. package/dist/r/src/components/composites/account-menu/index.ts +6 -1
  36. package/dist/r/src/components/composites/help-center/help-center.tsx +104 -0
  37. package/dist/r/src/components/composites/help-center/index.ts +2 -0
  38. package/dist/r/src/components/composites/update-dialog/update-dialog.tsx +200 -27
  39. package/dist/r/update-dialog.json +1 -0
  40. package/package.json +1 -1
  41. package/registry.json +55 -0
  42. package/src/components/composites/about-dialog/about-dialog.tsx +161 -0
  43. package/src/components/composites/about-dialog/index.ts +2 -0
  44. package/src/components/composites/account-menu/account-menu.tsx +76 -13
  45. package/src/components/composites/account-menu/index.ts +6 -1
  46. package/src/components/composites/help-center/help-center.tsx +104 -0
  47. package/src/components/composites/help-center/index.ts +2 -0
  48. package/src/components/composites/update-dialog/update-dialog.tsx +200 -27
  49. package/src/index.ts +2 -0
@@ -8,9 +8,19 @@ import {
8
8
  splitProps,
9
9
  } from 'solid-js'
10
10
  import type { Accessor } from 'solid-js'
11
- import { Check, Download, ExternalLink, LoaderCircle, RefreshCw, Sparkles } from 'lucide-solid'
11
+ import {
12
+ Check,
13
+ CircleStop,
14
+ Download,
15
+ ExternalLink,
16
+ LoaderCircle,
17
+ Monitor,
18
+ RefreshCw,
19
+ Sparkles,
20
+ } from 'lucide-solid'
12
21
  import { Badge } from '../../ui/badge'
13
22
  import { Button } from '../../ui/button'
23
+ import { ActionButton } from '../action-button'
14
24
  import {
15
25
  Dialog,
16
26
  DialogClose,
@@ -79,6 +89,9 @@ export type UpdatePhase =
79
89
  | 'downloading'
80
90
  | 'installing'
81
91
  | 'installed'
92
+ | 'cancelling'
93
+ | 'cancelled'
94
+ | 'unavailable'
82
95
  | 'failed'
83
96
 
84
97
  /** A snapshot of the updater. Every field the dialog renders, and nothing else. */
@@ -124,6 +137,12 @@ export type UpdateAdapter = Readonly<{
124
137
  * wrong one.
125
138
  */
126
139
  install(expectedVersion: string): Promise<UpdateState>
140
+ /** Optional native cancellation, available only while work is in flight. */
141
+ cancel?(): Promise<UpdateState>
142
+ /** Enable status polling during native work; omit for push-driven adapters. */
143
+ pollIntervalMs?: number
144
+ /** Route external links through the native shell when needed. */
145
+ openExternal?(url: string): Promise<void>
127
146
  /** False in a browser build, where there is no installer to drive. */
128
147
  isDesktopRuntime(): boolean
129
148
  }>
@@ -132,6 +151,10 @@ export type UpdateDialogProps = {
132
151
  adapter: UpdateAdapter
133
152
  /** The application's name, used in the title and the copy. */
134
153
  appName?: string
154
+ /** The actual desktop application icon, supplied by its host. */
155
+ appIcon?: string
156
+ /** Complete build-time history, also available before the native status arrives. */
157
+ changelog?: string
135
158
  /** Shown before the first status arrives, so the dialog is never blank. */
136
159
  fallbackVersion?: string
137
160
  /** Copy for the adapter's non-desktop case. */
@@ -181,7 +204,10 @@ function errorMessage(caught: unknown, fallback: string): string {
181
204
  */
182
205
  function isBusy(state: UpdateState | null): boolean {
183
206
  return (
184
- state?.phase === 'checking' || state?.phase === 'downloading' || state?.phase === 'installing'
207
+ state?.phase === 'checking' ||
208
+ state?.phase === 'downloading' ||
209
+ state?.phase === 'installing' ||
210
+ state?.phase === 'cancelling'
185
211
  )
186
212
  }
187
213
 
@@ -189,6 +215,8 @@ export function UpdateDialog(props: UpdateDialogProps) {
189
215
  const [local] = splitProps(props, [
190
216
  'adapter',
191
217
  'appName',
218
+ 'appIcon',
219
+ 'changelog',
192
220
  'fallbackVersion',
193
221
  'desktopOnlyMessage',
194
222
  'open',
@@ -216,6 +244,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
216
244
  const [busy, setBusy] = createSignal(false)
217
245
  const [error, setError] = createSignal('')
218
246
  const [checkFailed, setCheckFailed] = createSignal(false)
247
+ const [cancelPending, setCancelPending] = createSignal(false)
219
248
  let lifecycleEpoch = 0
220
249
  let activeOpenEpoch: number | undefined
221
250
  let disposed = false
@@ -309,6 +338,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
309
338
  */
310
339
  createEffect(() => {
311
340
  if (!open() || !desktop()) {
341
+ setCancelPending(false)
312
342
  if (activeOpenEpoch !== undefined) {
313
343
  activeOpenEpoch = undefined
314
344
  lifecycleEpoch += 1
@@ -329,6 +359,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
329
359
  })
330
360
  setError('')
331
361
  setCheckFailed(false)
362
+ setCancelPending(false)
332
363
  setBusy(true)
333
364
  void (async () => {
334
365
  try {
@@ -357,10 +388,17 @@ export function UpdateDialog(props: UpdateDialogProps) {
357
388
  if (!version || !isCurrent()) return
358
389
  setBusy(true)
359
390
  setError('')
391
+ setState((current) =>
392
+ current ? { ...current, phase: 'downloading', downloadedBytes: 0, totalBytes: null } : current
393
+ )
360
394
  try {
361
395
  const next = await local.adapter.install(version)
362
396
  if (!isCurrent()) return
363
- setState(next)
397
+ setState(
398
+ cancelPending() && ['downloading', 'installing'].includes(next.phase)
399
+ ? { ...next, phase: 'cancelling' }
400
+ : next
401
+ )
364
402
  // A refused install answers with a normal payload whose phase is `failed`.
365
403
  // Without this the click looked like nothing happened.
366
404
  if (next.phase === 'failed') {
@@ -377,12 +415,87 @@ export function UpdateDialog(props: UpdateDialogProps) {
377
415
  }
378
416
  }
379
417
 
380
- const changelog = createMemo(() => plainTextFromMarkdown(state()?.changelog ?? ''))
418
+ // Poll only an open, active native operation. Epoch ownership prevents a late
419
+ // status response from replacing a reopened dialog's newer snapshot.
420
+ createEffect(() => {
421
+ const phase = state()?.phase
422
+ if (
423
+ !open() ||
424
+ !desktop() ||
425
+ !local.adapter.pollIntervalMs ||
426
+ !['downloading', 'installing', 'cancelling'].includes(phase ?? '')
427
+ )
428
+ return
429
+ const epoch = activeOpenEpoch
430
+ let active = true
431
+ let requestInFlight = false
432
+ const timer = window.setInterval(
433
+ () => {
434
+ if (requestInFlight) return
435
+ requestInFlight = true
436
+ void (async () => {
437
+ try {
438
+ const next = await local.adapter.getStatus()
439
+ if (!active || disposed || !open() || activeOpenEpoch !== epoch) return
440
+ setState(
441
+ cancelPending() && ['downloading', 'installing'].includes(next.phase)
442
+ ? { ...next, phase: 'cancelling' }
443
+ : next
444
+ )
445
+ if (next.phase === 'failed')
446
+ setError(errorMessage(next.error, 'Update installation failed'))
447
+ } catch (caught) {
448
+ if (active && !disposed && activeOpenEpoch === epoch)
449
+ setError(errorMessage(caught, 'Update progress is unavailable'))
450
+ } finally {
451
+ requestInFlight = false
452
+ }
453
+ })()
454
+ },
455
+ Math.max(250, local.adapter.pollIntervalMs)
456
+ )
457
+ onCleanup(() => {
458
+ active = false
459
+ window.clearInterval(timer)
460
+ })
461
+ })
462
+
463
+ const cancel = async () => {
464
+ if (!local.adapter.cancel || cancelPending() || state()?.phase === 'cancelling') return
465
+ const epoch = activeOpenEpoch
466
+ setError('')
467
+ setCancelPending(true)
468
+ setState((current) => (current ? { ...current, phase: 'cancelling' } : current))
469
+ try {
470
+ const next = await local.adapter.cancel()
471
+ if (!disposed && open() && activeOpenEpoch === epoch) setState(next)
472
+ } catch (caught) {
473
+ if (!disposed && open() && activeOpenEpoch === epoch) {
474
+ setError(errorMessage(caught, 'Could not cancel the update'))
475
+ await loadStatus(() => !disposed && open() && activeOpenEpoch === epoch)
476
+ }
477
+ } finally {
478
+ if (!disposed && open() && activeOpenEpoch === epoch) setCancelPending(false)
479
+ }
480
+ }
481
+
482
+ const openRelease = async (url: string) => {
483
+ try {
484
+ if (local.adapter.openExternal) await local.adapter.openExternal(url)
485
+ else window.open(url, '_blank', 'noopener,noreferrer')
486
+ } catch (caught) {
487
+ if (!disposed) setError(errorMessage(caught, 'Could not open the release page'))
488
+ }
489
+ }
490
+
491
+ const changelog = createMemo(() =>
492
+ plainTextFromMarkdown(state()?.changelog || local.changelog || '')
493
+ )
381
494
  const notes = () => {
382
495
  const value = state()?.releaseNotes
383
496
  return value ? plainTextFromMarkdown(value) : ''
384
497
  }
385
- const working = () => busy() || isBusy(state())
498
+ const working = () => busy() || cancelPending() || isBusy(state())
386
499
  const isCurrent = () => state()?.phase === 'current' && !error() && !checkFailed() && !working()
387
500
 
388
501
  const triggerLabel = () => {
@@ -432,14 +545,32 @@ export function UpdateDialog(props: UpdateDialogProps) {
432
545
 
433
546
  <DialogContent
434
547
  class={cn('max-w-3xl', local.class)}
435
- onOpenAutoFocus={focusRestoration.onOpenAutoFocus}
548
+ onKeyDown={(event: KeyboardEvent) => {
549
+ // An explanatory tooltip must not consume the popup's close shortcut,
550
+ // including while its exit animation still owns Kobalte's top layer.
551
+ if (event.key === 'Escape' && !event.defaultPrevented) {
552
+ event.preventDefault()
553
+ setOpen(false)
554
+ }
555
+ }}
556
+ onOpenAutoFocus={(event) => {
557
+ focusRestoration.onOpenAutoFocus(event)
558
+ // Start at the readable panel rather than opening an action's tooltip.
559
+ if (!event.defaultPrevented && event.currentTarget instanceof HTMLElement) {
560
+ event.preventDefault()
561
+ event.currentTarget.focus({ preventScroll: true })
562
+ }
563
+ }}
436
564
  onCloseAutoFocus={focusRestoration.onCloseAutoFocus}
437
565
  >
438
566
  <DialogHeader>
439
567
  <div class="flex items-center gap-3">
440
- <span class="flex size-10 items-center justify-center rounded-xl bg-primary text-primary-foreground">
441
- <Sparkles class="size-5" aria-hidden="true" />
442
- </span>
568
+ <Show
569
+ when={local.appIcon}
570
+ fallback={<Monitor class="size-10 text-primary" aria-hidden="true" />}
571
+ >
572
+ {(icon) => <img src={icon()} alt="" class="size-10" aria-hidden="true" />}
573
+ </Show>
443
574
  <div>
444
575
  <DialogTitle>Version &amp; updates</DialogTitle>
445
576
  <DialogDescription>
@@ -459,7 +590,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
459
590
  <p class="text-2xs font-semibold tracking-widest text-muted-foreground uppercase">
460
591
  Installed version
461
592
  </p>
462
- <p class="text-xl font-semibold tracking-tight">
593
+ <p class="text-xl font-semibold tracking-tight text-primary">
463
594
  v{state()?.currentVersion || local.fallbackVersion || '0.1.0'}
464
595
  </p>
465
596
  <p class="text-sm text-muted-foreground">
@@ -469,10 +600,14 @@ export function UpdateDialog(props: UpdateDialogProps) {
469
600
  <Show
470
601
  when={state()?.phase === 'available' && state()?.availableVersion}
471
602
  fallback={
472
- desktop()
473
- ? 'Check the release channel for the latest signed build.'
474
- : (local.desktopOnlyMessage ??
475
- `Open this dialog inside the desktop app to check for updates.`)
603
+ state()?.phase === 'unavailable'
604
+ ? 'No signed package is published for this platform.'
605
+ : state()?.phase === 'cancelled'
606
+ ? 'Update cancelled. You can retry when ready.'
607
+ : desktop()
608
+ ? 'Check the release channel for the latest signed build.'
609
+ : (local.desktopOnlyMessage ??
610
+ `Open this dialog inside the desktop app to check for updates.`)
476
611
  }
477
612
  >
478
613
  {`A newer release, v${state()?.availableVersion}, is ready.`}
@@ -483,10 +618,11 @@ export function UpdateDialog(props: UpdateDialogProps) {
483
618
  </Show>
484
619
  </p>
485
620
  </div>
486
- <Button
621
+ <ActionButton
487
622
  type="button"
488
623
  variant="outline"
489
624
  size="sm"
625
+ tooltip="Check the release channel for the latest signed app version."
490
626
  disabled={!desktop() || working()}
491
627
  onClick={() => void check()}
492
628
  >
@@ -494,11 +630,17 @@ export function UpdateDialog(props: UpdateDialogProps) {
494
630
  <LoaderCircle class="animate-spin" aria-hidden="true" />
495
631
  </Show>
496
632
  {checkFailed() ? 'Retry update check' : 'Check latest version'}
497
- </Button>
633
+ </ActionButton>
498
634
  </div>
499
635
  </section>
500
636
 
501
- <Show when={state()?.phase === 'available' && state()?.availableVersion}>
637
+ <Show
638
+ when={
639
+ state()?.availableVersion &&
640
+ state()?.phase !== 'installed' &&
641
+ state()?.phase !== 'unavailable'
642
+ }
643
+ >
502
644
  <section
503
645
  class="rounded-xl border border-primary/35 bg-primary-subtle p-4"
504
646
  aria-label="Available update"
@@ -507,7 +649,10 @@ export function UpdateDialog(props: UpdateDialogProps) {
507
649
  <div class="flex flex-col gap-1">
508
650
  <p class="flex items-center gap-2 text-sm font-semibold">
509
651
  <Sparkles class="size-4 text-primary" aria-hidden="true" />
510
- Version {state()?.availableVersion} is ready
652
+ Version {state()?.availableVersion}{' '}
653
+ {['downloading', 'installing', 'cancelling'].includes(state()?.phase ?? '')
654
+ ? 'is being prepared'
655
+ : 'is ready'}
511
656
  </p>
512
657
  <p class="text-sm text-muted-foreground">
513
658
  The signed installer is verified before {appName()} restarts.
@@ -518,12 +663,18 @@ export function UpdateDialog(props: UpdateDialogProps) {
518
663
  )}
519
664
  </Show>
520
665
  </div>
521
- <Button type="button" size="sm" disabled={working()} onClick={() => void install()}>
666
+ <ActionButton
667
+ type="button"
668
+ size="sm"
669
+ tooltip={`Download the signed update and restart ${appName()}.`}
670
+ disabled={working()}
671
+ onClick={() => void install()}
672
+ >
522
673
  <Show when={working()} fallback={<Download aria-hidden="true" />}>
523
674
  <LoaderCircle class="animate-spin" aria-hidden="true" />
524
675
  </Show>
525
676
  Install and restart
526
- </Button>
677
+ </ActionButton>
527
678
  </div>
528
679
  </section>
529
680
  </Show>
@@ -532,7 +683,13 @@ export function UpdateDialog(props: UpdateDialogProps) {
532
683
  Progress with both numbers: a bar alone does not distinguish a stalled
533
684
  download from a slow one, and "12.4 MB of 48.1 MB" does.
534
685
  */}
535
- <Show when={state()?.phase === 'downloading' || state()?.phase === 'installing'}>
686
+ <Show
687
+ when={
688
+ state()?.phase === 'downloading' ||
689
+ state()?.phase === 'installing' ||
690
+ state()?.phase === 'cancelling'
691
+ }
692
+ >
536
693
  <section class="grid gap-2" aria-label="Update progress">
537
694
  <Progress
538
695
  value={progressValue() ?? undefined}
@@ -554,6 +711,21 @@ export function UpdateDialog(props: UpdateDialogProps) {
554
711
  Applying the update…
555
712
  </Show>
556
713
  </p>
714
+ <Show when={local.adapter.cancel}>
715
+ <ActionButton
716
+ type="button"
717
+ variant="outline"
718
+ size="sm"
719
+ tooltip="Stop the current update download or installation."
720
+ disabled={cancelPending() || state()?.phase === 'cancelling'}
721
+ onClick={() => void cancel()}
722
+ >
723
+ <CircleStop aria-hidden="true" />
724
+ {cancelPending() || state()?.phase === 'cancelling'
725
+ ? 'Cancelling…'
726
+ : 'Cancel update'}
727
+ </ActionButton>
728
+ </Show>
557
729
  </section>
558
730
  </Show>
559
731
 
@@ -585,7 +757,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
585
757
  <Show when={notes()}>
586
758
  <div class="flex flex-col gap-2">
587
759
  <div class="flex items-center gap-2">
588
- <h2 id="update-release-notes" class="text-sm font-semibold">
760
+ <h2 id="update-release-notes" class="text-sm font-semibold text-primary">
589
761
  What changed in this release
590
762
  </h2>
591
763
  <Badge variant="subtle" size="sm">
@@ -608,11 +780,11 @@ export function UpdateDialog(props: UpdateDialogProps) {
608
780
 
609
781
  <Show when={changelog()}>
610
782
  <div class="flex flex-col gap-2">
611
- <h2 id="update-changelog" class="text-sm font-semibold">
783
+ <h2 id="update-changelog" class="text-sm font-semibold text-primary">
612
784
  Installed changelog
613
785
  </h2>
614
786
  <p class="text-xs text-muted-foreground">
615
- A plain-text history of the installed release channel.
787
+ All versions and updates included with this installation.
616
788
  </p>
617
789
  {/* A long text block with no focusable descendant: it needs its own tab
618
790
  stop, named by the heading above it, or a keyboard user cannot
@@ -632,17 +804,18 @@ export function UpdateDialog(props: UpdateDialogProps) {
632
804
  <DialogFooter>
633
805
  <Show when={state()?.releaseUrl}>
634
806
  {(url) => (
635
- <Button
807
+ <ActionButton
636
808
  type="button"
637
809
  variant="ghost"
638
810
  size="sm"
811
+ tooltip="Open the release history in your browser."
639
812
  // `noopener,noreferrer` because the release page is a different
640
813
  // origin and must not get a handle on this window.
641
- onClick={() => window.open(url(), '_blank', 'noopener,noreferrer')}
814
+ onClick={() => void openRelease(url())}
642
815
  >
643
816
  <ExternalLink aria-hidden="true" />
644
817
  View releases
645
- </Button>
818
+ </ActionButton>
646
819
  )}
647
820
  </Show>
648
821
  <DialogClose as={Button} variant="outline" size="sm">
package/src/index.ts CHANGED
@@ -108,6 +108,8 @@ export * from './components/composites/scene-controls'
108
108
  export * from './components/composites/stat'
109
109
  export * from './components/composites/appearance-editor'
110
110
  export * from './components/composites/update-dialog'
111
+ export * from './components/composites/about-dialog'
112
+ export * from './components/composites/help-center'
111
113
  export * from './components/composites/workspace-mark'
112
114
 
113
115
  /* --- Library ------------------------------------------------------------- */