@adea-ai/ui 0.87.0 → 0.89.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/components/composites/settings/index.d.ts +2 -0
- package/dist/components/composites/settings/index.d.ts.map +1 -1
- package/dist/components/composites/settings/index.js +3 -1
- package/dist/components/composites/settings/settings-layout.d.ts +28 -0
- package/dist/components/composites/settings/settings-layout.d.ts.map +1 -0
- package/dist/components/composites/settings/settings-layout.js +62 -0
- package/dist/components/composites/settings/settings-layout.js.map +1 -0
- package/dist/components/composites/settings/settings-navigation.d.ts +41 -0
- package/dist/components/composites/settings/settings-navigation.d.ts.map +1 -0
- package/dist/components/composites/settings/settings-navigation.js +102 -0
- package/dist/components/composites/settings/settings-navigation.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/layout/top-bar/top-bar.d.ts +3 -1
- package/dist/components/layout/top-bar/top-bar.d.ts.map +1 -1
- package/dist/components/layout/top-bar/top-bar.js +4 -4
- package/dist/components/layout/top-bar/top-bar.js.map +1 -1
- package/dist/components/ui/tabs/tabs.d.ts.map +1 -1
- package/dist/components/ui/tabs/tabs.js +70 -13
- package/dist/components/ui/tabs/tabs.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/r/registry.json +12 -0
- package/dist/r/settings.json +12 -0
- package/dist/r/src/components/composites/settings/index.ts +7 -0
- package/dist/r/src/components/composites/settings/settings-layout.tsx +65 -0
- package/dist/r/src/components/composites/settings/settings-navigation.tsx +135 -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/layout/top-bar/top-bar.tsx +9 -5
- package/dist/r/src/components/ui/tabs/tabs.tsx +80 -10
- package/package.json +2 -1
- package/registry.json +12 -0
- package/src/components/composites/settings/index.ts +7 -0
- package/src/components/composites/settings/settings-layout.tsx +65 -0
- package/src/components/composites/settings/settings-navigation.tsx +135 -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/layout/top-bar/top-bar.tsx +9 -5
- package/src/components/ui/tabs/tabs.tsx +80 -10
|
@@ -5,6 +5,15 @@ import type { ComponentProps, JSX, ValidComponent } from 'solid-js'
|
|
|
5
5
|
import { Show, createSignal, splitProps } from 'solid-js'
|
|
6
6
|
import { cn } from '../../../lib/utils'
|
|
7
7
|
|
|
8
|
+
function invokeEventHandler(handler: unknown, event: Event) {
|
|
9
|
+
if (Array.isArray(handler)) {
|
|
10
|
+
const [callback, data] = handler as [(data: unknown, event: Event) => void, unknown]
|
|
11
|
+
callback(data, event)
|
|
12
|
+
} else if (typeof handler === 'function') {
|
|
13
|
+
;(handler as (event: Event) => void)(event)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
/**
|
|
9
18
|
* SidebarNav.
|
|
10
19
|
*
|
|
@@ -96,36 +105,69 @@ export function SidebarNavFooter(props: ComponentProps<'div'>) {
|
|
|
96
105
|
)
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
/** Props forwarded to a collapsible section's disclosure button. */
|
|
109
|
+
export type SidebarNavSectionTriggerProps = Omit<
|
|
110
|
+
ComponentProps<'button'>,
|
|
111
|
+
'type' | 'aria-expanded' | 'onClick' | 'onKeyDown' | 'children' | 'class' | 'style'
|
|
112
|
+
> & {
|
|
113
|
+
/** Called for Alt+ArrowUp/Down; the host owns ordering and row identity. */
|
|
114
|
+
onReorder?: (direction: 'up' | 'down') => void
|
|
115
|
+
/** Runs before the section toggle and may cancel it with `preventDefault()`. */
|
|
116
|
+
onClick?: ComponentProps<'button'>['onClick']
|
|
117
|
+
/** Runs before shared Alt+Arrow handling and may cancel it with `preventDefault()`. */
|
|
118
|
+
onKeyDown?: ComponentProps<'button'>['onKeyDown']
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
/**
|
|
100
122
|
* A titled, collapsible group. Collapsible because a list of projects
|
|
101
123
|
* inevitably outgrows the column, and the user needs to put the ones they are
|
|
102
124
|
* not using away *without* them disappearing into a menu.
|
|
103
125
|
*/
|
|
104
|
-
export
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
export type SidebarNavSectionProps = ComponentProps<'div'> & {
|
|
127
|
+
label: string
|
|
128
|
+
/** Render a disclosure control instead of a plain heading. */
|
|
129
|
+
collapsible?: boolean
|
|
130
|
+
defaultOpen?: boolean
|
|
131
|
+
/** Controlled disclosure state. When supplied, the host owns changes. */
|
|
132
|
+
open?: boolean
|
|
133
|
+
/** Called for user-initiated disclosure changes. */
|
|
134
|
+
onOpenChange?: (open: boolean) => void
|
|
135
|
+
/** Native props and reorder callbacks for the disclosure control. */
|
|
136
|
+
triggerProps?: SidebarNavSectionTriggerProps
|
|
137
|
+
/** Count shown beside the label. */
|
|
138
|
+
count?: number
|
|
139
|
+
/** A control at the trailing edge of the heading row. */
|
|
140
|
+
action?: JSX.Element
|
|
141
|
+
/** Semantic heading element for a host's section hierarchy. */
|
|
142
|
+
headingAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function SidebarNavSection(props: SidebarNavSectionProps) {
|
|
118
146
|
const [local, rest] = splitProps(props, [
|
|
119
147
|
'class',
|
|
120
148
|
'label',
|
|
121
149
|
'collapsible',
|
|
122
150
|
'defaultOpen',
|
|
151
|
+
'open',
|
|
152
|
+
'onOpenChange',
|
|
153
|
+
'triggerProps',
|
|
123
154
|
'count',
|
|
124
155
|
'action',
|
|
125
156
|
'headingAs',
|
|
126
157
|
'children',
|
|
127
158
|
])
|
|
128
|
-
const [
|
|
159
|
+
const [uncontrolledOpen, setUncontrolledOpen] = createSignal(local.defaultOpen ?? true)
|
|
160
|
+
const open = () => local.open ?? uncontrolledOpen()
|
|
161
|
+
const setOpen = (next: boolean) => {
|
|
162
|
+
if (local.open === undefined) setUncontrolledOpen(next)
|
|
163
|
+
local.onOpenChange?.(next)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const [trigger, triggerRest] = splitProps(local.triggerProps ?? {}, [
|
|
167
|
+
'onClick',
|
|
168
|
+
'onKeyDown',
|
|
169
|
+
'onReorder',
|
|
170
|
+
])
|
|
129
171
|
|
|
130
172
|
const heading = (
|
|
131
173
|
<>
|
|
@@ -154,9 +196,22 @@ export function SidebarNavSection(
|
|
|
154
196
|
>
|
|
155
197
|
<Show when={local.collapsible} fallback={heading}>
|
|
156
198
|
<button
|
|
199
|
+
{...(triggerRest as ComponentProps<'button'>)}
|
|
157
200
|
type="button"
|
|
158
201
|
aria-expanded={open()}
|
|
159
|
-
onClick={() =>
|
|
202
|
+
onClick={(event) => {
|
|
203
|
+
invokeEventHandler(trigger.onClick, event)
|
|
204
|
+
if (event.defaultPrevented) return
|
|
205
|
+
setOpen(!open())
|
|
206
|
+
}}
|
|
207
|
+
onKeyDown={(event) => {
|
|
208
|
+
invokeEventHandler(trigger.onKeyDown, event)
|
|
209
|
+
if (event.defaultPrevented || !event.altKey) return
|
|
210
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return
|
|
211
|
+
event.preventDefault()
|
|
212
|
+
event.stopPropagation()
|
|
213
|
+
trigger.onReorder?.(event.key === 'ArrowUp' ? 'up' : 'down')
|
|
214
|
+
}}
|
|
160
215
|
class="flex min-w-0 flex-1 items-center gap-1.5 rounded-sm text-2xs font-medium tracking-wide text-sidebar-muted-foreground uppercase outline-none transition-colors ease-out hover:text-sidebar-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle"
|
|
161
216
|
>
|
|
162
217
|
{heading}
|
|
@@ -41,7 +41,7 @@ export function TopBar(props: TopBarProps) {
|
|
|
41
41
|
<header
|
|
42
42
|
data-slot="top-bar"
|
|
43
43
|
class={cn(
|
|
44
|
-
'h-topbar relative z-(--z-sticky) grid shrink-0 grid-cols-[
|
|
44
|
+
'h-topbar relative z-(--z-sticky) grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 border-b border-border bg-card text-card-foreground px-3 md:grid-cols-[minmax(0,1fr)_clamp(240px,22vw,480px)_minmax(0,1fr)]',
|
|
45
45
|
local.glass && 'glass-panel',
|
|
46
46
|
local.draggable && 'window-drag',
|
|
47
47
|
local.macosInset && 'window-inset-macos',
|
|
@@ -61,7 +61,7 @@ export function TopBarSection(props: ComponentProps<'div'> & { align?: 'start' |
|
|
|
61
61
|
<div
|
|
62
62
|
data-slot="top-bar-section"
|
|
63
63
|
class={cn(
|
|
64
|
-
'flex min-w-0 items-center gap-2 overflow-hidden',
|
|
64
|
+
'window-no-drag flex min-w-0 items-center gap-2 overflow-x-auto overflow-y-hidden',
|
|
65
65
|
local.align === 'end' && 'justify-end',
|
|
66
66
|
local.align !== 'end' && 'justify-start',
|
|
67
67
|
local.class
|
|
@@ -71,12 +71,16 @@ export function TopBarSection(props: ComponentProps<'div'> & { align?: 'start' |
|
|
|
71
71
|
)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export function TopBarTitle(props: ComponentProps<'div'>) {
|
|
75
|
-
const [local, rest] = splitProps(props, ['class'])
|
|
74
|
+
export function TopBarTitle(props: ComponentProps<'div'> & { align?: 'start' | 'center' }) {
|
|
75
|
+
const [local, rest] = splitProps(props, ['class', 'align'])
|
|
76
76
|
return (
|
|
77
77
|
<div
|
|
78
78
|
data-slot="top-bar-title"
|
|
79
|
-
class={cn(
|
|
79
|
+
class={cn(
|
|
80
|
+
'truncate text-sm font-medium',
|
|
81
|
+
local.align === 'center' && 'text-center',
|
|
82
|
+
local.class
|
|
83
|
+
)}
|
|
80
84
|
{...rest}
|
|
81
85
|
/>
|
|
82
86
|
)
|
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { Tabs as KobalteTabs } from '@kobalte/core/tabs'
|
|
2
|
-
import type { ComponentProps } from 'solid-js'
|
|
3
|
-
import {
|
|
2
|
+
import type { Accessor, ComponentProps } from 'solid-js'
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
createRenderEffect,
|
|
6
|
+
createSignal,
|
|
7
|
+
createUniqueId,
|
|
8
|
+
onCleanup,
|
|
9
|
+
splitProps,
|
|
10
|
+
useContext,
|
|
11
|
+
} from 'solid-js'
|
|
4
12
|
import { cn } from '../../../lib/utils'
|
|
5
13
|
|
|
14
|
+
type TabsIdContextValue = {
|
|
15
|
+
id: Accessor<string>
|
|
16
|
+
triggerIds: Accessor<ReadonlyMap<string, string>>
|
|
17
|
+
registerTriggerId: (value: string, id: string) => void
|
|
18
|
+
unregisterTriggerId: (value: string, id: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const TabsIdContext = createContext<TabsIdContextValue>()
|
|
22
|
+
|
|
23
|
+
function useTabsId() {
|
|
24
|
+
const tabs = useContext(TabsIdContext)
|
|
25
|
+
if (!tabs) throw new Error('[adea-ui]: Tabs parts must be used within Tabs.')
|
|
26
|
+
return tabs
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
/**
|
|
7
30
|
* Tabs.
|
|
8
31
|
*
|
|
@@ -14,12 +37,37 @@ import { cn } from '../../../lib/utils'
|
|
|
14
37
|
* where a full-width underline would collide with the panel's own header rule.
|
|
15
38
|
*/
|
|
16
39
|
export function Tabs(props: ComponentProps<typeof KobalteTabs>) {
|
|
17
|
-
const [local, rest] = splitProps(props, ['class'])
|
|
40
|
+
const [local, rest] = splitProps(props, ['class', 'id'])
|
|
41
|
+
const generatedId = `tabs-${createUniqueId()}`
|
|
42
|
+
const id = () => local.id ?? generatedId
|
|
43
|
+
const [triggerIds, setTriggerIds] = createSignal<ReadonlyMap<string, string>>(new Map())
|
|
44
|
+
const tabs: TabsIdContextValue = {
|
|
45
|
+
id,
|
|
46
|
+
triggerIds,
|
|
47
|
+
registerTriggerId: (value, triggerId) => {
|
|
48
|
+
setTriggerIds((current) => new Map(current).set(value, triggerId))
|
|
49
|
+
},
|
|
50
|
+
unregisterTriggerId: (value, triggerId) => {
|
|
51
|
+
setTriggerIds((current) => {
|
|
52
|
+
if (current.get(value) !== triggerId) return current
|
|
53
|
+
const next = new Map(current)
|
|
54
|
+
next.delete(value)
|
|
55
|
+
return next
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
18
60
|
return (
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
61
|
+
<TabsIdContext.Provider value={tabs}>
|
|
62
|
+
<KobalteTabs
|
|
63
|
+
id={id()}
|
|
64
|
+
class={cn(
|
|
65
|
+
'flex min-h-0 min-w-0 flex-col gap-2 data-[orientation=vertical]:flex-row',
|
|
66
|
+
local.class
|
|
67
|
+
)}
|
|
68
|
+
{...rest}
|
|
69
|
+
/>
|
|
70
|
+
</TabsIdContext.Provider>
|
|
23
71
|
)
|
|
24
72
|
}
|
|
25
73
|
|
|
@@ -50,10 +98,21 @@ export type TabsTriggerProps = ComponentProps<typeof KobalteTabs.Trigger> & {
|
|
|
50
98
|
}
|
|
51
99
|
|
|
52
100
|
export function TabsTrigger(props: TabsTriggerProps) {
|
|
53
|
-
const [local, rest] = splitProps(props, ['class', 'appearance'])
|
|
101
|
+
const [local, rest] = splitProps(props, ['class', 'appearance', 'id'])
|
|
102
|
+
const tabs = useTabsId()
|
|
103
|
+
const triggerId = () => local.id ?? `${tabs.id()}-trigger-${rest.value}`
|
|
104
|
+
|
|
105
|
+
// Register before panels render, including SSR where createEffect is skipped.
|
|
106
|
+
createRenderEffect(() => {
|
|
107
|
+
const value = rest.value
|
|
108
|
+
const id = triggerId()
|
|
109
|
+
tabs.registerTriggerId(value, id)
|
|
110
|
+
onCleanup(() => tabs.unregisterTriggerId(value, id))
|
|
111
|
+
})
|
|
54
112
|
|
|
55
113
|
return (
|
|
56
114
|
<KobalteTabs.Trigger
|
|
115
|
+
id={triggerId()}
|
|
57
116
|
class={cn(
|
|
58
117
|
'inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap text-sm font-medium',
|
|
59
118
|
'transition-colors ease-out outline-none select-none',
|
|
@@ -72,6 +131,17 @@ export function TabsTrigger(props: TabsTriggerProps) {
|
|
|
72
131
|
}
|
|
73
132
|
|
|
74
133
|
export function TabsContent(props: ComponentProps<typeof KobalteTabs.Content>) {
|
|
75
|
-
const [local, rest] = splitProps(props, ['class'])
|
|
76
|
-
|
|
134
|
+
const [local, rest] = splitProps(props, ['class', 'aria-labelledby'])
|
|
135
|
+
const tabs = useTabsId()
|
|
136
|
+
return (
|
|
137
|
+
<KobalteTabs.Content
|
|
138
|
+
aria-labelledby={
|
|
139
|
+
local['aria-labelledby'] ??
|
|
140
|
+
tabs.triggerIds().get(rest.value) ??
|
|
141
|
+
`${tabs.id()}-trigger-${rest.value}`
|
|
142
|
+
}
|
|
143
|
+
class={cn('min-h-0 min-w-0 flex-1 outline-none', local.class)}
|
|
144
|
+
{...rest}
|
|
145
|
+
/>
|
|
146
|
+
)
|
|
77
147
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adea-ai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.89.1",
|
|
4
4
|
"description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adea",
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"check:packed-layout-renderer": "bun scripts/check-packed-layout-renderer.ts",
|
|
108
108
|
"check:packed-appearance": "bun scripts/check-packed-appearance.ts",
|
|
109
109
|
"check:packed-native-select": "bun scripts/check-packed-native-select.ts",
|
|
110
|
+
"check:packed-settings-navigation": "bun scripts/check-packed-settings-navigation.ts",
|
|
110
111
|
"check:packed-paste-model": "node scripts/check-packed-paste-model.mjs"
|
|
111
112
|
},
|
|
112
113
|
"dependencies": {
|
package/registry.json
CHANGED
|
@@ -1960,6 +1960,8 @@
|
|
|
1960
1960
|
"description": "",
|
|
1961
1961
|
"dependencies": [],
|
|
1962
1962
|
"registryDependencies": [
|
|
1963
|
+
"@adea-ai/ui/sidebar-nav",
|
|
1964
|
+
"@adea-ai/ui/tabs",
|
|
1963
1965
|
"lib"
|
|
1964
1966
|
],
|
|
1965
1967
|
"files": [
|
|
@@ -1968,6 +1970,16 @@
|
|
|
1968
1970
|
"type": "registry:ui",
|
|
1969
1971
|
"target": "components/composites/settings/index.ts"
|
|
1970
1972
|
},
|
|
1973
|
+
{
|
|
1974
|
+
"path": "src/components/composites/settings/settings-layout.tsx",
|
|
1975
|
+
"type": "registry:ui",
|
|
1976
|
+
"target": "components/composites/settings/settings-layout.tsx"
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
"path": "src/components/composites/settings/settings-navigation.tsx",
|
|
1980
|
+
"type": "registry:ui",
|
|
1981
|
+
"target": "components/composites/settings/settings-navigation.tsx"
|
|
1982
|
+
},
|
|
1971
1983
|
{
|
|
1972
1984
|
"path": "src/components/composites/settings/settings.tsx",
|
|
1973
1985
|
"type": "registry:ui",
|
|
@@ -1 +1,8 @@
|
|
|
1
1
|
export { SettingsField, SettingsPage, SettingsRow, SettingsSection } from './settings'
|
|
2
|
+
export { SettingsLayout, type SettingsLayoutProps } from './settings-layout'
|
|
3
|
+
export {
|
|
4
|
+
SettingsNavigation,
|
|
5
|
+
type SettingsNavigationGroup,
|
|
6
|
+
type SettingsNavigationItem,
|
|
7
|
+
type SettingsNavigationProps,
|
|
8
|
+
} from './settings-navigation'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ComponentProps, JSX } from 'solid-js'
|
|
2
|
+
import { splitProps } from 'solid-js'
|
|
3
|
+
import { cn } from '#lib/utils'
|
|
4
|
+
import { Tabs } from '../../ui/tabs'
|
|
5
|
+
import { SettingsNavigation, type SettingsNavigationGroup } from './settings-navigation'
|
|
6
|
+
|
|
7
|
+
export type SettingsLayoutProps = Omit<
|
|
8
|
+
ComponentProps<typeof Tabs>,
|
|
9
|
+
'children' | 'orientation' | 'value' | 'defaultValue' | 'onChange'
|
|
10
|
+
> & {
|
|
11
|
+
/** Grouped navigation entries; the host owns their values and panel content. */
|
|
12
|
+
groups: readonly SettingsNavigationGroup[]
|
|
13
|
+
/** Accessible name for the vertical tab list. */
|
|
14
|
+
'aria-label': string
|
|
15
|
+
/** The host-controlled selected destination. */
|
|
16
|
+
value: string
|
|
17
|
+
/** Updates the host-controlled selected destination. */
|
|
18
|
+
onChange: (value: string) => void
|
|
19
|
+
/** Called when the already-selected destination is activated again. */
|
|
20
|
+
onReselect?: (value: string) => void
|
|
21
|
+
/** Reveal the selected row on value changes and window resizes. Defaults to nearest scroll. */
|
|
22
|
+
revealSelected?: boolean | ((trigger: HTMLButtonElement | undefined) => void)
|
|
23
|
+
/** Panels must be rendered as `TabsContent` descendants. */
|
|
24
|
+
children?: JSX.Element
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SettingsLayout.
|
|
29
|
+
*
|
|
30
|
+
* A controlled, vertical settings rail beside its scrollable panel viewport.
|
|
31
|
+
* The host keeps ownership of selection, URL/hash state and panel content.
|
|
32
|
+
* At narrow viewports the rail contracts while both columns remain shrinkable.
|
|
33
|
+
*/
|
|
34
|
+
export function SettingsLayout(props: SettingsLayoutProps) {
|
|
35
|
+
const [local, rest] = splitProps(props, [
|
|
36
|
+
'class',
|
|
37
|
+
'children',
|
|
38
|
+
'groups',
|
|
39
|
+
'aria-label',
|
|
40
|
+
'value',
|
|
41
|
+
'onReselect',
|
|
42
|
+
'revealSelected',
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Tabs
|
|
47
|
+
{...rest}
|
|
48
|
+
orientation="vertical"
|
|
49
|
+
value={local.value}
|
|
50
|
+
class={cn('min-h-0 min-w-0 flex-1 gap-0', local.class)}
|
|
51
|
+
>
|
|
52
|
+
<SettingsNavigation
|
|
53
|
+
aria-label={local['aria-label']}
|
|
54
|
+
groups={local.groups}
|
|
55
|
+
value={local.value}
|
|
56
|
+
onReselect={local.onReselect}
|
|
57
|
+
revealSelected={local.revealSelected}
|
|
58
|
+
class="w-24 md:w-sidebar"
|
|
59
|
+
/>
|
|
60
|
+
<div data-slot="settings-layout-panels" class="min-h-0 min-w-0 flex-1 overflow-y-auto p-4">
|
|
61
|
+
{local.children}
|
|
62
|
+
</div>
|
|
63
|
+
</Tabs>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { createEffect, createUniqueId, onCleanup, splitProps } from 'solid-js'
|
|
3
|
+
import { cn } from '#lib/utils'
|
|
4
|
+
import { sidebarNavItemClass, sidebarNavItemStateClass } from '../../layout/sidebar-nav/sidebar-nav'
|
|
5
|
+
import { TabsList, TabsTrigger, type TabsListProps } from '../../ui/tabs'
|
|
6
|
+
|
|
7
|
+
/** One host-owned destination in a settings tab list. Values must be unique. */
|
|
8
|
+
export type SettingsNavigationItem = {
|
|
9
|
+
value: string
|
|
10
|
+
label: string
|
|
11
|
+
icon?: JSX.Element
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A visible, accessibly named group of settings destinations. */
|
|
16
|
+
export type SettingsNavigationGroup = {
|
|
17
|
+
label: string
|
|
18
|
+
items: readonly SettingsNavigationItem[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SettingsNavigationProps = Omit<
|
|
22
|
+
TabsListProps,
|
|
23
|
+
'appearance' | 'children' | 'orientation'
|
|
24
|
+
> & {
|
|
25
|
+
/** Groups are presentation only; the host owns the tab values and panels. */
|
|
26
|
+
groups: readonly SettingsNavigationGroup[]
|
|
27
|
+
/** The same controlled value supplied to the enclosing `Tabs`. */
|
|
28
|
+
value: string
|
|
29
|
+
/** Called when the already-selected destination is activated again. */
|
|
30
|
+
onReselect?: (value: string) => void
|
|
31
|
+
/** Reveal the selected row on value changes and window resizes. Defaults to nearest scroll. */
|
|
32
|
+
revealSelected?: boolean | ((trigger: HTMLButtonElement | undefined) => void)
|
|
33
|
+
/** Every tab list needs a label independent of its visible group headings. */
|
|
34
|
+
'aria-label': string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* SettingsNavigation.
|
|
39
|
+
*
|
|
40
|
+
* A grouped vertical tab list for sibling settings panels. It composes the
|
|
41
|
+
* shared Tabs behaviour with the shared sidebar row recipe; the host supplies
|
|
42
|
+
* the controlled value, panel content, icons and any persistence or routing.
|
|
43
|
+
* Use ordinary tabs for a short, ungrouped row of view choices.
|
|
44
|
+
*
|
|
45
|
+
* Kobalte owns roving focus, arrow keys, selection and tab/panel ARIA. This
|
|
46
|
+
* component adds no URL, storage or host-specific state. Set
|
|
47
|
+
* `revealSelected={false}` when the surrounding layout does not use an internal
|
|
48
|
+
* scrolling viewport.
|
|
49
|
+
*/
|
|
50
|
+
export function SettingsNavigation(props: SettingsNavigationProps) {
|
|
51
|
+
const [local, rest] = splitProps(props, [
|
|
52
|
+
'class',
|
|
53
|
+
'aria-label',
|
|
54
|
+
'groups',
|
|
55
|
+
'value',
|
|
56
|
+
'onReselect',
|
|
57
|
+
'revealSelected',
|
|
58
|
+
])
|
|
59
|
+
const triggers = new Map<string, HTMLButtonElement>()
|
|
60
|
+
const groupIdPrefix = `settings-navigation-${createUniqueId()}`
|
|
61
|
+
|
|
62
|
+
createEffect(() => {
|
|
63
|
+
const value = local.value
|
|
64
|
+
const reveal = local.revealSelected
|
|
65
|
+
if (reveal === false) return
|
|
66
|
+
|
|
67
|
+
const revealCurrent = () => {
|
|
68
|
+
const trigger = triggers.get(value)
|
|
69
|
+
if (typeof reveal === 'function') reveal(trigger)
|
|
70
|
+
else trigger?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
revealCurrent()
|
|
74
|
+
window.addEventListener('resize', revealCurrent)
|
|
75
|
+
onCleanup(() => window.removeEventListener('resize', revealCurrent))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<TabsList
|
|
80
|
+
data-slot="settings-navigation"
|
|
81
|
+
aria-label={local['aria-label']}
|
|
82
|
+
orientation="vertical"
|
|
83
|
+
class={cn(
|
|
84
|
+
'flex max-h-full min-h-0 min-w-0 shrink-0 flex-col items-stretch justify-start gap-2 overflow-x-hidden overflow-y-auto px-2 py-2',
|
|
85
|
+
local.class
|
|
86
|
+
)}
|
|
87
|
+
{...rest}
|
|
88
|
+
>
|
|
89
|
+
{local.groups.map((group, groupIndex) => {
|
|
90
|
+
const groupId = `${groupIdPrefix}-group-${groupIndex}`
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
role="none"
|
|
94
|
+
data-slot="settings-navigation-group"
|
|
95
|
+
class="flex min-w-0 flex-col gap-0.5"
|
|
96
|
+
>
|
|
97
|
+
<span id={groupId} class="sr-only">
|
|
98
|
+
{group.label}
|
|
99
|
+
</span>
|
|
100
|
+
<span
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
class="px-2 py-1.5 text-2xs font-medium tracking-wide text-sidebar-muted-foreground uppercase"
|
|
103
|
+
>
|
|
104
|
+
{group.label}
|
|
105
|
+
</span>
|
|
106
|
+
{group.items.map((item) => (
|
|
107
|
+
<TabsTrigger
|
|
108
|
+
data-slot="settings-navigation-trigger"
|
|
109
|
+
orientation="vertical"
|
|
110
|
+
aria-describedby={groupId}
|
|
111
|
+
value={item.value}
|
|
112
|
+
disabled={item.disabled}
|
|
113
|
+
ref={(element: HTMLButtonElement | undefined) => {
|
|
114
|
+
if (element) triggers.set(item.value, element)
|
|
115
|
+
else triggers.delete(item.value)
|
|
116
|
+
}}
|
|
117
|
+
onClick={() => {
|
|
118
|
+
if (local.value === item.value) local.onReselect?.(item.value)
|
|
119
|
+
}}
|
|
120
|
+
class={cn(
|
|
121
|
+
sidebarNavItemClass,
|
|
122
|
+
sidebarNavItemStateClass.idle,
|
|
123
|
+
'h-row-lg w-full justify-start border-0 px-3 py-2 text-left data-[selected]:bg-primary-subtle data-[selected]:text-foreground'
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{item.icon}
|
|
127
|
+
<span class="min-w-0 truncate">{item.label}</span>
|
|
128
|
+
</TabsTrigger>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
})}
|
|
133
|
+
</TabsList>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -5,6 +5,15 @@ import type { ComponentProps, JSX, ValidComponent } from 'solid-js'
|
|
|
5
5
|
import { Show, createSignal, splitProps } from 'solid-js'
|
|
6
6
|
import { cn } from '#lib/utils'
|
|
7
7
|
|
|
8
|
+
function invokeEventHandler(handler: unknown, event: Event) {
|
|
9
|
+
if (Array.isArray(handler)) {
|
|
10
|
+
const [callback, data] = handler as [(data: unknown, event: Event) => void, unknown]
|
|
11
|
+
callback(data, event)
|
|
12
|
+
} else if (typeof handler === 'function') {
|
|
13
|
+
;(handler as (event: Event) => void)(event)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
/**
|
|
9
18
|
* SidebarNav.
|
|
10
19
|
*
|
|
@@ -96,36 +105,69 @@ export function SidebarNavFooter(props: ComponentProps<'div'>) {
|
|
|
96
105
|
)
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
/** Props forwarded to a collapsible section's disclosure button. */
|
|
109
|
+
export type SidebarNavSectionTriggerProps = Omit<
|
|
110
|
+
ComponentProps<'button'>,
|
|
111
|
+
'type' | 'aria-expanded' | 'onClick' | 'onKeyDown' | 'children' | 'class' | 'style'
|
|
112
|
+
> & {
|
|
113
|
+
/** Called for Alt+ArrowUp/Down; the host owns ordering and row identity. */
|
|
114
|
+
onReorder?: (direction: 'up' | 'down') => void
|
|
115
|
+
/** Runs before the section toggle and may cancel it with `preventDefault()`. */
|
|
116
|
+
onClick?: ComponentProps<'button'>['onClick']
|
|
117
|
+
/** Runs before shared Alt+Arrow handling and may cancel it with `preventDefault()`. */
|
|
118
|
+
onKeyDown?: ComponentProps<'button'>['onKeyDown']
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
/**
|
|
100
122
|
* A titled, collapsible group. Collapsible because a list of projects
|
|
101
123
|
* inevitably outgrows the column, and the user needs to put the ones they are
|
|
102
124
|
* not using away *without* them disappearing into a menu.
|
|
103
125
|
*/
|
|
104
|
-
export
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
export type SidebarNavSectionProps = ComponentProps<'div'> & {
|
|
127
|
+
label: string
|
|
128
|
+
/** Render a disclosure control instead of a plain heading. */
|
|
129
|
+
collapsible?: boolean
|
|
130
|
+
defaultOpen?: boolean
|
|
131
|
+
/** Controlled disclosure state. When supplied, the host owns changes. */
|
|
132
|
+
open?: boolean
|
|
133
|
+
/** Called for user-initiated disclosure changes. */
|
|
134
|
+
onOpenChange?: (open: boolean) => void
|
|
135
|
+
/** Native props and reorder callbacks for the disclosure control. */
|
|
136
|
+
triggerProps?: SidebarNavSectionTriggerProps
|
|
137
|
+
/** Count shown beside the label. */
|
|
138
|
+
count?: number
|
|
139
|
+
/** A control at the trailing edge of the heading row. */
|
|
140
|
+
action?: JSX.Element
|
|
141
|
+
/** Semantic heading element for a host's section hierarchy. */
|
|
142
|
+
headingAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function SidebarNavSection(props: SidebarNavSectionProps) {
|
|
118
146
|
const [local, rest] = splitProps(props, [
|
|
119
147
|
'class',
|
|
120
148
|
'label',
|
|
121
149
|
'collapsible',
|
|
122
150
|
'defaultOpen',
|
|
151
|
+
'open',
|
|
152
|
+
'onOpenChange',
|
|
153
|
+
'triggerProps',
|
|
123
154
|
'count',
|
|
124
155
|
'action',
|
|
125
156
|
'headingAs',
|
|
126
157
|
'children',
|
|
127
158
|
])
|
|
128
|
-
const [
|
|
159
|
+
const [uncontrolledOpen, setUncontrolledOpen] = createSignal(local.defaultOpen ?? true)
|
|
160
|
+
const open = () => local.open ?? uncontrolledOpen()
|
|
161
|
+
const setOpen = (next: boolean) => {
|
|
162
|
+
if (local.open === undefined) setUncontrolledOpen(next)
|
|
163
|
+
local.onOpenChange?.(next)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const [trigger, triggerRest] = splitProps(local.triggerProps ?? {}, [
|
|
167
|
+
'onClick',
|
|
168
|
+
'onKeyDown',
|
|
169
|
+
'onReorder',
|
|
170
|
+
])
|
|
129
171
|
|
|
130
172
|
const heading = (
|
|
131
173
|
<>
|
|
@@ -154,9 +196,22 @@ export function SidebarNavSection(
|
|
|
154
196
|
>
|
|
155
197
|
<Show when={local.collapsible} fallback={heading}>
|
|
156
198
|
<button
|
|
199
|
+
{...(triggerRest as ComponentProps<'button'>)}
|
|
157
200
|
type="button"
|
|
158
201
|
aria-expanded={open()}
|
|
159
|
-
onClick={() =>
|
|
202
|
+
onClick={(event) => {
|
|
203
|
+
invokeEventHandler(trigger.onClick, event)
|
|
204
|
+
if (event.defaultPrevented) return
|
|
205
|
+
setOpen(!open())
|
|
206
|
+
}}
|
|
207
|
+
onKeyDown={(event) => {
|
|
208
|
+
invokeEventHandler(trigger.onKeyDown, event)
|
|
209
|
+
if (event.defaultPrevented || !event.altKey) return
|
|
210
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return
|
|
211
|
+
event.preventDefault()
|
|
212
|
+
event.stopPropagation()
|
|
213
|
+
trigger.onReorder?.(event.key === 'ArrowUp' ? 'up' : 'down')
|
|
214
|
+
}}
|
|
160
215
|
class="flex min-w-0 flex-1 items-center gap-1.5 rounded-sm text-2xs font-medium tracking-wide text-sidebar-muted-foreground uppercase outline-none transition-colors ease-out hover:text-sidebar-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle"
|
|
161
216
|
>
|
|
162
217
|
{heading}
|