@estiva-app/ui 0.55.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 +8 -2
- package/dist/CollapsibleSection.d.ts.map +1 -1
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/ContainerHeader.d.ts +7 -1
- package/dist/ContainerHeader.d.ts.map +1 -1
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/ListColumn.d.ts +2 -0
- package/dist/ListColumn.d.ts.map +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/NavItem.d.ts.map +1 -1
- package/dist/NavTree.d.ts +11 -3
- package/dist/NavTree.d.ts.map +1 -1
- package/dist/PersonTrigger.d.ts.map +1 -1
- package/dist/SectionHeader.d.ts +24 -5
- package/dist/SectionHeader.d.ts.map +1 -1
- package/dist/SectionLabel.d.ts.map +1 -1
- package/dist/Select.d.ts.map +1 -1
- package/dist/SuggestionMenu.d.ts.map +1 -1
- package/dist/gates/index.js +1 -1
- package/dist/gates/index.js.map +2 -2
- package/dist/index.js +138 -77
- package/dist/index.js.map +4 -4
- package/dist/looks.d.ts +4 -0
- package/dist/looks.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +98 -26
- package/src/ChipInput.test.tsx +8 -0
- package/src/ChipInput.tsx +4 -2
- package/src/CollapsibleSection.mdx +1 -1
- package/src/CollapsibleSection.stories.tsx +14 -1
- package/src/CollapsibleSection.test.tsx +24 -0
- package/src/CollapsibleSection.tsx +11 -5
- package/src/CommandPalette.test.tsx +6 -0
- package/src/CommandPalette.tsx +3 -2
- package/src/ContainerHeader.mdx +4 -0
- package/src/ContainerHeader.stories.tsx +14 -0
- package/src/ContainerHeader.test.tsx +8 -0
- package/src/ContainerHeader.tsx +34 -6
- package/src/IconButton.tsx +2 -1
- package/src/ListColumn.test.tsx +8 -0
- package/src/ListColumn.tsx +4 -2
- package/src/Menu.tsx +5 -4
- package/src/NavItem.mdx +3 -1
- package/src/NavItem.test.tsx +32 -2
- package/src/NavItem.tsx +28 -12
- package/src/NavTree.mdx +16 -1
- package/src/NavTree.stories.tsx +49 -2
- package/src/NavTree.test.tsx +28 -0
- package/src/NavTree.tsx +27 -6
- package/src/PersonTrigger.tsx +6 -5
- package/src/SectionHeader.mdx +10 -3
- package/src/SectionHeader.stories.tsx +18 -0
- package/src/SectionHeader.test.tsx +89 -0
- package/src/SectionHeader.tsx +114 -26
- package/src/SectionLabel.tsx +6 -3
- package/src/Select.test.tsx +8 -0
- package/src/Select.tsx +5 -2
- package/src/Sidebar.tsx +1 -1
- package/src/SuggestionMenu.stories.tsx +15 -0
- package/src/SuggestionMenu.test.tsx +8 -0
- package/src/SuggestionMenu.tsx +4 -2
- package/src/eslint/no-copied-look.test.ts +2 -2
- package/src/gates/app-checks.ts +1 -1
- package/src/looks.ts +6 -0
package/src/SectionHeader.tsx
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
|
-
import { IconChevronRight } from '@tabler/icons-react'
|
|
2
|
+
import { IconChevronRight, IconDotsVertical } from '@tabler/icons-react'
|
|
3
3
|
import { useRender } from '@base-ui/react/use-render'
|
|
4
4
|
import { cn } from './cn'
|
|
5
5
|
import { SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES } from './looks'
|
|
6
6
|
import { IconButton } from './IconButton'
|
|
7
|
+
import { Menu } from './Menu'
|
|
7
8
|
import { SectionLabel } from './SectionLabel'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The 32px row a section starts with (Peek's SectionHeader, 2026-09-01):
|
|
11
12
|
* a SectionLabel, an optional collapse chevron that makes the title the
|
|
12
|
-
* toggle, and actions that appear while the row is hovered or focused,
|
|
13
|
-
* IconButtons with tooltips.
|
|
13
|
+
* toggle, and actions that appear while the row is hovered or keyboard-focused,
|
|
14
|
+
* as IconButtons with tooltips.
|
|
14
15
|
*
|
|
15
16
|
* Three things changed on 2026-09-09, for `CollapsibleSection`:
|
|
16
17
|
*
|
|
@@ -45,10 +46,19 @@ export interface SectionHeaderProps {
|
|
|
45
46
|
title: string
|
|
46
47
|
/** Collapsible: draws the chevron and makes the title a button that toggles. */
|
|
47
48
|
chevron?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* 16px, stroke 1.5, before the title — a folder's (Katerina, 5 October:
|
|
51
|
+
* Peek's Folders, the standard heading with a folder icon). With `chevron`
|
|
52
|
+
* the icon is what shows at rest and the arrow takes its place under the
|
|
53
|
+
* pointer or the keyboard, as a NavItem's icon turns into its arrow.
|
|
54
|
+
*/
|
|
55
|
+
icon?: ReactNode
|
|
48
56
|
isExpanded?: boolean
|
|
49
57
|
onToggle?: () => void
|
|
50
58
|
/**
|
|
51
|
-
* Beside the title and always visible, before the actions — a count.
|
|
59
|
+
* Beside the title and always visible, before the actions — a count. An
|
|
60
|
+
* `UnreadDot` instead sits in the last action's place (the ⋮'s, with a
|
|
61
|
+
* `menu`) and steps aside while the actions show, as a NavItem's dot does.
|
|
52
62
|
*
|
|
53
63
|
* The actions come and go with the hover; this does not, because a count is
|
|
54
64
|
* information rather than an affordance. Peek's Screener header carries its
|
|
@@ -58,7 +68,17 @@ export interface SectionHeaderProps {
|
|
|
58
68
|
trailing?: ReactNode
|
|
59
69
|
/** Right-aligned, in the order given. */
|
|
60
70
|
actions?: SectionAction[]
|
|
61
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* The heading's "More options" menu: `MenuItem`s — what acts on the whole
|
|
73
|
+
* section, a folder's Rename or Archive (Katerina, 5 October: Peek's Folders
|
|
74
|
+
* page goes, and its folder actions move onto the folder's heading). The ⋮
|
|
75
|
+
* is NavItem's, after the actions and revealed with them; it stays while its
|
|
76
|
+
* menu is open.
|
|
77
|
+
*/
|
|
78
|
+
menu?: ReactNode
|
|
79
|
+
/** The ⋮'s accessible name. Default "More options for" the title. */
|
|
80
|
+
menuLabel?: string
|
|
81
|
+
/** `hover` reveals the actions while the row is hovered or keyboard-focused (always on a screen with no hover); `always` keeps them. */
|
|
62
82
|
showActions?: 'hover' | 'always'
|
|
63
83
|
/**
|
|
64
84
|
* `fill` lights the row under the pointer when it does something (a toggle,
|
|
@@ -82,23 +102,74 @@ export interface SectionHeaderProps {
|
|
|
82
102
|
className?: string
|
|
83
103
|
}
|
|
84
104
|
|
|
85
|
-
export function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = 'hover', hover = 'fill', look = 'heading', render, className }: SectionHeaderProps) {
|
|
105
|
+
export function SectionHeader({ title, chevron = false, icon, isExpanded = true, onToggle, trailing, actions, menu, menuLabel, showActions = 'hover', hover = 'fill', look = 'heading', render, className }: SectionHeaderProps) {
|
|
86
106
|
const row = look === 'row'
|
|
107
|
+
/*
|
|
108
|
+
One arrow for everything that folds, a NavItem's 16px, and the title 8px
|
|
109
|
+
after it as a row's label is (Katerina, 5 October: a folder heading and
|
|
110
|
+
the rows under it fold with one arrow; then every folding heading, Peek's
|
|
111
|
+
Starred too). It was 12px with a 4px gap; with an icon, the arrow now
|
|
112
|
+
fills the icon's place exactly.
|
|
113
|
+
*/
|
|
114
|
+
const acts = (actions && actions.length > 0) || menu != null
|
|
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'
|
|
87
144
|
const titleElement = useRender({
|
|
88
145
|
render: render ?? (chevron ? <button type="button" onClick={onToggle} aria-expanded={isExpanded} /> : <span />),
|
|
89
146
|
props: {
|
|
90
147
|
// `text-left`: a button centres its text. `h-full` and `flex-1`: the
|
|
91
148
|
// whole row up to the actions is the hit target, as it was when the
|
|
92
149
|
// row itself carried the click.
|
|
93
|
-
|
|
150
|
+
// An icon is a row's: NavItem's 8px after it.
|
|
151
|
+
className: cn('flex h-full min-w-0 flex-1 items-center text-left', row || chevron || icon != null ? 'gap-2' : 'gap-1'),
|
|
94
152
|
children: (
|
|
95
153
|
<>
|
|
96
|
-
{
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
154
|
+
{icon != null ? (
|
|
155
|
+
<span className="relative flex size-4 shrink-0 items-center justify-center text-text-secondary">
|
|
156
|
+
<span className={cn('flex', chevron && 'transition-opacity group-hover:opacity-0 group-has-[:focus-visible]:opacity-0')}>{icon}</span>
|
|
157
|
+
{chevron && (
|
|
158
|
+
<IconChevronRight
|
|
159
|
+
size={16}
|
|
160
|
+
stroke={1.5}
|
|
161
|
+
className={cn('absolute opacity-0 transition-[opacity,transform] duration-150 group-hover:opacity-100 group-has-[:focus-visible]:opacity-100', isExpanded && 'rotate-90')}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
</span>
|
|
165
|
+
) : (
|
|
166
|
+
chevron && (
|
|
167
|
+
<IconChevronRight
|
|
168
|
+
size={16}
|
|
169
|
+
stroke={1.5}
|
|
170
|
+
className={cn('shrink-0 transition-transform duration-150', !row && 'text-text-secondary', isExpanded && 'rotate-90')}
|
|
171
|
+
/>
|
|
172
|
+
)
|
|
102
173
|
)}
|
|
103
174
|
{look === 'quiet' ? (
|
|
104
175
|
<span className="min-w-0 truncate text-caption text-text-muted">{title}</span>
|
|
@@ -116,7 +187,8 @@ export function SectionHeader({ title, chevron = false, isExpanded = true, onTog
|
|
|
116
187
|
return (
|
|
117
188
|
<div
|
|
118
189
|
className={cn(
|
|
119
|
-
'
|
|
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',
|
|
120
192
|
!row && 'transition-colors',
|
|
121
193
|
// A row is NavItem's shape and colour: 6px corners, secondary words that brighten on hover.
|
|
122
194
|
row ? cn(SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES) : 'rounded-lg',
|
|
@@ -124,7 +196,10 @@ export function SectionHeader({ title, chevron = false, isExpanded = true, onTog
|
|
|
124
196
|
// lights up, a fixed heading over rows does not (2026-09-09, the
|
|
125
197
|
// Sidebar's fixed group).
|
|
126
198
|
// A quiet label never lights up: it is a label; its button does.
|
|
127
|
-
hover === 'fill' && look !== 'quiet' && (chevron ||
|
|
199
|
+
hover === 'fill' && look !== 'quiet' && (chevron || acts) && 'hover:bg-bg-hover',
|
|
200
|
+
// While its menu is open the pointer is on the menu, not the row: the
|
|
201
|
+
// row keeps its hover look, as a NavItem's does.
|
|
202
|
+
hover === 'fill' && look !== 'quiet' && menu != null && 'has-[[data-section-menu]_[aria-expanded=true]]:bg-bg-hover',
|
|
128
203
|
className,
|
|
129
204
|
)}
|
|
130
205
|
>
|
|
@@ -132,23 +207,36 @@ export function SectionHeader({ title, chevron = false, isExpanded = true, onTog
|
|
|
132
207
|
{/* Outside the title button, like the actions: a chip inside a button
|
|
133
208
|
would be part of the button's accessible name and part of its hit
|
|
134
209
|
target, and the count is neither. */}
|
|
135
|
-
{trailing != null && <div className=
|
|
136
|
-
{
|
|
210
|
+
{trailing != null && <div className={cn('flex shrink-0 items-center', trailingClasses)}>{trailing}</div>}
|
|
211
|
+
{acts && (
|
|
137
212
|
<div
|
|
138
213
|
// `-mr-1`: an IconButton is 24px around a 16px icon, so at the row's
|
|
139
214
|
// `px-2` its icon ended 12px from the edge while a NavItem's number
|
|
140
215
|
// ends 8px from it; pulled 4px, the icon and the number share a
|
|
141
216
|
// right edge (Katerina, 2026-09-09; measured 4px off, then 0).
|
|
142
|
-
className=
|
|
143
|
-
'-mr-1 flex shrink-0 items-center gap-1',
|
|
144
|
-
showActions === 'hover' && 'opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100',
|
|
145
|
-
)}
|
|
217
|
+
className="-mr-1 flex shrink-0 items-center"
|
|
146
218
|
>
|
|
147
|
-
{
|
|
148
|
-
|
|
149
|
-
{action.
|
|
150
|
-
|
|
151
|
-
|
|
219
|
+
<div className={cn('flex items-center gap-1', reveal)}>
|
|
220
|
+
{actions?.map((action) => (
|
|
221
|
+
<IconButton key={action.tooltip} tooltip={action.tooltip} aria-label={action.tooltip} onClick={action.onClick}>
|
|
222
|
+
{action.icon}
|
|
223
|
+
</IconButton>
|
|
224
|
+
))}
|
|
225
|
+
{menu != null && (
|
|
226
|
+
<div data-section-menu className="flex items-center">
|
|
227
|
+
<Menu
|
|
228
|
+
align="right"
|
|
229
|
+
trigger={
|
|
230
|
+
<IconButton tooltip="More options" aria-label={menuLabel ?? `More options for ${title}`}>
|
|
231
|
+
<IconDotsVertical size={16} stroke={1.5} />
|
|
232
|
+
</IconButton>
|
|
233
|
+
}
|
|
234
|
+
>
|
|
235
|
+
{menu}
|
|
236
|
+
</Menu>
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
152
240
|
</div>
|
|
153
241
|
)}
|
|
154
242
|
</div>
|
package/src/SectionLabel.tsx
CHANGED
|
@@ -45,9 +45,12 @@ export function SectionLabel({
|
|
|
45
45
|
return (
|
|
46
46
|
<span
|
|
47
47
|
className={cn(
|
|
48
|
-
'text-h5 leading-3
|
|
49
|
-
//
|
|
50
|
-
//
|
|
48
|
+
'text-h5 leading-3',
|
|
49
|
+
// Ship's type in every theme but Leaf, whose section labels are the
|
|
50
|
+
// micro-label (Katerina, 28 September) unless the group folds (2
|
|
51
|
+
// October). Signal dropped its micro-label on 5 October: every one of
|
|
52
|
+
// Peek's section labels is Ship's type, folding or not, so Desk's
|
|
53
|
+
// Screener and its Urgent and Open work read as one.
|
|
51
54
|
!folds && 'leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest',
|
|
52
55
|
tone === 'secondary' ? 'text-text-secondary' : tone === 'muted' ? 'text-text-muted' : 'text-text-primary',
|
|
53
56
|
truncate && 'min-w-0 truncate',
|
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}
|
package/src/Sidebar.tsx
CHANGED
|
@@ -35,7 +35,7 @@ export function Sidebar({ 'aria-label': ariaLabel = 'Workspace', children, class
|
|
|
35
35
|
{/* The rows scroll in a ScrollArea: the bar takes no width, so the
|
|
36
36
|
column's padding reads the same with a long list as with a short
|
|
37
37
|
one (Katerina, 2026-09-08). The padding and the gap are the box's. */}
|
|
38
|
-
<ScrollArea className="min-h-0 flex-1" contentClassName="flex flex-col gap-
|
|
38
|
+
<ScrollArea className="min-h-0 flex-1" contentClassName="flex flex-col gap-0.5 px-2.5 py-3">
|
|
39
39
|
{children}
|
|
40
40
|
</ScrollArea>
|
|
41
41
|
</nav>
|
|
@@ -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 />}
|
|
@@ -27,7 +27,7 @@ const write = (path: string, text: string) => {
|
|
|
27
27
|
mkdirSync(dirname(join(root, path)), { recursive: true })
|
|
28
28
|
writeFileSync(join(root, path), text)
|
|
29
29
|
}
|
|
30
|
-
const SECTION_LABEL = 'text-h5 leading-3
|
|
30
|
+
const SECTION_LABEL = 'text-h5 leading-3 leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-secondary'
|
|
31
31
|
const registry = (entries: { name: string; sourceFile: string; looks: string[] }[]) => JSON.stringify({ schemaVersion: 2, entries })
|
|
32
32
|
write('app/package.json', '{ "name": "app" }')
|
|
33
33
|
write(
|
|
@@ -78,7 +78,7 @@ tester.run('no-copied-look', noCopiedLook, {
|
|
|
78
78
|
{
|
|
79
79
|
name: 'the same look, the words in another order and one class more',
|
|
80
80
|
filename: app,
|
|
81
|
-
code: probe('<span className="text-text-secondary
|
|
81
|
+
code: probe('<span className="text-text-secondary leaf:tracking-widest leaf:uppercase text-h5 leaf:text-small leaf:font-mono leading-3 shrink-0">x</span>'),
|
|
82
82
|
errors: [{ messageId: 'usePart' }],
|
|
83
83
|
},
|
|
84
84
|
{
|
package/src/gates/app-checks.ts
CHANGED
|
@@ -235,7 +235,7 @@ export function appChecks(h: GateHelpers, { app = '.', page, chain = { ref: 'UIG
|
|
|
235
235
|
{ what: 'a title= is an error naming WithTooltip', run: probe(`import { Button } from '@estiva-app/ui'\n${component('<Button title="Delete">x</Button>')}`, 'error', 'WithTooltip') },
|
|
236
236
|
]),
|
|
237
237
|
ticket('UIG-25', [
|
|
238
|
-
{ what: "SectionLabel's class list, typed by hand, is a warning", run: probe(component('<span className="text-h5 leading-3
|
|
238
|
+
{ what: "SectionLabel's class list, typed by hand, is a warning", run: probe(component('<span className="text-h5 leading-3 leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest text-text-secondary">Label</span>'), 'warning', 'SectionLabel') },
|
|
239
239
|
]),
|
|
240
240
|
ticket('UIG-27', [
|
|
241
241
|
{ what: 'the installed package has Link', run: () => h.contains(installed, /\bLink\b/, 'the installed @estiva-app/ui exports Link') },
|
package/src/looks.ts
CHANGED
|
@@ -48,6 +48,12 @@ export const BARE_INPUT_CLASSES = 'bg-transparent text-text-primary placeholder:
|
|
|
48
48
|
export const SIDEBAR_ROW_CLASSES = 'rounded-lg transition-colors'
|
|
49
49
|
export const SIDEBAR_ROW_TEXT_CLASSES = 'text-text-secondary hover:text-text-primary'
|
|
50
50
|
|
|
51
|
+
/** A quiet icon control: secondary, brightening on a hover fill — IconButton's `muted`. */
|
|
52
|
+
export const MUTED_CONTROL_CLASSES = 'text-text-secondary hover:bg-bg-hover hover:text-text-primary'
|
|
53
|
+
|
|
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. */
|
|
55
|
+
export const MENU_TRIGGER_CLASSES = 'cursor-pointer rounded-md transition-colors hover:bg-bg-hover data-[popup-open]:bg-bg-hover'
|
|
56
|
+
|
|
51
57
|
/**
|
|
52
58
|
* A panel that slides open and shut on Base UI's Collapsible: Base UI measures
|
|
53
59
|
* it and writes its height to a variable, `auto` again once the slide ends, and
|