@estiva-app/ui 0.12.2 → 0.12.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/Button.tsx CHANGED
@@ -66,7 +66,13 @@ export function Button({
66
66
  focusableWhenDisabled={!!disabledReason}
67
67
  className={(state) =>
68
68
  cn(
69
- 'inline-flex items-center justify-center gap-1 rounded-md transition-colors font-sans font-medium',
69
+ // `self-center` for the same reason as IconButton's: a flex parent
70
+ // with no `items-*` stretches its children, and a button stretched to
71
+ // a column's height is not a button. `h-8` and `h-6` below set the
72
+ // height, and stretching overrode them. No caller in the suite wants
73
+ // a stretched one — checked across both apps and this package on
74
+ // 2026-09-11, and none passes `w-full` either.
75
+ 'inline-flex items-center justify-center gap-1 rounded-md transition-colors font-sans font-medium self-center',
70
76
  size === 'default' && 'h-8 text-btn-default',
71
77
  size === 'small' && 'h-6 text-btn-small',
72
78
  // Extra right padding beside a leading icon, for optical balance.
@@ -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
+ }
@@ -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
  })
@@ -49,7 +49,12 @@ export function IconButton({
49
49
  focusableWhenDisabled={!!disabledReason}
50
50
  className={(state) =>
51
51
  cn(
52
- 'flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 cursor-pointer',
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
+ 'flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 self-center cursor-pointer',
53
58
  !state.disabled && variant === 'primary' && 'bg-accent-primary hover:bg-accent-hover text-text-inverse',
54
59
  !state.disabled && variant === 'muted' && 'text-text-secondary hover:bg-bg-hover hover:text-text-primary',
55
60
  !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`, hover fill, 14px text, destructive
24
- * ones in the error colour; section headings as a SectionLabel in a 32px
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
- // tall: as tall as its content, never shorter than 40px (Katerina,
372
- // 2026-09-01) a single-line picker row sits at 40, a row with a
373
- // 32px face and a role line comes out at its natural 48. One rule,
374
- // not a hand-picked height per file.
375
- size === 'tall' ? 'min-h-10 gap-3 px-3 py-1.5' : 'gap-2 px-2 py-1.5',
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
  )