@adea-ai/ui 0.85.0 → 0.88.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 +17 -1
- package/dist/NOTICE +34 -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/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/layout/sidebar-nav/index.d.ts +1 -1
- package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts +18 -2
- package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts.map +1 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.js +42 -11
- package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
- 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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -7
- 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/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 +112 -1
- package/dist/r/src/components/composites/action-button/action-button.tsx +5 -1
- 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/layout/sidebar-nav/index.ts +2 -0
- package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +71 -16
- 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 +112 -1
- package/src/components/composites/action-button/action-button.tsx +5 -1
- 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/layout/sidebar-nav/index.ts +2 -0
- package/src/components/layout/sidebar-nav/sidebar-nav.tsx +71 -16
- 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 +3 -0
- package/src/lib/clipboard.ts +41 -0
- package/src/lib/download.ts +29 -0
- package/src/lib/form-field.ts +93 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup, onMount, on, type JSX } from 'solid-js'
|
|
2
|
+
import { Move, X } from 'lucide-solid'
|
|
3
|
+
import { ActionButton } from '../../composites/action-button'
|
|
4
|
+
import { ScrollArea } from '../../ui/scroll-area'
|
|
5
|
+
import {
|
|
6
|
+
type FloatingPreviewFrame,
|
|
7
|
+
type FloatingPreviewResizeDirection,
|
|
8
|
+
type FloatingPreviewSize,
|
|
9
|
+
resolveFloatingPreviewFrame,
|
|
10
|
+
resizeFloatingPreview,
|
|
11
|
+
} from './model'
|
|
12
|
+
|
|
13
|
+
const directions: readonly {
|
|
14
|
+
direction: FloatingPreviewResizeDirection
|
|
15
|
+
class: string
|
|
16
|
+
glyph: string
|
|
17
|
+
}[] = [
|
|
18
|
+
{
|
|
19
|
+
direction: 'north',
|
|
20
|
+
class: 'absolute -top-3 right-6 left-6 h-control-xs w-auto cursor-ns-resize touch-none',
|
|
21
|
+
glyph: '↕',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
direction: 'south',
|
|
25
|
+
class: 'absolute -bottom-3 right-6 left-6 h-control-xs w-auto cursor-ns-resize touch-none',
|
|
26
|
+
glyph: '↕',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
direction: 'east',
|
|
30
|
+
class: 'absolute -right-3 top-6 bottom-6 w-6 h-auto cursor-ew-resize touch-none',
|
|
31
|
+
glyph: '↔',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
direction: 'west',
|
|
35
|
+
class: 'absolute -left-3 top-6 bottom-6 w-6 h-auto cursor-ew-resize touch-none',
|
|
36
|
+
glyph: '↔',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
direction: 'northwest',
|
|
40
|
+
class: 'absolute -top-3 -left-3 size-control-xs cursor-nwse-resize touch-none',
|
|
41
|
+
glyph: '↖',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
direction: 'northeast',
|
|
45
|
+
class: 'absolute -top-3 -right-3 size-control-xs cursor-nesw-resize touch-none',
|
|
46
|
+
glyph: '↗',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
direction: 'southwest',
|
|
50
|
+
class: 'absolute -bottom-3 -left-3 size-control-xs cursor-nesw-resize touch-none',
|
|
51
|
+
glyph: '↙',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
direction: 'southeast',
|
|
55
|
+
class: 'absolute -bottom-3 -right-3 size-control-xs cursor-nwse-resize touch-none',
|
|
56
|
+
glyph: '↘',
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
export type FloatingPreviewProps = {
|
|
61
|
+
label: string
|
|
62
|
+
source: FloatingPreviewSize
|
|
63
|
+
onClose?: () => void
|
|
64
|
+
actions?: JSX.Element
|
|
65
|
+
children: JSX.Element
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A host-bounded movable preview frame. Content and actions remain caller-owned. */
|
|
69
|
+
export function FloatingPreview(props: FloatingPreviewProps) {
|
|
70
|
+
const [frame, setFrame] = createSignal<FloatingPreviewFrame>({ x: 0, y: 0, width: 1, height: 1 })
|
|
71
|
+
// oxlint-disable-next-line no-unassigned-vars -- assigned through Solid's ref callback.
|
|
72
|
+
let bounds: HTMLDivElement | undefined
|
|
73
|
+
let observer: ResizeObserver | undefined
|
|
74
|
+
let gesture:
|
|
75
|
+
| {
|
|
76
|
+
id: number
|
|
77
|
+
x: number
|
|
78
|
+
y: number
|
|
79
|
+
start: FloatingPreviewFrame
|
|
80
|
+
direction: FloatingPreviewResizeDirection | null
|
|
81
|
+
}
|
|
82
|
+
| undefined
|
|
83
|
+
|
|
84
|
+
const measure = () => {
|
|
85
|
+
const rect = bounds?.getBoundingClientRect()
|
|
86
|
+
if (!rect) return
|
|
87
|
+
setFrame((current) =>
|
|
88
|
+
resolveFloatingPreviewFrame({
|
|
89
|
+
width: current.width > 1 ? current.width : null,
|
|
90
|
+
position: current.width > 1 ? { x: current.x, y: current.y } : null,
|
|
91
|
+
source: props.source,
|
|
92
|
+
container: { width: rect.width, height: rect.height },
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
createEffect(
|
|
97
|
+
on(
|
|
98
|
+
() => [props.source.width, props.source.height] as const,
|
|
99
|
+
() => measure(),
|
|
100
|
+
{ defer: true }
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
onMount(() => {
|
|
104
|
+
if (bounds) {
|
|
105
|
+
observer = new ResizeObserver(measure)
|
|
106
|
+
observer.observe(bounds)
|
|
107
|
+
measure()
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
onCleanup(() => observer?.disconnect())
|
|
111
|
+
|
|
112
|
+
function start(event: PointerEvent, direction: FloatingPreviewResizeDirection | null) {
|
|
113
|
+
if (event.button !== 0 || gesture) return
|
|
114
|
+
event.preventDefault()
|
|
115
|
+
event.stopPropagation()
|
|
116
|
+
gesture = { id: event.pointerId, x: event.clientX, y: event.clientY, start: frame(), direction }
|
|
117
|
+
;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
|
|
118
|
+
}
|
|
119
|
+
function move(event: PointerEvent) {
|
|
120
|
+
if (!gesture || gesture.id !== event.pointerId) return
|
|
121
|
+
const rect = bounds?.getBoundingClientRect()
|
|
122
|
+
if (!rect) return
|
|
123
|
+
const container = { width: rect.width, height: rect.height }
|
|
124
|
+
const delta = { x: event.clientX - gesture.x, y: event.clientY - gesture.y }
|
|
125
|
+
setFrame(
|
|
126
|
+
gesture.direction
|
|
127
|
+
? resizeFloatingPreview({
|
|
128
|
+
start: gesture.start,
|
|
129
|
+
direction: gesture.direction,
|
|
130
|
+
delta,
|
|
131
|
+
source: props.source,
|
|
132
|
+
container,
|
|
133
|
+
})
|
|
134
|
+
: resolveFloatingPreviewFrame({
|
|
135
|
+
width: gesture.start.width,
|
|
136
|
+
position: { x: gesture.start.x + delta.x, y: gesture.start.y + delta.y },
|
|
137
|
+
source: props.source,
|
|
138
|
+
container,
|
|
139
|
+
})
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
function end(event: PointerEvent) {
|
|
143
|
+
if (gesture?.id === event.pointerId) gesture = undefined
|
|
144
|
+
}
|
|
145
|
+
function keyboardMove(event: KeyboardEvent) {
|
|
146
|
+
const delta = { x: 0, y: 0 }
|
|
147
|
+
if (event.key === 'ArrowLeft') delta.x = -10
|
|
148
|
+
else if (event.key === 'ArrowRight') delta.x = 10
|
|
149
|
+
else if (event.key === 'ArrowUp') delta.y = -10
|
|
150
|
+
else if (event.key === 'ArrowDown') delta.y = 10
|
|
151
|
+
else return
|
|
152
|
+
event.preventDefault()
|
|
153
|
+
const rect = bounds?.getBoundingClientRect()
|
|
154
|
+
if (!rect) return
|
|
155
|
+
const current = frame()
|
|
156
|
+
setFrame(
|
|
157
|
+
resolveFloatingPreviewFrame({
|
|
158
|
+
width: current.width,
|
|
159
|
+
position: { x: current.x + delta.x, y: current.y + delta.y },
|
|
160
|
+
source: props.source,
|
|
161
|
+
container: { width: rect.width, height: rect.height },
|
|
162
|
+
})
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
function keyboardResize(event: KeyboardEvent, direction: FloatingPreviewResizeDirection) {
|
|
166
|
+
if (!event.key.startsWith('Arrow')) return
|
|
167
|
+
event.preventDefault()
|
|
168
|
+
const rect = bounds?.getBoundingClientRect()
|
|
169
|
+
if (!rect) return
|
|
170
|
+
const delta = {
|
|
171
|
+
x: event.key === 'ArrowRight' ? 10 : event.key === 'ArrowLeft' ? -10 : 0,
|
|
172
|
+
y: event.key === 'ArrowDown' ? 10 : event.key === 'ArrowUp' ? -10 : 0,
|
|
173
|
+
}
|
|
174
|
+
setFrame(
|
|
175
|
+
resizeFloatingPreview({
|
|
176
|
+
start: frame(),
|
|
177
|
+
direction,
|
|
178
|
+
delta,
|
|
179
|
+
source: props.source,
|
|
180
|
+
container: { width: rect.width, height: rect.height },
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div
|
|
187
|
+
ref={bounds}
|
|
188
|
+
class="pointer-events-none absolute inset-0 z-(--z-sticky) overflow-hidden"
|
|
189
|
+
aria-label={props.label}
|
|
190
|
+
>
|
|
191
|
+
<section
|
|
192
|
+
class="pointer-events-auto absolute flex flex-col overflow-visible rounded-lg border border-border bg-card text-card-foreground shadow-lg"
|
|
193
|
+
aria-label={props.label}
|
|
194
|
+
style={{
|
|
195
|
+
left: `${frame().x}px`,
|
|
196
|
+
top: `${frame().y}px`,
|
|
197
|
+
width: `${frame().width}px`,
|
|
198
|
+
height: `${frame().height}px`,
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<header class="flex h-control-sm items-center gap-1 border-b border-border p-1">
|
|
202
|
+
<ActionButton
|
|
203
|
+
type="button"
|
|
204
|
+
variant="ghost"
|
|
205
|
+
size="sm"
|
|
206
|
+
class="flex-1 cursor-move touch-none"
|
|
207
|
+
aria-label={`Move ${props.label}`}
|
|
208
|
+
tooltip={`Move ${props.label}. Use arrow keys to adjust its position.`}
|
|
209
|
+
onPointerDown={(event) => start(event, null)}
|
|
210
|
+
onPointerMove={move}
|
|
211
|
+
onPointerUp={end}
|
|
212
|
+
onPointerCancel={end}
|
|
213
|
+
onLostPointerCapture={end}
|
|
214
|
+
onKeyDown={keyboardMove}
|
|
215
|
+
>
|
|
216
|
+
<>
|
|
217
|
+
<Move aria-hidden="true" />
|
|
218
|
+
Move
|
|
219
|
+
</>
|
|
220
|
+
</ActionButton>
|
|
221
|
+
<div class="flex items-center gap-1">{props.actions}</div>
|
|
222
|
+
{props.onClose && (
|
|
223
|
+
<ActionButton
|
|
224
|
+
type="button"
|
|
225
|
+
variant="ghost"
|
|
226
|
+
size="icon-sm"
|
|
227
|
+
aria-label={`Close ${props.label}`}
|
|
228
|
+
tooltip={`Close ${props.label}.`}
|
|
229
|
+
onClick={props.onClose}
|
|
230
|
+
>
|
|
231
|
+
<X aria-hidden="true" />
|
|
232
|
+
</ActionButton>
|
|
233
|
+
)}
|
|
234
|
+
</header>
|
|
235
|
+
<ScrollArea role="region" aria-label={`${props.label} content`} class="flex-1" tabIndex={0}>
|
|
236
|
+
{props.children}
|
|
237
|
+
</ScrollArea>
|
|
238
|
+
{directions.map(({ direction, class: className, glyph }) => (
|
|
239
|
+
<ActionButton
|
|
240
|
+
type="button"
|
|
241
|
+
variant="ghost"
|
|
242
|
+
size="icon-sm"
|
|
243
|
+
class={className}
|
|
244
|
+
data-direction={direction}
|
|
245
|
+
aria-label={`Resize ${props.label} ${direction}`}
|
|
246
|
+
tooltip={`Resize ${props.label} from the ${direction} edge. Use arrow keys to resize.`}
|
|
247
|
+
onPointerDown={(event) => start(event, direction)}
|
|
248
|
+
onPointerMove={move}
|
|
249
|
+
onPointerUp={end}
|
|
250
|
+
onPointerCancel={end}
|
|
251
|
+
onLostPointerCapture={end}
|
|
252
|
+
onKeyDown={(event) => keyboardResize(event, direction)}
|
|
253
|
+
>
|
|
254
|
+
<span aria-hidden="true">{glyph}</span>
|
|
255
|
+
</ActionButton>
|
|
256
|
+
))}
|
|
257
|
+
</section>
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
}
|