@estiva-app/ui 0.8.0 → 0.10.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/dist/Avatar.d.ts +13 -1
- package/dist/Avatar.d.ts.map +1 -1
- package/dist/AvatarGroup.d.ts.map +1 -1
- package/dist/Banner.d.ts +16 -1
- package/dist/Banner.d.ts.map +1 -1
- package/dist/Breadcrumb.d.ts +9 -0
- package/dist/Breadcrumb.d.ts.map +1 -1
- package/dist/Button.d.ts +12 -2
- package/dist/Button.d.ts.map +1 -1
- package/dist/Checkbox.d.ts +2 -0
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/Chip.d.ts +3 -2
- package/dist/Chip.d.ts.map +1 -1
- package/dist/ChipInput.d.ts +3 -1
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/ConfirmDialog.d.ts +5 -0
- package/dist/ConfirmDialog.d.ts.map +1 -1
- package/dist/DialogShell.d.ts +27 -3
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/EditableText.d.ts +6 -0
- package/dist/EditableText.d.ts.map +1 -1
- package/dist/Field.d.ts +42 -25
- package/dist/Field.d.ts.map +1 -1
- package/dist/IconButton.d.ts +3 -2
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts +25 -13
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Menu.d.ts +65 -82
- package/dist/Menu.d.ts.map +1 -1
- package/dist/PersonTrigger.d.ts.map +1 -1
- package/dist/Popover.d.ts +86 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/PreviewCard.d.ts +39 -0
- package/dist/PreviewCard.d.ts.map +1 -0
- package/dist/Reaction.d.ts.map +1 -1
- package/dist/ReactionPicker.d.ts +55 -0
- package/dist/ReactionPicker.d.ts.map +1 -0
- package/dist/SearchInput.d.ts +5 -0
- package/dist/SearchInput.d.ts.map +1 -1
- package/dist/Select.d.ts +26 -16
- package/dist/Select.d.ts.map +1 -1
- package/dist/Tabs.d.ts +11 -1
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/TextInput.d.ts +7 -1
- package/dist/TextInput.d.ts.map +1 -1
- package/dist/Textarea.d.ts +8 -1
- package/dist/Textarea.d.ts.map +1 -1
- package/dist/Toolbar.d.ts +95 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Tooltip.d.ts +36 -10
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/cn.d.ts.map +1 -1
- package/dist/fit.d.ts +7 -72
- package/dist/fit.d.ts.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +882 -763
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/src/Avatar.mdx +17 -0
- package/src/Avatar.name.test.tsx +99 -0
- package/src/Avatar.tsx +25 -3
- package/src/AvatarGroup.tsx +4 -1
- package/src/Banner.mdx +10 -2
- package/src/Banner.stories.tsx +29 -0
- package/src/Banner.test.tsx +60 -0
- package/src/Banner.tsx +35 -3
- package/src/Breadcrumb.mdx +5 -2
- package/src/Breadcrumb.test.tsx +44 -0
- package/src/Breadcrumb.tsx +10 -2
- package/src/Button.compose.test.tsx +119 -0
- package/src/Button.mdx +7 -5
- package/src/Button.test.tsx +2 -1
- package/src/Button.tsx +21 -5
- package/src/Checkbox.tsx +3 -0
- package/src/Chip.tsx +3 -2
- package/src/ChipInput.tsx +4 -0
- package/src/ConfirmDialog.mdx +20 -1
- package/src/ConfirmDialog.test.tsx +109 -0
- package/src/ConfirmDialog.tsx +6 -0
- package/src/DialogShell.mdx +18 -4
- package/src/DialogShell.stories.tsx +56 -6
- package/src/DialogShell.test.tsx +184 -0
- package/src/DialogShell.tsx +108 -39
- package/src/EditableText.mdx +6 -1
- package/src/EditableText.test.tsx +156 -0
- package/src/EditableText.tsx +23 -5
- package/src/Field.mdx +23 -3
- package/src/Field.stories.tsx +35 -0
- package/src/Field.test.tsx +117 -27
- package/src/Field.tsx +86 -58
- package/src/IconButton.mdx +6 -2
- package/src/IconButton.test.tsx +5 -2
- package/src/IconButton.tsx +12 -6
- package/src/IdentityMenu.mdx +29 -6
- package/src/IdentityMenu.stories.tsx +41 -6
- package/src/IdentityMenu.tsx +74 -47
- package/src/Kbd.stories.tsx +3 -3
- package/src/Menu.mdx +88 -75
- package/src/Menu.stories.tsx +120 -52
- package/src/Menu.test.tsx +315 -0
- package/src/Menu.tsx +341 -251
- package/src/MenuItem.stories.tsx +135 -98
- package/src/NavItem.mdx +1 -1
- package/src/PersonTrigger.mdx +20 -14
- package/src/PersonTrigger.tsx +23 -1
- package/src/Popover.mdx +130 -0
- package/src/Popover.stories.tsx +222 -0
- package/src/Popover.test.tsx +188 -0
- package/src/Popover.tsx +153 -0
- package/src/PreviewCard.mdx +98 -0
- package/src/PreviewCard.stories.tsx +168 -0
- package/src/PreviewCard.test.tsx +83 -0
- package/src/PreviewCard.tsx +91 -0
- package/src/Rail.mdx +63 -0
- package/src/Rail.stories.tsx +81 -0
- package/src/Reaction.mdx +4 -2
- package/src/Reaction.tsx +4 -2
- package/src/ReactionPicker.mdx +85 -0
- package/src/ReactionPicker.stories.tsx +120 -0
- package/src/ReactionPicker.test.tsx +118 -0
- package/src/ReactionPicker.tsx +88 -0
- package/src/SearchInput.mdx +6 -2
- package/src/SearchInput.tsx +7 -1
- package/src/Select.mdx +16 -5
- package/src/Select.test.tsx +157 -0
- package/src/Select.tsx +89 -214
- package/src/Sidebar.mdx +6 -2
- package/src/Tabs.mdx +5 -0
- package/src/Tabs.test.tsx +15 -0
- package/src/Tabs.tsx +17 -2
- package/src/TextInput.mdx +8 -2
- package/src/TextInput.tsx +10 -7
- package/src/Textarea.mdx +6 -2
- package/src/Textarea.tsx +14 -9
- package/src/Toolbar.mdx +104 -0
- package/src/Toolbar.stories.tsx +142 -0
- package/src/Toolbar.test.tsx +198 -0
- package/src/Toolbar.tsx +158 -0
- package/src/Tooltip.mdx +43 -5
- package/src/Tooltip.stories.tsx +26 -0
- package/src/Tooltip.test.tsx +195 -0
- package/src/Tooltip.tsx +150 -48
- package/src/cn.ts +1 -1
- package/src/fit.test.ts +101 -0
- package/src/fit.ts +19 -63
- package/src/index.ts +6 -3
- package/stories/Choosing.mdx +8 -3
- package/stories/TokensPage.tsx +6 -2
- package/tailwind-preset.js +12 -0
- package/tokens.css +8 -0
- package/src/Menu.fit.test.ts +0 -90
- package/src/Select.fit.test.ts +0 -101
package/stories/Choosing.mdx
CHANGED
|
@@ -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
|
|
57
|
-
| A
|
|
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
|
-
|
|
|
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
|
|
package/stories/TokensPage.tsx
CHANGED
|
@@ -88,9 +88,9 @@ const FAMILIES: Family[] = [
|
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
label: 'Accent',
|
|
91
|
-
blurb: 'The brand colour: the primary button, the checked box, the brand chip. Muted is its wash, outline its thin border.',
|
|
91
|
+
blurb: 'The brand colour: the primary button, the checked box, the brand chip. Muted is its opaque wash, wash the translucent one a row tints with on hover, outline its thin border.',
|
|
92
92
|
utilities: COLOUR_UTILITIES,
|
|
93
|
-
tokens: [colour('accent', 'primary', 'fill'), colour('accent', 'hover', 'fill'), colour('accent', 'muted', 'fill'), colour('accent', 'outline', 'outline')],
|
|
93
|
+
tokens: [colour('accent', 'primary', 'fill'), colour('accent', 'hover', 'fill'), colour('accent', 'muted', 'fill'), colour('accent', 'wash', 'fill'), colour('accent', 'outline', 'outline')],
|
|
94
94
|
},
|
|
95
95
|
{
|
|
96
96
|
label: 'Tones',
|
|
@@ -100,6 +100,10 @@ const FAMILIES: Family[] = [
|
|
|
100
100
|
colour(tone, 'default', 'text'),
|
|
101
101
|
colour(tone, 'muted', 'fill'),
|
|
102
102
|
colour(tone, 'outline', 'outline'),
|
|
103
|
+
// Only success has a translucent wash so far, because only success needed
|
|
104
|
+
// one: it is the tint on a resolved thread. The others gain one when a
|
|
105
|
+
// component asks, not before.
|
|
106
|
+
...(tone === 'success' ? [colour(tone, 'wash', 'fill')] : []),
|
|
103
107
|
]),
|
|
104
108
|
},
|
|
105
109
|
{
|
package/tailwind-preset.js
CHANGED
|
@@ -154,6 +154,11 @@ export default {
|
|
|
154
154
|
'warning-outline': 'var(--warning-outline)',
|
|
155
155
|
'success-outline': 'var(--success-outline)',
|
|
156
156
|
'error-outline': 'var(--error-outline)',
|
|
157
|
+
// The same hues as the outlines, at a fill's strength rather than an
|
|
158
|
+
// edge's: `-outline` is 30%, `-wash` is 11%. Peek tinted rows with its
|
|
159
|
+
// own copies of these before they moved here.
|
|
160
|
+
'accent-wash': 'var(--accent-wash)',
|
|
161
|
+
'success-wash': 'var(--success-wash)',
|
|
157
162
|
'scrim': 'var(--scrim)',
|
|
158
163
|
},
|
|
159
164
|
boxShadow: {
|
|
@@ -164,7 +169,14 @@ export default {
|
|
|
164
169
|
// glow. Every theme defines it; the shared inputs use it under `signal:`.
|
|
165
170
|
'focus-ring': 'var(--focus-ring)',
|
|
166
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).
|
|
167
177
|
'glow-warning': 'var(--glow-warning)',
|
|
178
|
+
'glow-success': 'var(--glow-success)',
|
|
179
|
+
'glow-accent': 'var(--glow-accent)',
|
|
168
180
|
'highlight-inset': 'var(--highlight-inset)',
|
|
169
181
|
},
|
|
170
182
|
dropShadow: {
|
package/tokens.css
CHANGED
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
--warning-outline: rgba(245, 158, 11, 0.3);
|
|
73
73
|
--success-outline: rgba(14, 160, 111, 0.3);
|
|
74
74
|
--error-outline: rgba(239, 68, 68, 0.3);
|
|
75
|
+
--accent-wash: rgba(139, 92, 246, 0.11);
|
|
76
|
+
--success-wash: rgba(14, 160, 111, 0.11);
|
|
75
77
|
--glow-warning: 0 0 5px rgba(245, 158, 11, 0.4);
|
|
76
78
|
--glow-success: 0 0 5px rgba(14, 160, 111, 0.7);
|
|
77
79
|
--glow-accent: 0 0 5px rgba(139, 92, 246, 0.6);
|
|
@@ -133,6 +135,8 @@
|
|
|
133
135
|
--warning-outline: rgba(251, 191, 36, 0.3);
|
|
134
136
|
--success-outline: rgba(52, 211, 153, 0.3);
|
|
135
137
|
--error-outline: rgba(248, 113, 113, 0.3);
|
|
138
|
+
--accent-wash: rgba(167, 139, 250, 0.11);
|
|
139
|
+
--success-wash: rgba(52, 211, 153, 0.11);
|
|
136
140
|
--glow-warning: 0 0 5px rgba(251, 191, 36, 0.4);
|
|
137
141
|
--glow-success: 0 0 5px rgba(52, 211, 153, 0.7);
|
|
138
142
|
--glow-accent: 0 0 5px rgba(167, 139, 250, 0.6);
|
|
@@ -198,6 +202,8 @@
|
|
|
198
202
|
--warning-outline: rgba(255, 176, 32, 0.3);
|
|
199
203
|
--success-outline: rgba(63, 222, 140, 0.3);
|
|
200
204
|
--error-outline: rgba(255, 107, 107, 0.3);
|
|
205
|
+
--accent-wash: rgba(86, 200, 255, 0.11);
|
|
206
|
+
--success-wash: rgba(63, 222, 140, 0.11);
|
|
201
207
|
--glow-warning: 0 0 5px rgba(255, 176, 32, 0.4);
|
|
202
208
|
--glow-success: 0 0 5px rgba(63, 222, 140, 0.7);
|
|
203
209
|
--glow-accent: 0 0 5px rgba(86, 200, 255, 0.6);
|
|
@@ -269,6 +275,8 @@
|
|
|
269
275
|
--warning-outline: rgba(224, 176, 74, 0.3);
|
|
270
276
|
--success-outline: rgba(70, 192, 138, 0.3);
|
|
271
277
|
--error-outline: rgba(229, 105, 122, 0.3);
|
|
278
|
+
--accent-wash: rgba(75, 88, 212, 0.11);
|
|
279
|
+
--success-wash: rgba(70, 192, 138, 0.11);
|
|
272
280
|
--glow-warning: 0 0 5px rgba(224, 176, 74, 0.4);
|
|
273
281
|
--glow-success: 0 0 5px rgba(70, 192, 138, 0.7);
|
|
274
282
|
--glow-accent: 0 0 5px rgba(75, 88, 212, 0.6);
|
package/src/Menu.fit.test.ts
DELETED
|
@@ -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
|
-
})
|
package/src/Select.fit.test.ts
DELETED
|
@@ -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
|
-
})
|