@adea-ai/ui 0.109.0 → 0.110.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 +14 -0
- package/dist/components/composites/update-dialog/update-dialog.js +1 -1
- package/dist/components/layout/contextual-sidebar/contextual-sidebar.d.ts.map +1 -1
- package/dist/components/layout/contextual-sidebar/contextual-sidebar.js +45 -35
- package/dist/components/layout/contextual-sidebar/contextual-sidebar.js.map +1 -1
- package/dist/components/layout/contextual-sidebar/mobile-contextual-sidebar.d.ts +12 -0
- package/dist/components/layout/contextual-sidebar/mobile-contextual-sidebar.d.ts.map +1 -0
- package/dist/components/layout/contextual-sidebar/mobile-contextual-sidebar.js +36 -0
- package/dist/components/layout/contextual-sidebar/mobile-contextual-sidebar.js.map +1 -0
- package/dist/components/layout/split-layout/index.js +2 -2
- package/dist/components/layout/split-layout/model.d.ts +13 -0
- package/dist/components/layout/split-layout/model.d.ts.map +1 -1
- package/dist/components/layout/split-layout/model.js +55 -18
- package/dist/components/layout/split-layout/model.js.map +1 -1
- package/dist/components/ui/alert-dialog/alert-dialog.d.ts.map +1 -1
- package/dist/components/ui/alert-dialog/alert-dialog.js +2 -6
- package/dist/components/ui/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/components/ui/board/board.js +1 -1
- package/dist/components/ui/dialog/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog/dialog.js +2 -6
- package/dist/components/ui/dialog/dialog.js.map +1 -1
- package/dist/components/ui/item/item.d.ts +1 -1
- package/dist/components/ui/sheet/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet/sheet.js +2 -5
- package/dist/components/ui/sheet/sheet.js.map +1 -1
- package/dist/components/ui/typography/typography.d.ts.map +1 -1
- package/dist/components/ui/typography/typography.js +2 -2
- package/dist/components/ui/typography/typography.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/lib/overlay.d.ts +5 -0
- package/dist/lib/overlay.d.ts.map +1 -1
- package/dist/lib/overlay.js +6 -1
- package/dist/lib/overlay.js.map +1 -1
- package/dist/r/alert-dialog.json +0 -1
- package/dist/r/contextual-sidebar.json +5 -0
- package/dist/r/dialog.json +0 -1
- package/dist/r/registry.json +5 -3
- package/dist/r/sheet.json +0 -1
- package/dist/r/src/components/layout/contextual-sidebar/contextual-sidebar.tsx +41 -16
- package/dist/r/src/components/layout/contextual-sidebar/mobile-contextual-sidebar.tsx +28 -0
- package/dist/r/src/components/layout/split-layout/model.ts +62 -10
- package/dist/r/src/components/ui/alert-dialog/alert-dialog.tsx +8 -8
- package/dist/r/src/components/ui/dialog/dialog.tsx +8 -8
- package/dist/r/src/components/ui/sheet/sheet.tsx +2 -7
- package/dist/r/src/components/ui/typography/typography.tsx +3 -2
- package/dist/r/src/lib/overlay.ts +6 -0
- package/package.json +1 -1
- package/registry.json +5 -3
- package/src/components/layout/contextual-sidebar/contextual-sidebar.tsx +41 -16
- package/src/components/layout/contextual-sidebar/mobile-contextual-sidebar.tsx +28 -0
- package/src/components/layout/split-layout/model.ts +62 -10
- package/src/components/ui/alert-dialog/alert-dialog.tsx +8 -8
- package/src/components/ui/dialog/dialog.tsx +8 -8
- package/src/components/ui/sheet/sheet.tsx +2 -7
- package/src/components/ui/typography/typography.tsx +3 -2
- package/src/lib/overlay.ts +6 -0
package/dist/r/registry.json
CHANGED
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
"@kobalte/core@^0.13.14"
|
|
62
62
|
],
|
|
63
63
|
"registryDependencies": [
|
|
64
|
-
"@adea-ai/ui/typography",
|
|
65
64
|
"lib"
|
|
66
65
|
],
|
|
67
66
|
"files": [
|
|
@@ -569,7 +568,6 @@
|
|
|
569
568
|
"lucide-solid@^1.51.0"
|
|
570
569
|
],
|
|
571
570
|
"registryDependencies": [
|
|
572
|
-
"@adea-ai/ui/typography",
|
|
573
571
|
"lib"
|
|
574
572
|
],
|
|
575
573
|
"files": [
|
|
@@ -1213,7 +1211,6 @@
|
|
|
1213
1211
|
],
|
|
1214
1212
|
"registryDependencies": [
|
|
1215
1213
|
"@adea-ai/ui/dialog",
|
|
1216
|
-
"@adea-ai/ui/typography",
|
|
1217
1214
|
"lib"
|
|
1218
1215
|
],
|
|
1219
1216
|
"files": [
|
|
@@ -1609,6 +1606,11 @@
|
|
|
1609
1606
|
"path": "src/components/layout/contextual-sidebar/index.ts",
|
|
1610
1607
|
"type": "registry:ui",
|
|
1611
1608
|
"target": "components/layout/contextual-sidebar/index.ts"
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
"path": "src/components/layout/contextual-sidebar/mobile-contextual-sidebar.tsx",
|
|
1612
|
+
"type": "registry:ui",
|
|
1613
|
+
"target": "components/layout/contextual-sidebar/mobile-contextual-sidebar.tsx"
|
|
1612
1614
|
}
|
|
1613
1615
|
]
|
|
1614
1616
|
},
|
package/dist/r/sheet.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Accessor, JSX } from 'solid-js'
|
|
2
2
|
import { Show, createSignal, createUniqueId, onCleanup, onMount } from 'solid-js'
|
|
3
3
|
import { cn } from '../../../lib/utils'
|
|
4
|
-
import { Sheet, SheetContent, SheetTitle } from '../../ui/sheet'
|
|
5
4
|
import {
|
|
6
5
|
SidebarNav,
|
|
7
6
|
SidebarNavContent,
|
|
@@ -11,6 +10,9 @@ import {
|
|
|
11
10
|
} from '../sidebar-nav/sidebar-nav'
|
|
12
11
|
import { PixelResizeHandle } from '../pixel-resize-handle/pixel-resize-handle'
|
|
13
12
|
|
|
13
|
+
type MobileModule = typeof import('./mobile-contextual-sidebar')
|
|
14
|
+
type MobileLoad = MobileModule | { error: unknown }
|
|
15
|
+
|
|
14
16
|
export type ContextualSidebarRenderContext = {
|
|
15
17
|
/** Whether the slot is being rendered inside the modal mobile sheet. */
|
|
16
18
|
mobile: boolean
|
|
@@ -67,6 +69,22 @@ export type ContextualSidebarProps = {
|
|
|
67
69
|
*/
|
|
68
70
|
export function ContextualSidebar(props: ContextualSidebarProps) {
|
|
69
71
|
const [isNarrowViewport, setIsNarrowViewport] = createSignal(false)
|
|
72
|
+
const [mobileLoad, setMobileLoad] = createSignal<MobileLoad>()
|
|
73
|
+
let mobileRequested = false
|
|
74
|
+
// Keep the resolved component mounted across viewport changes for Sheet cleanup.
|
|
75
|
+
const loadMobile = () => {
|
|
76
|
+
if (mobileRequested) return
|
|
77
|
+
mobileRequested = true
|
|
78
|
+
// Native module caching shares successful loads without keeping a rejected promise.
|
|
79
|
+
void import('./mobile-contextual-sidebar').then(
|
|
80
|
+
(module) => {
|
|
81
|
+
if (mobileRequested) setMobileLoad(module)
|
|
82
|
+
},
|
|
83
|
+
(error: unknown) => {
|
|
84
|
+
if (mobileRequested) setMobileLoad({ error })
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
}
|
|
70
88
|
const [mobileMount, setMobileMount] = createSignal<
|
|
71
89
|
{ element: HTMLElement; connected: boolean } | undefined
|
|
72
90
|
>()
|
|
@@ -81,6 +99,7 @@ export function ContextualSidebar(props: ContextualSidebarProps) {
|
|
|
81
99
|
const media = window.matchMedia('(max-width: 48rem)')
|
|
82
100
|
const updateViewport = () => {
|
|
83
101
|
const wasNarrow = isNarrowViewport()
|
|
102
|
+
if (media.matches) loadMobile()
|
|
84
103
|
// Close before mounting a modal. A desktop-expanded host state is not
|
|
85
104
|
// mobile intent, and opening the Sheet for one frame can steal focus.
|
|
86
105
|
if (media.matches && !wasNarrow) props.onOpenChange(false)
|
|
@@ -90,10 +109,14 @@ export function ContextualSidebar(props: ContextualSidebarProps) {
|
|
|
90
109
|
// A route can mount after a wide-loaded host crosses into mobile. Clear
|
|
91
110
|
// that stale seed, but preserve a sidebar intentionally opened at a
|
|
92
111
|
// narrow boot viewport.
|
|
112
|
+
if (media.matches) loadMobile()
|
|
93
113
|
if (media.matches && props.wideViewportAtLoad && props.open) props.onOpenChange(false)
|
|
94
114
|
setIsNarrowViewport(media.matches)
|
|
95
115
|
media.addEventListener('change', updateViewport)
|
|
96
|
-
onCleanup(() =>
|
|
116
|
+
onCleanup(() => {
|
|
117
|
+
mobileRequested = false
|
|
118
|
+
media.removeEventListener('change', updateViewport)
|
|
119
|
+
})
|
|
97
120
|
})
|
|
98
121
|
|
|
99
122
|
const renderSidebar = (mobile: boolean) => {
|
|
@@ -160,21 +183,23 @@ export function ContextualSidebar(props: ContextualSidebarProps) {
|
|
|
160
183
|
}
|
|
161
184
|
|
|
162
185
|
return (
|
|
163
|
-
|
|
164
|
-
open={isNarrowViewport() && props.open}
|
|
165
|
-
onOpenChange={(open) => props.onOpenChange(open)}
|
|
166
|
-
>
|
|
186
|
+
<>
|
|
167
187
|
<Show when={!isNarrowViewport()}>{(_wide) => renderSidebar(false)}</Show>
|
|
168
|
-
<Show when={
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
<Show when={mobileLoad()} keyed>
|
|
189
|
+
{(loaded) => {
|
|
190
|
+
if ('error' in loaded) throw loaded.error
|
|
191
|
+
return (
|
|
192
|
+
<loaded.MobileSidebar
|
|
193
|
+
open={isNarrowViewport() && props.open}
|
|
194
|
+
onOpenChange={props.onOpenChange}
|
|
195
|
+
label={props.label}
|
|
196
|
+
sheetClass={props.sheetClass}
|
|
197
|
+
restoreFocusRef={props.restoreFocusRef}
|
|
198
|
+
renderSidebar={() => renderSidebar(true)}
|
|
199
|
+
/>
|
|
200
|
+
)
|
|
201
|
+
}}
|
|
177
202
|
</Show>
|
|
178
|
-
|
|
203
|
+
</>
|
|
179
204
|
)
|
|
180
205
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Accessor, JSX } from 'solid-js'
|
|
2
|
+
import { cn } from '../../../lib/utils'
|
|
3
|
+
import { Sheet, SheetContent } from '../../ui/sheet'
|
|
4
|
+
|
|
5
|
+
export type MobileContextualSidebarProps = {
|
|
6
|
+
open: boolean
|
|
7
|
+
onOpenChange(open: boolean): void
|
|
8
|
+
label: string
|
|
9
|
+
sheetClass?: string
|
|
10
|
+
restoreFocusRef?: Accessor<HTMLElement | undefined>
|
|
11
|
+
renderSidebar(): JSX.Element
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Sheet wrappers are loaded only when a contextual sidebar enters mobile mode. */
|
|
15
|
+
export function MobileSidebar(props: MobileContextualSidebarProps) {
|
|
16
|
+
return (
|
|
17
|
+
<Sheet open={props.open} onOpenChange={props.onOpenChange}>
|
|
18
|
+
<SheetContent
|
|
19
|
+
side="start"
|
|
20
|
+
aria-label={props.label}
|
|
21
|
+
class={cn('gap-0 p-0', props.sheetClass)}
|
|
22
|
+
restoreFocusRef={props.restoreFocusRef}
|
|
23
|
+
>
|
|
24
|
+
{props.renderSidebar()}
|
|
25
|
+
</SheetContent>
|
|
26
|
+
</Sheet>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -41,6 +41,10 @@ export type SplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readon
|
|
|
41
41
|
splitId: string
|
|
42
42
|
}>
|
|
43
43
|
|
|
44
|
+
export type BalancedSplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<
|
|
45
|
+
Pick<SplitPaneInput<L>, 'placement' | 'leaf' | 'splitId'>
|
|
46
|
+
>
|
|
47
|
+
|
|
44
48
|
function replaceLeaf<L extends SplitLayoutLeaf>(
|
|
45
49
|
node: SplitLayoutNode<L>,
|
|
46
50
|
leafId: string,
|
|
@@ -86,14 +90,19 @@ function assertUniqueInput<L extends SplitLayoutLeaf>(
|
|
|
86
90
|
if (typeof splitId !== 'string' || splitId.length === 0)
|
|
87
91
|
throw new Error('invalid_state: split identity must be nonempty')
|
|
88
92
|
const ids = new Set<string>()
|
|
93
|
+
const splitIds: string[] = []
|
|
89
94
|
const visit = (node: SplitLayoutNode<L>) => {
|
|
90
95
|
if (ids.has(node.id)) throw new Error('invalid_state: duplicate pane id')
|
|
91
96
|
ids.add(node.id)
|
|
92
|
-
if (node.kind === 'split')
|
|
97
|
+
if (node.kind === 'split') {
|
|
98
|
+
splitIds.push(node.id)
|
|
99
|
+
node.children.forEach(visit)
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
visit(state.center)
|
|
95
103
|
if (ids.has(leaf.id) || ids.has(splitId) || leaf.id === splitId)
|
|
96
104
|
throw new Error('invalid_state: duplicate pane id')
|
|
105
|
+
return splitIds
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
/** Visual-tree validation only; applications still decode and scope persisted preferences. */
|
|
@@ -163,6 +172,51 @@ export function splitPane<L extends SplitLayoutLeaf>(
|
|
|
163
172
|
return { ...state, center, focusedLeafId: input.leaf.id, closed: [] }
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Add a pane beside the selected leaf and reflow the visual tree into a
|
|
177
|
+
* balanced row-major grid. One or two leaves share one row; from three leaves
|
|
178
|
+
* onward, leaves fill a first row and then a second row, with no more than four
|
|
179
|
+
* columns in either. Each row gives its leaves equal width and the two rows
|
|
180
|
+
* equal height. Existing opaque leaf objects are preserved. Split IDs are
|
|
181
|
+
* recycled across rebuilt branches, whose axis and descendants can change;
|
|
182
|
+
* resize actions must resolve against the current tree. Ratios are recomputed.
|
|
183
|
+
* This is opt-in: callers
|
|
184
|
+
* that need an explicit axis and ratio should keep using `splitPane`.
|
|
185
|
+
*/
|
|
186
|
+
export function splitPaneBalanced<L extends SplitLayoutLeaf>(
|
|
187
|
+
state: SplitLayoutState<L>,
|
|
188
|
+
targetLeafId: string,
|
|
189
|
+
input: BalancedSplitPaneInput<L>
|
|
190
|
+
): SplitLayoutState<L> {
|
|
191
|
+
const leaves = [...listLeaves(state.center)]
|
|
192
|
+
const targetIndex = leaves.findIndex((leaf) => leaf.id === targetLeafId)
|
|
193
|
+
if (targetIndex < 0) return state
|
|
194
|
+
if (leaves.length >= MAX_LAYOUT_LEAVES)
|
|
195
|
+
throw new Error('limit_exceeded: center layout has eight leaves')
|
|
196
|
+
const splitIds = assertUniqueInput(state, input.leaf, input.splitId)
|
|
197
|
+
|
|
198
|
+
const insertionIndex = targetIndex + (input.placement === 'before' ? 0 : 1)
|
|
199
|
+
leaves.splice(insertionIndex, 0, input.leaf)
|
|
200
|
+
|
|
201
|
+
const build = (start: number, end: number, vertical = false): SplitLayoutNode<L> => {
|
|
202
|
+
if (end - start === 1) return leaves[start]!
|
|
203
|
+
const middle = start + Math.ceil((end - start) / 2)
|
|
204
|
+
// The new identity belongs to the lowest branch separating the selected
|
|
205
|
+
// pane from its new neighbour. Other branches reuse the existing ID pool.
|
|
206
|
+
const id = middle === targetIndex + 1 ? input.splitId : splitIds.shift()!
|
|
207
|
+
return {
|
|
208
|
+
kind: 'split',
|
|
209
|
+
id,
|
|
210
|
+
direction: vertical ? 'column' : 'row',
|
|
211
|
+
ratio: vertical ? 0.5 : (middle - start) / (end - start),
|
|
212
|
+
children: [build(start, middle), build(middle, end)],
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const center = build(0, leaves.length, leaves.length > 2)
|
|
216
|
+
assertLayoutTree(center)
|
|
217
|
+
return { ...state, center, focusedLeafId: input.leaf.id, closed: [] }
|
|
218
|
+
}
|
|
219
|
+
|
|
166
220
|
export function closePane<L extends SplitLayoutLeaf>(
|
|
167
221
|
state: SplitLayoutState<L>,
|
|
168
222
|
leafId: string,
|
|
@@ -287,21 +341,19 @@ export function resizeSplit<L extends SplitLayoutLeaf>(
|
|
|
287
341
|
export function normalizeLayout<L extends SplitLayoutLeaf>(
|
|
288
342
|
state: SplitLayoutState<L>
|
|
289
343
|
): SplitLayoutState<L> {
|
|
290
|
-
const visit = (node: SplitLayoutNode<L>):
|
|
291
|
-
if (node.kind === 'leaf') return
|
|
344
|
+
const visit = (node: SplitLayoutNode<L>): SplitLayoutNode<L> => {
|
|
345
|
+
if (node.kind === 'leaf') return node
|
|
292
346
|
const first = visit(node.children[0])
|
|
293
347
|
const second = visit(node.children[1])
|
|
294
348
|
const ratio = Number.isFinite(node.ratio)
|
|
295
349
|
? Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, node.ratio))
|
|
296
350
|
: (MIN_SPLIT_RATIO + MAX_SPLIT_RATIO) / 2
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
changed,
|
|
301
|
-
}
|
|
351
|
+
return first === node.children[0] && second === node.children[1] && ratio === node.ratio
|
|
352
|
+
? node
|
|
353
|
+
: { ...node, ratio, children: [first, second] }
|
|
302
354
|
}
|
|
303
|
-
const
|
|
304
|
-
return
|
|
355
|
+
const center = visit(state.center)
|
|
356
|
+
return center === state.center ? state : { ...state, center }
|
|
305
357
|
}
|
|
306
358
|
|
|
307
359
|
/** The leaf immediately after (`1`) or before (`-1`) the given leaf in reading order. */
|
|
@@ -5,9 +5,14 @@ import {
|
|
|
5
5
|
// Use the alert-specific export rather than DialogRoot.Content, which other imports can replace.
|
|
6
6
|
import type { ComponentProps } from 'solid-js'
|
|
7
7
|
import { createEffect, createSignal, splitProps } from 'solid-js'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
overlayMotion,
|
|
10
|
+
overlayPositioner,
|
|
11
|
+
overlayScrim,
|
|
12
|
+
overlaySurface,
|
|
13
|
+
overlayTitle,
|
|
14
|
+
} from '../../../lib/overlay'
|
|
9
15
|
import { cn } from '../../../lib/utils'
|
|
10
|
-
import { headingVariants } from '../typography'
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* AlertDialog.
|
|
@@ -116,12 +121,7 @@ export function AlertDialogFooter(props: ComponentProps<'div'>) {
|
|
|
116
121
|
|
|
117
122
|
export function AlertDialogTitle(props: ComponentProps<typeof KobalteAlertDialog.Title>) {
|
|
118
123
|
const [local, rest] = splitProps(props, ['class'])
|
|
119
|
-
return (
|
|
120
|
-
<KobalteAlertDialog.Title
|
|
121
|
-
class={cn(headingVariants({ size: 'card', leading: 'none' }), local.class)}
|
|
122
|
-
{...rest}
|
|
123
|
-
/>
|
|
124
|
-
)
|
|
124
|
+
return <KobalteAlertDialog.Title class={cn(overlayTitle, local.class)} {...rest} />
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export function AlertDialogDescription(
|
|
@@ -7,9 +7,14 @@ import {
|
|
|
7
7
|
import { X } from 'lucide-solid'
|
|
8
8
|
import type { Accessor, ComponentProps, JSX } from 'solid-js'
|
|
9
9
|
import { createRenderEffect, onCleanup, Show, splitProps } from 'solid-js'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
overlayMotion,
|
|
12
|
+
overlayPositioner,
|
|
13
|
+
overlayScrim,
|
|
14
|
+
overlaySurface,
|
|
15
|
+
overlayTitle,
|
|
16
|
+
} from '../../../lib/overlay'
|
|
11
17
|
import { cn } from '../../../lib/utils'
|
|
12
|
-
import { headingVariants } from '../typography'
|
|
13
18
|
|
|
14
19
|
type FocusCycle = {
|
|
15
20
|
content: HTMLElement
|
|
@@ -326,12 +331,7 @@ export function DialogFooter(props: ComponentProps<'div'>) {
|
|
|
326
331
|
|
|
327
332
|
export function DialogTitle(props: ComponentProps<typeof KobalteDialog.Title>) {
|
|
328
333
|
const [local, rest] = splitProps(props, ['class'])
|
|
329
|
-
return (
|
|
330
|
-
<KobalteDialog.Title
|
|
331
|
-
class={cn(headingVariants({ size: 'card', leading: 'none' }), local.class)}
|
|
332
|
-
{...rest}
|
|
333
|
-
/>
|
|
334
|
-
)
|
|
334
|
+
return <KobalteDialog.Title class={cn(overlayTitle, local.class)} {...rest} />
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
export function DialogDescription(props: ComponentProps<typeof KobalteDialog.Description>) {
|
|
@@ -8,9 +8,9 @@ import { X } from 'lucide-solid'
|
|
|
8
8
|
import type { Accessor, ComponentProps, JSX } from 'solid-js'
|
|
9
9
|
import { Show, splitProps } from 'solid-js'
|
|
10
10
|
import { cva, type VariantProps } from '../../../lib/variants'
|
|
11
|
+
import { overlayTitle } from '../../../lib/overlay'
|
|
11
12
|
import { cn } from '../../../lib/utils'
|
|
12
13
|
import { createDialogFocusRestoration, DialogOverlay } from '../dialog/dialog'
|
|
13
|
-
import { headingVariants } from '../typography'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Sheet.
|
|
@@ -215,12 +215,7 @@ export function SheetFooter(props: ComponentProps<'div'>) {
|
|
|
215
215
|
|
|
216
216
|
export function SheetTitle(props: ComponentProps<typeof KobalteDialog.Title>) {
|
|
217
217
|
const [local, rest] = splitProps(props, ['class'])
|
|
218
|
-
return (
|
|
219
|
-
<KobalteDialog.Title
|
|
220
|
-
class={cn(headingVariants({ size: 'card', leading: 'none' }), local.class)}
|
|
221
|
-
{...rest}
|
|
222
|
-
/>
|
|
223
|
-
)
|
|
218
|
+
return <KobalteDialog.Title class={cn(overlayTitle, local.class)} {...rest} />
|
|
224
219
|
}
|
|
225
220
|
|
|
226
221
|
export function SheetDescription(props: ComponentProps<typeof KobalteDialog.Description>) {
|
|
@@ -25,7 +25,8 @@ import { cn } from '../../../lib/utils'
|
|
|
25
25
|
* stack already sets the rhythm (a card or dialog header), and is the only other
|
|
26
26
|
* option.
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
// cva only returns a closure. A heading-only consumer must discard Text's recipe.
|
|
29
|
+
export const headingVariants = /* @__PURE__ */ cva('font-semibold', {
|
|
29
30
|
variants: {
|
|
30
31
|
size: {
|
|
31
32
|
/** 30px. An app-level headline — a first-run or empty-app screen. Once per app. */
|
|
@@ -130,7 +131,7 @@ export function Heading<T extends ValidComponent = 'h2'>(props: HeadingProps<T>)
|
|
|
130
131
|
*
|
|
131
132
|
* Defaults: `body` renders a `p`, `code` a `code`, every other variant a `span`.
|
|
132
133
|
*/
|
|
133
|
-
export const textVariants = cva('', {
|
|
134
|
+
export const textVariants = /* @__PURE__ */ cva('', {
|
|
134
135
|
variants: {
|
|
135
136
|
variant: {
|
|
136
137
|
/** 14px / 400. Body copy and descriptions. */
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
export const overlaySurface =
|
|
15
15
|
'bg-popover text-popover-foreground rounded-xl border border-border shadow-lg'
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Overlay titles have one fixed typography role. Keep this small recipe shared
|
|
19
|
+
* without loading the configurable Heading component into every dialog.
|
|
20
|
+
*/
|
|
21
|
+
export const overlayTitle = 'font-semibold text-base tracking-tight leading-none'
|
|
22
|
+
|
|
17
23
|
/** The span of the portal wrapper that centres a dialog-like overlay. */
|
|
18
24
|
export const overlayPositioner = 'fixed inset-0 z-(--z-dialog) grid place-items-center p-4'
|
|
19
25
|
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
"@kobalte/core@^0.13.14"
|
|
62
62
|
],
|
|
63
63
|
"registryDependencies": [
|
|
64
|
-
"@adea-ai/ui/typography",
|
|
65
64
|
"lib"
|
|
66
65
|
],
|
|
67
66
|
"files": [
|
|
@@ -569,7 +568,6 @@
|
|
|
569
568
|
"lucide-solid@^1.51.0"
|
|
570
569
|
],
|
|
571
570
|
"registryDependencies": [
|
|
572
|
-
"@adea-ai/ui/typography",
|
|
573
571
|
"lib"
|
|
574
572
|
],
|
|
575
573
|
"files": [
|
|
@@ -1213,7 +1211,6 @@
|
|
|
1213
1211
|
],
|
|
1214
1212
|
"registryDependencies": [
|
|
1215
1213
|
"@adea-ai/ui/dialog",
|
|
1216
|
-
"@adea-ai/ui/typography",
|
|
1217
1214
|
"lib"
|
|
1218
1215
|
],
|
|
1219
1216
|
"files": [
|
|
@@ -1609,6 +1606,11 @@
|
|
|
1609
1606
|
"path": "src/components/layout/contextual-sidebar/index.ts",
|
|
1610
1607
|
"type": "registry:ui",
|
|
1611
1608
|
"target": "components/layout/contextual-sidebar/index.ts"
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
"path": "src/components/layout/contextual-sidebar/mobile-contextual-sidebar.tsx",
|
|
1612
|
+
"type": "registry:ui",
|
|
1613
|
+
"target": "components/layout/contextual-sidebar/mobile-contextual-sidebar.tsx"
|
|
1612
1614
|
}
|
|
1613
1615
|
]
|
|
1614
1616
|
},
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Accessor, JSX } from 'solid-js'
|
|
2
2
|
import { Show, createSignal, createUniqueId, onCleanup, onMount } from 'solid-js'
|
|
3
3
|
import { cn } from '#lib/utils'
|
|
4
|
-
import { Sheet, SheetContent, SheetTitle } from '../../ui/sheet'
|
|
5
4
|
import {
|
|
6
5
|
SidebarNav,
|
|
7
6
|
SidebarNavContent,
|
|
@@ -11,6 +10,9 @@ import {
|
|
|
11
10
|
} from '../sidebar-nav/sidebar-nav'
|
|
12
11
|
import { PixelResizeHandle } from '../pixel-resize-handle/pixel-resize-handle'
|
|
13
12
|
|
|
13
|
+
type MobileModule = typeof import('./mobile-contextual-sidebar')
|
|
14
|
+
type MobileLoad = MobileModule | { error: unknown }
|
|
15
|
+
|
|
14
16
|
export type ContextualSidebarRenderContext = {
|
|
15
17
|
/** Whether the slot is being rendered inside the modal mobile sheet. */
|
|
16
18
|
mobile: boolean
|
|
@@ -67,6 +69,22 @@ export type ContextualSidebarProps = {
|
|
|
67
69
|
*/
|
|
68
70
|
export function ContextualSidebar(props: ContextualSidebarProps) {
|
|
69
71
|
const [isNarrowViewport, setIsNarrowViewport] = createSignal(false)
|
|
72
|
+
const [mobileLoad, setMobileLoad] = createSignal<MobileLoad>()
|
|
73
|
+
let mobileRequested = false
|
|
74
|
+
// Keep the resolved component mounted across viewport changes for Sheet cleanup.
|
|
75
|
+
const loadMobile = () => {
|
|
76
|
+
if (mobileRequested) return
|
|
77
|
+
mobileRequested = true
|
|
78
|
+
// Native module caching shares successful loads without keeping a rejected promise.
|
|
79
|
+
void import('./mobile-contextual-sidebar').then(
|
|
80
|
+
(module) => {
|
|
81
|
+
if (mobileRequested) setMobileLoad(module)
|
|
82
|
+
},
|
|
83
|
+
(error: unknown) => {
|
|
84
|
+
if (mobileRequested) setMobileLoad({ error })
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
}
|
|
70
88
|
const [mobileMount, setMobileMount] = createSignal<
|
|
71
89
|
{ element: HTMLElement; connected: boolean } | undefined
|
|
72
90
|
>()
|
|
@@ -81,6 +99,7 @@ export function ContextualSidebar(props: ContextualSidebarProps) {
|
|
|
81
99
|
const media = window.matchMedia('(max-width: 48rem)')
|
|
82
100
|
const updateViewport = () => {
|
|
83
101
|
const wasNarrow = isNarrowViewport()
|
|
102
|
+
if (media.matches) loadMobile()
|
|
84
103
|
// Close before mounting a modal. A desktop-expanded host state is not
|
|
85
104
|
// mobile intent, and opening the Sheet for one frame can steal focus.
|
|
86
105
|
if (media.matches && !wasNarrow) props.onOpenChange(false)
|
|
@@ -90,10 +109,14 @@ export function ContextualSidebar(props: ContextualSidebarProps) {
|
|
|
90
109
|
// A route can mount after a wide-loaded host crosses into mobile. Clear
|
|
91
110
|
// that stale seed, but preserve a sidebar intentionally opened at a
|
|
92
111
|
// narrow boot viewport.
|
|
112
|
+
if (media.matches) loadMobile()
|
|
93
113
|
if (media.matches && props.wideViewportAtLoad && props.open) props.onOpenChange(false)
|
|
94
114
|
setIsNarrowViewport(media.matches)
|
|
95
115
|
media.addEventListener('change', updateViewport)
|
|
96
|
-
onCleanup(() =>
|
|
116
|
+
onCleanup(() => {
|
|
117
|
+
mobileRequested = false
|
|
118
|
+
media.removeEventListener('change', updateViewport)
|
|
119
|
+
})
|
|
97
120
|
})
|
|
98
121
|
|
|
99
122
|
const renderSidebar = (mobile: boolean) => {
|
|
@@ -160,21 +183,23 @@ export function ContextualSidebar(props: ContextualSidebarProps) {
|
|
|
160
183
|
}
|
|
161
184
|
|
|
162
185
|
return (
|
|
163
|
-
|
|
164
|
-
open={isNarrowViewport() && props.open}
|
|
165
|
-
onOpenChange={(open) => props.onOpenChange(open)}
|
|
166
|
-
>
|
|
186
|
+
<>
|
|
167
187
|
<Show when={!isNarrowViewport()}>{(_wide) => renderSidebar(false)}</Show>
|
|
168
|
-
<Show when={
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
<Show when={mobileLoad()} keyed>
|
|
189
|
+
{(loaded) => {
|
|
190
|
+
if ('error' in loaded) throw loaded.error
|
|
191
|
+
return (
|
|
192
|
+
<loaded.MobileSidebar
|
|
193
|
+
open={isNarrowViewport() && props.open}
|
|
194
|
+
onOpenChange={props.onOpenChange}
|
|
195
|
+
label={props.label}
|
|
196
|
+
sheetClass={props.sheetClass}
|
|
197
|
+
restoreFocusRef={props.restoreFocusRef}
|
|
198
|
+
renderSidebar={() => renderSidebar(true)}
|
|
199
|
+
/>
|
|
200
|
+
)
|
|
201
|
+
}}
|
|
177
202
|
</Show>
|
|
178
|
-
|
|
203
|
+
</>
|
|
179
204
|
)
|
|
180
205
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Accessor, JSX } from 'solid-js'
|
|
2
|
+
import { cn } from '#lib/utils'
|
|
3
|
+
import { Sheet, SheetContent } from '../../ui/sheet'
|
|
4
|
+
|
|
5
|
+
export type MobileContextualSidebarProps = {
|
|
6
|
+
open: boolean
|
|
7
|
+
onOpenChange(open: boolean): void
|
|
8
|
+
label: string
|
|
9
|
+
sheetClass?: string
|
|
10
|
+
restoreFocusRef?: Accessor<HTMLElement | undefined>
|
|
11
|
+
renderSidebar(): JSX.Element
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Sheet wrappers are loaded only when a contextual sidebar enters mobile mode. */
|
|
15
|
+
export function MobileSidebar(props: MobileContextualSidebarProps) {
|
|
16
|
+
return (
|
|
17
|
+
<Sheet open={props.open} onOpenChange={props.onOpenChange}>
|
|
18
|
+
<SheetContent
|
|
19
|
+
side="start"
|
|
20
|
+
aria-label={props.label}
|
|
21
|
+
class={cn('gap-0 p-0', props.sheetClass)}
|
|
22
|
+
restoreFocusRef={props.restoreFocusRef}
|
|
23
|
+
>
|
|
24
|
+
{props.renderSidebar()}
|
|
25
|
+
</SheetContent>
|
|
26
|
+
</Sheet>
|
|
27
|
+
)
|
|
28
|
+
}
|