@estiva-app/ui 0.19.0 → 0.20.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/AttachmentCard.d.ts.map +1 -1
- package/dist/Breadcrumb.d.ts.map +1 -1
- package/dist/Button.d.ts +6 -1
- package/dist/Button.d.ts.map +1 -1
- package/dist/Card.d.ts +6 -1
- package/dist/Card.d.ts.map +1 -1
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/Divider.d.ts +7 -0
- package/dist/Divider.d.ts.map +1 -1
- package/dist/IconButton.d.ts +12 -2
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Link.d.ts +6 -1
- package/dist/Link.d.ts.map +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Popover.d.ts.map +1 -1
- package/dist/ReactionPicker.d.ts.map +1 -1
- package/dist/SectionLabel.d.ts +6 -1
- package/dist/SectionLabel.d.ts.map +1 -1
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/eslint/index.d.ts +2 -0
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.js +514 -6
- package/dist/eslint/index.js.map +4 -4
- package/dist/eslint/no-restyled-part.d.ts +81 -0
- package/dist/eslint/no-restyled-part.d.ts.map +1 -0
- package/dist/index.js +290 -266
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/src/AttachmentCard.tsx +7 -2
- package/src/Banner.tsx +2 -2
- package/src/Breadcrumb.tsx +9 -4
- package/src/Button.mdx +7 -0
- package/src/Button.stories.tsx +4 -1
- package/src/Button.tsx +10 -2
- package/src/Card.mdx +7 -0
- package/src/Card.stories.tsx +11 -0
- package/src/Card.tsx +8 -0
- package/src/CommandPalette.tsx +1 -1
- package/src/ConfirmDialog.tsx +2 -2
- package/src/DialogShell.tsx +6 -7
- package/src/Divider.mdx +3 -0
- package/src/Divider.tsx +12 -2
- package/src/EditableText.mdx +5 -0
- package/src/IconButton.mdx +9 -0
- package/src/IconButton.stories.tsx +10 -1
- package/src/IconButton.tsx +22 -3
- package/src/IdentityMenu.tsx +4 -1
- package/src/Link.mdx +6 -0
- package/src/Link.stories.tsx +12 -0
- package/src/Link.tsx +7 -2
- package/src/Menu.stories.tsx +1 -1
- package/src/Menu.test.tsx +2 -2
- package/src/Menu.tsx +6 -3
- package/src/Person.stories.tsx +5 -5
- package/src/Popover.test.tsx +2 -2
- package/src/Popover.tsx +3 -2
- package/src/PreviewCard.tsx +1 -1
- package/src/Property.stories.tsx +2 -2
- package/src/ReactionPicker.tsx +6 -3
- package/src/ScrollArea.stories.tsx +21 -7
- package/src/SectionLabel.mdx +3 -2
- package/src/SectionLabel.stories.tsx +3 -0
- package/src/SectionLabel.tsx +7 -2
- package/src/Tooltip.tsx +12 -14
- package/src/eslint/index.test.ts +32 -7
- package/src/eslint/index.ts +10 -1
- package/src/eslint/no-restyled-part.test.ts +301 -0
- package/src/eslint/no-restyled-part.ts +647 -0
package/package.json
CHANGED
package/src/AttachmentCard.tsx
CHANGED
|
@@ -186,6 +186,7 @@ export function AttachmentCard({
|
|
|
186
186
|
const failed = state === 'failed'
|
|
187
187
|
const warning = state === 'warning'
|
|
188
188
|
return (
|
|
189
|
+
// @estiva-escape: a failed or warning file's strong hairline is AttachmentCard's own state; Card's attention hairlines are the soft ones (UIG-9, Katerina 17 September)
|
|
189
190
|
<Card
|
|
190
191
|
fill="elevated"
|
|
191
192
|
className={cn('group relative flex items-center gap-2 w-[200px] p-1.5 pr-3', failed ? 'border-error-default' : warning && 'border-warning-default', className)}
|
|
@@ -233,6 +234,7 @@ export function AttachmentCard({
|
|
|
233
234
|
|
|
234
235
|
// Fades in with the card's hover or keyboard focus, and keeps its slot when hidden, so revealing it never shifts the row.
|
|
235
236
|
const download = onDownload ? (
|
|
237
|
+
// @estiva-escape: Download shows when its card is pointed at or focused, AttachmentCard's own action; IconButton has no reveal on its card's hover (UIG-9, Katerina 17 September)
|
|
236
238
|
<IconButton
|
|
237
239
|
variant="muted"
|
|
238
240
|
tooltip="Download"
|
|
@@ -251,6 +253,7 @@ export function AttachmentCard({
|
|
|
251
253
|
return (
|
|
252
254
|
// `role="status"`: a plain box may not carry a name (axe, aria-prohibited-attr), and a
|
|
253
255
|
// status says what it is doing — loading — to a reader that cannot see the pulse.
|
|
256
|
+
// @estiva-escape: the loading pulse is AttachmentCard's own state; Card has no loading state (UIG-9, Katerina 17 September)
|
|
254
257
|
<Card role="status" fill="inset" aria-busy="true" aria-label={`Loading ${name}`} className={cn('w-[180px] h-28 animate-pulse', className)} {...props}>
|
|
255
258
|
{null}
|
|
256
259
|
</Card>
|
|
@@ -259,6 +262,7 @@ export function AttachmentCard({
|
|
|
259
262
|
|
|
260
263
|
if (state === 'unreadable') {
|
|
261
264
|
return (
|
|
265
|
+
// @estiva-escape: a file that could not be read is faded as well as dashed, AttachmentCard's own state; Card has no faded state (UIG-9, Katerina 17 September)
|
|
262
266
|
<Card fill="inset" unreadable className={cn('flex items-center gap-2 w-[240px] p-1.5 opacity-70', className)} {...props}>
|
|
263
267
|
<div className={TILE_CLASSES}>
|
|
264
268
|
<TypeIcon name={name} />
|
|
@@ -274,7 +278,7 @@ export function AttachmentCard({
|
|
|
274
278
|
if (image && src) {
|
|
275
279
|
const picture = <img src={src} alt={name} className="w-full h-28 object-cover" />
|
|
276
280
|
return (
|
|
277
|
-
<Card fill="inset" hover="hairline" className={cn('group relative flex flex-col w-[180px]
|
|
281
|
+
<Card fill="inset" hover="hairline" clip className={cn('group relative flex flex-col w-[180px]', className)} {...props}>
|
|
278
282
|
{onOpen ? (
|
|
279
283
|
<BaseButton
|
|
280
284
|
type="button"
|
|
@@ -310,9 +314,10 @@ export function AttachmentCard({
|
|
|
310
314
|
</>
|
|
311
315
|
)
|
|
312
316
|
return (
|
|
317
|
+
// @estiva-escape: a file that cannot be opened is faded, AttachmentCard's own state; Card has no faded state (UIG-9, Katerina 17 September)
|
|
313
318
|
<Card fill="inset" hover={href ? 'hairline' : 'none'} className={cn('group flex items-center gap-2 w-[240px] p-1.5 pr-1', !href && 'opacity-70', className)} {...props}>
|
|
314
319
|
{href ? (
|
|
315
|
-
<Link href={href} external variant="plain" className="flex items-center gap-2 min-w-0 flex-1
|
|
320
|
+
<Link href={href} external variant="plain" className="flex items-center gap-2 min-w-0 flex-1" onClick={(event) => event.stopPropagation()}>
|
|
316
321
|
{body}
|
|
317
322
|
</Link>
|
|
318
323
|
) : (
|
package/src/Banner.tsx
CHANGED
|
@@ -57,9 +57,9 @@ export function Banner({ tone, children, onDismiss, dismissLabel = 'Dismiss', cl
|
|
|
57
57
|
return (
|
|
58
58
|
<div role={role} className={cn('flex items-center gap-3 px-4 py-2 text-body-2', TONE_STYLES[tone], className)}>
|
|
59
59
|
<span className="min-w-0 flex-1">{children}</span>
|
|
60
|
-
{/* `
|
|
60
|
+
{/* `current` so the ✕ takes the tone's colour rather than the
|
|
61
61
|
muted grey an IconButton wears on a neutral surface. */}
|
|
62
|
-
<IconButton aria-label={dismissLabel} onClick={onDismiss} className="-mr-1 shrink-0
|
|
62
|
+
<IconButton variant="current" aria-label={dismissLabel} onClick={onDismiss} className="-mr-1 shrink-0">
|
|
63
63
|
<IconX size={16} stroke={1.5} />
|
|
64
64
|
</IconButton>
|
|
65
65
|
</div>
|
package/src/Breadcrumb.tsx
CHANGED
|
@@ -91,10 +91,15 @@ export function Breadcrumb({ items, className }: BreadcrumbProps) {
|
|
|
91
91
|
const crumb = item.href ? (
|
|
92
92
|
// `plain` adds no look of its own: a crumb keeps the trail's size and
|
|
93
93
|
// tone, and brightens on hover. The package's own Link, so a trail
|
|
94
|
-
// follows whatever a link learns to do (UIG-5).
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
// follows whatever a link learns to do (UIG-5). The size and tone are
|
|
95
|
+
// on a box around it, which the link takes them from, rather than
|
|
96
|
+
// pushed into it (UIG-9); the link is still the one element that
|
|
97
|
+
// truncates and is measured.
|
|
98
|
+
<span className={cn('flex min-w-0', item.mono && 'shrink-0 font-mono text-caption', tone, 'hover:text-text-primary')}>
|
|
99
|
+
<Link ref={setLabelRef} variant="plain" truncate href={item.href} onClick={item.onClick}>
|
|
100
|
+
{item.label}
|
|
101
|
+
</Link>
|
|
102
|
+
</span>
|
|
98
103
|
) : (
|
|
99
104
|
<span ref={setLabelRef} className={text} aria-current={last ? 'page' : undefined}>
|
|
100
105
|
{item.label}
|
package/src/Button.mdx
CHANGED
|
@@ -61,6 +61,13 @@ import { IconPlus } from '@tabler/icons-react'
|
|
|
61
61
|
wrapper — what a caller gets is the `<button>`, and a `Menu.Trigger` or
|
|
62
62
|
a `Dialog.Close` can be it.
|
|
63
63
|
|
|
64
|
+
- `variant="resolve"` is `primary` in every theme but Signal, where it is
|
|
65
|
+
outlined at rest and green when pointed at — the Resolve button. `IconButton`
|
|
66
|
+
has the same look.
|
|
67
|
+
- **Placed from outside, never restyled.** `className` takes space, size,
|
|
68
|
+
flex and grid, and position; a colour, a text size, a border, a corner or a
|
|
69
|
+
shadow is refused by the lint (`estiva/no-restyled-part`, UIG-9).
|
|
70
|
+
|
|
64
71
|
## Keys
|
|
65
72
|
|
|
66
73
|
| Key | Does |
|
package/src/Button.stories.tsx
CHANGED
|
@@ -7,7 +7,7 @@ const meta = {
|
|
|
7
7
|
component: Button,
|
|
8
8
|
args: { children: 'Button', variant: 'muted', size: 'default', disabled: false },
|
|
9
9
|
argTypes: {
|
|
10
|
-
variant: { control: 'inline-radio', options: ['primary', 'outlined', 'muted', 'destructive'] },
|
|
10
|
+
variant: { control: 'inline-radio', options: ['primary', 'outlined', 'muted', 'destructive', 'resolve'] },
|
|
11
11
|
size: { control: 'inline-radio', options: ['default', 'small'] },
|
|
12
12
|
leadingIcon: { control: false },
|
|
13
13
|
},
|
|
@@ -83,3 +83,6 @@ export const AllVariants: Story = {
|
|
|
83
83
|
</div>
|
|
84
84
|
),
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
/** `resolve`: primary, and in Signal outlined at rest and green when pointed at — Resolve. */
|
|
88
|
+
export const Resolve: Story = { args: { variant: 'resolve', children: 'Resolve' } }
|
package/src/Button.tsx
CHANGED
|
@@ -24,7 +24,12 @@ import { TooltipTrigger } from './Tooltip'
|
|
|
24
24
|
* (Base UI's `focusableWhenDisabled`), and shows the reason as a tooltip on
|
|
25
25
|
* hover. Ship wrote that wrapper by hand six times.
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* `resolve` is `primary` in every theme but Signal, where it is outlined at rest
|
|
29
|
+
* and green when pointed at — Peek's Resolve (UIG-9, 17 September; Peek passed
|
|
30
|
+
* it as classes before). `IconButton` has the same look.
|
|
31
|
+
*/
|
|
32
|
+
export type ButtonVariant = 'primary' | 'outlined' | 'muted' | 'destructive' | 'resolve'
|
|
28
33
|
export type ButtonSize = 'default' | 'small'
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -86,7 +91,7 @@ export function Button({
|
|
|
86
91
|
// Extra right padding beside a leading icon, for optical balance.
|
|
87
92
|
size === 'default' && (hasLeadingIcon ? 'pl-2 pr-3' : 'px-2'),
|
|
88
93
|
size === 'small' && (hasLeadingIcon ? 'pl-1.5 pr-2' : 'px-1.5'),
|
|
89
|
-
!state.disabled && variant === 'primary' && 'bg-accent-primary hover:bg-accent-hover text-text-inverse cursor-pointer signal:font-semibold',
|
|
94
|
+
!state.disabled && (variant === 'primary' || variant === 'resolve') && 'bg-accent-primary hover:bg-accent-hover text-text-inverse cursor-pointer signal:font-semibold',
|
|
90
95
|
!state.disabled && variant === 'outlined' && 'border border-border-default hover:bg-bg-hover text-text-primary cursor-pointer',
|
|
91
96
|
!state.disabled && variant === 'muted' && 'hover:bg-bg-hover text-text-primary cursor-pointer',
|
|
92
97
|
!state.disabled && variant === 'destructive' && 'hover:bg-error-muted text-error-default cursor-pointer',
|
|
@@ -98,6 +103,9 @@ export function Button({
|
|
|
98
103
|
// and a prevented `onClick`), so nothing else needs it.
|
|
99
104
|
state.disabled && !disabledReason && 'pointer-events-none',
|
|
100
105
|
state.disabled && variant === 'outlined' && 'border border-border-default',
|
|
106
|
+
// Disabled too, as it always drew.
|
|
107
|
+
variant === 'resolve' &&
|
|
108
|
+
'signal:bg-transparent signal:border signal:border-border-default signal:text-text-primary signal:shadow-none signal:hover:bg-success-muted signal:hover:border-success-outline signal:hover:text-success-default signal:transition-colors',
|
|
101
109
|
className,
|
|
102
110
|
)
|
|
103
111
|
}
|
package/src/Card.mdx
CHANGED
|
@@ -69,6 +69,13 @@ import { Card } from '@estiva-app/ui'
|
|
|
69
69
|
state: set it when the pointer enters, and clear it when the pointer leaves
|
|
70
70
|
and nothing of the card's is open.
|
|
71
71
|
|
|
72
|
+
- `clip` cuts what is inside to the card's rounded corners — a strip or a
|
|
73
|
+
picture that runs edge to edge. Without it their square corners show.
|
|
74
|
+
- **Placed from outside, never restyled.** `className` takes space, size,
|
|
75
|
+
flex and grid, and position; a colour, a text size, a border, a corner or a
|
|
76
|
+
shadow is refused by the lint (`estiva/no-restyled-part`, UIG-9). What is inside the card is yours: its padding and its words go on
|
|
77
|
+
your own element inside.
|
|
78
|
+
|
|
72
79
|
## Props
|
|
73
80
|
|
|
74
81
|
<Controls of={CardStories.Surface} />
|
package/src/Card.stories.tsx
CHANGED
|
@@ -110,3 +110,14 @@ export const AllFills: Story = {
|
|
|
110
110
|
</div>
|
|
111
111
|
),
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
/** `clip`: a strip that runs edge to edge is cut to the card's rounded corners. */
|
|
115
|
+
export const Clip: Story = {
|
|
116
|
+
parameters: { controls: { disable: true } },
|
|
117
|
+
render: () => (
|
|
118
|
+
<Card clip className="flex w-[240px] flex-col">
|
|
119
|
+
<div className="flex h-6 items-center bg-bg-inset px-2 text-caption text-text-primary">Huddle</div>
|
|
120
|
+
<div className="p-3 text-body-2 text-text-primary">What the card is about.</div>
|
|
121
|
+
</Card>
|
|
122
|
+
),
|
|
123
|
+
}
|
package/src/Card.tsx
CHANGED
|
@@ -80,6 +80,11 @@ export interface CardProps extends ComponentPropsWithRef<'div'> {
|
|
|
80
80
|
attention?: CardAttention
|
|
81
81
|
/** It stands for something that could not be read: the hairline is dashed. */
|
|
82
82
|
unreadable?: boolean
|
|
83
|
+
/**
|
|
84
|
+
* What is inside is cut to the card's rounded corners: a strip or a picture that
|
|
85
|
+
* runs edge to edge (UIG-9, 17 September; the apps passed `overflow-hidden`).
|
|
86
|
+
*/
|
|
87
|
+
clip?: boolean
|
|
83
88
|
children: ReactNode
|
|
84
89
|
}
|
|
85
90
|
|
|
@@ -93,6 +98,7 @@ export function Card({
|
|
|
93
98
|
active = false,
|
|
94
99
|
attention,
|
|
95
100
|
unreadable = false,
|
|
101
|
+
clip = false,
|
|
96
102
|
className,
|
|
97
103
|
children,
|
|
98
104
|
...props
|
|
@@ -114,12 +120,14 @@ export function Card({
|
|
|
114
120
|
active && 'bg-bg-selected border-accent-primary',
|
|
115
121
|
!active && attention && ATTENTION_CLASSES[attention],
|
|
116
122
|
unreadable && 'border-dashed',
|
|
123
|
+
clip && 'overflow-hidden',
|
|
117
124
|
href && 'block',
|
|
118
125
|
className,
|
|
119
126
|
)
|
|
120
127
|
if (href !== undefined) {
|
|
121
128
|
const { ref, ...anchorProps } = props as ComponentPropsWithRef<'a'>
|
|
122
129
|
return (
|
|
130
|
+
// @estiva-escape: a card that leads somewhere is drawn on Link's anchor (UIG-27 ruling 1), so this frame is Card's own look, not Link restyled
|
|
123
131
|
<Link href={href} variant="plain" ref={ref} className={classes} {...anchorProps}>
|
|
124
132
|
{children}
|
|
125
133
|
</Link>
|
package/src/CommandPalette.tsx
CHANGED
|
@@ -395,7 +395,7 @@ export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups
|
|
|
395
395
|
{/* A heading labels the rows, it is not one of them: read
|
|
396
396
|
secondary, as in a menu (MenuSection). */}
|
|
397
397
|
<Autocomplete.GroupLabel className="flex h-7 shrink-0 items-center px-3">
|
|
398
|
-
<SectionLabel
|
|
398
|
+
<SectionLabel tone="secondary">{group.value}</SectionLabel>
|
|
399
399
|
</Autocomplete.GroupLabel>
|
|
400
400
|
<Autocomplete.Collection>
|
|
401
401
|
{(row: CommandPaletteRow) => (
|
package/src/ConfirmDialog.tsx
CHANGED
|
@@ -43,7 +43,6 @@ export function ConfirmDialog({ title, children, confirmLabel, destructive = fal
|
|
|
43
43
|
alert
|
|
44
44
|
title={title}
|
|
45
45
|
onClose={onClose}
|
|
46
|
-
bodyClassName="flex flex-col gap-3 text-body-2 text-text-primary"
|
|
47
46
|
footer={
|
|
48
47
|
<>
|
|
49
48
|
<Button variant="muted" onClick={onClose} disabled={busy}>
|
|
@@ -55,7 +54,8 @@ export function ConfirmDialog({ title, children, confirmLabel, destructive = fal
|
|
|
55
54
|
</>
|
|
56
55
|
}
|
|
57
56
|
>
|
|
58
|
-
{
|
|
57
|
+
{/* The words' look on a box of the dialog's own, not pushed into DialogShell's body (UIG-9). */}
|
|
58
|
+
<div className="flex flex-col gap-3 text-body-2 text-text-primary">{children}</div>
|
|
59
59
|
</DialogShell>
|
|
60
60
|
)
|
|
61
61
|
}
|
package/src/DialogShell.tsx
CHANGED
|
@@ -147,13 +147,12 @@ export function DialogShell({ title, onClose, headerContent, footer, children, b
|
|
|
147
147
|
the caller's layout go on the content, so the bar is drawn over
|
|
148
148
|
the padding rather than beside it. */}
|
|
149
149
|
{bodyMaxHeight ? (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
viewportClassName={bodyMaxHeight}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
</ScrollArea>
|
|
150
|
+
// The line above the footer on a box of the shell's own, not pushed into ScrollArea (UIG-9).
|
|
151
|
+
<div className={cn(footer != null && 'border-b border-border-subtle')}>
|
|
152
|
+
<ScrollArea viewportClassName={bodyMaxHeight} contentClassName={cn('pl-5 pr-4 py-4', bodyClassName)}>
|
|
153
|
+
{children}
|
|
154
|
+
</ScrollArea>
|
|
155
|
+
</div>
|
|
157
156
|
) : (
|
|
158
157
|
<div className={cn('pl-5 pr-4 py-4', footer != null && 'border-b border-border-subtle', bodyClassName)}>{children}</div>
|
|
159
158
|
)}
|
package/src/Divider.mdx
CHANGED
|
@@ -46,6 +46,9 @@ import { Divider } from '@estiva-app/ui'
|
|
|
46
46
|
<Divider label="New since you last read this" tone="warning" />
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
- **Inside a `Menu` or a `Popover` it has no inset**: the hairline runs the
|
|
50
|
+
width of the rows. The panel tells it so; nothing is passed in.
|
|
51
|
+
|
|
49
52
|
## Props
|
|
50
53
|
|
|
51
54
|
<Controls of={DividerStories.Default} />
|
package/src/Divider.tsx
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
1
2
|
import { Separator } from '@base-ui/react/separator'
|
|
2
3
|
import { cn } from './cn'
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Inside a `Menu` or a `Popover` a hairline runs edge to edge of the rows,
|
|
7
|
+
* with no inset of its own. The two panels used to reach in for it
|
|
8
|
+
* (`[&>[role=separator]]:mx-0`); they say so through this instead (UIG-9,
|
|
9
|
+
* 17 September). Not exported: only those two panels set it.
|
|
10
|
+
*/
|
|
11
|
+
export const DividerInPanel = createContext(false)
|
|
12
|
+
|
|
4
13
|
/**
|
|
5
14
|
* Peek's Divider (2026-08-28): a hairline in `border-subtle`, inset 12px each
|
|
6
15
|
* side. Plus what Ship added: `orientation="vertical"` — the same hairline
|
|
@@ -30,13 +39,14 @@ export interface DividerProps {
|
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
export function Divider({ orientation = 'horizontal', label, tone = 'default', className }: DividerProps) {
|
|
42
|
+
const inset = useContext(DividerInPanel) ? 'mx-0' : 'mx-3'
|
|
33
43
|
if (label && orientation === 'horizontal') {
|
|
34
44
|
const line = tone === 'warning' ? 'bg-warning-muted' : 'bg-border-subtle'
|
|
35
45
|
return (
|
|
36
46
|
<Separator
|
|
37
47
|
orientation="horizontal"
|
|
38
48
|
aria-label={label}
|
|
39
|
-
className={cn('flex shrink-0 items-center gap-2
|
|
49
|
+
className={cn('flex shrink-0 items-center gap-2', inset, className)}
|
|
40
50
|
>
|
|
41
51
|
<span aria-hidden="true" className={cn('h-px flex-1', line)} />
|
|
42
52
|
<span className={cn('shrink-0 text-caption', tone === 'warning' ? 'text-warning-default' : 'text-text-muted')}>{label}</span>
|
|
@@ -51,7 +61,7 @@ export function Divider({ orientation = 'horizontal', label, tone = 'default', c
|
|
|
51
61
|
// column shrinks to nothing, and a hairline that renders 0px tall is a
|
|
52
62
|
// hairline nobody can see — Peek's `/` menu had been drawing two of
|
|
53
63
|
// them, measured 0px, since it was built (2026-09-05).
|
|
54
|
-
className={cn('shrink-0 bg-border-subtle', orientation === 'horizontal' ? 'h-px
|
|
64
|
+
className={cn('shrink-0 bg-border-subtle', orientation === 'horizontal' ? cn('h-px', inset) : 'w-px self-stretch', className)}
|
|
55
65
|
/>
|
|
56
66
|
)
|
|
57
67
|
}
|
package/src/EditableText.mdx
CHANGED
|
@@ -50,6 +50,11 @@ import { EditableText } from '@estiva-app/ui'
|
|
|
50
50
|
raw references, rendered richer when read) without losing the original
|
|
51
51
|
on commit.
|
|
52
52
|
|
|
53
|
+
- **Its text size is where it sits**, like `Person`'s: put `text-h2` on your own
|
|
54
|
+
element around it, and the text and the field both take it (a field inherits
|
|
55
|
+
the page's font). `className` places it — `-mx-2` to line its text up with
|
|
56
|
+
a header's inset — and a size passed there is refused by the lint (UIG-9).
|
|
57
|
+
|
|
53
58
|
## Keys
|
|
54
59
|
|
|
55
60
|
| Input | What happens |
|
package/src/IconButton.mdx
CHANGED
|
@@ -40,6 +40,15 @@ import { IconPencil } from '@tabler/icons-react'
|
|
|
40
40
|
control. The `tooltip` shows sighted hover users the same word; it does
|
|
41
41
|
not replace the label.
|
|
42
42
|
- The icon is 16px at stroke 1.5.
|
|
43
|
+
- `variant="current"` takes the colour of what it sits in — the ✕ on a
|
|
44
|
+
`Banner` wears the banner's tone. `variant="resolve"` is `muted` that turns
|
|
45
|
+
green when pointed at in Signal: Resolve, wherever it appears.
|
|
46
|
+
- `pressed` is a toggle that is on — Bold while the selection is bold: the
|
|
47
|
+
active fill, and `aria-pressed`. A `ToolbarButton` takes it too.
|
|
48
|
+
- `glow` lights it in Signal — the send arrow while there is something to send.
|
|
49
|
+
- **Placed from outside, never restyled.** `className` takes space, size,
|
|
50
|
+
flex and grid, and position; a colour, a text size, a border, a corner or a
|
|
51
|
+
shadow is refused by the lint (`estiva/no-restyled-part`, UIG-9).
|
|
43
52
|
- `type` defaults to `"button"`; it is a native `<button>` on Base UI's
|
|
44
53
|
Button, and every native prop passes through.
|
|
45
54
|
- `disabledReason="Read only"` disables it, keeps it reachable by Tab,
|
|
@@ -8,7 +8,7 @@ const meta = {
|
|
|
8
8
|
// An icon-only button owes its name; axe's button-name rule fails without it.
|
|
9
9
|
args: { variant: 'muted', disabled: false, 'aria-label': 'Settings', children: <IconSettings className="size-4" stroke={1.5} /> },
|
|
10
10
|
argTypes: {
|
|
11
|
-
variant: { control: 'inline-radio', options: ['muted', 'outlined', 'primary'] },
|
|
11
|
+
variant: { control: 'inline-radio', options: ['muted', 'outlined', 'primary', 'current', 'resolve'] },
|
|
12
12
|
tooltipPlacement: { control: 'inline-radio', options: ['top', 'bottom'] },
|
|
13
13
|
children: { control: false },
|
|
14
14
|
},
|
|
@@ -63,3 +63,12 @@ export const InATallRow: Story = {
|
|
|
63
63
|
</div>
|
|
64
64
|
),
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/** `resolve`: muted, and green when pointed at in Signal — Resolve. (`current` takes the colour it sits in: see Banner's ✕.) */
|
|
68
|
+
export const Resolve: Story = { args: { variant: 'resolve', 'aria-label': 'Resolve', tooltip: 'Resolve' } }
|
|
69
|
+
|
|
70
|
+
/** `pressed`: on — the active fill and `aria-pressed`, like Bold while the selection is bold. */
|
|
71
|
+
export const Pressed: Story = { args: { pressed: true, 'aria-label': 'Bold' } }
|
|
72
|
+
|
|
73
|
+
/** `glow`: Signal's glow, on the send arrow while there is something to send. */
|
|
74
|
+
export const Glow: Story = { args: { variant: 'primary', glow: true, 'aria-label': 'Send' } }
|
package/src/IconButton.tsx
CHANGED
|
@@ -14,11 +14,21 @@ import { TooltipTrigger } from './Tooltip'
|
|
|
14
14
|
* `disabledReason` disables it, keeps it reachable by keyboard, and shows
|
|
15
15
|
* the reason in place of the tooltip — see Button.
|
|
16
16
|
*/
|
|
17
|
-
export type IconButtonVariant = 'muted' | 'outlined' | 'primary'
|
|
17
|
+
export type IconButtonVariant = 'muted' | 'outlined' | 'primary' | 'current' | 'resolve'
|
|
18
18
|
|
|
19
19
|
/** `ComponentPropsWithRef` so a `ref` reaches the element — see Button. */
|
|
20
20
|
export interface IconButtonProps extends ComponentPropsWithRef<'button'> {
|
|
21
|
+
/**
|
|
22
|
+
* `current` takes the colour of what it sits in — the ✕ on a `Banner` wears
|
|
23
|
+
* the banner's tone. `resolve` is `muted` that turns green when pointed at in
|
|
24
|
+
* Signal, Peek's Resolve (UIG-9, 17 September; before, each app passed these
|
|
25
|
+
* as classes).
|
|
26
|
+
*/
|
|
21
27
|
variant?: IconButtonVariant
|
|
28
|
+
/** On, like a Bold button while the selection is bold: the active fill, and `aria-pressed`. */
|
|
29
|
+
pressed?: boolean
|
|
30
|
+
/** A glow in Signal — the send arrow while there is something to send. */
|
|
31
|
+
glow?: boolean
|
|
22
32
|
tooltip?: string
|
|
23
33
|
/** A key hint drawn as the `Kbd` chip inside the tooltip — for a button
|
|
24
34
|
* whose only other affordance is a keyboard shortcut. */
|
|
@@ -33,6 +43,8 @@ export interface IconButtonProps extends ComponentPropsWithRef<'button'> {
|
|
|
33
43
|
|
|
34
44
|
export function IconButton({
|
|
35
45
|
variant = 'muted',
|
|
46
|
+
pressed,
|
|
47
|
+
glow = false,
|
|
36
48
|
className,
|
|
37
49
|
children,
|
|
38
50
|
disabled,
|
|
@@ -65,11 +77,17 @@ export function IconButton({
|
|
|
65
77
|
// stretched and carries no `self-center` — see the note there.
|
|
66
78
|
'flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 self-center cursor-pointer',
|
|
67
79
|
!state.disabled && variant === 'primary' && 'bg-accent-primary hover:bg-accent-hover text-text-inverse',
|
|
68
|
-
!state.disabled && variant === 'muted' && 'text-text-secondary hover:bg-bg-hover hover:text-text-primary',
|
|
80
|
+
!state.disabled && (variant === 'muted' || variant === 'resolve') && 'text-text-secondary hover:bg-bg-hover hover:text-text-primary',
|
|
69
81
|
!state.disabled && variant === 'outlined' && 'border border-border-default hover:bg-bg-hover text-text-secondary',
|
|
82
|
+
!state.disabled && variant === 'current' && 'hover:bg-bg-hover',
|
|
70
83
|
state.disabled && variant === 'primary' && 'bg-bg-disabled text-text-disabled',
|
|
71
|
-
state.disabled && variant === 'muted' && 'text-text-disabled',
|
|
84
|
+
state.disabled && (variant === 'muted' || variant === 'resolve') && 'text-text-disabled',
|
|
72
85
|
state.disabled && variant === 'outlined' && 'border border-border-default text-text-disabled',
|
|
86
|
+
// Always, disabled too: the colour of where it sits, and Resolve's green.
|
|
87
|
+
variant === 'current' && 'text-current',
|
|
88
|
+
variant === 'resolve' && 'signal:hover:bg-success-muted signal:hover:text-success-default',
|
|
89
|
+
pressed && 'bg-bg-active text-text-primary',
|
|
90
|
+
glow && 'signal:shadow-glow-accent',
|
|
73
91
|
// `pointer-events-none` only where the button is truly out of reach:
|
|
74
92
|
// with a `disabledReason` the button IS the tooltip's trigger, and a
|
|
75
93
|
// trigger the pointer cannot land on never opens one. Base UI already
|
|
@@ -79,6 +97,7 @@ export function IconButton({
|
|
|
79
97
|
className,
|
|
80
98
|
)
|
|
81
99
|
}
|
|
100
|
+
aria-pressed={pressed}
|
|
82
101
|
{...props}
|
|
83
102
|
>
|
|
84
103
|
{children}
|
package/src/IdentityMenu.tsx
CHANGED
|
@@ -95,7 +95,10 @@ export function IdentityRows({ me, signedIn, relayUrl, idBase, onCopyKey, onSign
|
|
|
95
95
|
<>
|
|
96
96
|
<MenuSection label={signedIn ? 'Signed in as' : 'Acting as'}>
|
|
97
97
|
<MenuRow>
|
|
98
|
-
|
|
98
|
+
{/* A name takes the size of where it sits (UIG-9). */}
|
|
99
|
+
<div className="flex min-w-0 text-body-2-strong">
|
|
100
|
+
<Person name={me.name} picture={me.picture} fallback="Anonymous" size={28} />
|
|
101
|
+
</div>
|
|
99
102
|
</MenuRow>
|
|
100
103
|
<Note>
|
|
101
104
|
{signedIn
|
package/src/Link.mdx
CHANGED
|
@@ -54,6 +54,12 @@ import { Link } from '@estiva-app/ui'
|
|
|
54
54
|
draw the border, the fill and the hover.
|
|
55
55
|
- If your router also exports a `Link`, rename one of them on import.
|
|
56
56
|
|
|
57
|
+
- `truncate` keeps a title that is a link on one line, cut with an ellipsis.
|
|
58
|
+
Its size and colour still come from the text around it.
|
|
59
|
+
- `quiet`, `underlined` and `plain` take their size and colour from where they
|
|
60
|
+
sit, so put those on your own element around the link, not on the link —
|
|
61
|
+
the lint refuses a look passed in `className` (UIG-9).
|
|
62
|
+
|
|
57
63
|
## Props
|
|
58
64
|
|
|
59
65
|
<Controls of={LinkStories.Text} />
|
package/src/Link.stories.tsx
CHANGED
|
@@ -100,3 +100,15 @@ export const AllVariants: Story = {
|
|
|
100
100
|
</div>
|
|
101
101
|
),
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
/** `truncate`: a title that is a link, on one line. Its size and colour come from the box around it. */
|
|
105
|
+
export const Truncate: Story = {
|
|
106
|
+
parameters: { controls: { disable: true } },
|
|
107
|
+
render: () => (
|
|
108
|
+
<div className="w-[200px] text-body-2 text-text-primary">
|
|
109
|
+
<Link href="#" variant="quiet" truncate className="block">
|
|
110
|
+
Assignee avatar and full name is missing in the files panel
|
|
111
|
+
</Link>
|
|
112
|
+
</div>
|
|
113
|
+
),
|
|
114
|
+
}
|
package/src/Link.tsx
CHANGED
|
@@ -41,15 +41,20 @@ export interface LinkProps extends ComponentPropsWithRef<'a'> {
|
|
|
41
41
|
* noreferrer` so the page it opens cannot reach back into this one.
|
|
42
42
|
*/
|
|
43
43
|
external?: boolean
|
|
44
|
+
/**
|
|
45
|
+
* One line, cut with an ellipsis when it does not fit — a title that is a link.
|
|
46
|
+
* Its size and colour still come from the text around it (UIG-9, 17 September).
|
|
47
|
+
*/
|
|
48
|
+
truncate?: boolean
|
|
44
49
|
children: ReactNode
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
export function Link({ href, variant = 'text', external = false, className, children, ...props }: LinkProps) {
|
|
52
|
+
export function Link({ href, variant = 'text', external = false, truncate = false, className, children, ...props }: LinkProps) {
|
|
48
53
|
return (
|
|
49
54
|
<a
|
|
50
55
|
href={href}
|
|
51
56
|
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
52
|
-
className={cn(VARIANT_CLASSES[variant], className)}
|
|
57
|
+
className={cn(VARIANT_CLASSES[variant], truncate && 'truncate', className)}
|
|
53
58
|
{...props}
|
|
54
59
|
>
|
|
55
60
|
{children}
|
package/src/Menu.stories.tsx
CHANGED
|
@@ -73,7 +73,7 @@ export const WithARow: Story = {
|
|
|
73
73
|
<MenuPanel className="w-64">
|
|
74
74
|
<MenuRow>
|
|
75
75
|
<span className="min-w-0 flex-1 truncate text-body-2 text-text-primary">Item one</span>
|
|
76
|
-
<SectionLabel
|
|
76
|
+
<SectionLabel tone="secondary">Label</SectionLabel>
|
|
77
77
|
</MenuRow>
|
|
78
78
|
<Divider className="my-1" />
|
|
79
79
|
<MenuItem label="An action" onClick={() => {}} />
|
package/src/Menu.test.tsx
CHANGED
|
@@ -339,8 +339,8 @@ describe('Menu, padding', () => {
|
|
|
339
339
|
)
|
|
340
340
|
await userEvent.click(screen.getByRole('button', { name: 'Open' }))
|
|
341
341
|
const row = await screen.findByRole('menuitem', { name: 'Item one' })
|
|
342
|
-
// The scrolling content is the box
|
|
343
|
-
const content = row.
|
|
342
|
+
// The scrolling content is the box the rows sit in directly.
|
|
343
|
+
const content = row.parentElement as HTMLElement
|
|
344
344
|
const classes = content.className.split(/\s+/)
|
|
345
345
|
expect(classes).toContain('p-1')
|
|
346
346
|
expect(classes).not.toContain('p-2')
|
package/src/Menu.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { createContext, useContext, type ComponentPropsWithRef, type ReactElemen
|
|
|
2
2
|
import { IconChevronRight } from '@tabler/icons-react'
|
|
3
3
|
import { Menu as BaseMenu } from '@base-ui/react/menu'
|
|
4
4
|
import { cn } from './cn'
|
|
5
|
+
import { DividerInPanel } from './Divider'
|
|
5
6
|
import { triggerDisabled } from './triggerDisabled'
|
|
6
7
|
import { ScrollArea } from './ScrollArea'
|
|
7
8
|
import { Kbd } from './Kbd'
|
|
@@ -241,8 +242,10 @@ export function Menu({ trigger, align = 'left', openOnHover = false, open, onOpe
|
|
|
241
242
|
* sentence was written at 0.12.6 and the prop was not: a `p-1` on
|
|
242
243
|
* `className` added 4px to these 8px instead of replacing them
|
|
243
244
|
* (PLAN Finding 60). The prop exists now. */}
|
|
244
|
-
<ScrollArea viewportClassName="max-h-[var(--available-height)]" contentClassName={cn('flex flex-col p-2
|
|
245
|
-
<MenuContext.Provider value={{ openOnHover }}>
|
|
245
|
+
<ScrollArea viewportClassName="max-h-[var(--available-height)]" contentClassName={cn('flex flex-col p-2', contentClassName)}>
|
|
246
|
+
<MenuContext.Provider value={{ openOnHover }}>
|
|
247
|
+
<DividerInPanel.Provider value={true}>{children}</DividerInPanel.Provider>
|
|
248
|
+
</MenuContext.Provider>
|
|
246
249
|
</ScrollArea>
|
|
247
250
|
</BaseMenu.Popup>
|
|
248
251
|
</BaseMenu.Positioner>
|
|
@@ -561,7 +564,7 @@ export function MenuSection({ label, children, className }: { label: string; chi
|
|
|
561
564
|
const inMenu = useContext(MenuContext) !== null
|
|
562
565
|
const heading = (
|
|
563
566
|
<div className={cn('flex h-8 items-center px-2', className)}>
|
|
564
|
-
<SectionLabel
|
|
567
|
+
<SectionLabel tone="secondary">{label}</SectionLabel>
|
|
565
568
|
</div>
|
|
566
569
|
)
|
|
567
570
|
if (!inMenu) {
|
package/src/Person.stories.tsx
CHANGED
|
@@ -26,15 +26,15 @@ export const OwnFallback: Story = {
|
|
|
26
26
|
parameters: { a11y: { config: { rules: [{ id: 'color-contrast', enabled: false }] } } },
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** The text size is the
|
|
29
|
+
/** The text size is where it sits: the box around it sets the words, `size` sets the face. */
|
|
30
30
|
export const Sizes: Story = {
|
|
31
31
|
parameters: { controls: { disable: true } },
|
|
32
32
|
render: () => (
|
|
33
33
|
<div className="flex flex-col items-start gap-3">
|
|
34
|
-
<Person name="Ana Duarte" size={16}
|
|
35
|
-
<Person name="Ana Duarte" size={20}
|
|
36
|
-
<Person name="Ravi Mehta" size={22}
|
|
37
|
-
<Person name="Ravi Mehta" size={28}
|
|
34
|
+
<div className="flex text-caption"><Person name="Ana Duarte" size={16} /></div>
|
|
35
|
+
<div className="flex text-body-2"><Person name="Ana Duarte" size={20} /></div>
|
|
36
|
+
<div className="flex text-body-2"><Person name="Ravi Mehta" size={22} /></div>
|
|
37
|
+
<div className="flex text-body-1"><Person name="Ravi Mehta" size={28} /></div>
|
|
38
38
|
</div>
|
|
39
39
|
),
|
|
40
40
|
}
|
package/src/Popover.test.tsx
CHANGED
|
@@ -233,8 +233,8 @@ describe('Popover, centred on its anchor', () => {
|
|
|
233
233
|
* as on 0.12.5.
|
|
234
234
|
*/
|
|
235
235
|
describe('Popover, padding', () => {
|
|
236
|
-
// The scrolling content is the box
|
|
237
|
-
const contentOf = (child: HTMLElement) => child.
|
|
236
|
+
// The scrolling content is the box the children sit in directly.
|
|
237
|
+
const contentOf = (child: HTMLElement) => child.parentElement as HTMLElement
|
|
238
238
|
|
|
239
239
|
it('pads its content 8px by default', async () => {
|
|
240
240
|
render(
|
package/src/Popover.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, type ReactElement, type ReactNode, type RefObject } from 'react'
|
|
2
2
|
import { Popover as BasePopover } from '@base-ui/react/popover'
|
|
3
3
|
import { cn } from './cn'
|
|
4
|
+
import { DividerInPanel } from './Divider'
|
|
4
5
|
import { triggerDisabled } from './triggerDisabled'
|
|
5
6
|
import { ScrollArea } from './ScrollArea'
|
|
6
7
|
import { MenuPanel } from './Menu'
|
|
@@ -200,9 +201,9 @@ export function Popover({ trigger, anchor, align = 'left', side = 'bottom', open
|
|
|
200
201
|
— see the props. */}
|
|
201
202
|
<ScrollArea
|
|
202
203
|
viewportClassName={maxHeight ?? 'max-h-[var(--available-height)]'}
|
|
203
|
-
contentClassName={cn('flex flex-col p-2
|
|
204
|
+
contentClassName={cn('flex flex-col p-2', contentClassName)}
|
|
204
205
|
>
|
|
205
|
-
{children}
|
|
206
|
+
<DividerInPanel.Provider value={true}>{children}</DividerInPanel.Provider>
|
|
206
207
|
</ScrollArea>
|
|
207
208
|
</BasePopover.Popup>
|
|
208
209
|
</BasePopover.Positioner>
|
package/src/PreviewCard.tsx
CHANGED
|
@@ -83,7 +83,7 @@ export function PreviewCard({ content, children, side = 'right', delay = OPEN_DE
|
|
|
83
83
|
className={cn('w-[360px] p-3 outline-none', className)}
|
|
84
84
|
render={<MenuPanel />}
|
|
85
85
|
>
|
|
86
|
-
<ScrollArea viewportClassName="max-h-[calc(min(300px,var(--available-height))_-_1.5rem)]" contentClassName="flex flex-col gap-3
|
|
86
|
+
<ScrollArea viewportClassName="max-h-[calc(min(300px,var(--available-height))_-_1.5rem)]" contentClassName="flex flex-col gap-3">
|
|
87
87
|
{content}
|
|
88
88
|
</ScrollArea>
|
|
89
89
|
</BasePreviewCard.Popup>
|