@estiva-app/ui 0.9.0 → 0.10.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 (154) hide show
  1. package/dist/Avatar.d.ts +13 -1
  2. package/dist/Avatar.d.ts.map +1 -1
  3. package/dist/AvatarGroup.d.ts.map +1 -1
  4. package/dist/Banner.d.ts +16 -1
  5. package/dist/Banner.d.ts.map +1 -1
  6. package/dist/Breadcrumb.d.ts +9 -0
  7. package/dist/Breadcrumb.d.ts.map +1 -1
  8. package/dist/Button.d.ts +12 -2
  9. package/dist/Button.d.ts.map +1 -1
  10. package/dist/Checkbox.d.ts +2 -0
  11. package/dist/Checkbox.d.ts.map +1 -1
  12. package/dist/Chip.d.ts +3 -2
  13. package/dist/Chip.d.ts.map +1 -1
  14. package/dist/ChipInput.d.ts +3 -1
  15. package/dist/ChipInput.d.ts.map +1 -1
  16. package/dist/ConfirmDialog.d.ts +5 -0
  17. package/dist/ConfirmDialog.d.ts.map +1 -1
  18. package/dist/DialogShell.d.ts +27 -3
  19. package/dist/DialogShell.d.ts.map +1 -1
  20. package/dist/EditableText.d.ts +6 -0
  21. package/dist/EditableText.d.ts.map +1 -1
  22. package/dist/Field.d.ts +42 -25
  23. package/dist/Field.d.ts.map +1 -1
  24. package/dist/IconButton.d.ts +3 -2
  25. package/dist/IconButton.d.ts.map +1 -1
  26. package/dist/IdentityMenu.d.ts +25 -13
  27. package/dist/IdentityMenu.d.ts.map +1 -1
  28. package/dist/Menu.d.ts +65 -82
  29. package/dist/Menu.d.ts.map +1 -1
  30. package/dist/PersonTrigger.d.ts.map +1 -1
  31. package/dist/Popover.d.ts +86 -0
  32. package/dist/Popover.d.ts.map +1 -0
  33. package/dist/PreviewCard.d.ts +39 -0
  34. package/dist/PreviewCard.d.ts.map +1 -0
  35. package/dist/Reaction.d.ts.map +1 -1
  36. package/dist/ReactionPicker.d.ts +55 -0
  37. package/dist/ReactionPicker.d.ts.map +1 -0
  38. package/dist/SearchInput.d.ts +5 -0
  39. package/dist/SearchInput.d.ts.map +1 -1
  40. package/dist/Select.d.ts +26 -16
  41. package/dist/Select.d.ts.map +1 -1
  42. package/dist/Tabs.d.ts +11 -1
  43. package/dist/Tabs.d.ts.map +1 -1
  44. package/dist/TextInput.d.ts +7 -1
  45. package/dist/TextInput.d.ts.map +1 -1
  46. package/dist/Textarea.d.ts +8 -1
  47. package/dist/Textarea.d.ts.map +1 -1
  48. package/dist/Toolbar.d.ts +95 -0
  49. package/dist/Toolbar.d.ts.map +1 -0
  50. package/dist/Tooltip.d.ts +36 -10
  51. package/dist/Tooltip.d.ts.map +1 -1
  52. package/dist/cn.d.ts.map +1 -1
  53. package/dist/fit.d.ts +7 -72
  54. package/dist/fit.d.ts.map +1 -1
  55. package/dist/index.d.ts +6 -3
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +888 -763
  58. package/dist/index.js.map +4 -4
  59. package/dist/triggerDisabled.d.ts +10 -0
  60. package/dist/triggerDisabled.d.ts.map +1 -0
  61. package/package.json +1 -1
  62. package/src/Avatar.mdx +17 -0
  63. package/src/Avatar.name.test.tsx +99 -0
  64. package/src/Avatar.tsx +25 -3
  65. package/src/AvatarGroup.tsx +4 -1
  66. package/src/Banner.mdx +10 -2
  67. package/src/Banner.stories.tsx +29 -0
  68. package/src/Banner.test.tsx +60 -0
  69. package/src/Banner.tsx +35 -3
  70. package/src/Breadcrumb.mdx +5 -2
  71. package/src/Breadcrumb.test.tsx +44 -0
  72. package/src/Breadcrumb.tsx +10 -2
  73. package/src/Button.compose.test.tsx +119 -0
  74. package/src/Button.mdx +7 -5
  75. package/src/Button.test.tsx +2 -1
  76. package/src/Button.tsx +21 -5
  77. package/src/Checkbox.tsx +3 -0
  78. package/src/Chip.tsx +3 -2
  79. package/src/ChipInput.tsx +4 -0
  80. package/src/ConfirmDialog.mdx +20 -1
  81. package/src/ConfirmDialog.test.tsx +109 -0
  82. package/src/ConfirmDialog.tsx +6 -0
  83. package/src/DialogShell.mdx +18 -4
  84. package/src/DialogShell.stories.tsx +56 -6
  85. package/src/DialogShell.test.tsx +184 -0
  86. package/src/DialogShell.tsx +108 -39
  87. package/src/EditableText.mdx +6 -1
  88. package/src/EditableText.test.tsx +156 -0
  89. package/src/EditableText.tsx +23 -5
  90. package/src/Field.mdx +23 -3
  91. package/src/Field.stories.tsx +35 -0
  92. package/src/Field.test.tsx +144 -26
  93. package/src/Field.tsx +94 -59
  94. package/src/IconButton.mdx +6 -2
  95. package/src/IconButton.test.tsx +5 -2
  96. package/src/IconButton.tsx +12 -6
  97. package/src/IdentityMenu.mdx +29 -6
  98. package/src/IdentityMenu.stories.tsx +41 -6
  99. package/src/IdentityMenu.tsx +74 -47
  100. package/src/Kbd.stories.tsx +3 -3
  101. package/src/Menu.mdx +88 -75
  102. package/src/Menu.stories.tsx +120 -52
  103. package/src/Menu.test.tsx +315 -0
  104. package/src/Menu.tsx +347 -251
  105. package/src/MenuItem.stories.tsx +135 -98
  106. package/src/NavItem.mdx +1 -1
  107. package/src/PersonTrigger.mdx +20 -14
  108. package/src/PersonTrigger.tsx +23 -1
  109. package/src/Popover.mdx +130 -0
  110. package/src/Popover.stories.tsx +222 -0
  111. package/src/Popover.test.tsx +188 -0
  112. package/src/Popover.tsx +156 -0
  113. package/src/PreviewCard.mdx +98 -0
  114. package/src/PreviewCard.stories.tsx +168 -0
  115. package/src/PreviewCard.test.tsx +83 -0
  116. package/src/PreviewCard.tsx +91 -0
  117. package/src/Rail.mdx +63 -0
  118. package/src/Rail.stories.tsx +81 -0
  119. package/src/Reaction.mdx +4 -2
  120. package/src/Reaction.tsx +4 -2
  121. package/src/ReactionPicker.mdx +85 -0
  122. package/src/ReactionPicker.stories.tsx +120 -0
  123. package/src/ReactionPicker.test.tsx +118 -0
  124. package/src/ReactionPicker.tsx +88 -0
  125. package/src/SearchInput.mdx +6 -2
  126. package/src/SearchInput.tsx +7 -1
  127. package/src/Select.mdx +16 -5
  128. package/src/Select.test.tsx +157 -0
  129. package/src/Select.tsx +89 -214
  130. package/src/Sidebar.mdx +6 -2
  131. package/src/Tabs.mdx +5 -0
  132. package/src/Tabs.test.tsx +15 -0
  133. package/src/Tabs.tsx +17 -2
  134. package/src/TextInput.mdx +8 -2
  135. package/src/TextInput.tsx +10 -7
  136. package/src/Textarea.mdx +6 -2
  137. package/src/Textarea.tsx +14 -9
  138. package/src/Toolbar.mdx +104 -0
  139. package/src/Toolbar.stories.tsx +142 -0
  140. package/src/Toolbar.test.tsx +253 -0
  141. package/src/Toolbar.tsx +158 -0
  142. package/src/Tooltip.mdx +43 -5
  143. package/src/Tooltip.stories.tsx +26 -0
  144. package/src/Tooltip.test.tsx +195 -0
  145. package/src/Tooltip.tsx +150 -48
  146. package/src/cn.ts +1 -1
  147. package/src/fit.test.ts +101 -0
  148. package/src/fit.ts +19 -63
  149. package/src/index.ts +6 -3
  150. package/src/triggerDisabled.ts +13 -0
  151. package/stories/Choosing.mdx +8 -3
  152. package/tailwind-preset.js +7 -0
  153. package/src/Menu.fit.test.ts +0 -90
  154. package/src/Select.fit.test.ts +0 -101
@@ -0,0 +1,13 @@
1
+ import type { ReactElement } from 'react'
2
+
3
+ /**
4
+ * Whether the element a `Menu` or `Popover` was handed as its trigger is
5
+ * disabled — by `disabled`, or by a `disabledReason` (which disables the
6
+ * button and keeps it reachable). Base UI's trigger parts keep a disabled
7
+ * state of their own and write it over the rendered button's, so they have to
8
+ * be told (Finding 39, 2026-09-08).
9
+ */
10
+ export function triggerDisabled(trigger: ReactElement): boolean {
11
+ const props = trigger.props as { disabled?: boolean; disabledReason?: string }
12
+ return !!(props.disabled || props.disabledReason)
13
+ }
@@ -53,8 +53,12 @@ fork freely, owe nothing back, mention it on the package's ticket.
53
53
 
54
54
  | You need | Reach for |
55
55
  |---|---|
56
- | A list of verbs from a trigger | **Menu** — Escape and outside-click come with it |
57
- | A hint on hover | **Tooltip** / **WithTooltip** |
56
+ | A list of verbs from a trigger | **Menu** — Escape, outside-click and the arrow keys come with it |
57
+ | A panel of *content* from a trigger — a field, a toolbar, a small form | **Popover** the menu's surface without a menu's keyboard |
58
+ | More of a thing on hover | **PreviewCard** — unlike a tooltip, you can point at it |
59
+ | A hint on hover or on keyboard focus | **Tooltip** / **WithTooltip** — one `TooltipProvider` at the app root shares the delay |
60
+ | A strip of controls that act on one thing | **Toolbar** — one Tab stop for the row, arrow keys along it |
61
+ | Reactions to choose from | **ReactionPicker** — the row; **Reaction** is the pill it produces |
58
62
  | Anything with a title, body and footer | **DialogShell** |
59
63
  | "Are you sure?" | **ConfirmDialog** |
60
64
 
@@ -76,7 +80,8 @@ fork freely, owe nothing back, mention it on the package's ticket.
76
80
  | Data is on its way | **Skeleton** — shaped like what will arrive |
77
81
  | Nothing to show | **EmptyState** — says what would fill it |
78
82
  | It happened, briefly | **Toast**, via `useToast` inside a `ToastProvider` |
79
- | The whole surface has something to say | **Banner** — the strip under the top bar |
83
+ | It happened, and must not be missed | **Toast** with `durationMs: 0` and an action it floats and stays until dismissed (D21) |
84
+ | The whole surface has something to say | **Banner** — the strip under the top bar, full width; `onDismiss` if the reader closes it rather than the app |
80
85
 
81
86
  ## The frame
82
87
 
@@ -169,7 +169,14 @@ export default {
169
169
  // glow. Every theme defines it; the shared inputs use it under `signal:`.
170
170
  'focus-ring': 'var(--focus-ring)',
171
171
  // Signal's glow and inner highlight (D16); the components use them under `signal:`.
172
+ // All three glows are here as box shadows, and the two below are also
173
+ // drop shadows: a glow on a surface is a box shadow, a glow on an icon
174
+ // is a filter that follows its shape. Peek's composer wrote its send
175
+ // button's glow as an arbitrary value for want of this one
176
+ // (ADOPTION P22).
172
177
  'glow-warning': 'var(--glow-warning)',
178
+ 'glow-success': 'var(--glow-success)',
179
+ 'glow-accent': 'var(--glow-accent)',
173
180
  'highlight-inset': 'var(--highlight-inset)',
174
181
  },
175
182
  dropShadow: {
@@ -1,90 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { clampBox, fitMenu, fitSubmenu } from './fit'
3
-
4
- /**
5
- * The Menu shell's viewport geometry (Katerina, 2026-09-03): the identity
6
- * menu vanished under a z-indexed panel header, and a reply menu's highlight
7
- * submenu was cut off by the right screen edge — its hand-rolled flip read a
8
- * ref that was never attached, so it measured nothing. The portal fixes the
9
- * covering; these pin the pure half of the fitting.
10
- */
11
- const viewport = { width: 1280, height: 800 }
12
-
13
- describe('fitMenu without a cap (the Menu shell)', () => {
14
- it('lets a tall panel stand at its full height when the room is there — no scrollbar', () => {
15
- // The identity panel: ~450px tall on a 900px screen. With Select's 288
16
- // cap it grew a scrollbar at full height; without one it just stands.
17
- const fit = fitMenu({
18
- anchor: { left: 1100, top: 20, bottom: 56 },
19
- menu: { width: 288, contentHeight: 450 },
20
- viewport: { width: 1440, height: 900 },
21
- })
22
- expect(fit.maxHeight).toBeGreaterThanOrEqual(450)
23
- })
24
-
25
- it('still caps to the room when the screen truly runs out', () => {
26
- const fit = fitMenu({
27
- anchor: { left: 100, top: 20, bottom: 56 },
28
- menu: { width: 288, contentHeight: 1200 },
29
- viewport: { width: 1440, height: 900 },
30
- })
31
- expect(fit.maxHeight).toBe(900 - 56 - 4 - 8)
32
- })
33
- })
34
-
35
- describe('clampBox', () => {
36
- it('leaves a well-placed box exactly where the caller put it', () => {
37
- const fit = clampBox({ box: { left: 100, top: 200, width: 300, height: 400 }, viewport })
38
- expect(fit).toEqual({ left: 100, top: 200, maxHeight: 400 })
39
- })
40
-
41
- it('slides a box back inside the right edge, margin kept', () => {
42
- const fit = clampBox({ box: { left: 1100, top: 200, width: 300, height: 400 }, viewport })
43
- expect(fit.left).toBe(1280 - 300 - 8)
44
- })
45
-
46
- it('never pushes a box past the left edge either', () => {
47
- const fit = clampBox({ box: { left: -40, top: 200, width: 300, height: 400 }, viewport })
48
- expect(fit.left).toBe(8)
49
- })
50
-
51
- it('slides a box up so its tail stays on screen', () => {
52
- const fit = clampBox({ box: { left: 100, top: 700, width: 300, height: 400 }, viewport })
53
- expect(fit.top).toBe(800 - 400 - 8)
54
- })
55
-
56
- it('caps a box taller than the viewport and pins it to the top margin', () => {
57
- const fit = clampBox({ box: { left: 100, top: 100, width: 300, height: 900 }, viewport })
58
- expect(fit.maxHeight).toBe(800 - 16)
59
- expect(fit.top).toBe(8)
60
- })
61
-
62
- it('keeps the 120px floor — scrollable beats invisible', () => {
63
- const fit = clampBox({ box: { left: 100, top: 200, width: 300, height: 40 }, viewport })
64
- expect(fit.maxHeight).toBeGreaterThanOrEqual(120)
65
- })
66
- })
67
-
68
- describe('fitSubmenu', () => {
69
- const panel = { width: 160, height: 200 }
70
-
71
- it('opens to the right of a row with room', () => {
72
- const fit = fitSubmenu({ row: { left: 500, right: 740, top: 300 }, panel, viewport })
73
- expect(fit).toEqual({ left: 744, top: 300 })
74
- })
75
-
76
- it('flips to the left at the right screen edge (the highlight submenu cut)', () => {
77
- const fit = fitSubmenu({ row: { left: 1000, right: 1240, top: 300 }, panel, viewport })
78
- expect(fit.left).toBe(1000 - 4 - 160)
79
- })
80
-
81
- it('never lands past the left edge even when flipped from a narrow spot', () => {
82
- const fit = fitSubmenu({ row: { left: 60, right: 1240, top: 300 }, panel, viewport })
83
- expect(fit.left).toBe(8)
84
- })
85
-
86
- it('slides up when the tail would run past the bottom', () => {
87
- const fit = fitSubmenu({ row: { left: 500, right: 740, top: 700 }, panel, viewport })
88
- expect(fit.top).toBe(800 - 200 - 8)
89
- })
90
- })
@@ -1,101 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { fitMenu } from './Select'
3
-
4
- /** Select's calls carry its option-list cap (the old max-h-72); the cap is
5
- * the caller's now, so these pin it alongside the geometry. */
6
- const fitSelect = (args: Omit<Parameters<typeof fitMenu>[0], 'cap'>) => fitMenu({ ...args, cap: 288 })
7
-
8
- /**
9
- * The Select menu's viewport geometry (Katerina, 2026-09-01): the files-panel
10
- * picker was cut off at the right edge, its tail ran past the bottom of the
11
- * screen, and it could not be scrolled. `fitMenu` is the pure half of the
12
- * fix; these pin its decisions.
13
- */
14
- const viewport = { width: 1280, height: 800 }
15
-
16
- describe('fitMenu', () => {
17
- it('leaves a comfortable menu exactly where the trigger put it', () => {
18
- const fit = fitSelect({
19
- anchor: { left: 100, top: 200, bottom: 232 },
20
- menu: { width: 240, contentHeight: 180 },
21
- viewport,
22
- })
23
- expect(fit.left).toBe(100)
24
- expect(fit.top).toBe(236)
25
- expect(fit.bottom).toBeUndefined()
26
- expect(fit.maxHeight).toBe(288) // room to spare — the cap is the only limit
27
- })
28
-
29
- it('clamps a menu that would run off the right edge (the files-panel cut)', () => {
30
- const fit = fitSelect({
31
- anchor: { left: 1200, top: 200, bottom: 232 },
32
- menu: { width: 340, contentHeight: 180 },
33
- viewport,
34
- })
35
- // 1280 - 340 - 8 margin: fully on screen, margin kept.
36
- expect(fit.left).toBe(932)
37
- })
38
-
39
- it('never pushes a menu past the LEFT edge either', () => {
40
- const fit = fitSelect({
41
- anchor: { left: -20, top: 200, bottom: 232 },
42
- menu: { width: 240, contentHeight: 180 },
43
- viewport,
44
- })
45
- expect(fit.left).toBe(8)
46
- })
47
-
48
- it('caps height to the room below, so the last option is never off screen', () => {
49
- const fit = fitSelect({
50
- anchor: { left: 100, top: 560, bottom: 592 },
51
- menu: { width: 240, contentHeight: 200 },
52
- viewport,
53
- })
54
- // Room below = 800 - 592 - 4 gap - 8 margin = 196 < content 200 …but the
55
- // room above (548) is not better than needed-below comparison? It is —
56
- // openUp triggers only when below < min(content, cap) AND above > below.
57
- // Here above (548) > below (196), content 200 > 196 → opens upward.
58
- expect(fit.bottom).toBe(800 - 560 + 4)
59
- expect(fit.maxHeight).toBe(288)
60
- })
61
-
62
- it('opens upward when the room above is better (the low trigger)', () => {
63
- const fit = fitSelect({
64
- anchor: { left: 100, top: 700, bottom: 732 },
65
- menu: { width: 240, contentHeight: 400 },
66
- viewport,
67
- })
68
- expect(fit.top).toBeUndefined()
69
- expect(fit.bottom).toBe(800 - 700 + 4) // anchored to the trigger's top
70
- expect(fit.maxHeight).toBe(288) // still capped
71
- })
72
-
73
- it('stays below when the room below is bad but the room above is worse', () => {
74
- const fit = fitSelect({
75
- anchor: { left: 100, top: 60, bottom: 92 },
76
- menu: { width: 240, contentHeight: 400 },
77
- viewport,
78
- })
79
- expect(fit.top).toBe(96)
80
- // Room below = 800 - 92 - 12 = 696 ≥ cap → full cap available.
81
- expect(fit.maxHeight).toBe(288)
82
- })
83
-
84
- it('keeps a 120px floor in a cramped corner — scrollable beats invisible', () => {
85
- const fit = fitSelect({
86
- anchor: { left: 100, top: 740, bottom: 772 },
87
- menu: { width: 240, contentHeight: 400 },
88
- viewport: { width: 1280, height: 800 },
89
- })
90
- expect(fit.maxHeight).toBeGreaterThanOrEqual(120)
91
- })
92
-
93
- it('caps a long list at 288 with plenty of room — the old max-h-72', () => {
94
- const fit = fitSelect({
95
- anchor: { left: 100, top: 100, bottom: 132 },
96
- menu: { width: 240, contentHeight: 900 },
97
- viewport,
98
- })
99
- expect(fit.maxHeight).toBe(288)
100
- })
101
- })