@adea-ai/ui 0.84.0 → 0.87.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 +90 -1
- package/dist/NOTICE +60 -0
- package/dist/components/composites/action-button/action-button.d.ts.map +1 -1
- package/dist/components/composites/action-button/action-button.js +2 -2
- package/dist/components/composites/action-button/action-button.js.map +1 -1
- package/dist/components/composites/catalog-browser/catalog-browser-group.d.ts +20 -0
- package/dist/components/composites/catalog-browser/catalog-browser-group.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-browser-group.js +195 -0
- package/dist/components/composites/catalog-browser/catalog-browser-group.js.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-browser.d.ts +3 -0
- package/dist/components/composites/catalog-browser/catalog-browser.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-browser.js +378 -0
- package/dist/components/composites/catalog-browser/catalog-browser.js.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-detail.d.ts +21 -0
- package/dist/components/composites/catalog-browser/catalog-detail.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-detail.js +92 -0
- package/dist/components/composites/catalog-browser/catalog-detail.js.map +1 -0
- package/dist/components/composites/catalog-browser/catalog-types.d.ts +78 -0
- package/dist/components/composites/catalog-browser/catalog-types.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/index.d.ts +4 -0
- package/dist/components/composites/catalog-browser/index.d.ts.map +1 -0
- package/dist/components/composites/catalog-browser/index.js +3 -0
- package/dist/components/composites/list-row/list-row.d.ts +6 -3
- package/dist/components/composites/list-row/list-row.d.ts.map +1 -1
- package/dist/components/composites/list-row/list-row.js +69 -7
- package/dist/components/composites/list-row/list-row.js.map +1 -1
- package/dist/components/composites/settings/settings.d.ts +9 -11
- package/dist/components/composites/settings/settings.d.ts.map +1 -1
- package/dist/components/composites/settings/settings.js +48 -33
- package/dist/components/composites/settings/settings.js.map +1 -1
- package/dist/components/conversation/message-composer.js +1 -1
- package/dist/components/conversation/scroll-follow.d.ts.map +1 -1
- package/dist/components/conversation/scroll-follow.js +95 -15
- package/dist/components/conversation/scroll-follow.js.map +1 -1
- package/dist/components/layout/app-shell/index.d.ts +1 -0
- package/dist/components/layout/app-shell/index.d.ts.map +1 -1
- package/dist/components/layout/app-shell/index.js +2 -1
- package/dist/components/layout/app-shell/skip-link.d.ts +11 -0
- package/dist/components/layout/app-shell/skip-link.d.ts.map +1 -0
- package/dist/components/layout/app-shell/skip-link.js +44 -0
- package/dist/components/layout/app-shell/skip-link.js.map +1 -0
- package/dist/components/layout/floating-preview/floating-preview.d.ts +12 -0
- package/dist/components/layout/floating-preview/floating-preview.d.ts.map +1 -0
- package/dist/components/layout/floating-preview/floating-preview.js +283 -0
- package/dist/components/layout/floating-preview/floating-preview.js.map +1 -0
- package/dist/components/layout/floating-preview/index.d.ts +3 -0
- package/dist/components/layout/floating-preview/index.d.ts.map +1 -0
- package/dist/components/layout/floating-preview/index.js +3 -0
- package/dist/components/layout/floating-preview/model.d.ts +74 -0
- package/dist/components/layout/floating-preview/model.d.ts.map +1 -0
- package/dist/components/layout/floating-preview/model.js +146 -0
- package/dist/components/layout/floating-preview/model.js.map +1 -0
- package/dist/components/theme/index.d.ts +1 -0
- package/dist/components/theme/index.d.ts.map +1 -1
- package/dist/components/theme/index.js +2 -1
- package/dist/components/theme/theme-mode-toggle.d.ts +10 -0
- package/dist/components/theme/theme-mode-toggle.d.ts.map +1 -0
- package/dist/components/theme/theme-mode-toggle.js +82 -0
- package/dist/components/theme/theme-mode-toggle.js.map +1 -0
- package/dist/components/ui/checkbox/checkbox.d.ts +2 -0
- package/dist/components/ui/checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/ui/checkbox/checkbox.js +24 -3
- package/dist/components/ui/checkbox/checkbox.js.map +1 -1
- package/dist/components/ui/combobox/combobox.d.ts.map +1 -1
- package/dist/components/ui/combobox/combobox.js +41 -4
- package/dist/components/ui/combobox/combobox.js.map +1 -1
- package/dist/components/ui/combobox/index.d.ts +1 -0
- package/dist/components/ui/combobox/index.d.ts.map +1 -1
- package/dist/components/ui/combobox/index.js +2 -1
- package/dist/components/ui/combobox/value-combobox.d.ts +19 -0
- package/dist/components/ui/combobox/value-combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox/value-combobox.js +42 -0
- package/dist/components/ui/combobox/value-combobox.js.map +1 -0
- package/dist/components/ui/empty/empty-state.d.ts +15 -0
- package/dist/components/ui/empty/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty/empty-state.js +96 -0
- package/dist/components/ui/empty/empty-state.js.map +1 -0
- package/dist/components/ui/empty/index.d.ts +1 -0
- package/dist/components/ui/empty/index.d.ts.map +1 -1
- package/dist/components/ui/empty/index.js +2 -1
- package/dist/components/ui/entity-icon/entity-icon.d.ts +4 -1
- package/dist/components/ui/entity-icon/entity-icon.d.ts.map +1 -1
- package/dist/components/ui/entity-icon/entity-icon.js +37 -13
- package/dist/components/ui/entity-icon/entity-icon.js.map +1 -1
- package/dist/components/ui/field/field.d.ts +36 -13
- package/dist/components/ui/field/field.d.ts.map +1 -1
- package/dist/components/ui/field/field.js +146 -24
- package/dist/components/ui/field/field.js.map +1 -1
- package/dist/components/ui/field/index.d.ts +3 -1
- package/dist/components/ui/field/index.d.ts.map +1 -1
- package/dist/components/ui/field/index.js +3 -2
- package/dist/components/ui/field/number-field.d.ts +19 -0
- package/dist/components/ui/field/number-field.d.ts.map +1 -0
- package/dist/components/ui/field/number-field.js +76 -0
- package/dist/components/ui/field/number-field.js.map +1 -0
- package/dist/components/ui/input/index.d.ts +1 -0
- package/dist/components/ui/input/index.d.ts.map +1 -1
- package/dist/components/ui/input/index.js +2 -1
- package/dist/components/ui/input/input-control.d.ts +11 -0
- package/dist/components/ui/input/input-control.d.ts.map +1 -0
- package/dist/components/ui/input/input-control.js +24 -0
- package/dist/components/ui/input/input-control.js.map +1 -0
- package/dist/components/ui/input/input.d.ts.map +1 -1
- package/dist/components/ui/input/input.js +56 -28
- package/dist/components/ui/input/input.js.map +1 -1
- package/dist/components/ui/input-group/index.d.ts +1 -0
- package/dist/components/ui/input-group/index.d.ts.map +1 -1
- package/dist/components/ui/input-group/index.js +2 -1
- package/dist/components/ui/input-group/input-group.d.ts.map +1 -1
- package/dist/components/ui/input-group/input-group.js +42 -5
- package/dist/components/ui/input-group/input-group.js.map +1 -1
- package/dist/components/ui/input-group/secret-input-group.d.ts +11 -0
- package/dist/components/ui/input-group/secret-input-group.d.ts.map +1 -0
- package/dist/components/ui/input-group/secret-input-group.js +67 -0
- package/dist/components/ui/input-group/secret-input-group.js.map +1 -0
- package/dist/components/ui/native-select/native-select.d.ts.map +1 -1
- package/dist/components/ui/native-select/native-select.js +39 -4
- package/dist/components/ui/native-select/native-select.js.map +1 -1
- package/dist/components/ui/progress/progress.d.ts.map +1 -1
- package/dist/components/ui/progress/progress.js +30 -18
- package/dist/components/ui/progress/progress.js.map +1 -1
- package/dist/components/ui/radio-group/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group/radio-group.js +41 -4
- package/dist/components/ui/radio-group/radio-group.js.map +1 -1
- package/dist/components/ui/switch/switch.d.ts +2 -0
- package/dist/components/ui/switch/switch.d.ts.map +1 -1
- package/dist/components/ui/switch/switch.js +24 -3
- package/dist/components/ui/switch/switch.js.map +1 -1
- package/dist/components/ui/tabs/tabs.js +1 -1
- package/dist/components/ui/tabs/tabs.js.map +1 -1
- package/dist/components/ui/text-link/index.d.ts +2 -0
- package/dist/components/ui/text-link/index.d.ts.map +1 -0
- package/dist/components/ui/text-link/index.js +2 -0
- package/dist/components/ui/text-link/text-link.d.ts +9 -0
- package/dist/components/ui/text-link/text-link.d.ts.map +1 -0
- package/dist/components/ui/text-link/text-link.js +30 -0
- package/dist/components/ui/text-link/text-link.js.map +1 -0
- package/dist/components/ui/textarea/textarea.d.ts.map +1 -1
- package/dist/components/ui/textarea/textarea.js +41 -4
- package/dist/components/ui/textarea/textarea.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -8
- package/dist/lib/clipboard.d.ts +3 -0
- package/dist/lib/clipboard.d.ts.map +1 -0
- package/dist/lib/clipboard.js +35 -0
- package/dist/lib/clipboard.js.map +1 -0
- package/dist/lib/download.d.ts +3 -0
- package/dist/lib/download.d.ts.map +1 -0
- package/dist/lib/download.js +31 -0
- package/dist/lib/download.js.map +1 -0
- package/dist/lib/form-field.d.ts +37 -0
- package/dist/lib/form-field.d.ts.map +1 -0
- package/dist/lib/form-field.js +47 -0
- package/dist/lib/form-field.js.map +1 -0
- package/dist/r/app-shell.json +5 -0
- package/dist/r/catalog-browser.json +47 -0
- package/dist/r/combobox.json +5 -0
- package/dist/r/empty.json +10 -1
- package/dist/r/field.json +7 -0
- package/dist/r/floating-preview.json +30 -0
- package/dist/r/input-group.json +6 -0
- package/dist/r/input.json +5 -0
- package/dist/r/lib.json +15 -0
- package/dist/r/list-row.json +1 -0
- package/dist/r/registry.json +159 -1
- package/dist/r/src/components/composites/action-button/action-button.tsx +5 -1
- package/dist/r/src/components/composites/catalog-browser/catalog-browser-group.tsx +148 -0
- package/dist/r/src/components/composites/catalog-browser/catalog-browser.tsx +258 -0
- package/dist/r/src/components/composites/catalog-browser/catalog-detail.tsx +64 -0
- package/dist/r/src/components/composites/catalog-browser/catalog-types.ts +83 -0
- package/dist/r/src/components/composites/catalog-browser/index.ts +10 -0
- package/dist/r/src/components/composites/list-row/list-row.tsx +81 -8
- package/dist/r/src/components/composites/settings/settings.tsx +67 -37
- package/dist/r/src/components/conversation/scroll-follow.ts +109 -15
- package/dist/r/src/components/layout/app-shell/index.ts +1 -0
- package/dist/r/src/components/layout/app-shell/skip-link.tsx +35 -0
- package/dist/r/src/components/layout/floating-preview/floating-preview.tsx +260 -0
- package/dist/r/src/components/layout/floating-preview/index.ts +2 -0
- package/dist/r/src/components/layout/floating-preview/model.ts +285 -0
- package/dist/r/src/components/theme/index.ts +1 -0
- package/dist/r/src/components/theme/theme-mode-toggle.tsx +56 -0
- package/dist/r/src/components/ui/checkbox/checkbox.tsx +20 -3
- package/dist/r/src/components/ui/combobox/combobox.tsx +24 -1
- package/dist/r/src/components/ui/combobox/index.ts +1 -0
- package/dist/r/src/components/ui/combobox/value-combobox.tsx +38 -0
- package/dist/r/src/components/ui/empty/empty-state.tsx +71 -0
- package/dist/r/src/components/ui/empty/index.ts +1 -0
- package/dist/r/src/components/ui/entity-icon/entity-icon.tsx +33 -2
- package/dist/r/src/components/ui/field/field.tsx +102 -5
- package/dist/r/src/components/ui/field/index.ts +5 -0
- package/dist/r/src/components/ui/field/number-field.tsx +71 -0
- package/dist/r/src/components/ui/input/index.ts +1 -0
- package/dist/r/src/components/ui/input/input-control.tsx +38 -0
- package/dist/r/src/components/ui/input/input.tsx +32 -20
- package/dist/r/src/components/ui/input-group/index.ts +1 -0
- package/dist/r/src/components/ui/input-group/input-group.tsx +25 -2
- package/dist/r/src/components/ui/input-group/secret-input-group.tsx +58 -0
- package/dist/r/src/components/ui/native-select/native-select.tsx +27 -1
- package/dist/r/src/components/ui/progress/progress.tsx +12 -8
- package/dist/r/src/components/ui/radio-group/radio-group.tsx +30 -2
- package/dist/r/src/components/ui/switch/switch.tsx +20 -3
- package/dist/r/src/components/ui/tabs/tabs.tsx +1 -1
- package/dist/r/src/components/ui/text-link/index.ts +2 -0
- package/dist/r/src/components/ui/text-link/text-link.tsx +28 -0
- package/dist/r/src/components/ui/textarea/textarea.tsx +24 -1
- package/dist/r/src/lib/clipboard.ts +41 -0
- package/dist/r/src/lib/download.ts +29 -0
- package/dist/r/src/lib/form-field.ts +93 -0
- package/dist/r/text-link.json +22 -0
- package/dist/r/theming.json +6 -0
- package/package.json +1 -1
- package/registry.json +159 -1
- package/src/components/composites/action-button/action-button.tsx +5 -1
- package/src/components/composites/catalog-browser/catalog-browser-group.tsx +148 -0
- package/src/components/composites/catalog-browser/catalog-browser.tsx +258 -0
- package/src/components/composites/catalog-browser/catalog-detail.tsx +64 -0
- package/src/components/composites/catalog-browser/catalog-types.ts +83 -0
- package/src/components/composites/catalog-browser/index.ts +10 -0
- package/src/components/composites/list-row/list-row.tsx +81 -8
- package/src/components/composites/settings/settings.tsx +67 -37
- package/src/components/conversation/scroll-follow.ts +109 -15
- package/src/components/layout/app-shell/index.ts +1 -0
- package/src/components/layout/app-shell/skip-link.tsx +35 -0
- package/src/components/layout/floating-preview/floating-preview.tsx +260 -0
- package/src/components/layout/floating-preview/index.ts +2 -0
- package/src/components/layout/floating-preview/model.ts +285 -0
- package/src/components/theme/index.ts +1 -0
- package/src/components/theme/theme-mode-toggle.tsx +56 -0
- package/src/components/ui/checkbox/checkbox.tsx +20 -3
- package/src/components/ui/combobox/combobox.tsx +24 -1
- package/src/components/ui/combobox/index.ts +1 -0
- package/src/components/ui/combobox/value-combobox.tsx +38 -0
- package/src/components/ui/empty/empty-state.tsx +71 -0
- package/src/components/ui/empty/index.ts +1 -0
- package/src/components/ui/entity-icon/entity-icon.tsx +33 -2
- package/src/components/ui/field/field.tsx +102 -5
- package/src/components/ui/field/index.ts +5 -0
- package/src/components/ui/field/number-field.tsx +71 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/input/input-control.tsx +38 -0
- package/src/components/ui/input/input.tsx +32 -20
- package/src/components/ui/input-group/index.ts +1 -0
- package/src/components/ui/input-group/input-group.tsx +25 -2
- package/src/components/ui/input-group/secret-input-group.tsx +58 -0
- package/src/components/ui/native-select/native-select.tsx +27 -1
- package/src/components/ui/progress/progress.tsx +12 -8
- package/src/components/ui/radio-group/radio-group.tsx +30 -2
- package/src/components/ui/switch/switch.tsx +20 -3
- package/src/components/ui/tabs/tabs.tsx +1 -1
- package/src/components/ui/text-link/index.ts +2 -0
- package/src/components/ui/text-link/text-link.tsx +28 -0
- package/src/components/ui/textarea/textarea.tsx +24 -1
- package/src/index.ts +4 -0
- package/src/lib/clipboard.ts +41 -0
- package/src/lib/download.ts +29 -0
- package/src/lib/form-field.ts +93 -0
|
@@ -2,6 +2,7 @@ import type { PolymorphicProps } from '@kobalte/core/polymorphic'
|
|
|
2
2
|
import { Polymorphic } from '@kobalte/core/polymorphic'
|
|
3
3
|
import type { ComponentProps, JSX, ValidComponent } from 'solid-js'
|
|
4
4
|
import { Show, splitProps } from 'solid-js'
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
|
|
5
6
|
import { cn } from '#lib/utils'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -18,9 +19,8 @@ import { cn } from '#lib/utils'
|
|
|
18
19
|
* arithmetic.
|
|
19
20
|
*
|
|
20
21
|
* A row is only interactive when it is given an `as` that can be activated
|
|
21
|
-
* (an anchor, a button) or an `onClick`.
|
|
22
|
-
*
|
|
23
|
-
* nothing is a lie.
|
|
22
|
+
* (an anchor, a button) or an `onClick`. Click handlers default to a native
|
|
23
|
+
* button; a row without either remains a `div` with no hover treatment.
|
|
24
24
|
*/
|
|
25
25
|
export type ListRowProps<T extends ValidComponent = 'div'> = PolymorphicProps<
|
|
26
26
|
T,
|
|
@@ -36,30 +36,50 @@ export type ListRowProps<T extends ValidComponent = 'div'> = PolymorphicProps<
|
|
|
36
36
|
trailing?: JSX.Element
|
|
37
37
|
/** A dimmed second line under the main content. */
|
|
38
38
|
description?: string
|
|
39
|
+
/** A short explanation shown on pointer hover and keyboard focus. */
|
|
40
|
+
tooltip?: string
|
|
41
|
+
/** Native button type; defaults to `button` for button rows. */
|
|
42
|
+
type?: ComponentProps<'button'>['type']
|
|
39
43
|
}
|
|
40
44
|
>
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
type ListRowBaseProps<T extends ValidComponent = 'div'> = ListRowProps<T> & {
|
|
47
|
+
as?: T
|
|
48
|
+
type?: ComponentProps<'button'>['type']
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ListRowBase<T extends ValidComponent = 'div'>(props: ListRowBaseProps<T>) {
|
|
43
52
|
const [local, rest] = splitProps(props as ListRowProps, [
|
|
53
|
+
'as',
|
|
44
54
|
'class',
|
|
45
55
|
'selected',
|
|
46
56
|
'dense',
|
|
47
57
|
'leading',
|
|
48
58
|
'trailing',
|
|
49
59
|
'description',
|
|
60
|
+
'tooltip',
|
|
61
|
+
'type',
|
|
62
|
+
'tabIndex',
|
|
50
63
|
'children',
|
|
51
64
|
])
|
|
52
65
|
|
|
53
66
|
return (
|
|
54
67
|
<Polymorphic
|
|
55
|
-
as=
|
|
68
|
+
as={(local.as ?? 'div') as T}
|
|
69
|
+
type={local.as === 'button' ? (local.type ?? 'button') : local.type}
|
|
70
|
+
tabIndex={local.tabIndex ?? (local.as === 'button' || local.as === 'a' ? 0 : undefined)}
|
|
56
71
|
aria-current={local.selected ? 'true' : undefined}
|
|
57
72
|
data-selected={local.selected ? '' : undefined}
|
|
58
73
|
class={cn(
|
|
59
74
|
'group/row flex min-w-0 items-center gap-2.5 rounded-md px-2 text-sm',
|
|
60
75
|
'transition-colors ease-out outline-none',
|
|
61
76
|
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
62
|
-
{
|
|
77
|
+
{
|
|
78
|
+
'h-row-sm': local.dense && !local.description,
|
|
79
|
+
'h-row-md': !local.dense && !local.description,
|
|
80
|
+
'min-h-row-sm py-1': local.dense && local.description,
|
|
81
|
+
'min-h-row-md py-1.5': !local.dense && local.description,
|
|
82
|
+
},
|
|
63
83
|
{
|
|
64
84
|
'bg-primary-subtle text-foreground': local.selected,
|
|
65
85
|
'hover:bg-surface-hover': !local.selected,
|
|
@@ -76,11 +96,25 @@ export function ListRow<T extends ValidComponent = 'div'>(props: ListRowProps<T>
|
|
|
76
96
|
<span class="flex min-w-0 flex-1 flex-col">
|
|
77
97
|
<span class="truncate">{local.children}</span>
|
|
78
98
|
<Show when={local.description}>
|
|
79
|
-
<span
|
|
99
|
+
<span
|
|
100
|
+
data-slot="list-row-description"
|
|
101
|
+
class={cn(
|
|
102
|
+
'truncate text-xs',
|
|
103
|
+
local.selected ? 'text-foreground' : 'text-muted-foreground'
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{local.description}
|
|
107
|
+
</span>
|
|
80
108
|
</Show>
|
|
81
109
|
</span>
|
|
82
110
|
<Show when={local.trailing}>
|
|
83
|
-
<span
|
|
111
|
+
<span
|
|
112
|
+
data-slot="list-row-trailing"
|
|
113
|
+
class={cn(
|
|
114
|
+
'ms-auto flex shrink-0 items-center gap-1',
|
|
115
|
+
local.selected ? 'text-foreground' : 'text-muted-foreground'
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
84
118
|
{local.trailing}
|
|
85
119
|
</span>
|
|
86
120
|
</Show>
|
|
@@ -88,6 +122,45 @@ export function ListRow<T extends ValidComponent = 'div'>(props: ListRowProps<T>
|
|
|
88
122
|
)
|
|
89
123
|
}
|
|
90
124
|
|
|
125
|
+
type ListRowTooltipTargetProps<T extends ValidComponent = 'div'> = Omit<
|
|
126
|
+
ListRowBaseProps<T>,
|
|
127
|
+
'as'
|
|
128
|
+
> & { rowAs?: T }
|
|
129
|
+
|
|
130
|
+
/** Adapts TooltipTrigger's polymorphic target to ListRow's semantic element. */
|
|
131
|
+
function ListRowTooltipTarget<T extends ValidComponent = 'div'>(
|
|
132
|
+
props: ListRowTooltipTargetProps<T>
|
|
133
|
+
) {
|
|
134
|
+
const [local, rest] = splitProps(props as ListRowTooltipTargetProps, ['rowAs'])
|
|
135
|
+
return <ListRowBase as={local.rowAs} {...(rest as ListRowBaseProps<T>)} />
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function ListRow<T extends ValidComponent = 'div'>(props: ListRowProps<T>) {
|
|
139
|
+
const [local, rest] = splitProps(props as ListRowProps, ['as', 'tooltip'])
|
|
140
|
+
const hasClickHandler = () => Boolean(rest.onClick || rest['on:click'])
|
|
141
|
+
const rowAs = () => (local.as ?? (hasClickHandler() ? 'button' : 'div')) as T
|
|
142
|
+
const rowType = () => (rowAs() === 'button' ? (rest.type ?? 'button') : rest.type)
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Show
|
|
146
|
+
when={local.tooltip}
|
|
147
|
+
fallback={<ListRowBase as={rowAs()} type={rowType()} {...(rest as ListRowBaseProps<T>)} />}
|
|
148
|
+
>
|
|
149
|
+
<Tooltip>
|
|
150
|
+
<TooltipTrigger
|
|
151
|
+
as={ListRowTooltipTarget}
|
|
152
|
+
rowAs={rowAs()}
|
|
153
|
+
type={rowType()}
|
|
154
|
+
{...(rest as ListRowTooltipTargetProps<T>)}
|
|
155
|
+
>
|
|
156
|
+
{rest.children}
|
|
157
|
+
</TooltipTrigger>
|
|
158
|
+
<TooltipContent>{local.tooltip}</TooltipContent>
|
|
159
|
+
</Tooltip>
|
|
160
|
+
</Show>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
91
164
|
/**
|
|
92
165
|
* A group of rows with an optional heading and a scrolling body. The heading is
|
|
93
166
|
* a plain label rather than a control: grouping is for reading, and a
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentProps, JSX } from 'solid-js'
|
|
2
|
-
import { Show, splitProps } from 'solid-js'
|
|
2
|
+
import { createUniqueId, Show, splitProps } from 'solid-js'
|
|
3
|
+
import { createFormFieldContext, FormFieldContext } from '#lib/form-field'
|
|
3
4
|
import { cn } from '#lib/utils'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -10,13 +11,10 @@ import { cn } from '#lib/utils'
|
|
|
10
11
|
* placement all wander unless something fixes them.
|
|
11
12
|
*
|
|
12
13
|
* The row is a two-column layout at a fixed label width, so controls line up
|
|
13
|
-
* down the page regardless of how long the labels are.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `SettingsRow` puts the control on the trailing side and the label leading;
|
|
18
|
-
* `SettingsField` stacks them for a control too wide for a row (a textarea, a
|
|
19
|
-
* list). Both share the same label and description treatment.
|
|
14
|
+
* down the page regardless of how long the labels are. `SettingsRow` also
|
|
15
|
+
* exposes its label and description through the shared form-field context to
|
|
16
|
+
* the first control inside it. `SettingsField` is the visual stacked layout;
|
|
17
|
+
* use `FormField` when the control needs label, help, or validation wiring.
|
|
20
18
|
*/
|
|
21
19
|
export function SettingsSection(
|
|
22
20
|
props: ComponentProps<'section'> & {
|
|
@@ -24,9 +22,18 @@ export function SettingsSection(
|
|
|
24
22
|
description?: string
|
|
25
23
|
/** A control for the section as a whole, e.g. a master toggle. */
|
|
26
24
|
action?: JSX.Element
|
|
25
|
+
/** Use a spaced content stack for fields, alerts, and cards instead of divided rows. */
|
|
26
|
+
bodyLayout?: 'rows' | 'content'
|
|
27
27
|
}
|
|
28
28
|
) {
|
|
29
|
-
const [local, rest] = splitProps(props, [
|
|
29
|
+
const [local, rest] = splitProps(props, [
|
|
30
|
+
'class',
|
|
31
|
+
'title',
|
|
32
|
+
'description',
|
|
33
|
+
'action',
|
|
34
|
+
'bodyLayout',
|
|
35
|
+
'children',
|
|
36
|
+
])
|
|
30
37
|
|
|
31
38
|
return (
|
|
32
39
|
<section
|
|
@@ -45,7 +52,16 @@ export function SettingsSection(
|
|
|
45
52
|
<div class="flex shrink-0 items-center gap-2">{local.action}</div>
|
|
46
53
|
</Show>
|
|
47
54
|
</div>
|
|
48
|
-
<div
|
|
55
|
+
<div
|
|
56
|
+
data-slot="settings-section-body"
|
|
57
|
+
class={cn(
|
|
58
|
+
local.bodyLayout === 'content'
|
|
59
|
+
? 'flex flex-col gap-4'
|
|
60
|
+
: 'divide-y divide-border rounded-xl border border-border'
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{local.children}
|
|
64
|
+
</div>
|
|
49
65
|
</section>
|
|
50
66
|
)
|
|
51
67
|
}
|
|
@@ -54,10 +70,9 @@ export function SettingsSection(
|
|
|
54
70
|
* One setting: a label and description on the leading side, a control on the
|
|
55
71
|
* trailing side.
|
|
56
72
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* from the label, which is stable across renders and readable in a snapshot.
|
|
73
|
+
* The label and description are linked to a shared control through field
|
|
74
|
+
* context. An explicit `aria-label` or `aria-labelledby` on the control keeps
|
|
75
|
+
* precedence; otherwise the visible row label names it.
|
|
61
76
|
*/
|
|
62
77
|
export function SettingsRow(
|
|
63
78
|
props: ComponentProps<'div'> & {
|
|
@@ -77,34 +92,49 @@ export function SettingsRow(
|
|
|
77
92
|
'bare',
|
|
78
93
|
'children',
|
|
79
94
|
])
|
|
95
|
+
const rowId = createUniqueId()
|
|
96
|
+
const labelId = `${rowId}-label`
|
|
97
|
+
const descriptionId = `${rowId}-description`
|
|
98
|
+
const field = createFormFieldContext({
|
|
99
|
+
defaultControlId: rowId,
|
|
100
|
+
labelId,
|
|
101
|
+
descriptionId,
|
|
102
|
+
hint: () => local.description,
|
|
103
|
+
error: () => undefined,
|
|
104
|
+
group: () => false,
|
|
105
|
+
})
|
|
80
106
|
|
|
81
107
|
return (
|
|
82
|
-
<
|
|
83
|
-
data-slot="settings-row"
|
|
84
|
-
class={cn(
|
|
85
|
-
'flex gap-4 p-4',
|
|
86
|
-
local.orientation === 'vertical'
|
|
87
|
-
? 'flex-col'
|
|
88
|
-
: 'flex-col sm:flex-row sm:items-center sm:justify-between',
|
|
89
|
-
local.class
|
|
90
|
-
)}
|
|
91
|
-
{...rest}
|
|
92
|
-
>
|
|
93
|
-
<div class="flex min-w-0 flex-col gap-0.5">
|
|
94
|
-
<div data-slot="settings-row-label" class="text-sm font-medium">
|
|
95
|
-
{local.label}
|
|
96
|
-
</div>
|
|
97
|
-
<Show when={local.description}>
|
|
98
|
-
<p class="max-w-prose text-sm text-muted-foreground text-pretty">{local.description}</p>
|
|
99
|
-
</Show>
|
|
100
|
-
</div>
|
|
108
|
+
<FormFieldContext.Provider value={field}>
|
|
101
109
|
<div
|
|
102
|
-
data-slot="settings-row
|
|
103
|
-
class={cn(
|
|
110
|
+
data-slot="settings-row"
|
|
111
|
+
class={cn(
|
|
112
|
+
'flex gap-4 p-4',
|
|
113
|
+
local.orientation === 'vertical'
|
|
114
|
+
? 'flex-col'
|
|
115
|
+
: 'flex-col sm:flex-row sm:items-center sm:justify-between',
|
|
116
|
+
local.class
|
|
117
|
+
)}
|
|
118
|
+
{...rest}
|
|
104
119
|
>
|
|
105
|
-
|
|
120
|
+
<div class="flex min-w-0 flex-col gap-0.5">
|
|
121
|
+
<div id={labelId} data-slot="settings-row-label" class="text-sm font-medium">
|
|
122
|
+
{local.label}
|
|
123
|
+
</div>
|
|
124
|
+
<Show when={local.description}>
|
|
125
|
+
<p id={descriptionId} class="max-w-prose text-sm text-muted-foreground text-pretty">
|
|
126
|
+
{local.description}
|
|
127
|
+
</p>
|
|
128
|
+
</Show>
|
|
129
|
+
</div>
|
|
130
|
+
<div
|
|
131
|
+
data-slot="settings-row-control"
|
|
132
|
+
class={cn('shrink-0', local.orientation === 'vertical' && 'w-full')}
|
|
133
|
+
>
|
|
134
|
+
{local.children}
|
|
135
|
+
</div>
|
|
106
136
|
</div>
|
|
107
|
-
</
|
|
137
|
+
</FormFieldContext.Provider>
|
|
108
138
|
)
|
|
109
139
|
}
|
|
110
140
|
|
|
@@ -28,6 +28,7 @@ const geometry = (element: HTMLDivElement) => ({
|
|
|
28
28
|
scrollHeight: element.scrollHeight,
|
|
29
29
|
clientHeight: element.clientHeight,
|
|
30
30
|
})
|
|
31
|
+
const noop = () => {}
|
|
31
32
|
|
|
32
33
|
/** Internal plain-scroller binding; host-owned restoration can disable follow. */
|
|
33
34
|
export function createScrollFollow(options: {
|
|
@@ -45,6 +46,9 @@ export function createScrollFollow(options: {
|
|
|
45
46
|
let lastWriteClientHeight = -1
|
|
46
47
|
let previousTop = -1
|
|
47
48
|
let lastScrollClientHeight = 0
|
|
49
|
+
let pendingRestore: { frame: number; fromTop: number; target: number } | undefined
|
|
50
|
+
let pendingRestoreUserIntent = false
|
|
51
|
+
let removeRestoreIntentListeners = noop
|
|
48
52
|
const reportPosition = () => {
|
|
49
53
|
if (scroller)
|
|
50
54
|
untrack(() =>
|
|
@@ -60,12 +64,12 @@ export function createScrollFollow(options: {
|
|
|
60
64
|
// Instant writes keep the self-scroll reference synchronized. A smooth
|
|
61
65
|
// animation would produce intermediate positions that resemble user input.
|
|
62
66
|
element.scrollTop = target
|
|
63
|
-
lastWriteTop =
|
|
67
|
+
lastWriteTop = element.scrollTop
|
|
64
68
|
lastWriteClientHeight = element.clientHeight
|
|
65
|
-
previousTop =
|
|
69
|
+
previousTop = element.scrollTop
|
|
66
70
|
}
|
|
67
71
|
const pinAuto = () => {
|
|
68
|
-
if (!scroller || !options.enabled()) return
|
|
72
|
+
if (!scroller || !options.enabled() || pendingRestore) return
|
|
69
73
|
const geom = geometry(scroller)
|
|
70
74
|
const result = evaluateAutoPin({
|
|
71
75
|
stick,
|
|
@@ -84,6 +88,26 @@ export function createScrollFollow(options: {
|
|
|
84
88
|
}
|
|
85
89
|
const onScroll = () => {
|
|
86
90
|
if (!scroller) return
|
|
91
|
+
if (pendingRestore) {
|
|
92
|
+
// A reset can clamp the outgoing offset while the incoming rows are
|
|
93
|
+
// shorter. That notification is layout, not reader intent, and must not
|
|
94
|
+
// cancel the snapshot waiting for the new rows to commit.
|
|
95
|
+
const maxTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight)
|
|
96
|
+
const clampedFromTop = Math.min(pendingRestore.fromTop, maxTop)
|
|
97
|
+
if (
|
|
98
|
+
pendingRestore.fromTop - clampedFromTop > 1 &&
|
|
99
|
+
Math.abs(scroller.scrollTop - clampedFromTop) <= 1
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
// An unexplained offset change can also be a browser clamp. Only actual
|
|
103
|
+
// user scroll intent followed by a non-clamp movement cancels restoration.
|
|
104
|
+
if (!pendingRestoreUserIntent) return
|
|
105
|
+
cancelAnimationFrame(pendingRestore.frame)
|
|
106
|
+
pendingRestore = undefined
|
|
107
|
+
pendingRestoreUserIntent = false
|
|
108
|
+
removeRestoreIntentListeners()
|
|
109
|
+
removeRestoreIntentListeners = noop
|
|
110
|
+
}
|
|
87
111
|
if (!options.enabled()) {
|
|
88
112
|
reportPosition()
|
|
89
113
|
return
|
|
@@ -125,7 +149,21 @@ export function createScrollFollow(options: {
|
|
|
125
149
|
options.resetKey()
|
|
126
150
|
const enabled = options.enabled()
|
|
127
151
|
// Capture before any reporting callback can replace the host snapshot.
|
|
128
|
-
const
|
|
152
|
+
const candidate = untrack(() => options.initialPosition?.())
|
|
153
|
+
const initial =
|
|
154
|
+
candidate && Number.isFinite(candidate.top) && candidate.top >= 0 ? candidate : undefined
|
|
155
|
+
let scheduledRestore: typeof pendingRestore
|
|
156
|
+
let observer: ResizeObserver | undefined
|
|
157
|
+
onCleanup(() => {
|
|
158
|
+
if (scheduledRestore) {
|
|
159
|
+
cancelAnimationFrame(scheduledRestore.frame)
|
|
160
|
+
if (pendingRestore === scheduledRestore) pendingRestore = undefined
|
|
161
|
+
}
|
|
162
|
+
pendingRestoreUserIntent = false
|
|
163
|
+
removeRestoreIntentListeners()
|
|
164
|
+
removeRestoreIntentListeners = noop
|
|
165
|
+
observer?.disconnect()
|
|
166
|
+
})
|
|
129
167
|
stick = true
|
|
130
168
|
lastWriteTop = -1
|
|
131
169
|
lastWriteClientHeight = -1
|
|
@@ -135,19 +173,75 @@ export function createScrollFollow(options: {
|
|
|
135
173
|
if (!enabled || !scroller) return
|
|
136
174
|
// Snapshot updates must not become another positioning owner. Consume the
|
|
137
175
|
// host's snapshot only at entry/reset, then native reader intent owns follow.
|
|
138
|
-
if (initial
|
|
139
|
-
stick = initial.following
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
176
|
+
if (initial) {
|
|
177
|
+
stick = enabled && initial.following
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const observe = () => {
|
|
181
|
+
if (!enabled || typeof ResizeObserver === 'undefined') return
|
|
182
|
+
observer = new ResizeObserver(pinAuto)
|
|
183
|
+
observer.observe(scroller!)
|
|
184
|
+
if (content) observer.observe(content)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const watchRestoreIntent = () => {
|
|
188
|
+
const element = scroller
|
|
189
|
+
if (!element) return
|
|
190
|
+
const markIntent = (event: Event) => {
|
|
191
|
+
if (!pendingRestore) return
|
|
192
|
+
if (event.type === 'pointerdown' && event.target !== element) return
|
|
193
|
+
if (event.type === 'keydown') {
|
|
194
|
+
if (event.target !== element) return
|
|
195
|
+
const key = (event as KeyboardEvent).key
|
|
196
|
+
if (!['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Home', 'End', ' '].includes(key))
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
pendingRestoreUserIntent = true
|
|
200
|
+
}
|
|
201
|
+
element.addEventListener('wheel', markIntent, { passive: true })
|
|
202
|
+
element.addEventListener('pointerdown', markIntent)
|
|
203
|
+
element.addEventListener('keydown', markIntent)
|
|
204
|
+
removeRestoreIntentListeners = () => {
|
|
205
|
+
element.removeEventListener('wheel', markIntent)
|
|
206
|
+
element.removeEventListener('pointerdown', markIntent)
|
|
207
|
+
element.removeEventListener('keydown', markIntent)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (initial && !stick) {
|
|
212
|
+
// The identity effect can run before Solid has inserted the new rows. A
|
|
213
|
+
// synchronous write is clamped by the outgoing short transcript, so defer
|
|
214
|
+
// restoration until the browser has committed the new content and layout.
|
|
215
|
+
const restore = {
|
|
216
|
+
frame: 0,
|
|
217
|
+
fromTop: scroller.scrollTop,
|
|
218
|
+
target: initial.top,
|
|
219
|
+
}
|
|
220
|
+
scheduledRestore = restore
|
|
221
|
+
pendingRestore = restore
|
|
222
|
+
pendingRestoreUserIntent = false
|
|
223
|
+
watchRestoreIntent()
|
|
224
|
+
// Keep resize observation alive during the pending frame. Its callback is
|
|
225
|
+
// guarded above, and remains active if user input cancels restoration.
|
|
226
|
+
observe()
|
|
227
|
+
restore.frame = requestAnimationFrame(() => {
|
|
228
|
+
if (pendingRestore !== restore || !scroller) return
|
|
229
|
+
pendingRestore = undefined
|
|
230
|
+
pendingRestoreUserIntent = false
|
|
231
|
+
removeRestoreIntentListeners()
|
|
232
|
+
removeRestoreIntentListeners = noop
|
|
233
|
+
writePin(scroller, restore.target)
|
|
234
|
+
setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))
|
|
235
|
+
reportPosition()
|
|
236
|
+
})
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!initial && enabled) writePin(scroller, bottomTarget(geometry(scroller)))
|
|
241
|
+
else if (initial && stick) writePin(scroller, bottomTarget(geometry(scroller)))
|
|
144
242
|
setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))
|
|
145
243
|
reportPosition()
|
|
146
|
-
|
|
147
|
-
const observer = new ResizeObserver(pinAuto)
|
|
148
|
-
observer.observe(scroller)
|
|
149
|
-
if (content) observer.observe(content)
|
|
150
|
-
onCleanup(() => observer.disconnect())
|
|
244
|
+
observe()
|
|
151
245
|
})
|
|
152
246
|
|
|
153
247
|
return {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComponentProps } from 'solid-js'
|
|
2
|
+
import { splitProps } from 'solid-js'
|
|
3
|
+
import { cn } from '#lib/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A keyboard-first link to the main content region.
|
|
7
|
+
*
|
|
8
|
+
* Place it before the shell's navigation and set the destination element's
|
|
9
|
+
* `id` to match `href`. The link stays translated above the viewport until it
|
|
10
|
+
* receives focus, then moves into view. Its text remains available to assistive
|
|
11
|
+
* technology while it is visually hidden.
|
|
12
|
+
*/
|
|
13
|
+
export function SkipLink(props: ComponentProps<'a'>) {
|
|
14
|
+
const [local, rest] = splitProps(props, ['class', 'children', 'href', 'tabIndex'])
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<a
|
|
18
|
+
data-slot="skip-link"
|
|
19
|
+
class={cn(
|
|
20
|
+
'fixed top-0 start-2 z-(--z-toast) -translate-y-full',
|
|
21
|
+
'inline-flex items-center rounded-md border border-border bg-background',
|
|
22
|
+
'px-control-md py-2 text-sm font-medium text-foreground shadow-lg',
|
|
23
|
+
'underline decoration-2 underline-offset-2 outline-none transition-transform duration-150',
|
|
24
|
+
'focus:translate-y-0 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
25
|
+
'motion-reduce:transition-none',
|
|
26
|
+
local.class
|
|
27
|
+
)}
|
|
28
|
+
href={local.href ?? '#main-content'}
|
|
29
|
+
tabIndex={local.tabIndex ?? 0}
|
|
30
|
+
{...rest}
|
|
31
|
+
>
|
|
32
|
+
{local.children === undefined ? 'Skip to main content' : local.children}
|
|
33
|
+
</a>
|
|
34
|
+
)
|
|
35
|
+
}
|