@adea-ai/ui 0.123.0 → 0.124.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/dist/NOTICE +18 -0
- package/dist/components/layout/split-layout/resize-controller-core-retry.d.ts +13 -0
- package/dist/components/layout/split-layout/resize-controller-core-retry.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-core-retry.js +63 -0
- package/dist/components/layout/split-layout/resize-controller-core-retry.js.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-core.d.ts +13 -0
- package/dist/components/layout/split-layout/resize-controller-core.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-core.js +63 -0
- package/dist/components/layout/split-layout/resize-controller-core.js.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-retry-2.d.ts +3 -0
- package/dist/components/layout/split-layout/resize-controller-retry-2.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-retry-2.js +2 -0
- package/dist/components/layout/split-layout/resize-controller-retry.d.ts +2 -0
- package/dist/components/layout/split-layout/resize-controller-retry.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller-retry.js +2 -0
- package/dist/components/layout/split-layout/resize-controller.d.ts +2 -0
- package/dist/components/layout/split-layout/resize-controller.d.ts.map +1 -0
- package/dist/components/layout/split-layout/resize-controller.js +2 -0
- package/dist/components/layout/split-layout/split-layout.d.ts.map +1 -1
- package/dist/components/layout/split-layout/split-layout.js +171 -60
- package/dist/components/layout/split-layout/split-layout.js.map +1 -1
- package/dist/components/theme/appearance-panel.js +1 -1
- package/dist/components/theme/index.d.ts +1 -1
- package/dist/components/theme/index.d.ts.map +1 -1
- package/dist/components/theme/index.js +5 -2
- package/dist/components/theme/theme-context.d.ts +13 -0
- package/dist/components/theme/theme-context.d.ts.map +1 -0
- package/dist/components/theme/theme-context.js +41 -0
- package/dist/components/theme/theme-context.js.map +1 -0
- package/dist/components/theme/theme-document.d.ts +76 -0
- package/dist/components/theme/theme-document.d.ts.map +1 -0
- package/dist/components/theme/theme-document.js +93 -0
- package/dist/components/theme/theme-document.js.map +1 -0
- package/dist/components/theme/theme-persistence.d.ts +50 -0
- package/dist/components/theme/theme-persistence.d.ts.map +1 -0
- package/dist/components/theme/theme-persistence.js +86 -0
- package/dist/components/theme/theme-persistence.js.map +1 -0
- package/dist/components/theme/theme-provider.d.ts +43 -75
- package/dist/components/theme/theme-provider.d.ts.map +1 -1
- package/dist/components/theme/theme-provider.js +50 -116
- package/dist/components/theme/theme-provider.js.map +1 -1
- package/dist/components/theme/theme-state.d.ts +142 -0
- package/dist/components/theme/theme-state.d.ts.map +1 -0
- package/dist/components/theme/theme-state.js +104 -0
- package/dist/components/theme/theme-state.js.map +1 -0
- package/dist/components/theme/theme-toggle.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/lib/appearance-font-settings.d.ts +1 -1
- package/dist/lib/appearance-font-settings.d.ts.map +1 -1
- package/dist/lib/appearance-font-settings.js.map +1 -1
- package/dist/lib/themes.d.ts +48 -0
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +150 -36
- package/dist/lib/themes.js.map +1 -1
- package/dist/r/registry.json +46 -0
- package/dist/r/split-layout.json +26 -0
- package/dist/r/src/components/layout/split-layout/resize-controller-core-retry.tsx +66 -0
- package/dist/r/src/components/layout/split-layout/resize-controller-core.tsx +66 -0
- package/dist/r/src/components/layout/split-layout/resize-controller-retry-2.tsx +2 -0
- package/dist/r/src/components/layout/split-layout/resize-controller-retry.tsx +1 -0
- package/dist/r/src/components/layout/split-layout/resize-controller.tsx +1 -0
- package/dist/r/src/components/layout/split-layout/split-layout.tsx +197 -49
- package/dist/r/src/components/theme/index.ts +3 -0
- package/dist/r/src/components/theme/theme-context.ts +43 -0
- package/dist/r/src/components/theme/theme-document.ts +168 -0
- package/dist/r/src/components/theme/theme-persistence.ts +140 -0
- package/dist/r/src/components/theme/theme-provider.tsx +126 -253
- package/dist/r/src/components/theme/theme-state.ts +225 -0
- package/dist/r/src/lib/appearance-font-settings.ts +1 -1
- package/dist/r/src/lib/themes.ts +181 -40
- package/dist/r/theming.json +20 -0
- package/package.json +1 -1
- package/registry.json +46 -0
- package/src/components/layout/split-layout/resize-controller-core-retry.tsx +66 -0
- package/src/components/layout/split-layout/resize-controller-core.tsx +66 -0
- package/src/components/layout/split-layout/resize-controller-retry-2.tsx +2 -0
- package/src/components/layout/split-layout/resize-controller-retry.tsx +1 -0
- package/src/components/layout/split-layout/resize-controller.tsx +1 -0
- package/src/components/layout/split-layout/split-layout.tsx +197 -49
- package/src/components/theme/index.ts +3 -0
- package/src/components/theme/theme-context.ts +43 -0
- package/src/components/theme/theme-document.ts +168 -0
- package/src/components/theme/theme-persistence.ts +140 -0
- package/src/components/theme/theme-provider.tsx +126 -253
- package/src/components/theme/theme-state.ts +225 -0
- package/src/index.ts +3 -0
- package/src/lib/appearance-font-settings.ts +1 -1
- package/src/lib/themes.ts +181 -40
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Resizable from '@corvu/resizable'
|
|
2
|
+
import { type Accessor, type JSX } from 'solid-js'
|
|
3
|
+
import {
|
|
4
|
+
listLeaves,
|
|
5
|
+
MIN_SPLIT_RATIO,
|
|
6
|
+
MAX_SPLIT_RATIO,
|
|
7
|
+
type SplitLayoutBranch,
|
|
8
|
+
type SplitLayoutLeaf,
|
|
9
|
+
} from './model'
|
|
10
|
+
|
|
11
|
+
export type SplitLayoutResizeControllerProps<L extends SplitLayoutLeaf> = {
|
|
12
|
+
branch: Accessor<SplitLayoutBranch<L>>
|
|
13
|
+
rect: Accessor<JSX.CSSProperties>
|
|
14
|
+
domId: (id: string) => string
|
|
15
|
+
root: () => HTMLDivElement | undefined
|
|
16
|
+
labelForSeparator?: (branch: SplitLayoutBranch<L>) => string
|
|
17
|
+
onResize: (splitId: string, ratio: number) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Corvu stays behind the first real split while the pane owners remain mounted. */
|
|
21
|
+
export default function SplitLayoutResizeController<L extends SplitLayoutLeaf>(
|
|
22
|
+
props: SplitLayoutResizeControllerProps<L>
|
|
23
|
+
) {
|
|
24
|
+
const id = () => props.branch().id
|
|
25
|
+
return (
|
|
26
|
+
<Resizable
|
|
27
|
+
orientation={props.branch().direction === 'row' ? 'horizontal' : 'vertical'}
|
|
28
|
+
sizes={[props.branch().ratio, 1 - props.branch().ratio]}
|
|
29
|
+
keyboardDelta={0.05}
|
|
30
|
+
onSizesChange={(sizes) => {
|
|
31
|
+
// Corvu also reports panel registration/unregistration while
|
|
32
|
+
// mounting or disposing a controller. Those incomplete or
|
|
33
|
+
// hidden-host sizes are not user resize intent.
|
|
34
|
+
const root = props.root()
|
|
35
|
+
if (!root?.isConnected || root.getClientRects().length === 0) return
|
|
36
|
+
if (
|
|
37
|
+
sizes.length !== 2 ||
|
|
38
|
+
sizes.some((size) => !Number.isFinite(size)) ||
|
|
39
|
+
Math.abs(sizes[0]! + sizes[1]! - 1) > 0.000001
|
|
40
|
+
)
|
|
41
|
+
return
|
|
42
|
+
if (sizes[0] !== undefined && Math.abs(sizes[0] - props.branch().ratio) > 0.000001)
|
|
43
|
+
props.onResize(id(), sizes[0])
|
|
44
|
+
}}
|
|
45
|
+
style={props.rect()}
|
|
46
|
+
class="pointer-events-none absolute flex min-h-0 min-w-0 data-[orientation=vertical]:flex-col"
|
|
47
|
+
>
|
|
48
|
+
<Resizable.Panel minSize={MIN_SPLIT_RATIO} maxSize={MAX_SPLIT_RATIO} aria-hidden="true" />
|
|
49
|
+
<Resizable.Handle
|
|
50
|
+
aria-label={
|
|
51
|
+
props.labelForSeparator?.(props.branch()) ??
|
|
52
|
+
(props.branch().direction === 'row' ? 'Resize pane columns' : 'Resize pane rows')
|
|
53
|
+
}
|
|
54
|
+
aria-orientation={props.branch().direction === 'row' ? 'vertical' : 'horizontal'}
|
|
55
|
+
aria-controls={listLeaves(props.branch())
|
|
56
|
+
.map((leaf) => props.domId(leaf.id))
|
|
57
|
+
.join(' ')}
|
|
58
|
+
aria-valuemin={10}
|
|
59
|
+
aria-valuemax={90}
|
|
60
|
+
aria-valuenow={Math.round(props.branch().ratio * 100)}
|
|
61
|
+
class="pointer-events-auto relative w-px shrink-0 bg-border after:absolute after:inset-y-0 after:-inset-x-1 after:w-3 focus-visible:bg-primary focus-visible:outline-none hover:bg-primary data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:inset-x-0 data-[orientation=vertical]:after:-inset-y-1 data-[orientation=vertical]:after:h-3 data-[orientation=vertical]:after:w-full"
|
|
62
|
+
/>
|
|
63
|
+
<Resizable.Panel minSize={MIN_SPLIT_RATIO} maxSize={MAX_SPLIT_RATIO} aria-hidden="true" />
|
|
64
|
+
</Resizable>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './resize-controller-core'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './resize-controller-core'
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import Resizable from '@corvu/resizable'
|
|
2
1
|
import {
|
|
3
2
|
For,
|
|
4
|
-
|
|
3
|
+
createEffect,
|
|
5
4
|
createComputed,
|
|
5
|
+
createMemo,
|
|
6
6
|
createUniqueId,
|
|
7
7
|
createSignal,
|
|
8
|
+
onMount,
|
|
8
9
|
onCleanup,
|
|
9
10
|
on,
|
|
11
|
+
Show,
|
|
10
12
|
type Accessor,
|
|
11
13
|
type JSX,
|
|
12
14
|
} from 'solid-js'
|
|
15
|
+
import { Dynamic } from 'solid-js/web'
|
|
13
16
|
import { X, GripVertical } from 'lucide-solid'
|
|
14
17
|
import { ActionButton } from '../../composites/action-button'
|
|
18
|
+
import { Button } from '../../ui/button'
|
|
15
19
|
import { cn } from '../../../lib/utils'
|
|
16
20
|
import { computeLayoutFrames, type LayoutRect } from './geometry'
|
|
17
21
|
import { paneDropIntent, type PaneDropIntent } from './drop'
|
|
@@ -21,12 +25,16 @@ const DRAG_PREVIEW_MAX_WIDTH = 384
|
|
|
21
25
|
const DRAG_PREVIEW_MAX_HEIGHT = 256
|
|
22
26
|
import {
|
|
23
27
|
listLeaves,
|
|
24
|
-
MIN_SPLIT_RATIO,
|
|
25
|
-
MAX_SPLIT_RATIO,
|
|
26
28
|
type SplitLayoutBranch,
|
|
27
29
|
type SplitLayoutLeaf,
|
|
28
30
|
type SplitLayoutState,
|
|
29
31
|
} from './model'
|
|
32
|
+
import type { SplitLayoutResizeControllerProps } from './resize-controller-core'
|
|
33
|
+
|
|
34
|
+
type ResizeController<L extends SplitLayoutLeaf> = (
|
|
35
|
+
props: SplitLayoutResizeControllerProps<L>
|
|
36
|
+
) => JSX.Element
|
|
37
|
+
type ResizeControllerState<L extends SplitLayoutLeaf> = ResizeController<L> | false | 'error'
|
|
30
38
|
|
|
31
39
|
export type SplitLayoutProps<L extends SplitLayoutLeaf> = {
|
|
32
40
|
state: SplitLayoutState<L>
|
|
@@ -59,6 +67,16 @@ function rectStyle(rect: LayoutRect): JSX.CSSProperties {
|
|
|
59
67
|
height: `${rect.height * 100}%`,
|
|
60
68
|
}
|
|
61
69
|
}
|
|
70
|
+
function separatorPlaceholderStyle(branch: SplitLayoutBranch, rect: LayoutRect): JSX.CSSProperties {
|
|
71
|
+
const horizontal = branch.direction === 'row'
|
|
72
|
+
return {
|
|
73
|
+
left: `${(rect.x + (horizontal ? rect.width * branch.ratio : 0)) * 100}%`,
|
|
74
|
+
top: `${(rect.y + (horizontal ? 0 : rect.height * branch.ratio)) * 100}%`,
|
|
75
|
+
width: horizontal ? '1px' : `${rect.width * 100}%`,
|
|
76
|
+
height: horizontal ? `${rect.height * 100}%` : '1px',
|
|
77
|
+
transform: horizontal ? 'translateX(-50%)' : 'translateY(-50%)',
|
|
78
|
+
}
|
|
79
|
+
}
|
|
62
80
|
function dragPreviewPlaceholder(source: HTMLElement, message: string) {
|
|
63
81
|
const placeholder = document.createElement('div')
|
|
64
82
|
placeholder.className = cn(
|
|
@@ -171,14 +189,40 @@ function cloneDragPreviewNode(source: Node): Node | undefined {
|
|
|
171
189
|
export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L>) {
|
|
172
190
|
let root: HTMLDivElement | undefined
|
|
173
191
|
let pendingFrame: number | undefined
|
|
192
|
+
let disposed = false
|
|
193
|
+
let resizeLoadGeneration = 0
|
|
194
|
+
let retryWithAlternateImport = false
|
|
195
|
+
let alternateImportAttempts = 0
|
|
196
|
+
let focusHandleAfterRetry = false
|
|
197
|
+
let focusRecoveryButtonAfterError = false
|
|
198
|
+
let focusRestoreEpoch = 0
|
|
199
|
+
let focusEpoch = 0
|
|
200
|
+
let pendingFocusFrame: number | undefined
|
|
174
201
|
const refs = new Map<string, HTMLElement>()
|
|
175
202
|
const domIds = new Map<string, string>()
|
|
176
203
|
const prefix = createUniqueId()
|
|
204
|
+
const resizeReloadWarningId = `${prefix}-resize-reload-warning`
|
|
177
205
|
let nextDomId = 0
|
|
178
206
|
let nextDragId = 0
|
|
179
207
|
let dragPreview: HTMLElement | undefined
|
|
180
208
|
const [drag, setDrag] = createSignal<{ id: string; token: string }>()
|
|
181
209
|
const [drop, setDrop] = createSignal<{ id: string; intent: PaneDropIntent }>()
|
|
210
|
+
const [resizeController, setResizeController] = createSignal<ResizeControllerState<L>>()
|
|
211
|
+
const cancelPendingFocusRestore = () => {
|
|
212
|
+
if (pendingFocusFrame !== undefined) cancelAnimationFrame(pendingFocusFrame)
|
|
213
|
+
pendingFocusFrame = undefined
|
|
214
|
+
}
|
|
215
|
+
const trackFocusChange = (event: FocusEvent) => {
|
|
216
|
+
if (event.target === document.body) return
|
|
217
|
+
focusEpoch += 1
|
|
218
|
+
focusHandleAfterRetry = false
|
|
219
|
+
focusRecoveryButtonAfterError = false
|
|
220
|
+
cancelPendingFocusRestore()
|
|
221
|
+
}
|
|
222
|
+
onMount(() => {
|
|
223
|
+
document.addEventListener('focusin', trackFocusChange)
|
|
224
|
+
onCleanup(() => document.removeEventListener('focusin', trackFocusChange))
|
|
225
|
+
})
|
|
182
226
|
const clearDrag = () => {
|
|
183
227
|
setDrag(undefined)
|
|
184
228
|
setDrop(undefined)
|
|
@@ -221,6 +265,79 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
|
|
|
221
265
|
frame.node.kind === 'split' ? [frame.node] : []
|
|
222
266
|
)
|
|
223
267
|
)
|
|
268
|
+
createEffect(() => {
|
|
269
|
+
if (branches().length === 0) {
|
|
270
|
+
resizeLoadGeneration += 1
|
|
271
|
+
focusHandleAfterRetry = false
|
|
272
|
+
focusRecoveryButtonAfterError = false
|
|
273
|
+
cancelPendingFocusRestore()
|
|
274
|
+
// Keep a loaded controller so a later split does not re-import a facade
|
|
275
|
+
// that may have failed before recovery. Clear only transient UI state.
|
|
276
|
+
if (resizeController() === false || resizeController() === 'error')
|
|
277
|
+
setResizeController(undefined)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
if (resizeController() !== undefined) return
|
|
281
|
+
const retry = retryWithAlternateImport
|
|
282
|
+
const generation = ++resizeLoadGeneration
|
|
283
|
+
setResizeController(false)
|
|
284
|
+
const controllerImport = retry
|
|
285
|
+
? alternateImportAttempts++ === 0
|
|
286
|
+
? import('./resize-controller-retry')
|
|
287
|
+
: import('./resize-controller-retry-2')
|
|
288
|
+
: import('./resize-controller')
|
|
289
|
+
void controllerImport.then(
|
|
290
|
+
({ default: Controller }) => {
|
|
291
|
+
if (disposed || generation !== resizeLoadGeneration || branches().length === 0) return
|
|
292
|
+
const shouldFocusHandle = focusHandleAfterRetry && focusEpoch === focusRestoreEpoch
|
|
293
|
+
focusHandleAfterRetry = false
|
|
294
|
+
focusRecoveryButtonAfterError = false
|
|
295
|
+
retryWithAlternateImport = false
|
|
296
|
+
alternateImportAttempts = 0
|
|
297
|
+
setResizeController(() => Controller)
|
|
298
|
+
if (shouldFocusHandle)
|
|
299
|
+
pendingFocusFrame = requestAnimationFrame(() => {
|
|
300
|
+
pendingFocusFrame = undefined
|
|
301
|
+
if (
|
|
302
|
+
disposed ||
|
|
303
|
+
generation !== resizeLoadGeneration ||
|
|
304
|
+
branches().length === 0 ||
|
|
305
|
+
focusEpoch !== focusRestoreEpoch
|
|
306
|
+
)
|
|
307
|
+
return
|
|
308
|
+
const currentFocus = root?.ownerDocument.activeElement
|
|
309
|
+
if (currentFocus && currentFocus !== root?.ownerDocument.body && currentFocus !== root)
|
|
310
|
+
return
|
|
311
|
+
root?.querySelector<HTMLElement>('[data-corvu-resizable-handle]')?.focus({
|
|
312
|
+
preventScroll: true,
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
},
|
|
316
|
+
() => {
|
|
317
|
+
// A rejected dynamic import can remain cached under its specifier.
|
|
318
|
+
// The next split must start from the alternate module entry as well.
|
|
319
|
+
retryWithAlternateImport = true
|
|
320
|
+
if (!disposed && generation === resizeLoadGeneration && branches().length > 0) {
|
|
321
|
+
setResizeController('error')
|
|
322
|
+
const shouldFocusRetry = focusRecoveryButtonAfterError && focusEpoch === focusRestoreEpoch
|
|
323
|
+
if (shouldFocusRetry)
|
|
324
|
+
pendingFocusFrame = requestAnimationFrame(() => {
|
|
325
|
+
pendingFocusFrame = undefined
|
|
326
|
+
if (
|
|
327
|
+
disposed ||
|
|
328
|
+
generation !== resizeLoadGeneration ||
|
|
329
|
+
branches().length === 0 ||
|
|
330
|
+
focusEpoch !== focusRestoreEpoch
|
|
331
|
+
)
|
|
332
|
+
return
|
|
333
|
+
root?.querySelector<HTMLButtonElement>('[data-resize-retry]')?.focus({
|
|
334
|
+
preventScroll: true,
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
})
|
|
224
341
|
createComputed(() => {
|
|
225
342
|
const current = drag()
|
|
226
343
|
if (current && (!props.onMove || !leafMap().has(current.id))) clearDrag()
|
|
@@ -288,6 +405,10 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
|
|
|
288
405
|
)
|
|
289
406
|
)
|
|
290
407
|
onCleanup(() => {
|
|
408
|
+
disposed = true
|
|
409
|
+
focusHandleAfterRetry = false
|
|
410
|
+
focusRecoveryButtonAfterError = false
|
|
411
|
+
cancelPendingFocusRestore()
|
|
291
412
|
if (pendingFrame !== undefined) cancelAnimationFrame(pendingFrame)
|
|
292
413
|
clearDrag()
|
|
293
414
|
refs.clear()
|
|
@@ -452,62 +573,89 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
|
|
|
452
573
|
}}
|
|
453
574
|
</For>
|
|
454
575
|
<span class="sr-only" role="status">
|
|
455
|
-
{dropLabel()
|
|
576
|
+
{dropLabel() ||
|
|
577
|
+
(resizeController() === 'error'
|
|
578
|
+
? alternateImportAttempts >= 2
|
|
579
|
+
? 'Resize controls did not load after repeated attempts. Reloading may discard unsaved pane content.'
|
|
580
|
+
: 'Resize controls did not load.'
|
|
581
|
+
: '')}
|
|
456
582
|
</span>
|
|
457
583
|
<For each={branches().map((branch) => branch.id)}>
|
|
458
584
|
{(id) => {
|
|
459
585
|
const initial = branchMap().get(id)!
|
|
460
586
|
const branch: Accessor<SplitLayoutBranch<L>> = () => branchMap().get(id) ?? initial
|
|
587
|
+
const rect = () => frames().get(id)?.rect ?? { x: 0, y: 0, width: 0, height: 0 }
|
|
461
588
|
return (
|
|
462
|
-
<
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
589
|
+
<Show
|
|
590
|
+
when={typeof resizeController() === 'function'}
|
|
591
|
+
fallback={
|
|
592
|
+
<div
|
|
593
|
+
role="separator"
|
|
594
|
+
aria-label={
|
|
595
|
+
props.labelForSeparator?.(branch()) ??
|
|
596
|
+
(branch().direction === 'row' ? 'Resize pane columns' : 'Resize pane rows')
|
|
597
|
+
}
|
|
598
|
+
aria-orientation={branch().direction === 'row' ? 'vertical' : 'horizontal'}
|
|
599
|
+
aria-controls={listLeaves(branch())
|
|
600
|
+
.map((leaf) => domId(leaf.id))
|
|
601
|
+
.join(' ')}
|
|
602
|
+
aria-valuemin={10}
|
|
603
|
+
aria-valuemax={90}
|
|
604
|
+
aria-valuenow={Math.round(branch().ratio * 100)}
|
|
605
|
+
aria-disabled="true"
|
|
606
|
+
tabIndex={-1}
|
|
607
|
+
data-direction={branch().direction}
|
|
608
|
+
style={separatorPlaceholderStyle(branch(), rect())}
|
|
609
|
+
class="pointer-events-none absolute bg-border"
|
|
610
|
+
/>
|
|
611
|
+
}
|
|
482
612
|
>
|
|
483
|
-
<
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
(branch().direction === 'row' ? 'Resize pane columns' : 'Resize pane rows')
|
|
492
|
-
}
|
|
493
|
-
aria-orientation={branch().direction === 'row' ? 'vertical' : 'horizontal'}
|
|
494
|
-
aria-controls={listLeaves(branch())
|
|
495
|
-
.map((leaf) => domId(leaf.id))
|
|
496
|
-
.join(' ')}
|
|
497
|
-
aria-valuemin={10}
|
|
498
|
-
aria-valuemax={90}
|
|
499
|
-
aria-valuenow={Math.round(branch().ratio * 100)}
|
|
500
|
-
class="pointer-events-auto relative w-px shrink-0 bg-border after:absolute after:inset-y-0 after:-inset-x-1 after:w-3 focus-visible:bg-primary focus-visible:outline-none hover:bg-primary data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:inset-x-0 data-[orientation=vertical]:after:-inset-y-1 data-[orientation=vertical]:after:h-3 data-[orientation=vertical]:after:w-full"
|
|
501
|
-
/>
|
|
502
|
-
<Resizable.Panel
|
|
503
|
-
minSize={MIN_SPLIT_RATIO}
|
|
504
|
-
maxSize={MAX_SPLIT_RATIO}
|
|
505
|
-
aria-hidden="true"
|
|
613
|
+
<Dynamic
|
|
614
|
+
component={resizeController() as ResizeController<L>}
|
|
615
|
+
branch={branch}
|
|
616
|
+
rect={() => rectStyle(rect())}
|
|
617
|
+
domId={domId}
|
|
618
|
+
root={() => root}
|
|
619
|
+
labelForSeparator={props.labelForSeparator}
|
|
620
|
+
onResize={props.onResize}
|
|
506
621
|
/>
|
|
507
|
-
</
|
|
622
|
+
</Show>
|
|
508
623
|
)
|
|
509
624
|
}}
|
|
510
625
|
</For>
|
|
626
|
+
{resizeController() === 'error' ? (
|
|
627
|
+
<div class="absolute top-2 right-2 z-(--z-menu) flex max-w-sm flex-col items-end gap-2">
|
|
628
|
+
{alternateImportAttempts >= 2 ? (
|
|
629
|
+
<p
|
|
630
|
+
id={resizeReloadWarningId}
|
|
631
|
+
role="alert"
|
|
632
|
+
class="rounded-md border bg-background p-2 text-sm shadow-md"
|
|
633
|
+
>
|
|
634
|
+
Reloading may discard unsaved pane content.
|
|
635
|
+
</p>
|
|
636
|
+
) : null}
|
|
637
|
+
<Button
|
|
638
|
+
variant="outline"
|
|
639
|
+
size="sm"
|
|
640
|
+
data-resize-retry
|
|
641
|
+
aria-describedby={alternateImportAttempts >= 2 ? resizeReloadWarningId : undefined}
|
|
642
|
+
onClick={(event) => {
|
|
643
|
+
if (alternateImportAttempts >= 2) {
|
|
644
|
+
window.location.reload()
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
const ownsFocus = document.activeElement === event.currentTarget
|
|
648
|
+
focusHandleAfterRetry = ownsFocus
|
|
649
|
+
focusRecoveryButtonAfterError = ownsFocus
|
|
650
|
+
focusRestoreEpoch = focusEpoch
|
|
651
|
+
retryWithAlternateImport = true
|
|
652
|
+
setResizeController(undefined)
|
|
653
|
+
}}
|
|
654
|
+
>
|
|
655
|
+
{alternateImportAttempts >= 2 ? 'Reload page to retry resizing' : 'Retry pane resizing'}
|
|
656
|
+
</Button>
|
|
657
|
+
</div>
|
|
658
|
+
) : null}
|
|
511
659
|
</div>
|
|
512
660
|
)
|
|
513
661
|
}
|
|
@@ -11,11 +11,14 @@ export {
|
|
|
11
11
|
ThemeProvider,
|
|
12
12
|
defaultThemeSelection,
|
|
13
13
|
inlineScriptLiteral,
|
|
14
|
+
localStoragePersistence,
|
|
14
15
|
resolvedDensity,
|
|
15
16
|
themeScript,
|
|
17
|
+
useOptionalTheme,
|
|
16
18
|
useTheme,
|
|
17
19
|
type ThemeContextValue,
|
|
18
20
|
type ThemeDensity,
|
|
21
|
+
type ThemePersistence,
|
|
19
22
|
type ThemeProviderProps,
|
|
20
23
|
type ThemeSelection,
|
|
21
24
|
} from './theme-provider'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createContext, useContext, type Context } from 'solid-js'
|
|
2
|
+
import type { ThemeContextValue } from './theme-state'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where the context object lives: on the global, under a registered symbol.
|
|
6
|
+
*
|
|
7
|
+
* A context is an object identity, and a bundle can legitimately load this module
|
|
8
|
+
* twice — a dev graph where a relative import and a package subpath are distinct
|
|
9
|
+
* module records, or two copies of the package at different versions. Each copy
|
|
10
|
+
* would then call `createContext` and own its own object; a provider from one and a
|
|
11
|
+
* `useOptionalTheme()` from the other would never meet, and the second would return
|
|
12
|
+
* `undefined` as though no provider were mounted. `Symbol.for` is shared across
|
|
13
|
+
* every copy in a realm, so they all find the one context the first created.
|
|
14
|
+
*
|
|
15
|
+
* Both copies must agree on its shape. The context value only ever grows, which is
|
|
16
|
+
* why a newer provider under an older reader is safe and the reverse is a missing
|
|
17
|
+
* member, not a mismatch.
|
|
18
|
+
*/
|
|
19
|
+
const CONTEXT_KEY = Symbol.for('adea.ui.theme')
|
|
20
|
+
|
|
21
|
+
type ContextHost = { [CONTEXT_KEY]?: Context<ThemeContextValue | undefined> }
|
|
22
|
+
|
|
23
|
+
export function themeContext(): Context<ThemeContextValue | undefined> {
|
|
24
|
+
const host = globalThis as ContextHost
|
|
25
|
+
return (host[CONTEXT_KEY] ??= createContext<ThemeContextValue>())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useTheme(): ThemeContextValue {
|
|
29
|
+
const value = useContext(themeContext())
|
|
30
|
+
if (!value) throw new Error('useTheme must be called inside a <ThemeProvider>.')
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The theme context, or `undefined` outside a `ThemeProvider`.
|
|
36
|
+
*
|
|
37
|
+
* For a component that can run both ways — a shared control rendered in a harness,
|
|
38
|
+
* a story, or a host that has not mounted a provider — and would rather degrade
|
|
39
|
+
* than throw.
|
|
40
|
+
*/
|
|
41
|
+
export function useOptionalTheme(): ThemeContextValue | undefined {
|
|
42
|
+
return useContext(themeContext())
|
|
43
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyAppearanceFontSettings,
|
|
3
|
+
normalizeAppearanceEditorFontSettings,
|
|
4
|
+
} from '../../lib/appearance-font-settings'
|
|
5
|
+
import {
|
|
6
|
+
accentVariables,
|
|
7
|
+
themeCssVariables,
|
|
8
|
+
type ThemeAppearance,
|
|
9
|
+
type ThemeVariant,
|
|
10
|
+
} from '../../lib/themes'
|
|
11
|
+
import { resolvedDensity, type ThemeSelection } from './theme-state'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The document half of the theme provider: the single writer of `<html>`.
|
|
15
|
+
*
|
|
16
|
+
* Written against the few members of an element it uses, so the rules can be tested
|
|
17
|
+
* on a stand-in and the provider's effect stays a one-line call.
|
|
18
|
+
*/
|
|
19
|
+
export type ThemeRoot = Pick<
|
|
20
|
+
HTMLElement,
|
|
21
|
+
'classList' | 'dataset' | 'style' | 'removeAttribute' | 'setAttribute'
|
|
22
|
+
>
|
|
23
|
+
|
|
24
|
+
export type ThemeDocumentState = {
|
|
25
|
+
/** What the document shows: the committed selection, or a preview laid over it. */
|
|
26
|
+
selection: ThemeSelection
|
|
27
|
+
appearance: ThemeAppearance
|
|
28
|
+
variant: ThemeVariant
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ThemeDocumentOptions = {
|
|
32
|
+
/**
|
|
33
|
+
* Rewrites the custom properties just before they are written: the variant's
|
|
34
|
+
* roles with the accent laid over them. It sees every property of the pass and
|
|
35
|
+
* must return the full map; a name it drops is not written.
|
|
36
|
+
*/
|
|
37
|
+
transformTokens?: (tokens: Record<string, string>) => Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The custom properties a state paints: the variant's roles, with the accent
|
|
42
|
+
* selection laid over them, then `transformTokens`.
|
|
43
|
+
*
|
|
44
|
+
* The accent is applied *here* rather than left to the `[data-accent]` blocks in
|
|
45
|
+
* `theme.css`, because the properties are written inline and an inline declaration
|
|
46
|
+
* outranks every stylesheet selector however specific it is. The blocks still exist
|
|
47
|
+
* and are still correct — they are what styles the window before this runs — but
|
|
48
|
+
* they cannot win from here, so leaving the accent to them meant the attribute was
|
|
49
|
+
* set, the rules were present, and nothing changed on screen.
|
|
50
|
+
*/
|
|
51
|
+
export function themeDocumentTokens(
|
|
52
|
+
state: ThemeDocumentState,
|
|
53
|
+
options: ThemeDocumentOptions = {}
|
|
54
|
+
): { tokens: Record<string, string>; accented: boolean } {
|
|
55
|
+
const properties = { ...themeCssVariables(state.variant) }
|
|
56
|
+
const accent = accentVariables(state.selection.accent, state.appearance, state.selection)
|
|
57
|
+
if (accent) Object.assign(properties, accent)
|
|
58
|
+
return {
|
|
59
|
+
tokens: options.transformTokens ? options.transformTokens(properties) : properties,
|
|
60
|
+
accented: accent !== undefined,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Write a state onto a root and report which custom properties it owns.
|
|
66
|
+
*
|
|
67
|
+
* Pass the previous call's return as `previous` and any palette property that pass
|
|
68
|
+
* wrote and this one does not is removed. For the catalogue the two sets are the
|
|
69
|
+
* same, so nothing is ever removed; it matters for a host variant that carries
|
|
70
|
+
* `tokens` the catalogue does not, which would otherwise stay inline, beating the
|
|
71
|
+
* stylesheet, after the user switched away from it.
|
|
72
|
+
*/
|
|
73
|
+
export function applyThemeToRoot(
|
|
74
|
+
root: ThemeRoot,
|
|
75
|
+
state: ThemeDocumentState,
|
|
76
|
+
options: ThemeDocumentOptions = {},
|
|
77
|
+
previous?: ReadonlySet<string>
|
|
78
|
+
): ReadonlySet<string> {
|
|
79
|
+
const { tokens, accented } = themeDocumentTokens(state, options)
|
|
80
|
+
|
|
81
|
+
for (const [name, value] of Object.entries(tokens)) {
|
|
82
|
+
root.style.setProperty(name, value)
|
|
83
|
+
}
|
|
84
|
+
if (previous) {
|
|
85
|
+
for (const name of previous) {
|
|
86
|
+
if (!(name in tokens)) root.style.removeProperty(name)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
root.classList.toggle('dark', state.appearance === 'dark')
|
|
91
|
+
root.dataset['theme'] = state.variant.id
|
|
92
|
+
root.dataset['appearance'] = state.appearance
|
|
93
|
+
root.style.colorScheme = state.appearance
|
|
94
|
+
|
|
95
|
+
if (!accented) root.removeAttribute('data-accent')
|
|
96
|
+
else root.dataset['accent'] = state.selection.accent
|
|
97
|
+
|
|
98
|
+
applyAppearanceFontSettings(
|
|
99
|
+
root,
|
|
100
|
+
normalizeAppearanceEditorFontSettings(state.selection.fonts, state.selection.font).settings
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Density is a pure token shift, so unlike the colour axes there is nothing to
|
|
104
|
+
// compute: the attribute selects a block in `theme.css`. It is applied here for
|
|
105
|
+
// the same reason as the others — this is the one place that knows what the
|
|
106
|
+
// current selection is, and it is the only writer of these attributes.
|
|
107
|
+
if (resolvedDensity(state.selection) === 'comfortable') root.removeAttribute('data-density')
|
|
108
|
+
else root.dataset['density'] = resolvedDensity(state.selection)
|
|
109
|
+
|
|
110
|
+
return new Set(Object.keys(tokens))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** What {@link applyWithoutTransitions} needs of a document. */
|
|
114
|
+
export type TransitionDocument = {
|
|
115
|
+
head: { appendChild(node: unknown): unknown }
|
|
116
|
+
createElement(tag: 'style'): {
|
|
117
|
+
textContent: string | null
|
|
118
|
+
offsetHeight?: number
|
|
119
|
+
remove(): void
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Run `apply` with every CSS transition switched off for the frame that carries
|
|
125
|
+
* it, so a theme switch cannot animate page-wide.
|
|
126
|
+
*
|
|
127
|
+
* Without this, flipping the custom properties on `<html>` starts a transition on
|
|
128
|
+
* every element that declares one, and each fades from the old palette to the new
|
|
129
|
+
* over its own duration: a theme change that looks like a rendering fault. The
|
|
130
|
+
* rule is removed two frames later, after the browser has committed the new styles
|
|
131
|
+
* with transitions off, and `restore` runs it early (the provider calls it when it
|
|
132
|
+
* is disposed mid-switch).
|
|
133
|
+
*
|
|
134
|
+
* `schedule` is the frame source; it defaults to `requestAnimationFrame` and falls
|
|
135
|
+
* back to running at once where there is none, which leaves transitions on rather
|
|
136
|
+
* than off.
|
|
137
|
+
*/
|
|
138
|
+
export function applyWithoutTransitions(
|
|
139
|
+
doc: TransitionDocument,
|
|
140
|
+
apply: () => void,
|
|
141
|
+
schedule: (callback: () => void) => void = defaultSchedule
|
|
142
|
+
): () => void {
|
|
143
|
+
const style = doc.createElement('style')
|
|
144
|
+
style.textContent = '*{transition:none !important}'
|
|
145
|
+
doc.head.appendChild(style)
|
|
146
|
+
// Reading a layout property forces the browser to commit the rule before the
|
|
147
|
+
// properties change; without it the two can be batched into one style recalc.
|
|
148
|
+
void style.offsetHeight
|
|
149
|
+
let removed = false
|
|
150
|
+
const restore = () => {
|
|
151
|
+
if (removed) return
|
|
152
|
+
removed = true
|
|
153
|
+
style.remove()
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
apply()
|
|
157
|
+
} catch (error) {
|
|
158
|
+
restore()
|
|
159
|
+
throw error
|
|
160
|
+
}
|
|
161
|
+
schedule(() => schedule(restore))
|
|
162
|
+
return restore
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function defaultSchedule(callback: () => void): void {
|
|
166
|
+
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(callback)
|
|
167
|
+
else callback()
|
|
168
|
+
}
|