@estiva-app/ui 0.12.2 → 0.12.4
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/Button.d.ts.map +1 -1
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/index.js +31 -6
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/src/Button.stories.tsx +29 -0
- package/src/Button.test.tsx +18 -0
- package/src/Button.tsx +11 -0
- package/src/IconButton.stories.tsx +16 -0
- package/src/IconButton.test.tsx +16 -0
- package/src/IconButton.tsx +12 -1
- package/src/Menu.test.tsx +13 -0
- package/src/Menu.tsx +11 -8
package/package.json
CHANGED
package/src/Button.stories.tsx
CHANGED
|
@@ -30,6 +30,35 @@ export const Disabled: Story = { args: { variant: 'primary', disabled: true } }
|
|
|
30
30
|
export const WithAReason: Story = { args: { variant: 'primary', disabledReason: 'Sign in to add items' } }
|
|
31
31
|
|
|
32
32
|
/** Every variant × size × icon × disabled combination on one canvas. */
|
|
33
|
+
/* The box it is dropped into cannot change its height: a Button states `h-8`
|
|
34
|
+
(or `h-6` when small), and a stretching parent only stretches a child whose
|
|
35
|
+
height is `auto`. The row below says no `items-*` at all, which is the box
|
|
36
|
+
that drew an IconButton 228px tall — this stays 32. */
|
|
37
|
+
export const InATallRow: Story = {
|
|
38
|
+
parameters: { controls: { disable: true } },
|
|
39
|
+
render: () => (
|
|
40
|
+
<div className="flex h-[260px] w-[240px] justify-center rounded-lg border border-border-subtle p-4">
|
|
41
|
+
<Button variant="outlined">Button</Button>
|
|
42
|
+
</div>
|
|
43
|
+
),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* And the caller says where it sits. This column asks for the left; a Button
|
|
47
|
+
carries no `align-self` of its own, so the left is what it gets. */
|
|
48
|
+
export const InAColumnThatAsksForTheLeft: Story = {
|
|
49
|
+
parameters: { controls: { disable: true } },
|
|
50
|
+
render: () => (
|
|
51
|
+
<div className="flex w-[240px] flex-col items-start gap-2 rounded-lg border border-border-subtle p-4">
|
|
52
|
+
<Button variant="outlined" size="small">
|
|
53
|
+
Copy link
|
|
54
|
+
</Button>
|
|
55
|
+
<Button variant="outlined" size="small">
|
|
56
|
+
Pair with another Folder…
|
|
57
|
+
</Button>
|
|
58
|
+
</div>
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
export const AllVariants: Story = {
|
|
34
63
|
parameters: { controls: { disable: true } },
|
|
35
64
|
render: () => (
|
package/src/Button.test.tsx
CHANGED
|
@@ -105,6 +105,24 @@ describe('Button', () => {
|
|
|
105
105
|
expect(held).not.toHaveBeenCalled()
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
/* jsdom computes no layout, so the class list is what a test can hold on to.
|
|
109
|
+
Both halves matter: the height is why a stretching parent cannot change
|
|
110
|
+
this button, and the absence of `align-self` is why its caller keeps the
|
|
111
|
+
say on where it sits. The pixels behind both were measured in Chrome on
|
|
112
|
+
2026-09-12 — 32px in a 260px row either way, and left in a column that
|
|
113
|
+
asks for the left. */
|
|
114
|
+
it('states its own height and no alignment of its own', () => {
|
|
115
|
+
const { rerender } = render(<Button>Default</Button>)
|
|
116
|
+
const defaultButton = screen.getByRole('button', { name: 'Default' })
|
|
117
|
+
expect(defaultButton.className).toContain('h-8')
|
|
118
|
+
expect(defaultButton.className).not.toContain('self-')
|
|
119
|
+
|
|
120
|
+
rerender(<Button size="small">Small</Button>)
|
|
121
|
+
const smallButton = screen.getByRole('button', { name: 'Small' })
|
|
122
|
+
expect(smallButton.className).toContain('h-6')
|
|
123
|
+
expect(smallButton.className).not.toContain('self-')
|
|
124
|
+
})
|
|
125
|
+
|
|
108
126
|
it('passes native props through', () => {
|
|
109
127
|
render(
|
|
110
128
|
<Button form="f1" aria-pressed="true" data-x="y" className="mt-2">
|
package/src/Button.tsx
CHANGED
|
@@ -66,6 +66,17 @@ export function Button({
|
|
|
66
66
|
focusableWhenDisabled={!!disabledReason}
|
|
67
67
|
className={(state) =>
|
|
68
68
|
cn(
|
|
69
|
+
// No `self-center` here, deliberately — `IconButton` needs it and this
|
|
70
|
+
// does not, and the difference is the `h-8` / `h-6` on the next two
|
|
71
|
+
// lines. `align-items: stretch` only stretches a child whose cross
|
|
72
|
+
// size is `auto`; a Button always states its height, so a stretching
|
|
73
|
+
// parent cannot change it. Measured in Chrome on 2026-09-12, in the
|
|
74
|
+
// 260px row of Finding 49: 32px with `self-center` and 32px without.
|
|
75
|
+
// What the class DID do was override the caller on the other axis —
|
|
76
|
+
// in a column it beats the parent's `items-start`, which centred the
|
|
77
|
+
// four actions in Ship's `ProjectRail` and the trigger in five of
|
|
78
|
+
// this package's own story frames, each of which asks for the top.
|
|
79
|
+
// A control does not get to decide where its caller puts it.
|
|
69
80
|
'inline-flex items-center justify-center gap-1 rounded-md transition-colors font-sans font-medium',
|
|
70
81
|
size === 'default' && 'h-8 text-btn-default',
|
|
71
82
|
size === 'small' && 'h-6 text-btn-small',
|
|
@@ -47,3 +47,19 @@ export const AllVariants: Story = {
|
|
|
47
47
|
</div>
|
|
48
48
|
),
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A button is the size of its icon and its padding, whatever box it is dropped
|
|
53
|
+
* into. A flex parent with no `items-*` of its own stretches whatever is in it,
|
|
54
|
+
* and this one is 260px tall; the button stays 24px square.
|
|
55
|
+
*/
|
|
56
|
+
export const InATallRow: Story = {
|
|
57
|
+
parameters: { controls: { disable: true } },
|
|
58
|
+
render: () => (
|
|
59
|
+
<div className="flex h-[260px] w-[200px] justify-end rounded-lg border border-border-subtle p-4">
|
|
60
|
+
<IconButton aria-label="Settings">
|
|
61
|
+
<IconSettings className="size-4" stroke={1.5} />
|
|
62
|
+
</IconButton>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
}
|
package/src/IconButton.test.tsx
CHANGED
|
@@ -102,4 +102,20 @@ describe('IconButton', () => {
|
|
|
102
102
|
await user.tab()
|
|
103
103
|
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'After' }))
|
|
104
104
|
})
|
|
105
|
+
|
|
106
|
+
/*
|
|
107
|
+
A flex parent with no `items-*` stretches its children. `shrink-0` does
|
|
108
|
+
not stop that — it is the other direction — so this button grew to 228px
|
|
109
|
+
tall in a 260px row (Katerina, 2026-09-11). jsdom computes no layout, so
|
|
110
|
+
the class is what can be asserted here; the story beside it is what shows
|
|
111
|
+
the pixels.
|
|
112
|
+
*/
|
|
113
|
+
it('keeps its own size in a flex parent that would stretch it', () => {
|
|
114
|
+
render(
|
|
115
|
+
<div className="flex h-[260px]">
|
|
116
|
+
<IconButton aria-label="Edit">{icon}</IconButton>
|
|
117
|
+
</div>,
|
|
118
|
+
)
|
|
119
|
+
expect(screen.getByRole('button', { name: 'Edit' }).className).toContain('self-center')
|
|
120
|
+
})
|
|
105
121
|
})
|
package/src/IconButton.tsx
CHANGED
|
@@ -49,7 +49,18 @@ export function IconButton({
|
|
|
49
49
|
focusableWhenDisabled={!!disabledReason}
|
|
50
50
|
className={(state) =>
|
|
51
51
|
cn(
|
|
52
|
-
|
|
52
|
+
// `shrink-0` stops a flex parent squashing the button; `self-center`
|
|
53
|
+
// stops one stretching it. Two different failures, and both have
|
|
54
|
+
// happened here: a row with no `items-*` drew this 24 wide and 228
|
|
55
|
+
// tall in Peek's TopicMoreMenu story (Katerina, 2026-09-11). A button
|
|
56
|
+
// is the size of its icon and its padding, whatever box it lands in.
|
|
57
|
+
//
|
|
58
|
+
// This is the one of the two that needs it: an IconButton states no
|
|
59
|
+
// height, so its cross size is `auto` and a stretching parent takes
|
|
60
|
+
// it. Measured in the same 260px row on 2026-09-12: 24px with this
|
|
61
|
+
// class, 226px without. `Button` states `h-8`/`h-6`, so it cannot be
|
|
62
|
+
// stretched and carries no `self-center` — see the note there.
|
|
63
|
+
'flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 self-center cursor-pointer',
|
|
53
64
|
!state.disabled && variant === 'primary' && 'bg-accent-primary hover:bg-accent-hover text-text-inverse',
|
|
54
65
|
!state.disabled && variant === 'muted' && 'text-text-secondary hover:bg-bg-hover hover:text-text-primary',
|
|
55
66
|
!state.disabled && variant === 'outlined' && 'border border-border-default hover:bg-bg-hover text-text-secondary',
|
package/src/Menu.test.tsx
CHANGED
|
@@ -288,6 +288,19 @@ describe('rows on a bare MenuPanel', () => {
|
|
|
288
288
|
expect(screen.queryByRole('menuitem')).toBeNull()
|
|
289
289
|
})
|
|
290
290
|
|
|
291
|
+
it('a default row is never shorter than 36px, and a tall one never shorter than 40', () => {
|
|
292
|
+
render(
|
|
293
|
+
<MenuPanel>
|
|
294
|
+
<MenuItem label="Default" onClick={() => {}} />
|
|
295
|
+
<MenuItem size="tall" label="Tall" onClick={() => {}} />
|
|
296
|
+
</MenuPanel>,
|
|
297
|
+
)
|
|
298
|
+
// jsdom computes no layout, so the floor class is what it can assert;
|
|
299
|
+
// the story beside it is what shows the pixels.
|
|
300
|
+
expect(screen.getByRole('button', { name: 'Default' }).className).toContain('min-h-9')
|
|
301
|
+
expect(screen.getByRole('button', { name: 'Tall' }).className).toContain('min-h-10')
|
|
302
|
+
})
|
|
303
|
+
|
|
291
304
|
it('its onClick still runs', async () => {
|
|
292
305
|
const user = userEvent.setup()
|
|
293
306
|
const onPick = vi.fn()
|
package/src/Menu.tsx
CHANGED
|
@@ -20,9 +20,9 @@ import { SectionLabel } from './SectionLabel'
|
|
|
20
20
|
*
|
|
21
21
|
* What every menu shares, kept exactly: an elevated container with a
|
|
22
22
|
* hairline border, 8px radius, 8px padding and the large shadow; items that
|
|
23
|
-
* are 8px-radius rows, `px-2 py-1.5
|
|
24
|
-
* ones in the error colour; section headings
|
|
25
|
-
* row; and the two exits every menu has — Escape and a click outside.
|
|
23
|
+
* are 8px-radius rows, `px-2 py-1.5` and never shorter than 36px, hover
|
|
24
|
+
* fill, 14px text, destructive ones in the error colour; section headings
|
|
25
|
+
* as a SectionLabel in a 32px row; and the two exits every menu has — Escape and a click outside.
|
|
26
26
|
*
|
|
27
27
|
* **What stage 4 changed, and it is the first thing a keyboard user notices:
|
|
28
28
|
* the arrow keys walk the rows.** ↑ and ↓ move between items and wrap, Home
|
|
@@ -368,11 +368,14 @@ function menuItemClassName({ size, selected, className }: { size: 'default' | 't
|
|
|
368
368
|
// on exactly the same :hover, so they cannot come apart, and a row
|
|
369
369
|
// lights and unlights crisply as the pointer crosses it.
|
|
370
370
|
'group flex w-full shrink-0 cursor-pointer items-center rounded-lg text-left outline-none hover:bg-bg-hover data-[highlighted]:bg-bg-hover',
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
|
|
371
|
+
// Both sizes are as tall as their content and never shorter than a
|
|
372
|
+
// floor: 36px for a default row (Katerina, 2026-09-11), 40px for a
|
|
373
|
+
// tall one (Katerina, 2026-09-01). A single-line action row sits at
|
|
374
|
+
// 36, a picker row at 40, a row with a 32px face and a role line
|
|
375
|
+
// comes out at its natural 48. One rule, not a hand-picked height
|
|
376
|
+
// per file — Peek's Later menu had been forcing `h-9` for exactly
|
|
377
|
+
// this 36px and was the only menu in either app out of step.
|
|
378
|
+
size === 'tall' ? 'min-h-10 gap-3 px-3 py-1.5' : 'min-h-9 gap-2 px-2 py-1.5',
|
|
376
379
|
selected && 'bg-bg-hover',
|
|
377
380
|
className,
|
|
378
381
|
)
|