@estiva-app/ui 0.33.0 → 0.34.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/README.md +8 -1
- package/dist/AttachmentCard.d.ts.map +1 -1
- package/dist/Button.d.ts.map +1 -1
- package/dist/Card.d.ts.map +1 -1
- package/dist/Chip.d.ts.map +1 -1
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/Lightbox.d.ts.map +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/NavItem.d.ts.map +1 -1
- package/dist/PersonTrigger.d.ts.map +1 -1
- package/dist/Reaction.d.ts.map +1 -1
- package/dist/SearchInput.d.ts.map +1 -1
- package/dist/SectionLabel.d.ts +9 -2
- package/dist/SectionLabel.d.ts.map +1 -1
- package/dist/Select.d.ts.map +1 -1
- package/dist/TextInput.d.ts.map +1 -1
- package/dist/Textarea.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 +392 -14
- package/dist/eslint/index.js.map +4 -4
- package/dist/eslint/looks-of.d.ts +54 -0
- package/dist/eslint/looks-of.d.ts.map +1 -0
- package/dist/eslint/no-copied-look.d.ts +3 -0
- package/dist/eslint/no-copied-look.d.ts.map +1 -0
- package/dist/gates/index.js +1 -1
- package/dist/gates/index.js.map +2 -2
- package/dist/index.js +171 -133
- package/dist/index.js.map +3 -3
- package/dist/looks.d.ts +34 -0
- package/dist/looks.d.ts.map +1 -0
- package/dist/registry/app-3ALZE6XO.js +10 -0
- package/dist/registry/app.d.ts.map +1 -1
- package/dist/registry/{build-LGFCCOLR.js → build-NDXMVFQ5.js} +3 -3
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/{chunk-XCBN2EKW.js → chunk-3WJ7ZZ7Z.js} +8 -4
- package/dist/registry/chunk-3WJ7ZZ7Z.js.map +7 -0
- package/dist/registry/{chunk-E4JNV7PC.js → chunk-PZO4BVV3.js} +2 -1
- package/dist/registry/chunk-PZO4BVV3.js.map +7 -0
- package/dist/registry/chunk-XVII7P6G.js +595 -0
- package/dist/registry/chunk-XVII7P6G.js.map +7 -0
- package/dist/registry/cli.js +3 -3
- package/dist/registry/index.js +3 -3
- package/dist/registry/schema.d.ts +8 -0
- package/dist/registry/schema.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +347 -90
- package/src/AttachmentCard.tsx +2 -0
- package/src/Button.tsx +1 -0
- package/src/Card.stories.tsx +2 -2
- package/src/Card.tsx +1 -0
- package/src/Chip.tsx +2 -1
- package/src/ChipInput.tsx +4 -2
- package/src/CommandPalette.tsx +2 -0
- package/src/ContainerHeader.stories.tsx +2 -1
- package/src/DialogShell.tsx +1 -0
- package/src/Divider.stories.tsx +12 -10
- package/src/Lightbox.tsx +1 -0
- package/src/Link.stories.tsx +5 -4
- package/src/Menu.tsx +3 -1
- package/src/NavItem.tsx +1 -0
- package/src/PersonTrigger.tsx +1 -0
- package/src/Reaction.tsx +3 -1
- package/src/ReactionPicker.stories.tsx +3 -2
- package/src/ScrollArea.stories.tsx +13 -12
- package/src/SearchInput.tsx +5 -4
- package/src/SectionLabel.mdx +4 -2
- package/src/SectionLabel.stories.tsx +13 -0
- package/src/SectionLabel.tsx +19 -2
- package/src/Select.tsx +6 -4
- package/src/TextInput.tsx +6 -6
- package/src/Textarea.tsx +6 -4
- package/src/Tooltip.stories.tsx +3 -2
- package/src/Tooltip.tsx +2 -1
- package/src/eslint/index.test.ts +12 -8
- package/src/eslint/index.ts +13 -5
- package/src/eslint/looks-of.ts +302 -0
- package/src/eslint/no-copied-look.test.ts +155 -0
- package/src/eslint/no-copied-look.ts +203 -0
- package/src/eslint/no-restyled-part.ts +1 -1
- package/src/gates/app-checks.ts +1 -1
- package/src/gates/create-app.test.ts +1 -1
- package/src/gates/gates.test.ts +1 -1
- package/src/looks.ts +42 -0
- package/src/registry/app.ts +3 -0
- package/src/registry/build.ts +8 -0
- package/src/registry/registry.test.ts +1 -1
- package/src/registry/schema.ts +9 -0
- package/dist/registry/app-FXN4EI34.js +0 -10
- package/dist/registry/chunk-E4JNV7PC.js.map +0 -7
- package/dist/registry/chunk-NJN4MQAM.js +0 -397
- package/dist/registry/chunk-NJN4MQAM.js.map +0 -7
- package/dist/registry/chunk-XCBN2EKW.js.map +0 -7
- /package/dist/registry/{app-FXN4EI34.js.map → app-3ALZE6XO.js.map} +0 -0
- /package/dist/registry/{build-LGFCCOLR.js.map → build-NDXMVFQ5.js.map} +0 -0
package/src/AttachmentCard.tsx
CHANGED
|
@@ -89,6 +89,7 @@ const formatBytes = (bytes: number) => {
|
|
|
89
89
|
const typeLabelOf = (name: string) => extensionOf(name).toUpperCase() || 'FILE'
|
|
90
90
|
|
|
91
91
|
const TILE_CLASSES = 'size-9 rounded-md bg-bg-active flex items-center justify-center shrink-0 text-text-secondary'
|
|
92
|
+
// @estiva-escape: A6 (Katerina, 24 September: record only): a 12px medium name, the text style InputChip's label also uses, not a copy of a part
|
|
92
93
|
const NAME_CLASSES = 'text-caption font-medium text-text-primary truncate'
|
|
93
94
|
const NOTE_CLASSES = 'text-small tracking-wide leading-tight truncate'
|
|
94
95
|
|
|
@@ -389,6 +390,7 @@ export function AttachmentCard({
|
|
|
389
390
|
{/* On Base UI's Button, as InputChip's ✕ is (Katerina, 2026-09-14): IconButton is a 24px square
|
|
390
391
|
that fills on hover, and this is Peek's 20px round badge on the card's corner. */}
|
|
391
392
|
{onRemove && (
|
|
393
|
+
// @estiva-escape: N4 (Katerina, 24 September: record only): the small round button Lightbox's close also draws; a part if a third appears
|
|
392
394
|
<BaseButton
|
|
393
395
|
type="button"
|
|
394
396
|
aria-label={`Remove ${name}`}
|
package/src/Button.tsx
CHANGED
|
@@ -92,6 +92,7 @@ export function Button({
|
|
|
92
92
|
size === 'default' && (hasLeadingIcon ? 'pl-2 pr-3' : 'px-2'),
|
|
93
93
|
size === 'small' && (hasLeadingIcon ? 'pl-1.5 pr-2' : 'px-1.5'),
|
|
94
94
|
!state.disabled && (variant === 'primary' || variant === 'resolve') && 'bg-accent-primary hover:bg-accent-hover text-text-inverse cursor-pointer signal:font-semibold',
|
|
95
|
+
// @estiva-escape: W2 (Katerina, 24 September: leave alone): an outlined button shares four words with Reaction's pill by coincidence
|
|
95
96
|
!state.disabled && variant === 'outlined' && 'border border-border-default hover:bg-bg-hover text-text-primary cursor-pointer',
|
|
96
97
|
!state.disabled && variant === 'muted' && 'hover:bg-bg-hover text-text-primary cursor-pointer',
|
|
97
98
|
!state.disabled && variant === 'destructive' && 'hover:bg-error-muted text-error-default cursor-pointer',
|
package/src/Card.stories.tsx
CHANGED
|
@@ -30,10 +30,10 @@ export const Surface: Story = {}
|
|
|
30
30
|
export const Elevated: Story = {
|
|
31
31
|
args: { fill: 'elevated', className: 'p-2' },
|
|
32
32
|
render: (args) => (
|
|
33
|
-
<
|
|
33
|
+
<Card className="flex w-[300px] flex-col gap-2 p-2">
|
|
34
34
|
<Card {...args} />
|
|
35
35
|
<Card {...args} children={<Body title="Item two" />} />
|
|
36
|
-
</
|
|
36
|
+
</Card>
|
|
37
37
|
),
|
|
38
38
|
}
|
|
39
39
|
|
package/src/Card.tsx
CHANGED
|
@@ -134,6 +134,7 @@ export function Card({
|
|
|
134
134
|
)
|
|
135
135
|
}
|
|
136
136
|
return (
|
|
137
|
+
// @estiva-escape: N6 (Katerina, 24 September: leave alone): a card that cuts its content to its corners shares four words with the palette's window by coincidence
|
|
137
138
|
<div className={classes} {...props}>
|
|
138
139
|
{children}
|
|
139
140
|
</div>
|
package/src/Chip.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import { cn } from './cn'
|
|
3
|
+
import { CHIP_TEXT_CLASSES } from './looks'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Peek's Chip (2026-08-28), verbatim: a 20px pill in one of six colour
|
|
@@ -44,7 +45,7 @@ export function Chip({ type = 'neutral', label, leadingIcon, trailingIcon, class
|
|
|
44
45
|
// the screenshot diff caught every descender cut off. `clip` is the
|
|
45
46
|
// one overflow that leaves the other axis visible. A chip with no cap
|
|
46
47
|
// draws exactly as before.
|
|
47
|
-
<span className=
|
|
48
|
+
<span className={cn('min-w-0 overflow-x-clip text-ellipsis whitespace-nowrap signal:tracking-wide', CHIP_TEXT_CLASSES)}>{label}</span>
|
|
48
49
|
)}
|
|
49
50
|
{trailingIcon && <span className="flex size-3 shrink-0 items-center justify-center">{trailingIcon}</span>}
|
|
50
51
|
</div>
|
package/src/ChipInput.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { IconX } from '@tabler/icons-react'
|
|
|
5
5
|
import { cn } from './cn'
|
|
6
6
|
import { MenuItem, MenuPanel } from './Menu'
|
|
7
7
|
import { ScrollArea } from './ScrollArea'
|
|
8
|
+
import { BARE_INPUT_CLASSES, FIELD_SHELL_CLASSES } from './looks'
|
|
8
9
|
|
|
9
10
|
/* The chip's own look, written once: `InputChip` draws it for a caller who
|
|
10
11
|
wants a chip on its own, and `ChipInput` gives the same classes to Base UI's
|
|
@@ -61,6 +62,7 @@ export function InputChip({ label, leading, onRemove, removeLabel, truncate, cla
|
|
|
61
62
|
return (
|
|
62
63
|
<div className={cn(CHIP_BOX, chipPadding(!!leading, !!onRemove), className)}>
|
|
63
64
|
{leading && <span className="flex shrink-0 items-center">{leading}</span>}
|
|
65
|
+
{/* @estiva-escape: A6 (Katerina, 24 September: record only): a 12px medium label, the text style AttachmentCard's name also uses, not a copy of a part */}
|
|
64
66
|
<span className={cn(CHIP_LABEL, truncate && 'min-w-0 truncate')}>{label}</span>
|
|
65
67
|
{/* Base UI's `Button`, as every button in the package is (D6). Base UI
|
|
66
68
|
has no chip of its own — its only chips are `Combobox.Chip` and
|
|
@@ -244,7 +246,7 @@ export function ChipInput<T extends ChipInputOption = ChipInputOption>({
|
|
|
244
246
|
>
|
|
245
247
|
{/* The box the chips and the input share. `Combobox.Chips` is what makes
|
|
246
248
|
the two one control for the keyboard; the look is what it always was. */}
|
|
247
|
-
<Combobox.Chips ref={setBox} className=
|
|
249
|
+
<Combobox.Chips ref={setBox} className={cn(FIELD_SHELL_CLASSES, 'focus-within:border-border-focus focus-within:hover:border-border-focus px-3 py-1.5 flex flex-wrap items-center gap-1.5 min-h-[38px] cursor-text signal:transition-shadow signal:focus-within:shadow-focus-ring')}>
|
|
248
250
|
{value.map((option) => (
|
|
249
251
|
<Combobox.Chip key={option.id} className={cn(CHIP_BOX, chipPadding(!!chipLeading, true))}>
|
|
250
252
|
{chipLeading && <span className="flex shrink-0 items-center">{chipLeading(option)}</span>}
|
|
@@ -260,7 +262,7 @@ export function ChipInput<T extends ChipInputOption = ChipInputOption>({
|
|
|
260
262
|
aria-required={aria['aria-required']}
|
|
261
263
|
{...naming}
|
|
262
264
|
onKeyDown={onInputKeyDown}
|
|
263
|
-
className=
|
|
265
|
+
className={cn('flex-1 min-w-[120px] text-body-2 border-none', BARE_INPUT_CLASSES)}
|
|
264
266
|
/>
|
|
265
267
|
</Combobox.Chips>
|
|
266
268
|
|
package/src/CommandPalette.tsx
CHANGED
|
@@ -123,6 +123,7 @@ export function CommandPalette({ open, onOpenChange, label, where, modKey = 'Ctr
|
|
|
123
123
|
<Dialog.Portal>
|
|
124
124
|
<Dialog.Backdrop className="fixed inset-0 z-40 bg-scrim" />
|
|
125
125
|
<Dialog.Viewport className="fixed inset-0 z-50 flex items-start justify-center pt-[16vh]">
|
|
126
|
+
{/* @estiva-escape: A3 (Katerina, 24 September: leave CommandPalette alone): the palette's window keeps its own border */}
|
|
126
127
|
<Dialog.Popup
|
|
127
128
|
ref={popupRef}
|
|
128
129
|
aria-label={label}
|
|
@@ -373,6 +374,7 @@ export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups
|
|
|
373
374
|
<div className="flex h-12 shrink-0 items-center gap-3 border-b border-border-subtle px-5">
|
|
374
375
|
<IconSearch size={16} stroke={1.5} className="shrink-0 text-text-secondary" />
|
|
375
376
|
{chip && <LevelChip chip={chip} onBack={back} />}
|
|
377
|
+
{/* @estiva-escape: A5 (Katerina, 24 September: leave CommandPalette alone): the palette's search field keeps its own classes */}
|
|
376
378
|
<Autocomplete.Input
|
|
377
379
|
ref={inputRef}
|
|
378
380
|
data-command-palette-field=""
|
|
@@ -2,9 +2,10 @@ import type { Decorator, Meta, StoryObj } from '@storybook/react-vite'
|
|
|
2
2
|
import { IconEdit, IconSortDescending, IconX } from '@tabler/icons-react'
|
|
3
3
|
import { ContainerHeader } from './ContainerHeader'
|
|
4
4
|
import { IconButton } from './IconButton'
|
|
5
|
+
import { Card } from './Card'
|
|
5
6
|
|
|
6
7
|
// The top of a column: a surface with a hairline, so the bar's own hairline and height can be seen.
|
|
7
|
-
const inColumn: Decorator = (Story) => <
|
|
8
|
+
const inColumn: Decorator = (Story) => <Card clip className="w-96">{Story()}</Card>
|
|
8
9
|
|
|
9
10
|
const meta = {
|
|
10
11
|
title: 'Navigation/ContainerHeader',
|
package/src/DialogShell.tsx
CHANGED
|
@@ -86,6 +86,7 @@ export function DialogShell({ title, onClose, headerContent, footer, children, b
|
|
|
86
86
|
|
|
87
87
|
{/* Dialog */}
|
|
88
88
|
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-none">
|
|
89
|
+
{/* @estiva-escape: A3 (Katerina, 24 September: leave CommandPalette alone): a dialog's window and the palette's are left as they are */}
|
|
89
90
|
<Parts.Popup
|
|
90
91
|
ref={popupRef}
|
|
91
92
|
/*
|
package/src/Divider.stories.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import { Divider } from './Divider'
|
|
3
|
+
import { MenuPanel } from './Menu'
|
|
4
|
+
import { Card } from './Card'
|
|
3
5
|
|
|
4
6
|
/** A hairline in `border-subtle`, inset 12px each side — or standing up between two columns. */
|
|
5
7
|
const meta = {
|
|
@@ -13,22 +15,22 @@ type Story = StoryObj<typeof meta>
|
|
|
13
15
|
/** Between two blocks on the surface. */
|
|
14
16
|
export const Default: Story = {
|
|
15
17
|
render: () => (
|
|
16
|
-
<
|
|
18
|
+
<Card className="w-80 py-3">
|
|
17
19
|
<p className="px-3 pb-2 text-body-2 text-text-primary">Above the divider</p>
|
|
18
20
|
<Divider />
|
|
19
21
|
<p className="px-3 pt-2 text-body-2 text-text-primary">Below the divider</p>
|
|
20
|
-
</
|
|
22
|
+
</Card>
|
|
21
23
|
),
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** Inside a menu — on the elevated surface, full width (`mx-0`). */
|
|
25
27
|
export const InAMenu: Story = {
|
|
26
28
|
render: () => (
|
|
27
|
-
<
|
|
28
|
-
<div className="px-2 py-1.5">First action</div>
|
|
29
|
+
<MenuPanel className="w-72">
|
|
30
|
+
<div className="px-2 py-1.5 text-body-2 text-text-primary">First action</div>
|
|
29
31
|
<Divider className="mx-0 my-2" />
|
|
30
|
-
<div className="px-2 py-1.5">Second action</div>
|
|
31
|
-
</
|
|
32
|
+
<div className="px-2 py-1.5 text-body-2 text-text-primary">Second action</div>
|
|
33
|
+
</MenuPanel>
|
|
32
34
|
),
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -49,21 +51,21 @@ export const WithALabel: Story = {
|
|
|
49
51
|
// the label is muted caption text, 3.78:1 on --bg-surface in signal (AA 4.5:1).
|
|
50
52
|
parameters: { a11y: { config: { rules: [{ id: 'color-contrast', enabled: false }] } } },
|
|
51
53
|
render: () => (
|
|
52
|
-
<
|
|
54
|
+
<Card className="w-96 py-3"><div className="text-body-2 text-text-primary">
|
|
53
55
|
<p className="px-3 pb-2">Yesterday's last message</p>
|
|
54
56
|
<Divider label="Today" />
|
|
55
57
|
<p className="px-3 pt-2">Today's first</p>
|
|
56
|
-
</div>
|
|
58
|
+
</div></Card>
|
|
57
59
|
),
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/** Asking for attention: where "new since you last read this" begins. The warning colour, because the accent could not be read on Ship's background. */
|
|
61
63
|
export const Warning: Story = {
|
|
62
64
|
render: () => (
|
|
63
|
-
<
|
|
65
|
+
<Card className="w-96 py-3"><div className="text-body-2 text-text-primary">
|
|
64
66
|
<p className="px-3 pb-2">Yes — it re-reads the folder union and the archived one lands last.</p>
|
|
65
67
|
<Divider label="New since you last read this" tone="warning" />
|
|
66
68
|
<p className="px-3 pt-2">Same here, from Peek.</p>
|
|
67
|
-
</div>
|
|
69
|
+
</div></Card>
|
|
68
70
|
),
|
|
69
71
|
}
|
package/src/Lightbox.tsx
CHANGED
|
@@ -74,6 +74,7 @@ export function Lightbox({ src, alt, onClose, className }: LightboxProps) {
|
|
|
74
74
|
onClick={(e) => e.stopPropagation()}
|
|
75
75
|
className={cn('max-w-full max-h-full object-contain rounded-lg', className)}
|
|
76
76
|
/>
|
|
77
|
+
{/* @estiva-escape: N4 (Katerina, 24 September: record only): the small round button AttachmentCard's remove also draws; a part if a third appears */}
|
|
77
78
|
<Dialog.Close
|
|
78
79
|
aria-label="Close"
|
|
79
80
|
className="absolute top-4 right-4 size-8 rounded-full bg-bg-elevated border border-border-strong flex items-center justify-center text-text-secondary hover:text-text-primary"
|
package/src/Link.stories.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import { Link } from './Link'
|
|
3
|
+
import { Card } from './Card'
|
|
3
4
|
|
|
4
5
|
const meta = {
|
|
5
6
|
title: 'Navigation/Link',
|
|
@@ -52,9 +53,9 @@ export const Plain: Story = {
|
|
|
52
53
|
args: { variant: 'plain', children: undefined },
|
|
53
54
|
render: (args) => (
|
|
54
55
|
<Link {...args} className="block w-[280px]">
|
|
55
|
-
<
|
|
56
|
+
<Card hover="hairline" className="p-3"><div className="text-body-2 text-text-primary">
|
|
56
57
|
Item one
|
|
57
|
-
</div>
|
|
58
|
+
</div></Card>
|
|
58
59
|
</Link>
|
|
59
60
|
),
|
|
60
61
|
}
|
|
@@ -92,9 +93,9 @@ export const AllVariants: Story = {
|
|
|
92
93
|
<div className="flex flex-col gap-1">
|
|
93
94
|
<span className="text-body-2 text-text-secondary">plain —</span>
|
|
94
95
|
<Link href="#" variant="plain" className="block w-[280px]">
|
|
95
|
-
<
|
|
96
|
+
<Card hover="hairline" className="p-3"><div className="text-body-2 text-text-primary">
|
|
96
97
|
Item one
|
|
97
|
-
</div>
|
|
98
|
+
</div></Card>
|
|
98
99
|
</Link>
|
|
99
100
|
</div>
|
|
100
101
|
</div>
|
package/src/Menu.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { triggerDisabled } from './triggerDisabled'
|
|
|
7
7
|
import { ScrollArea } from './ScrollArea'
|
|
8
8
|
import { Kbd } from './Kbd'
|
|
9
9
|
import { SectionLabel } from './SectionLabel'
|
|
10
|
+
import { FLOATING_SURFACE_CLASSES } from './looks'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* THE menu shell (2026-09-01) — extracted once, for every menu in every app.
|
|
@@ -94,7 +95,8 @@ export function MenuPanel({ children, className, ...props }: MenuPanelProps) {
|
|
|
94
95
|
return (
|
|
95
96
|
<div
|
|
96
97
|
className={cn(
|
|
97
|
-
'flex flex-col
|
|
98
|
+
'flex flex-col p-2',
|
|
99
|
+
FLOATING_SURFACE_CLASSES,
|
|
98
100
|
/*
|
|
99
101
|
* A divider in a menu runs the width of the rows it separates.
|
|
100
102
|
*
|
package/src/NavItem.tsx
CHANGED
|
@@ -28,6 +28,7 @@ export interface NavItemProps extends Omit<ComponentPropsWithoutRef<'a'>, 'href'
|
|
|
28
28
|
|
|
29
29
|
export function NavItem({ label, href, count, countLabel, active = false, icon, className, ...props }: NavItemProps) {
|
|
30
30
|
return (
|
|
31
|
+
// @estiva-escape: N5 (Katerina, 24 September: record only): a row that fills on hover, as PersonTrigger's row also does
|
|
31
32
|
<a
|
|
32
33
|
href={href}
|
|
33
34
|
aria-current={active ? 'page' : undefined}
|
package/src/PersonTrigger.tsx
CHANGED
|
@@ -60,6 +60,7 @@ export function PersonTrigger({ name, picture, fallback, size, open = false, com
|
|
|
60
60
|
)
|
|
61
61
|
}
|
|
62
62
|
return (
|
|
63
|
+
// @estiva-escape: N5 (Katerina, 24 September: record only): a row that fills on hover, as NavItem also does
|
|
63
64
|
<BaseButton
|
|
64
65
|
type="button"
|
|
65
66
|
aria-haspopup="menu"
|
package/src/Reaction.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
|
2
2
|
import { Toggle } from '@base-ui/react/toggle'
|
|
3
3
|
import { cn } from './cn'
|
|
4
|
+
import { CHIP_TEXT_CLASSES } from './looks'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* A reaction: an emoji, how many people chose it, and whether you are one of
|
|
@@ -62,6 +63,7 @@ export interface ReactionProps extends Omit<ButtonHTMLAttributes<HTMLButtonEleme
|
|
|
62
63
|
|
|
63
64
|
export function Reaction({ emoji, count, pressed = false, className, ...props }: ReactionProps) {
|
|
64
65
|
return (
|
|
66
|
+
// @estiva-escape: W1 (Katerina, 24 September: leave alone): a reaction pill shares four words with a field's box by coincidence
|
|
65
67
|
<Toggle
|
|
66
68
|
// Controlled: a reaction is yours when the data says so, not when the
|
|
67
69
|
// pill was last pressed — a write can fail, and another client can
|
|
@@ -94,7 +96,7 @@ export function Reaction({ emoji, count, pressed = false, className, ...props }:
|
|
|
94
96
|
The old note said it must NEVER be merged; that stopped being true
|
|
95
97
|
when `cn()` was taught the ramp — `chip` is in it, and `cn.test.ts`
|
|
96
98
|
pins that. */}
|
|
97
|
-
<span className=
|
|
99
|
+
<span className={CHIP_TEXT_CLASSES}>
|
|
98
100
|
{count}
|
|
99
101
|
</span>
|
|
100
102
|
</Toggle>
|
|
@@ -6,6 +6,7 @@ import { Reaction } from './Reaction'
|
|
|
6
6
|
import { ReactionPicker, type ReactionOption } from './ReactionPicker'
|
|
7
7
|
import { Toolbar, ToolbarButton } from './Toolbar'
|
|
8
8
|
import { EmptyState } from './EmptyState'
|
|
9
|
+
import { Card } from './Card'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* The reactions on offer, to choose one from. **A `Reaction` is the answer;
|
|
@@ -52,9 +53,9 @@ export const FromATrigger: Story = {
|
|
|
52
53
|
const [chosen, setChosen] = useState<string | null>(null)
|
|
53
54
|
return (
|
|
54
55
|
<div className="flex w-[420px] flex-col items-end gap-3">
|
|
55
|
-
<
|
|
56
|
+
<Card className="w-full p-3"><div className="text-body-2 text-text-primary">
|
|
56
57
|
A card. Its actions sit at the corner, and the picker opens above them.
|
|
57
|
-
</div>
|
|
58
|
+
</div></Card>
|
|
58
59
|
<Toolbar aria-label="Card actions">
|
|
59
60
|
<ToolbarButton aria-label="Reply" tooltip="Reply">
|
|
60
61
|
<IconMessage2 size={16} stroke={1.5} />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Card } from './Card'
|
|
2
3
|
import { ScrollArea } from './ScrollArea'
|
|
3
4
|
|
|
4
5
|
/** A region that scrolls without taking width for its scrollbar. */
|
|
@@ -17,7 +18,7 @@ const rows = Array.from({ length: 40 }, (_, i) => `Row ${i + 1}`)
|
|
|
17
18
|
/** A list taller than its box. The bar shows while the pointer is over it or the list is moving. */
|
|
18
19
|
export const Default: Story = {
|
|
19
20
|
render: () => (
|
|
20
|
-
<
|
|
21
|
+
<Card className="h-[240px] w-[280px]">
|
|
21
22
|
<ScrollArea className="h-full" contentClassName="p-2">
|
|
22
23
|
<ul className="flex flex-col gap-px">
|
|
23
24
|
{rows.map((row) => (
|
|
@@ -27,14 +28,14 @@ export const Default: Story = {
|
|
|
27
28
|
))}
|
|
28
29
|
</ul>
|
|
29
30
|
</ScrollArea>
|
|
30
|
-
</
|
|
31
|
+
</Card>
|
|
31
32
|
),
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/** Rows that stick to the top as the list moves under them. The bar stays above them. */
|
|
35
36
|
export const StickyHeadings: Story = {
|
|
36
37
|
render: () => (
|
|
37
|
-
<
|
|
38
|
+
<Card className="h-[240px] w-[280px]">
|
|
38
39
|
<ScrollArea className="h-full">
|
|
39
40
|
{['Group one', 'Group two'].map((group) => (
|
|
40
41
|
<div key={group} className="flex flex-col">
|
|
@@ -47,14 +48,14 @@ export const StickyHeadings: Story = {
|
|
|
47
48
|
</div>
|
|
48
49
|
))}
|
|
49
50
|
</ScrollArea>
|
|
50
|
-
</
|
|
51
|
+
</Card>
|
|
51
52
|
),
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/** Nothing to scroll: the region draws exactly as a plain box would, and no bar. */
|
|
55
56
|
export const Fits: Story = {
|
|
56
57
|
render: () => (
|
|
57
|
-
<
|
|
58
|
+
<Card className="h-[240px] w-[280px]">
|
|
58
59
|
<ScrollArea className="h-full" contentClassName="p-2">
|
|
59
60
|
<ul className="flex flex-col gap-px">
|
|
60
61
|
{rows.slice(0, 4).map((row) => (
|
|
@@ -64,14 +65,14 @@ export const Fits: Story = {
|
|
|
64
65
|
))}
|
|
65
66
|
</ul>
|
|
66
67
|
</ScrollArea>
|
|
67
|
-
</
|
|
68
|
+
</Card>
|
|
68
69
|
),
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/** A row wider than its box — a table, a board. */
|
|
72
73
|
export const Horizontal: Story = {
|
|
73
74
|
render: () => (
|
|
74
|
-
<
|
|
75
|
+
<Card className="w-[280px]">
|
|
75
76
|
<ScrollArea orientation="horizontal" contentClassName="p-2">
|
|
76
77
|
<div className="flex w-max gap-2">
|
|
77
78
|
{rows.slice(0, 12).map((row) => (
|
|
@@ -81,14 +82,14 @@ export const Horizontal: Story = {
|
|
|
81
82
|
))}
|
|
82
83
|
</div>
|
|
83
84
|
</ScrollArea>
|
|
84
|
-
</
|
|
85
|
+
</Card>
|
|
85
86
|
),
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/** Both ways, with the corner where the two bars would meet. */
|
|
89
90
|
export const Both: Story = {
|
|
90
91
|
render: () => (
|
|
91
|
-
<
|
|
92
|
+
<Card className="h-[240px] w-[280px]">
|
|
92
93
|
<ScrollArea orientation="both" className="h-full" contentClassName="p-2">
|
|
93
94
|
<div className="flex w-max flex-col gap-px">
|
|
94
95
|
{rows.map((row) => (
|
|
@@ -98,14 +99,14 @@ export const Both: Story = {
|
|
|
98
99
|
))}
|
|
99
100
|
</div>
|
|
100
101
|
</ScrollArea>
|
|
101
|
-
</
|
|
102
|
+
</Card>
|
|
102
103
|
),
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/** A sideways region inside a scrolling page — a table in a content column. A wheel down over it moves the page; a swipe sideways moves the region. */
|
|
106
107
|
export const SidewaysInsideAPage: Story = {
|
|
107
108
|
render: () => (
|
|
108
|
-
<
|
|
109
|
+
<Card className="h-[240px] w-[280px]">
|
|
109
110
|
<ScrollArea className="h-full" contentClassName="p-2">
|
|
110
111
|
<div className="flex flex-col gap-px">
|
|
111
112
|
{rows.slice(0, 3).map((row) => (
|
|
@@ -131,6 +132,6 @@ export const SidewaysInsideAPage: Story = {
|
|
|
131
132
|
))}
|
|
132
133
|
</div>
|
|
133
134
|
</ScrollArea>
|
|
134
|
-
</
|
|
135
|
+
</Card>
|
|
135
136
|
),
|
|
136
137
|
}
|
package/src/SearchInput.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { type InputHTMLAttributes } from 'react'
|
|
|
2
2
|
import { Input } from '@base-ui/react/input'
|
|
3
3
|
import { cn } from './cn'
|
|
4
4
|
import { Kbd } from './Kbd'
|
|
5
|
+
import { BARE_INPUT_CLASSES, FIELD_SHELL_CLASSES } from './looks'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Peek's SearchInput (2026-09-01), verbatim: an inset field with a hairline
|
|
@@ -31,16 +32,16 @@ export function SearchInput({ shortcut, className, placeholder = 'Search…', ..
|
|
|
31
32
|
return (
|
|
32
33
|
<div
|
|
33
34
|
className={cn(
|
|
34
|
-
'flex gap-2 items-center px-3 py-2
|
|
35
|
-
|
|
35
|
+
'flex gap-2 items-center px-3 py-2',
|
|
36
|
+
FIELD_SHELL_CLASSES,
|
|
36
37
|
// Hover strengthens the border as it does on every other field (16 September). Here that
|
|
37
38
|
// is also the focus look, which was the stronger border before hover existed.
|
|
38
|
-
'
|
|
39
|
+
'focus-within:border-border-strong',
|
|
39
40
|
className,
|
|
40
41
|
)}
|
|
41
42
|
>
|
|
42
43
|
<Input
|
|
43
|
-
className=
|
|
44
|
+
className={cn('flex-1 min-w-0 text-input-value', BARE_INPUT_CLASSES)}
|
|
44
45
|
placeholder={placeholder}
|
|
45
46
|
{...props}
|
|
46
47
|
/>
|
package/src/SectionLabel.mdx
CHANGED
|
@@ -38,11 +38,13 @@ import { SectionLabel } from '@estiva-app/ui'
|
|
|
38
38
|
|
|
39
39
|
It is a bare span: the caller owns the row it sits in and the spacing
|
|
40
40
|
around it. Read it `tone="secondary"` where it labels rather than leads — menu
|
|
41
|
-
headings and the command palette's groups do.
|
|
41
|
+
headings and the command palette's groups do. `tone="muted"` where it marks a
|
|
42
|
+
place in a list rather than heading it — a date between messages. `truncate`
|
|
43
|
+
cuts a long label short with an ellipsis where its row has no room. A colour
|
|
42
44
|
passed in `className` is refused by the lint.
|
|
43
45
|
|
|
44
46
|
- Import it — never copy its classes. A copied class list drifts the first time
|
|
45
|
-
the label changes.
|
|
47
|
+
the label changes, and the lint warns on one (`estiva/no-copied-look`).
|
|
46
48
|
|
|
47
49
|
## What it owns
|
|
48
50
|
|
|
@@ -32,3 +32,16 @@ export const HeadingAList: Story = {
|
|
|
32
32
|
|
|
33
33
|
/** `tone="secondary"`: a heading inside a menu or a list of results labels the rows, it is not one of them. */
|
|
34
34
|
export const Secondary: Story = { args: { tone: 'secondary', children: 'Section' } }
|
|
35
|
+
|
|
36
|
+
/** `tone="muted"`: a label that marks a place in a list rather than heading it, a date between messages. */
|
|
37
|
+
export const Muted: Story = { args: { tone: 'muted', children: 'Yesterday' } }
|
|
38
|
+
|
|
39
|
+
/** `truncate`: a long label in a narrow row ends in an ellipsis instead of spilling. */
|
|
40
|
+
export const Truncated: Story = {
|
|
41
|
+
args: { truncate: true, children: 'Promoted to a topic whose title is far longer than its row' },
|
|
42
|
+
render: (args) => (
|
|
43
|
+
<div className="flex w-48">
|
|
44
|
+
<SectionLabel {...args} />
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
}
|
package/src/SectionLabel.tsx
CHANGED
|
@@ -13,17 +13,34 @@ import { cn } from './cn'
|
|
|
13
13
|
* the rows, it is not one of them (Katerina, 2026-09-01). `MenuSection` and
|
|
14
14
|
* `CommandPalette` passed it as a class before UIG-9 (17 September).
|
|
15
15
|
*
|
|
16
|
+
* `tone="muted"` is a label that marks a place in a list rather than heading
|
|
17
|
+
* it: a date between messages (Peek's `DateDivider`, UIG-25). `truncate` cuts a
|
|
18
|
+
* long label short with an ellipsis where its row has no room, as a date line in
|
|
19
|
+
* a narrow panel must.
|
|
20
|
+
*
|
|
16
21
|
* Deliberately NOT the same thing as Property's stacked field label (the
|
|
17
22
|
* 9px `menu`-token one): they were merged for a day and unmerged by
|
|
18
23
|
* Katerina's ruling (2026-09-01) — a section title and a field label are
|
|
19
24
|
* different voices, at different sizes, on purpose.
|
|
20
25
|
*/
|
|
21
|
-
export function SectionLabel({
|
|
26
|
+
export function SectionLabel({
|
|
27
|
+
children,
|
|
28
|
+
tone = 'primary',
|
|
29
|
+
truncate = false,
|
|
30
|
+
className,
|
|
31
|
+
}: {
|
|
32
|
+
children: ReactNode
|
|
33
|
+
tone?: 'primary' | 'secondary' | 'muted'
|
|
34
|
+
/** Cut a long label short with an ellipsis instead of letting it wrap or spill. */
|
|
35
|
+
truncate?: boolean
|
|
36
|
+
className?: string
|
|
37
|
+
}) {
|
|
22
38
|
return (
|
|
23
39
|
<span
|
|
24
40
|
className={cn(
|
|
25
41
|
'text-h5 leading-3 signal:font-mono signal:text-small signal:uppercase signal:tracking-widest',
|
|
26
|
-
tone === 'secondary' ? 'text-text-secondary' : 'text-text-primary',
|
|
42
|
+
tone === 'secondary' ? 'text-text-secondary' : tone === 'muted' ? 'text-text-muted' : 'text-text-primary',
|
|
43
|
+
truncate && 'min-w-0 truncate',
|
|
27
44
|
className,
|
|
28
45
|
)}
|
|
29
46
|
>
|
package/src/Select.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { cn } from './cn'
|
|
|
5
5
|
import { ScrollArea } from './ScrollArea'
|
|
6
6
|
import { MenuPanel, menuItemClassName } from './Menu'
|
|
7
7
|
import { TooltipTrigger } from './Tooltip'
|
|
8
|
+
import { FIELD_DISABLED_CLASSES, FIELD_SIZE_CLASSES, FIELD_TEXT_CLASSES } from './looks'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Peek's Select (2026-08-28), verbatim, plus what Ship added: an option may
|
|
@@ -73,6 +74,7 @@ export function Select({ value, onChange, options, size = 'default', ariaLabel,
|
|
|
73
74
|
// a native `disabled` button, which Tab skips and a tooltip cannot open on.
|
|
74
75
|
const held = Boolean(disabledReason)
|
|
75
76
|
const trigger = (
|
|
77
|
+
// @estiva-escape: N1 (Katerina, 24 September: record only): Select's box drops the hover while held, so it cannot take FIELD_SHELL_CLASSES as it is
|
|
76
78
|
<BaseSelect.Trigger
|
|
77
79
|
aria-label={ariaLabel}
|
|
78
80
|
aria-required={aria['aria-required']}
|
|
@@ -87,17 +89,17 @@ export function Select({ value, onChange, options, size = 'default', ariaLabel,
|
|
|
87
89
|
* the cap. Full-width callers are unaffected.
|
|
88
90
|
*/
|
|
89
91
|
'flex w-full min-w-0 max-w-full items-center justify-between gap-2 rounded-lg border bg-bg-inset text-left',
|
|
90
|
-
'border-border-default
|
|
92
|
+
'border-border-default outline-none transition-colors',
|
|
93
|
+
FIELD_TEXT_CLASSES,
|
|
91
94
|
// The focused border survives a hover: hover alone strengthens the
|
|
92
95
|
// hairline, but hover while focused must not grey the focus colour —
|
|
93
96
|
// the stacked variant outranks plain hover by specificity.
|
|
94
97
|
!held && 'hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus',
|
|
95
|
-
|
|
98
|
+
FIELD_DISABLED_CLASSES,
|
|
96
99
|
held && 'cursor-not-allowed bg-bg-disabled text-text-disabled',
|
|
97
100
|
'focus-visible:border-border-focus aria-expanded:border-border-focus',
|
|
98
101
|
'signal:transition-shadow signal:focus-visible:shadow-focus-ring',
|
|
99
|
-
size
|
|
100
|
-
size === 'small' && 'h-6 min-h-6 px-2 text-caption',
|
|
102
|
+
FIELD_SIZE_CLASSES[size],
|
|
101
103
|
className,
|
|
102
104
|
)}
|
|
103
105
|
>
|
package/src/TextInput.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { forwardRef, type InputHTMLAttributes } from 'react'
|
|
2
2
|
import { Input } from '@base-ui/react/input'
|
|
3
3
|
import { cn } from './cn'
|
|
4
|
+
import { FIELD_BOX_CLASSES, FIELD_DISABLED_CLASSES, FIELD_FOCUS_RING_CLASSES, FIELD_SIZE_CLASSES, FIELD_TEXT_CLASSES } from './looks'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Peek's TextInput (2026-08-28): the inset field with a 8px radius, 14px
|
|
@@ -35,14 +36,13 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
|
|
|
35
36
|
// The border strengthens on hover, as Select's and ChipInput's do (Katerina, 16 September:
|
|
36
37
|
// "aren't there hover states in text input and text area?"). Focus comes after hover in
|
|
37
38
|
// Tailwind's order, so a focused field keeps the focus border under the pointer.
|
|
38
|
-
|
|
39
|
+
FIELD_BOX_CLASSES,
|
|
39
40
|
// The small size is the small Select's trigger, class for class.
|
|
40
|
-
size
|
|
41
|
-
|
|
42
|
-
'text-text-primary placeholder:text-text-muted',
|
|
41
|
+
FIELD_SIZE_CLASSES[size],
|
|
42
|
+
FIELD_TEXT_CLASSES,
|
|
43
43
|
'outline-none transition-colors',
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
FIELD_DISABLED_CLASSES,
|
|
45
|
+
FIELD_FOCUS_RING_CLASSES,
|
|
46
46
|
className,
|
|
47
47
|
)}
|
|
48
48
|
{...props}
|
package/src/Textarea.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { forwardRef, type TextareaHTMLAttributes } from 'react'
|
|
2
2
|
import { Field as BaseField } from '@base-ui/react/field'
|
|
3
3
|
import { cn } from './cn'
|
|
4
|
+
import { FIELD_BOX_CLASSES, FIELD_DISABLED_CLASSES, FIELD_FOCUS_RING_CLASSES, FIELD_SIZE_CLASSES, FIELD_TEXT_CLASSES } from './looks'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Peek's Textarea (2026-08-28), verbatim: TextInput's look on a textarea
|
|
@@ -24,11 +25,12 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function
|
|
|
24
25
|
// The border strengthens on hover, as Select's and ChipInput's do (Katerina, 16 September:
|
|
25
26
|
// "aren't there hover states in text input and text area?"). Focus comes after hover in
|
|
26
27
|
// Tailwind's order, so a focused field keeps the focus border under the pointer.
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
FIELD_BOX_CLASSES,
|
|
29
|
+
FIELD_SIZE_CLASSES.default,
|
|
30
|
+
FIELD_TEXT_CLASSES,
|
|
29
31
|
'resize-none outline-none transition-colors',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
FIELD_DISABLED_CLASSES,
|
|
33
|
+
FIELD_FOCUS_RING_CLASSES,
|
|
32
34
|
className,
|
|
33
35
|
)}
|
|
34
36
|
{...(props as object)}
|