@estiva-app/ui 0.23.0 → 0.24.1

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.
Files changed (120) hide show
  1. package/dist/AppShell.d.ts.map +1 -1
  2. package/dist/AttachmentCard.d.ts.map +1 -1
  3. package/dist/Form.d.ts.map +1 -1
  4. package/dist/IconButton.d.ts +9 -1
  5. package/dist/IconButton.d.ts.map +1 -1
  6. package/dist/ListColumn.d.ts +41 -0
  7. package/dist/ListColumn.d.ts.map +1 -0
  8. package/dist/PreviewCard.d.ts +9 -2
  9. package/dist/PreviewCard.d.ts.map +1 -1
  10. package/dist/SectionHeader.d.ts +7 -1
  11. package/dist/SectionHeader.d.ts.map +1 -1
  12. package/dist/Select.d.ts +12 -3
  13. package/dist/Select.d.ts.map +1 -1
  14. package/dist/Toolbar.d.ts +18 -0
  15. package/dist/Toolbar.d.ts.map +1 -1
  16. package/dist/Tooltip.d.ts +13 -1
  17. package/dist/Tooltip.d.ts.map +1 -1
  18. package/dist/eslint/index.js +2 -1
  19. package/dist/eslint/index.js.map +2 -2
  20. package/dist/eslint/no-restyled-part.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +189 -143
  24. package/dist/index.js.map +4 -4
  25. package/package.json +1 -1
  26. package/registry.json +214 -11
  27. package/src/AppShell.mdx +18 -1
  28. package/src/AppShell.test.tsx +16 -0
  29. package/src/AppShell.tsx +4 -1
  30. package/src/AttachmentCard.mdx +12 -1
  31. package/src/AttachmentCard.test.tsx +10 -0
  32. package/src/AttachmentCard.tsx +8 -5
  33. package/src/Avatar.mdx +20 -5
  34. package/src/AvatarGroup.mdx +4 -0
  35. package/src/Banner.mdx +17 -8
  36. package/src/Breadcrumb.mdx +12 -0
  37. package/src/Button.mdx +17 -5
  38. package/src/Button.tsx +2 -2
  39. package/src/Card.mdx +14 -1
  40. package/src/Checkbox.mdx +11 -0
  41. package/src/Chip.mdx +4 -0
  42. package/src/ChipInput.mdx +13 -1
  43. package/src/CollapsibleSection.mdx +12 -3
  44. package/src/CommandPalette.mdx +14 -2
  45. package/src/ConfirmDialog.mdx +13 -2
  46. package/src/ContainerHeader.mdx +15 -2
  47. package/src/DialogShell.mdx +15 -1
  48. package/src/Divider.mdx +12 -1
  49. package/src/EditableText.mdx +17 -1
  50. package/src/EmptyState.mdx +17 -6
  51. package/src/Field.mdx +13 -5
  52. package/src/FieldLine.mdx +9 -1
  53. package/src/FilePicker.mdx +8 -0
  54. package/src/Form.mdx +11 -1
  55. package/src/Form.test.tsx +26 -0
  56. package/src/Form.tsx +7 -0
  57. package/src/IconButton.mdx +17 -3
  58. package/src/IconButton.stories.tsx +3 -0
  59. package/src/IconButton.test.tsx +25 -0
  60. package/src/IconButton.tsx +61 -46
  61. package/src/IdentityMenu.mdx +10 -1
  62. package/src/InlineChip.mdx +11 -0
  63. package/src/Kbd.mdx +6 -2
  64. package/src/Link.mdx +13 -1
  65. package/src/ListColumn.mdx +87 -0
  66. package/src/ListColumn.stories.tsx +136 -0
  67. package/src/ListColumn.test.tsx +50 -0
  68. package/src/ListColumn.tsx +63 -0
  69. package/src/Menu.mdx +18 -1
  70. package/src/MenuItem.mdx +15 -2
  71. package/src/NavItem.mdx +12 -0
  72. package/src/Person.mdx +7 -0
  73. package/src/PersonTrigger.mdx +11 -1
  74. package/src/PersonTrigger.tsx +1 -1
  75. package/src/Popover.mdx +23 -0
  76. package/src/PreviewCard.mdx +19 -4
  77. package/src/PreviewCard.tsx +11 -4
  78. package/src/ProgressBar.mdx +8 -0
  79. package/src/Property.mdx +4 -0
  80. package/src/Rail.mdx +12 -1
  81. package/src/RailItem.mdx +12 -0
  82. package/src/RailItem.tsx +1 -1
  83. package/src/Reaction.mdx +10 -0
  84. package/src/Reaction.tsx +1 -1
  85. package/src/ReactionPicker.mdx +8 -0
  86. package/src/ScrollArea.mdx +16 -2
  87. package/src/SearchInput.mdx +9 -0
  88. package/src/SectionHeader.mdx +18 -1
  89. package/src/SectionHeader.stories.tsx +9 -0
  90. package/src/SectionHeader.test.tsx +9 -0
  91. package/src/SectionHeader.tsx +9 -3
  92. package/src/SectionLabel.mdx +10 -2
  93. package/src/Select.mdx +24 -4
  94. package/src/Select.stories.tsx +4 -1
  95. package/src/Select.test.tsx +29 -0
  96. package/src/Select.tsx +36 -14
  97. package/src/Sidebar.mdx +8 -0
  98. package/src/Skeleton.mdx +8 -0
  99. package/src/Skeleton.tsx +1 -1
  100. package/src/Tabs.mdx +12 -1
  101. package/src/TextInput.mdx +9 -0
  102. package/src/TextInput.tsx +1 -1
  103. package/src/Textarea.mdx +8 -0
  104. package/src/Toast.mdx +12 -1
  105. package/src/Toolbar.mdx +20 -2
  106. package/src/Toolbar.stories.tsx +19 -2
  107. package/src/Toolbar.test.tsx +24 -1
  108. package/src/Toolbar.tsx +22 -0
  109. package/src/Tooltip.mdx +27 -2
  110. package/src/Tooltip.stories.tsx +26 -0
  111. package/src/Tooltip.test.tsx +51 -0
  112. package/src/Tooltip.tsx +17 -4
  113. package/src/TopBar.mdx +11 -0
  114. package/src/eslint/no-rebuilt-behaviour.ts +1 -1
  115. package/src/eslint/no-restyled-part.ts +1 -0
  116. package/src/heights.test.tsx +78 -0
  117. package/src/index.ts +2 -1
  118. package/src/pages.test.ts +142 -0
  119. package/src/registry/registry.test.ts +16 -8
  120. package/tailwind-preset.js +22 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
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/registry.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "kind": "package",
5
5
  "repo": "estiva-ui",
6
6
  "package": "@estiva-app/ui",
7
- "packageVersion": "0.23.0",
8
- "exports": 82,
9
- "typeExports": 92,
7
+ "packageVersion": "0.24.1",
8
+ "exports": 84,
9
+ "typeExports": 94,
10
10
  "files": null
11
11
  },
12
12
  "storybook": {
@@ -397,7 +397,7 @@
397
397
  "kind": "component",
398
398
  "importPath": "@estiva-app/ui",
399
399
  "sourceFile": "src/Button.tsx",
400
- "purpose": "The push-button for an action: a verb, clicked once — \"Create\", \"Save\", \"Delete project\".",
400
+ "purpose": "The push-button for an action: a verb, clicked once — \"Create\", \"Save\", \"Delete\".",
401
401
  "purposeFrom": "page",
402
402
  "props": [
403
403
  {
@@ -1436,7 +1436,7 @@
1436
1436
  },
1437
1437
  {
1438
1438
  "id": "page-keys",
1439
- "behaviour": "Closes on Escape, and takes its keys, by itself"
1439
+ "behaviour": "Takes its keys by itself"
1440
1440
  },
1441
1441
  {
1442
1442
  "id": "focus",
@@ -1927,6 +1927,12 @@
1927
1927
  "required": false,
1928
1928
  "note": "Why the action cannot succeed right now."
1929
1929
  },
1930
+ {
1931
+ "name": "href",
1932
+ "takes": "text",
1933
+ "required": false,
1934
+ "note": "A link that looks like an icon button: an anchor, so the address stays real for a modified click or a new tab, as `Link` keeps it."
1935
+ },
1930
1936
  {
1931
1937
  "name": "children",
1932
1938
  "takes": "anything",
@@ -2389,6 +2395,81 @@
2389
2395
  "docPage": "src/Link.mdx",
2390
2396
  "app": null
2391
2397
  },
2398
+ {
2399
+ "name": "ListColumn",
2400
+ "repo": "estiva-ui",
2401
+ "kind": "component",
2402
+ "importPath": "@estiva-app/ui",
2403
+ "sourceFile": "src/ListColumn.tsx",
2404
+ "purpose": "The list column of a page: 290px wide with a line on its right, its name in a `ContainerHeader`, and the list under it, which scrolls.",
2405
+ "purposeFrom": "page",
2406
+ "props": [
2407
+ {
2408
+ "name": "title",
2409
+ "takes": "anything",
2410
+ "required": true,
2411
+ "note": "The column's name, in its header. A node for a title that is more than words."
2412
+ },
2413
+ {
2414
+ "name": "chevron",
2415
+ "takes": "true/false",
2416
+ "required": false,
2417
+ "note": "A chevron after the title, as `ContainerHeader` draws it."
2418
+ },
2419
+ {
2420
+ "name": "actions",
2421
+ "takes": "anything",
2422
+ "required": false,
2423
+ "note": "The column's own actions, at the header's right — a `Toolbar` of `ToolbarButton`s, or one `IconButton`."
2424
+ },
2425
+ {
2426
+ "name": "above",
2427
+ "takes": "anything",
2428
+ "required": false,
2429
+ "note": "A row between the header and the list that stays put while the list scrolls — a field that adds to the list."
2430
+ },
2431
+ {
2432
+ "name": "spacing",
2433
+ "takes": "rows | sections",
2434
+ "required": false,
2435
+ "note": "The room between rows. `rows` (2px) for a list of one kind of row; `sections` (4px) for groups with labels and dividers of their own."
2436
+ },
2437
+ {
2438
+ "name": "collapsed",
2439
+ "takes": "true/false",
2440
+ "required": false,
2441
+ "note": "Closes the column with the rail: it narrows to nothing and fades."
2442
+ },
2443
+ {
2444
+ "name": "children",
2445
+ "takes": "anything",
2446
+ "required": true,
2447
+ "note": "The rows."
2448
+ },
2449
+ {
2450
+ "name": "className",
2451
+ "takes": "text",
2452
+ "required": false,
2453
+ "note": "Placement only."
2454
+ }
2455
+ ],
2456
+ "variants": [
2457
+ {
2458
+ "prop": "spacing",
2459
+ "values": [
2460
+ "rows",
2461
+ "sections"
2462
+ ]
2463
+ }
2464
+ ],
2465
+ "ownsBehaviours": [],
2466
+ "status": "stable",
2467
+ "migrationStage": null,
2468
+ "docsId": "layout-listcolumn--docs",
2469
+ "storyId": "layout-listcolumn--default",
2470
+ "docPage": "src/ListColumn.mdx",
2471
+ "app": null
2472
+ },
2392
2473
  {
2393
2474
  "name": "Menu",
2394
2475
  "repo": "estiva-ui",
@@ -2477,7 +2558,7 @@
2477
2558
  },
2478
2559
  {
2479
2560
  "id": "page-keys",
2480
- "behaviour": "Closes on Escape, and takes its keys, by itself"
2561
+ "behaviour": "Takes its keys by itself"
2481
2562
  },
2482
2563
  {
2483
2564
  "id": "follow",
@@ -3019,7 +3100,7 @@
3019
3100
  },
3020
3101
  {
3021
3102
  "id": "page-keys",
3022
- "behaviour": "Closes on Escape, and takes its keys, by itself"
3103
+ "behaviour": "Takes its keys by itself"
3023
3104
  },
3024
3105
  {
3025
3106
  "id": "follow",
@@ -3076,7 +3157,13 @@
3076
3157
  "name": "className",
3077
3158
  "takes": "text",
3078
3159
  "required": false,
3079
- "note": "On the card's surface: its width, its padding, a max height."
3160
+ "note": "On the card's surface: its width."
3161
+ },
3162
+ {
3163
+ "name": "contentClassName",
3164
+ "takes": "text",
3165
+ "required": false,
3166
+ "note": "On the scrolling content: its padding (default 12px) and rhythm."
3080
3167
  },
3081
3168
  {
3082
3169
  "name": "wrapperClassName",
@@ -3587,6 +3674,12 @@
3587
3674
  "required": false,
3588
3675
  "note": "`hover` reveals the actions while the row is hovered or focused; `always` keeps them."
3589
3676
  },
3677
+ {
3678
+ "name": "hover",
3679
+ "takes": "fill | none",
3680
+ "required": false,
3681
+ "note": "`fill` lights the row under the pointer when it does something (a toggle, actions); `none` keeps it still — a heading whose actions are always shown, where the buttons light up on their own (UIG-14, Katerina, 19 September)."
3682
+ },
3590
3683
  {
3591
3684
  "name": "render",
3592
3685
  "takes": "useRender.RenderProp",
@@ -3607,6 +3700,13 @@
3607
3700
  "hover",
3608
3701
  "always"
3609
3702
  ]
3703
+ },
3704
+ {
3705
+ "prop": "hover",
3706
+ "values": [
3707
+ "fill",
3708
+ "none"
3709
+ ]
3610
3710
  }
3611
3711
  ],
3612
3712
  "ownsBehaviours": [],
@@ -3719,6 +3819,12 @@
3719
3819
  "required": false,
3720
3820
  "note": null
3721
3821
  },
3822
+ {
3823
+ "name": "disabledReason",
3824
+ "takes": "text",
3825
+ "required": false,
3826
+ "note": "Why it cannot be changed right now (UIG-14, Katerina, 19 September)."
3827
+ },
3722
3828
  {
3723
3829
  "name": "className",
3724
3830
  "takes": "text",
@@ -3750,7 +3856,7 @@
3750
3856
  },
3751
3857
  {
3752
3858
  "id": "page-keys",
3753
- "behaviour": "Closes on Escape, and takes its keys, by itself"
3859
+ "behaviour": "Takes its keys by itself"
3754
3860
  },
3755
3861
  {
3756
3862
  "id": "follow",
@@ -3964,7 +4070,7 @@
3964
4070
  },
3965
4071
  {
3966
4072
  "id": "page-keys",
3967
- "behaviour": "Closes on Escape, and takes its keys, by itself"
4073
+ "behaviour": "Takes its keys by itself"
3968
4074
  },
3969
4075
  {
3970
4076
  "id": "walking",
@@ -4204,7 +4310,7 @@
4204
4310
  },
4205
4311
  {
4206
4312
  "id": "page-keys",
4207
- "behaviour": "Closes on Escape, and takes its keys, by itself"
4313
+ "behaviour": "Takes its keys by itself"
4208
4314
  },
4209
4315
  {
4210
4316
  "id": "walking",
@@ -4279,6 +4385,12 @@
4279
4385
  "required": false,
4280
4386
  "note": "Why the action cannot succeed right now."
4281
4387
  },
4388
+ {
4389
+ "name": "href",
4390
+ "takes": "text",
4391
+ "required": false,
4392
+ "note": "A link that looks like an icon button: an anchor, so the address stays real for a modified click or a new tab, as `Link` keeps it."
4393
+ },
4282
4394
  {
4283
4395
  "name": "children",
4284
4396
  "takes": "anything",
@@ -4346,6 +4458,91 @@
4346
4458
  "docPage": null,
4347
4459
  "app": null
4348
4460
  },
4461
+ {
4462
+ "name": "ToolbarLink",
4463
+ "repo": "estiva-ui",
4464
+ "kind": "component",
4465
+ "importPath": "@estiva-app/ui",
4466
+ "sourceFile": "src/Toolbar.tsx",
4467
+ "purpose": "An `IconButton` that is a link, and an item of a `Toolbar`: the arrow keys reach it like the buttons beside it.",
4468
+ "purposeFrom": "comment",
4469
+ "props": [
4470
+ {
4471
+ "name": "href",
4472
+ "takes": "text",
4473
+ "required": true,
4474
+ "note": null
4475
+ },
4476
+ {
4477
+ "name": "ref",
4478
+ "takes": "Ref<HTMLAnchorElement>",
4479
+ "required": false,
4480
+ "note": null
4481
+ },
4482
+ {
4483
+ "name": "variant",
4484
+ "takes": "muted | outlined | primary | current | resolve",
4485
+ "required": false,
4486
+ "note": "`current` takes the colour of what it sits in — the ✕ on a `Banner` wears the banner's tone."
4487
+ },
4488
+ {
4489
+ "name": "glow",
4490
+ "takes": "true/false",
4491
+ "required": false,
4492
+ "note": "A glow in Signal — the send arrow while there is something to send."
4493
+ },
4494
+ {
4495
+ "name": "tooltip",
4496
+ "takes": "text",
4497
+ "required": false,
4498
+ "note": null
4499
+ },
4500
+ {
4501
+ "name": "tooltipShortcut",
4502
+ "takes": "text",
4503
+ "required": false,
4504
+ "note": "A key hint drawn as the `Kbd` chip inside the tooltip — for a button whose only other affordance is a keyboard shortcut."
4505
+ },
4506
+ {
4507
+ "name": "tooltipPlacement",
4508
+ "takes": "top | bottom",
4509
+ "required": false,
4510
+ "note": null
4511
+ },
4512
+ {
4513
+ "name": "children",
4514
+ "takes": "anything",
4515
+ "required": true,
4516
+ "note": "The icon: 16px, stroke 1.5."
4517
+ }
4518
+ ],
4519
+ "variants": [
4520
+ {
4521
+ "prop": "variant",
4522
+ "values": [
4523
+ "muted",
4524
+ "outlined",
4525
+ "primary",
4526
+ "current",
4527
+ "resolve"
4528
+ ]
4529
+ },
4530
+ {
4531
+ "prop": "tooltipPlacement",
4532
+ "values": [
4533
+ "top",
4534
+ "bottom"
4535
+ ]
4536
+ }
4537
+ ],
4538
+ "ownsBehaviours": [],
4539
+ "status": "stable",
4540
+ "migrationStage": null,
4541
+ "docsId": "primitives-toolbar--docs",
4542
+ "storyId": "primitives-toolbar--default",
4543
+ "docPage": null,
4544
+ "app": null
4545
+ },
4349
4546
  {
4350
4547
  "name": "ToolbarSeparator",
4351
4548
  "repo": "estiva-ui",
@@ -4576,6 +4773,12 @@
4576
4773
  "required": false,
4577
4774
  "note": "Extra classes on the wrapper — e.g. `min-w-0 shrink` so a truncating label keeps truncating inside it."
4578
4775
  },
4776
+ {
4777
+ "name": "inline",
4778
+ "takes": "true/false",
4779
+ "required": false,
4780
+ "note": "For a trigger inside a line of text — a reference, a name in a sentence: the wrapper is a `<span>`, because a `<div>` is not allowed inside a paragraph (UIG-14, Katerina, 19 September)."
4781
+ },
4579
4782
  {
4580
4783
  "name": "children",
4581
4784
  "takes": "anything",
package/src/AppShell.mdx CHANGED
@@ -52,7 +52,6 @@ flex column. A page is a flex child of `main`, and says one of two things:
52
52
  Never `h-full`: the frame's content box is *at least* the viewport's
53
53
  height and grows with a tall page — which is what lets the bar know the
54
54
  page changed — so a percentage height has nothing to resolve against.
55
- Measured on all four of Ship's pages (2026-09-09).
56
55
 
57
56
  ```tsx
58
57
  import { AppShell, Banner, IdentityMenu, Rail, RailItem } from '@estiva-app/ui'
@@ -75,6 +74,24 @@ import { AppShell, Banner, IdentityMenu, Rail, RailItem } from '@estiva-app/ui'
75
74
  - In the floating manner the card owns `overflow-hidden` — your content
76
75
  manages its own scrolling inside it.
77
76
  - Desktop only for now — no narrow-screen behaviour is provided.
77
+ - In the Signal theme the floating frame draws the control-room dot grid on
78
+ its ground, behind the card. Nothing to add: the preset makes it.
79
+
80
+ - In the floating manner the bar takes the top 52px; the card sits 16px from
81
+ the right and the bottom, with 16px corners, beside the 64px rail.
82
+ - In the floating manner the card owns `overflow-hidden`, so anything in it
83
+ that can grow sits in a **ScrollArea** — a list column most of all, which
84
+ **ListColumn** draws with its own. Without one it is cut off at the fold.
85
+ - **Planned:** `SplitLayout`, resizable panels, in `@estiva-app/ui/layout`
86
+ (migration stage 8). It may take the list and side columns' widths.
87
+
88
+ ## What it owns
89
+
90
+ It does these by itself. Write none of them around it.
91
+
92
+ | It does | So you never write | Refused when written by hand |
93
+ |---|---|---|
94
+ | Scrolls the page's content column, in a ScrollArea | `overflow-auto` on the page or on `main` | |
78
95
 
79
96
  ## Props
80
97
 
@@ -36,4 +36,20 @@ describe('AppShell', () => {
36
36
  expect(main.className).toContain('overflow-hidden')
37
37
  expect(main.closest('[style*="overflow: scroll"]')).toBeNull()
38
38
  })
39
+
40
+ it('carries the Signal canvas on the floating frame, and only there', () => {
41
+ const { container, unmount } = render(
42
+ <AppShell variant="floating" nav={<nav>Nav</nav>}>
43
+ <p>Page</p>
44
+ </AppShell>,
45
+ )
46
+ expect((container.firstElementChild as HTMLElement).className).toContain('signal-canvas')
47
+ unmount()
48
+ const solid = render(
49
+ <AppShell nav={<nav>Nav</nav>}>
50
+ <p>Page</p>
51
+ </AppShell>,
52
+ )
53
+ expect(solid.container.querySelector('.signal-canvas')).toBeNull()
54
+ })
39
55
  })
package/src/AppShell.tsx CHANGED
@@ -45,7 +45,10 @@ export function AppShell({ variant = 'solid', menu, logo, search, identity, bann
45
45
 
46
46
  if (variant === 'floating') {
47
47
  return (
48
- <div className="relative h-full min-h-0 overflow-hidden bg-bg-base">
48
+ // `signal-canvas`: in the Signal theme, the preset draws the control-room dot
49
+ // grid behind everything in the frame (Peek's, moved here with the frame:
50
+ // UIG-14, Katerina, 19 September — the canvas is the theme's, not an app's).
51
+ <div className="signal-canvas relative h-full min-h-0 overflow-hidden bg-bg-base">
49
52
  {bar}
50
53
  <div className="flex h-full pb-4 pr-4 pt-[52px]">
51
54
  {nav}
@@ -53,10 +53,21 @@ import { AttachmentCard } from '@estiva-app/ui'
53
53
  - The name truncates. When it is cut off, the full name is on hover; a name
54
54
  that fits, and a size, show nothing extra. `noteHint` is always on hover, since
55
55
  it says more than the line.
56
- - The remove control is Peek's 20px round badge on the card's corner, a button
56
+ - The remove control is a 20px round badge on the card's corner, a button
57
57
  on Base UI's `Button` like InputChip's ✕ — not an `IconButton`, the 24px
58
58
  square that fills on hover.
59
59
 
60
+ - **Planned:** a Lightbox that opens a picture full screen, in migration stage 7.
61
+
62
+ ## What it owns
63
+
64
+ It does these by itself. Write none of them around it.
65
+
66
+ | It does | So you never write | Refused when written by hand |
67
+ |---|---|---|
68
+ | Stops the clicks on its picture, its download and its remove control, so they never also click the card or row around it | `stopPropagation` | |
69
+ | Shows the full name on hover when the name is cut off | a tooltip of your own | |
70
+
60
71
  ## Props
61
72
 
62
73
  <Controls of={AttachmentCardStories.Document} />
@@ -169,4 +169,14 @@ describe('AttachmentCard', () => {
169
169
  expect(onRemove).toHaveBeenCalledTimes(1)
170
170
  expect(outside).not.toHaveBeenCalled()
171
171
  })
172
+
173
+ it('the ✕ shows when the keyboard reaches it, not only on hover (C4)', async () => {
174
+ render(<AttachmentCard pending name="a.png" size={1} onRemove={() => {}} />)
175
+ const remove = screen.getByRole('button', { name: 'Remove a.png' })
176
+ // Hidden at rest; each of these reveals it. jsdom draws nothing, so the classes are the contract.
177
+ expect(remove.className).toContain('opacity-0')
178
+ for (const reveal of ['group-hover:opacity-100', 'group-focus-within:opacity-100', 'focus-visible:opacity-100']) expect(remove.className).toContain(reveal)
179
+ await userEvent.tab()
180
+ expect(document.activeElement).toBe(remove)
181
+ })
172
182
  })
@@ -20,7 +20,7 @@ import {
20
20
  import { Card } from './Card'
21
21
  import { IconButton } from './IconButton'
22
22
  import { Link } from './Link'
23
- import { WithTooltip } from './Tooltip'
23
+ import { WithTooltip, type WithTooltipProps } from './Tooltip'
24
24
  import { cn } from './cn'
25
25
 
26
26
  /**
@@ -98,7 +98,7 @@ function TypeIcon({ name }: { name: string }) {
98
98
  * the fallback face can fit and then not, in a line whose box never changes. The wrapper is `flex-col` so the
99
99
  * text stretches across it, as wide as it is without one.
100
100
  */
101
- function Truncating({ text, hint, className, wrapperClassName }: { text: string; hint?: string; className: string; wrapperClassName?: string }) {
101
+ function Truncating({ text, hint, className, wrapperClassName, placement }: { text: string; hint?: string; className: string; wrapperClassName?: string; placement?: WithTooltipProps['placement'] }) {
102
102
  const ref = useRef<HTMLSpanElement>(null)
103
103
  const [cut, setCut] = useState(false)
104
104
  useLayoutEffect(() => {
@@ -127,7 +127,7 @@ function Truncating({ text, hint, className, wrapperClassName }: { text: string;
127
127
  )
128
128
  if (!hint && !cut) return line
129
129
  return (
130
- <WithTooltip label={hint ?? text} wrapperClassName={cn('min-w-0 flex-col', wrapperClassName)}>
130
+ <WithTooltip label={hint ?? text} placement={placement} wrapperClassName={cn('min-w-0 flex-col', wrapperClassName)}>
131
131
  {line}
132
132
  </WithTooltip>
133
133
  )
@@ -206,7 +206,8 @@ export function AttachmentCard({
206
206
  )}
207
207
  </div>
208
208
  <div className="flex flex-col gap-[1px] min-w-0">
209
- <Truncating text={name} className={NAME_CLASSES} />
209
+ {/* Below, not above: above, a cut name's tooltip covered the ✕ on the corner (UIG-14, C4). */}
210
+ <Truncating text={name} className={NAME_CLASSES} placement={onRemove ? 'bottom' : undefined} />
210
211
  <Truncating
211
212
  text={(failed || warning ? note : state === 'uploading' ? (note ?? 'Uploading…') : (note ?? sizeText)) ?? ''}
212
213
  hint={noteHint}
@@ -219,7 +220,9 @@ export function AttachmentCard({
219
220
  <BaseButton
220
221
  type="button"
221
222
  aria-label={`Remove ${name}`}
222
- className="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-bg-elevated border border-border-strong flex items-center justify-center text-text-secondary hover:text-text-primary opacity-0 group-hover:opacity-100 transition-opacity"
223
+ // Shows with the card's hover and with keyboard focus, as Download does: hover-only left it invisible to
224
+ // the keyboard that had reached it (UIG-14, C4, Katerina 19 September).
225
+ className="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-bg-elevated border border-border-strong flex items-center justify-center text-text-secondary hover:text-text-primary opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 transition-opacity"
223
226
  onClick={(event) => {
224
227
  event.stopPropagation()
225
228
  onRemove()
package/src/Avatar.mdx CHANGED
@@ -13,13 +13,15 @@ theirs, or — when there is no name to take initials from — a silhouette.
13
13
  ## When
14
14
 
15
15
  - Anywhere a person appears at a glance: a row, a card, a picker, a byline.
16
- - The scale is 16 · 24 · 32 · 36 (default) pick from it rather than
17
- inventing sizes.
16
+ - The scale is 16 · 18 · 20 · 24 · 32 · 36 (default): 18 in a group's reply
17
+ row, 20 in a **Person**. Pick from it rather than inventing sizes.
18
18
 
19
19
  ## When not
20
20
 
21
21
  - Several people at once → **AvatarGroup**.
22
- - The face beside the name → **Person**.
22
+ - The face beside a name, with nothing else on the line → **Person**. A byline
23
+ that sets the name its own way — bold, with a time — is an Avatar beside its
24
+ own text.
23
25
  - The face as a button that opens a menu → **PersonTrigger**.
24
26
 
25
27
  ## How
@@ -37,7 +39,7 @@ import { Avatar } from '@estiva-app/ui'
37
39
  two words, skipping words that open with a symbol. An unnamed person is
38
40
  the silhouette, not a `?` — a question mark reads as an error badge.
39
41
  - The initials are centred on the capitals themselves — cap height to
40
- baseline — not on the line of text around them (D27). A single face can
42
+ baseline — not on the line of text around them. A single face can
41
43
  still sit up to half a pixel off: the screen rounds text to its pixel grid
42
44
  wherever the tile lands.
43
45
  - The colour behind initials is per person, chosen from the name, the same
@@ -52,7 +54,7 @@ import { Avatar } from '@estiva-app/ui'
52
54
  **A face is silent by default**, and that is the important half: almost every
53
55
  face in the suite sits beside the name it belongs to — in a `Person`, in a
54
56
  `MenuItem`'s `leading`, in a row — and a picture that spoke there would say the
55
- name twice. It used to. Measured 2026-09-08, before this was true: a `Person`
57
+ name twice. It used to: a `Person`
56
58
  inside a button announced **"AD Ana Duarte"** (the initials read as text), and
57
59
  with a picture **"Ana Duarte Ana Duarte"**.
58
60
 
@@ -66,6 +68,19 @@ a stack of members — and the tile becomes one image with one name.
66
68
 
67
69
  <Canvas of={AvatarStories.Palette} />
68
70
 
71
+ - **Planned:** a Byline part — face · name · time — in migration stage 7.
72
+
73
+ - `initialsFor(name)` and `hueFor(name)` give the same initials and colour where
74
+ the part itself cannot be drawn.
75
+
76
+ ## What it owns
77
+
78
+ It does these by itself. Write none of them around it.
79
+
80
+ | It does | So you never write | Refused when written by hand |
81
+ |---|---|---|
82
+ | Shows initials from the name when there is no picture, or when it fails to load | an `onError`, or initials of your own | |
83
+
69
84
  ## Props
70
85
 
71
86
  <Controls of={AvatarStories.Initials} />
@@ -34,6 +34,10 @@ import { AvatarGroup } from '@estiva-app/ui'
34
34
  - The ring rides on the Avatar itself (one box); don't wrap each face in a
35
35
  clipping window — a window minus a border crops the initials.
36
36
 
37
+ ## What it owns
38
+
39
+ Nothing. It only draws. Clicks and keys are yours.
40
+
37
41
  ## Props
38
42
 
39
43
  <Controls of={AvatarGroupStories.ThreeMembers} />
package/src/Banner.mdx CHANGED
@@ -12,8 +12,8 @@ line, one of four tones, gone when there is nothing to say.
12
12
 
13
13
  ## When
14
14
 
15
- - The outcome or state of the whole surface: "Public key copied.", the
16
- reason the last read failed, a read-only notice.
15
+ - The state of the whole surface: the reason the last read failed, a read-only
16
+ notice.
17
17
  - **`ok`** confirms, **`info`** informs, **`warning`** cautions,
18
18
  **`error`** reports a failure — and only `error` announces itself
19
19
  (`role="alert"`); the others are polite (`role="status"`).
@@ -22,11 +22,12 @@ line, one of four tones, gone when there is nothing to say.
22
22
 
23
23
  ## When not
24
24
 
25
- - Feedback about one action, briefly → **Toast**.
25
+ - Feedback about one action, briefly → **Toast**. A failure is the exception:
26
+ it stays, in the Banner, until the person acts on it.
26
27
  - Something that floats over the page and must **not** fade → still
27
28
  **Toast**, with `durationMs: 0` and an action. A banner is the strip
28
29
  under the header at the app's full width; a floating pill is not one, even
29
- a permanent one (Katerina, D21).
30
+ a permanent one.
30
31
  - A standing property of one thing → **Chip** on that thing.
31
32
  - More than one action, or a second line of text — that is a different
32
33
  component (not in the package yet).
@@ -44,9 +45,8 @@ import { Banner } from '@estiva-app/ui'
44
45
  - One line of text, `body-2`. If it needs a **second line**, it has outgrown
45
46
  this component.
46
47
  - `icon` puts a 16px icon before the line and `action` one small muted Button
47
- at the end of it, both in the tone (Katerina, 2026-09-18: Peek's strip above
48
- the composer became a Banner, `info`). With either, the banner is one line
49
- and the text truncates. One action at most.
48
+ at the end of it, both in the tone. With either, the banner is one line and
49
+ the text truncates. One action at most.
50
50
 
51
51
  <Canvas of={BannerStories.WithIconAndAction} />
52
52
 
@@ -56,11 +56,20 @@ import { Banner } from '@estiva-app/ui'
56
56
  </Banner>
57
57
  ```
58
58
 
59
- - `onDismiss` adds an ✕ at the right-hand end (Katerina, D21). Use it where
59
+ - `onDismiss` adds an ✕ at the right-hand end. Use it where
60
60
  the reader decides the notice is done with, rather than the app. The row
61
61
  is 40px rather than 36px with it, because the button is taller than the
62
62
  line of text; without it, nothing about the strip changes.
63
63
 
64
+ ## What it owns
65
+
66
+ It does these by itself. Write none of them around it.
67
+
68
+ | It does | So you never write | Refused when written by hand |
69
+ |---|---|---|
70
+ | Says what it is to assistive technology: an alert for an error, a status for the rest | `role="alert"` or `role="status"` | ✓ |
71
+ | Draws its own ✕ when you pass `onDismiss`, and its one small action when you pass `action` | a close button or an action button of your own | |
72
+
64
73
  ## Props
65
74
 
66
75
  <Controls of={BannerStories.Ok} />
@@ -56,6 +56,18 @@ import { Breadcrumb } from '@estiva-app/ui'
56
56
  `min-w-0` container so it can truncate at all.
57
57
  - The separators are `aria-hidden` — the trail reads as its places.
58
58
 
59
+ - 6px between each place and its separator.
60
+
61
+ ## What it owns
62
+
63
+ It does these by itself. Write none of them around it.
64
+
65
+ | It does | So you never write | Refused when written by hand |
66
+ |---|---|---|
67
+ | Shortens a trail that does not fit, and shows a cut crumb in full on hover | truncation code, or a tooltip | |
68
+ | Names itself as a navigation, and marks the last crumb as the current page | `<nav>`, `aria-label` or `aria-current` | |
69
+ | Hides the separators from screen readers, so the trail reads as its places | `aria-hidden` on each separator | |
70
+
59
71
  ## Props
60
72
 
61
73
  <Controls of={BreadcrumbStories.OnAnItem} />