@adea-ai/ui 0.88.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.
Files changed (34) hide show
  1. package/README.md +16 -0
  2. package/dist/components/composites/settings/index.d.ts +2 -0
  3. package/dist/components/composites/settings/index.d.ts.map +1 -1
  4. package/dist/components/composites/settings/index.js +3 -1
  5. package/dist/components/composites/settings/settings-layout.d.ts +28 -0
  6. package/dist/components/composites/settings/settings-layout.d.ts.map +1 -0
  7. package/dist/components/composites/settings/settings-layout.js +62 -0
  8. package/dist/components/composites/settings/settings-layout.js.map +1 -0
  9. package/dist/components/composites/settings/settings-navigation.d.ts +41 -0
  10. package/dist/components/composites/settings/settings-navigation.d.ts.map +1 -0
  11. package/dist/components/composites/settings/settings-navigation.js +102 -0
  12. package/dist/components/composites/settings/settings-navigation.js.map +1 -0
  13. package/dist/components/layout/top-bar/top-bar.d.ts +3 -1
  14. package/dist/components/layout/top-bar/top-bar.d.ts.map +1 -1
  15. package/dist/components/layout/top-bar/top-bar.js +4 -4
  16. package/dist/components/layout/top-bar/top-bar.js.map +1 -1
  17. package/dist/components/ui/tabs/tabs.d.ts.map +1 -1
  18. package/dist/components/ui/tabs/tabs.js +70 -13
  19. package/dist/components/ui/tabs/tabs.js.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/r/registry.json +12 -0
  22. package/dist/r/settings.json +12 -0
  23. package/dist/r/src/components/composites/settings/index.ts +7 -0
  24. package/dist/r/src/components/composites/settings/settings-layout.tsx +65 -0
  25. package/dist/r/src/components/composites/settings/settings-navigation.tsx +135 -0
  26. package/dist/r/src/components/layout/top-bar/top-bar.tsx +9 -5
  27. package/dist/r/src/components/ui/tabs/tabs.tsx +80 -10
  28. package/package.json +2 -1
  29. package/registry.json +12 -0
  30. package/src/components/composites/settings/index.ts +7 -0
  31. package/src/components/composites/settings/settings-layout.tsx +65 -0
  32. package/src/components/composites/settings/settings-navigation.tsx +135 -0
  33. package/src/components/layout/top-bar/top-bar.tsx +9 -5
  34. package/src/components/ui/tabs/tabs.tsx +80 -10
@@ -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
+ }
@@ -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-[minmax(0,1fr)_clamp(240px,22vw,480px)_minmax(0,1fr)] items-center gap-3 border-b border-border px-3',
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('truncate text-sm font-medium', local.class)}
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 { splitProps } from 'solid-js'
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
- <KobalteTabs
20
- class={cn('flex flex-col gap-2 data-[orientation=vertical]:flex-row', local.class)}
21
- {...rest}
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
- return <KobalteTabs.Content class={cn('flex-1 outline-none', local.class)} {...rest} />
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
  }