@estiva-app/ui 0.56.0 → 0.57.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/ChipInput.d.ts.map +1 -1
- package/dist/CollapsibleSection.d.ts +1 -1
- package/dist/CollapsibleSection.d.ts.map +1 -1
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/NavItem.d.ts.map +1 -1
- package/dist/NavTree.d.ts +2 -2
- package/dist/NavTree.d.ts.map +1 -1
- package/dist/SectionHeader.d.ts +4 -4
- package/dist/SectionHeader.d.ts.map +1 -1
- package/dist/Select.d.ts.map +1 -1
- package/dist/SuggestionMenu.d.ts.map +1 -1
- package/dist/index.js +33 -36
- package/dist/index.js.map +3 -3
- package/dist/looks.d.ts +2 -2
- package/dist/looks.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +8 -7
- package/src/ChipInput.test.tsx +8 -0
- package/src/ChipInput.tsx +4 -2
- package/src/CollapsibleSection.mdx +1 -1
- package/src/CollapsibleSection.test.tsx +24 -0
- package/src/CollapsibleSection.tsx +1 -1
- package/src/CommandPalette.test.tsx +6 -0
- package/src/CommandPalette.tsx +3 -2
- package/src/ListColumn.test.tsx +8 -0
- package/src/NavItem.mdx +3 -1
- package/src/NavItem.test.tsx +30 -0
- package/src/NavItem.tsx +10 -3
- package/src/NavTree.test.tsx +21 -0
- package/src/NavTree.tsx +7 -3
- package/src/SectionHeader.mdx +4 -3
- package/src/SectionHeader.test.tsx +68 -5
- package/src/SectionHeader.tsx +37 -25
- package/src/Select.test.tsx +8 -0
- package/src/Select.tsx +5 -2
- package/src/SuggestionMenu.stories.tsx +15 -0
- package/src/SuggestionMenu.test.tsx +8 -0
- package/src/SuggestionMenu.tsx +4 -2
- package/src/looks.ts +3 -3
package/src/SectionHeader.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
2
|
import { IconChevronRight, IconDotsVertical } from '@tabler/icons-react'
|
|
3
3
|
import { useRender } from '@base-ui/react/use-render'
|
|
4
4
|
import { cn } from './cn'
|
|
@@ -6,13 +6,12 @@ import { SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES } from './looks'
|
|
|
6
6
|
import { IconButton } from './IconButton'
|
|
7
7
|
import { Menu } from './Menu'
|
|
8
8
|
import { SectionLabel } from './SectionLabel'
|
|
9
|
-
import { UnreadDot } from './UnreadDot'
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* The 32px row a section starts with (Peek's SectionHeader, 2026-09-01):
|
|
13
12
|
* a SectionLabel, an optional collapse chevron that makes the title the
|
|
14
|
-
* toggle, and actions that appear while the row is hovered or focused,
|
|
15
|
-
* IconButtons with tooltips.
|
|
13
|
+
* toggle, and actions that appear while the row is hovered or keyboard-focused,
|
|
14
|
+
* as IconButtons with tooltips.
|
|
16
15
|
*
|
|
17
16
|
* Three things changed on 2026-09-09, for `CollapsibleSection`:
|
|
18
17
|
*
|
|
@@ -79,7 +78,7 @@ export interface SectionHeaderProps {
|
|
|
79
78
|
menu?: ReactNode
|
|
80
79
|
/** The ⋮'s accessible name. Default "More options for" the title. */
|
|
81
80
|
menuLabel?: string
|
|
82
|
-
/** `hover` reveals the actions while the row is hovered or focused; `always` keeps them. */
|
|
81
|
+
/** `hover` reveals the actions while the row is hovered or keyboard-focused (always on a screen with no hover); `always` keeps them. */
|
|
83
82
|
showActions?: 'hover' | 'always'
|
|
84
83
|
/**
|
|
85
84
|
* `fill` lights the row under the pointer when it does something (a toggle,
|
|
@@ -113,12 +112,35 @@ export function SectionHeader({ title, chevron = false, icon, isExpanded = true,
|
|
|
113
112
|
fills the icon's place exactly.
|
|
114
113
|
*/
|
|
115
114
|
const acts = (actions && actions.length > 0) || menu != null
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
/*
|
|
116
|
+
A dot sits in the place of the last button and steps aside while the
|
|
117
|
+
buttons show — the rows' dot and ⋮ share a place the same way (Katerina,
|
|
118
|
+
5 October). A count stays beside them: it is information (09-09).
|
|
119
|
+
|
|
120
|
+
"A dot" is whatever holds an `UnreadDot`, found by the `data-unread` it
|
|
121
|
+
draws, so the CSS decides (`has-[[data-unread]]`) rather than a check of
|
|
122
|
+
the element's type: that missed a dot in a tooltip or in an app's own
|
|
123
|
+
wrapper, which then sat beside the buttons as a count does. With no
|
|
124
|
+
buttons it sits on their axis all the same (`-mr-1`, as they are pulled).
|
|
125
|
+
In the last button's place it is the 24px slot at `right-1`, where the
|
|
126
|
+
buttons' box ends, gone while they show — on a screen with no hover, then,
|
|
127
|
+
always.
|
|
128
|
+
*/
|
|
129
|
+
const trailingClasses = !acts
|
|
130
|
+
? 'has-[[data-unread]]:-mr-1'
|
|
131
|
+
: showActions === 'hover' &&
|
|
132
|
+
'has-[[data-unread]]:pointer-events-none has-[[data-unread]]:absolute has-[[data-unread]]:inset-y-0 has-[[data-unread]]:right-1 has-[[data-unread]]:w-6 has-[[data-unread]]:justify-center has-[[data-unread]]:transition-opacity group-hover:has-[[data-unread]]:opacity-0 group-has-[:focus-visible]:has-[[data-unread]]:opacity-0 group-has-[[data-section-menu]_[aria-expanded=true]]:has-[[data-unread]]:opacity-0 [@media(hover:none)]:has-[[data-unread]]:opacity-0'
|
|
133
|
+
/*
|
|
134
|
+
The buttons show under the pointer, while the keyboard is anywhere in the
|
|
135
|
+
row — the title's toggle included, as a NavItem's ⋮ shows once its link
|
|
136
|
+
has keyboard focus; it was only one of the buttons themselves — and while
|
|
137
|
+
the menu is open. `aria-expanded` is asked of the buttons' own box: the
|
|
138
|
+
toggle says it too, and an open section would keep them for good.
|
|
139
|
+
|
|
140
|
+
A screen with no hover (a phone, a tablet) shows them always: nothing
|
|
141
|
+
there can point at the row, so hidden they could never be reached.
|
|
142
|
+
*/
|
|
143
|
+
const reveal = showActions === 'hover' && 'opacity-0 transition-opacity group-hover:opacity-100 group-has-[:focus-visible]:opacity-100 has-[[aria-expanded=true]]:opacity-100 [@media(hover:none)]:opacity-100'
|
|
122
144
|
const titleElement = useRender({
|
|
123
145
|
render: render ?? (chevron ? <button type="button" onClick={onToggle} aria-expanded={isExpanded} /> : <span />),
|
|
124
146
|
props: {
|
|
@@ -165,7 +187,8 @@ export function SectionHeader({ title, chevron = false, icon, isExpanded = true,
|
|
|
165
187
|
return (
|
|
166
188
|
<div
|
|
167
189
|
className={cn(
|
|
168
|
-
'
|
|
190
|
+
// `relative`: a dot in the last button's place is placed against the row.
|
|
191
|
+
'group relative flex h-[32px] shrink-0 items-center gap-1 px-2',
|
|
169
192
|
!row && 'transition-colors',
|
|
170
193
|
// A row is NavItem's shape and colour: 6px corners, secondary words that brighten on hover.
|
|
171
194
|
row ? cn(SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES) : 'rounded-lg',
|
|
@@ -184,18 +207,14 @@ export function SectionHeader({ title, chevron = false, icon, isExpanded = true,
|
|
|
184
207
|
{/* Outside the title button, like the actions: a chip inside a button
|
|
185
208
|
would be part of the button's accessible name and part of its hit
|
|
186
209
|
target, and the count is neither. */}
|
|
187
|
-
{trailing != null &&
|
|
188
|
-
// A dot with no buttons to share a place with still sits on their
|
|
189
|
-
// axis — the rows' dots and ⋮ (`-mr-1`, as the buttons are pulled).
|
|
190
|
-
<div className={cn('flex shrink-0 items-center', dot && !acts && '-mr-1')}>{trailing}</div>
|
|
191
|
-
)}
|
|
210
|
+
{trailing != null && <div className={cn('flex shrink-0 items-center', trailingClasses)}>{trailing}</div>}
|
|
192
211
|
{acts && (
|
|
193
212
|
<div
|
|
194
213
|
// `-mr-1`: an IconButton is 24px around a 16px icon, so at the row's
|
|
195
214
|
// `px-2` its icon ended 12px from the edge while a NavItem's number
|
|
196
215
|
// ends 8px from it; pulled 4px, the icon and the number share a
|
|
197
216
|
// right edge (Katerina, 2026-09-09; measured 4px off, then 0).
|
|
198
|
-
className="
|
|
217
|
+
className="-mr-1 flex shrink-0 items-center"
|
|
199
218
|
>
|
|
200
219
|
<div className={cn('flex items-center gap-1', reveal)}>
|
|
201
220
|
{actions?.map((action) => (
|
|
@@ -218,13 +237,6 @@ export function SectionHeader({ title, chevron = false, icon, isExpanded = true,
|
|
|
218
237
|
</div>
|
|
219
238
|
)}
|
|
220
239
|
</div>
|
|
221
|
-
{shared && (
|
|
222
|
-
// The dot in the last button's 24px place, on the axis of its icon;
|
|
223
|
-
// gone while the buttons show.
|
|
224
|
-
<div className="pointer-events-none absolute inset-y-0 right-0 flex w-6 items-center justify-center transition-opacity group-hover:opacity-0 group-has-[:focus-visible]/acts:opacity-0 group-has-[[aria-expanded=true]]/acts:opacity-0">
|
|
225
|
-
{trailing}
|
|
226
|
-
</div>
|
|
227
|
-
)}
|
|
228
240
|
</div>
|
|
229
241
|
)}
|
|
230
242
|
</div>
|
package/src/Select.test.tsx
CHANGED
|
@@ -104,6 +104,14 @@ describe('Select', () => {
|
|
|
104
104
|
expect(screen.getByRole('combobox', { name: 'Status' }).getAttribute('aria-expanded')).toBe('true')
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
+
it('puts 2px between options, as between a menu’s rows', async () => {
|
|
108
|
+
const user = userEvent.setup()
|
|
109
|
+
render(<Controlled initial="todo" />)
|
|
110
|
+
await user.click(screen.getByRole('combobox', { name: 'Status' }))
|
|
111
|
+
const [first] = await screen.findAllByRole('option')
|
|
112
|
+
expect(first.parentElement!.className.split(' ')).toContain('gap-0.5')
|
|
113
|
+
})
|
|
114
|
+
|
|
107
115
|
it('opens from the keyboard, moves with arrows, picks with Enter, and gives focus back', async () => {
|
|
108
116
|
const user = userEvent.setup()
|
|
109
117
|
const onChange = vi.fn()
|
package/src/Select.tsx
CHANGED
|
@@ -174,8 +174,11 @@ export function Select({ value, onChange, options, size = 'default', ariaLabel,
|
|
|
174
174
|
* to the right side". The rows keep their 4px inset, because the
|
|
175
175
|
* padding that was the panel's is the content's; and the cap loses
|
|
176
176
|
* its `- 0.5rem`, because that padding is inside the box that
|
|
177
|
-
* scrolls now, so 288px stays 288px.
|
|
178
|
-
|
|
177
|
+
* scrolls now, so 288px stays 288px.
|
|
178
|
+
*
|
|
179
|
+
* `gap-0.5`: 2px between the options, as between a menu's rows —
|
|
180
|
+
* they touched, while every menu had moved to 2px (0.56.0). */}
|
|
181
|
+
<ScrollArea viewportClassName="max-h-[min(288px,var(--available-height))]" contentClassName="flex flex-col gap-0.5 p-2">
|
|
179
182
|
{options.map((option) => (
|
|
180
183
|
<BaseSelect.Item
|
|
181
184
|
key={option.value}
|
|
@@ -83,6 +83,21 @@ export const Default: Story = {
|
|
|
83
83
|
),
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** No heading: rows the editor ranks itself, 2px apart as under one. */
|
|
87
|
+
export const NoHeading: Story = {
|
|
88
|
+
render: () => (
|
|
89
|
+
<AtACaret<Command>
|
|
90
|
+
ariaLabel="Commands"
|
|
91
|
+
width="w-[300px]"
|
|
92
|
+
maxHeight="max-h-[400px]"
|
|
93
|
+
sections={[{ items: FORMAT }]}
|
|
94
|
+
itemKey={(c) => c.id}
|
|
95
|
+
row={commandRow}
|
|
96
|
+
onSelect={() => {}}
|
|
97
|
+
/>
|
|
98
|
+
),
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
/** Tall rows: a face, a second line, and Enter's hint on the highlighted one. */
|
|
87
102
|
export const TallRows: Story = {
|
|
88
103
|
render: () => (
|
|
@@ -48,6 +48,14 @@ describe('SuggestionMenu', () => {
|
|
|
48
48
|
expect(screen.getByRole('group', { name: 'Second' })).toBeTruthy()
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
+
it('puts 2px between rows, with a heading or without one', async () => {
|
|
52
|
+
draw({ sections: [{ items: ['One', 'Two'] }, { label: 'Second', items: ['Three'] }] })
|
|
53
|
+
const list = await screen.findByRole('listbox')
|
|
54
|
+
// The rows with no heading are the listbox's own; under a heading, MenuSection's.
|
|
55
|
+
expect(list.className.split(' ')).toContain('gap-0.5')
|
|
56
|
+
expect(screen.getByRole('group', { name: 'Second' }).firstElementChild!.className.split(' ')).toContain('gap-0.5')
|
|
57
|
+
})
|
|
58
|
+
|
|
51
59
|
it('draws nothing when there is nothing to show', () => {
|
|
52
60
|
draw({ sections: [{ label: 'First', items: [] }] })
|
|
53
61
|
expect(screen.queryByRole('listbox')).toBeNull()
|
package/src/SuggestionMenu.tsx
CHANGED
|
@@ -177,8 +177,10 @@ export function SuggestionMenu<T>({ rect, editorElement, ariaLabel, width, maxHe
|
|
|
177
177
|
|
|
178
178
|
return (
|
|
179
179
|
<Popover anchor={rect} open={rect !== null} side="top" align="left" className={width} maxHeight={maxHeight} ariaLabel={ariaLabel}>
|
|
180
|
-
{/* The list the editor drives from its caret: a listbox, because Base UI's lists need a text field of their own.
|
|
181
|
-
|
|
180
|
+
{/* The list the editor drives from its caret: a listbox, because Base UI's lists need a text field of their own.
|
|
181
|
+
`gap-0.5`: 2px between rows, as a menu's — a section with a heading
|
|
182
|
+
had them from `MenuSection`, and rows without one touched. */}
|
|
183
|
+
<div id={listboxId} role="listbox" aria-label={ariaLabel} className="flex flex-col gap-0.5">
|
|
182
184
|
{shown.map((section, i) => (
|
|
183
185
|
<div key={section.label ?? i} className="contents">
|
|
184
186
|
{i > 0 && <MenuSeparator />}
|
package/src/looks.ts
CHANGED
|
@@ -45,15 +45,15 @@ export const BARE_INPUT_CLASSES = 'bg-transparent text-text-primary placeholder:
|
|
|
45
45
|
|
|
46
46
|
/** A sidebar row's shape and its words at rest, brightening under the pointer: NavItem, and SectionHeader's `row` (a folder among rows). */
|
|
47
47
|
// 8px corners, a MenuItem row's (Katerina, 2 October).
|
|
48
|
+
export const SIDEBAR_ROW_CLASSES = 'rounded-lg transition-colors'
|
|
49
|
+
export const SIDEBAR_ROW_TEXT_CLASSES = 'text-text-secondary hover:text-text-primary'
|
|
50
|
+
|
|
48
51
|
/** A quiet icon control: secondary, brightening on a hover fill — IconButton's `muted`. */
|
|
49
52
|
export const MUTED_CONTROL_CLASSES = 'text-text-secondary hover:bg-bg-hover hover:text-text-primary'
|
|
50
53
|
|
|
51
54
|
/** A control that opens a menu: a hover fill, held while its menu is up — PersonTrigger's row, and a ContainerHeader title with a menu. */
|
|
52
55
|
export const MENU_TRIGGER_CLASSES = 'cursor-pointer rounded-md transition-colors hover:bg-bg-hover data-[popup-open]:bg-bg-hover'
|
|
53
56
|
|
|
54
|
-
export const SIDEBAR_ROW_CLASSES = 'rounded-lg transition-colors'
|
|
55
|
-
export const SIDEBAR_ROW_TEXT_CLASSES = 'text-text-secondary hover:text-text-primary'
|
|
56
|
-
|
|
57
57
|
/**
|
|
58
58
|
* A panel that slides open and shut on Base UI's Collapsible: Base UI measures
|
|
59
59
|
* it and writes its height to a variable, `auto` again once the slide ends, and
|