@lotics/ui 38.1.0 → 40.0.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/AGENTS.md +5 -1
- package/MIGRATION.md +54 -0
- package/docs/catalog.md +19 -10
- package/package.json +1 -1
- package/src/page_header.tsx +20 -14
- package/src/popover.tsx +18 -3
package/AGENTS.md
CHANGED
|
@@ -30,7 +30,11 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
30
30
|
- **Responsive layout measures the CONTAINER, not the screen.** `useContainerSize()` reports the
|
|
31
31
|
nearest `SizeBoundary`; wrap one around any region whose width stops tracking its parent's, and
|
|
32
32
|
`Dialog`/`Drawer`/popover bodies already are boundaries so their contents get the panel
|
|
33
|
-
rather than the region they were opened from.
|
|
33
|
+
rather than the region they were opened from. The overlay's OWN presentation is not one of
|
|
34
|
+
these questions and is not yours to answer: `PopoverContent` reads the screen itself and takes
|
|
35
|
+
no prop, because it fills the viewport whatever box opened it — handing it a container width is
|
|
36
|
+
how a narrow column on a wide display opened a full-bleed phone sheet.
|
|
37
|
+
`useScreenSize()` answers a question about the DEVICE — is there a keyboard
|
|
34
38
|
worth a shortcut hint, are these touch targets — and reaching for it to decide SPACE is how a
|
|
35
39
|
panel a few hundred pixels wide lays its contents out for the whole display. (A component that
|
|
36
40
|
needs a number rather than a bucket — how many columns fit — measures its own box; `Table`
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,60 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## 40.0.0 — `PopoverContent` decides its own presentation; `small` is gone
|
|
8
|
+
|
|
9
|
+
`PopoverContent` took a `small` prop meaning "render as a bottom sheet". It now asks
|
|
10
|
+
`useScreenSize()` itself. **Delete the prop** — passing it is a type error.
|
|
11
|
+
|
|
12
|
+
```diff
|
|
13
|
+
- <PopoverContent small={small}>
|
|
14
|
+
+ <PopoverContent>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Why the prop had to go rather than be defaulted.** A popover is an overlay: it fills the
|
|
18
|
+
viewport regardless of which box it was opened from, so the only input is how wide that
|
|
19
|
+
viewport is — and there is exactly one right answer per viewport. Every caller had to
|
|
20
|
+
supply it by hand, and the two ways to get it wrong both shipped: one passed
|
|
21
|
+
`useContainerSize()` — the width of the *column* holding the trigger — so a narrow chat
|
|
22
|
+
column on a 1728px display opened a full-bleed modal sheet; another passed nothing and so
|
|
23
|
+
never became a sheet on a real phone. Leaving the prop as an override would have kept both
|
|
24
|
+
mistakes writable.
|
|
25
|
+
|
|
26
|
+
**What changes for a NARROW-VIEWPORT host.** The rule is the viewport, not the device, so a
|
|
27
|
+
surface that is a few hundred pixels wide on a desktop — a browser side panel, an embedded
|
|
28
|
+
frame — now gets the bottom sheet where a popover that passed nothing was previously always
|
|
29
|
+
anchored. If that is wrong for your surface, argue it as a change to the threshold or to what
|
|
30
|
+
the rule reads, for every consumer at once; do not reach for a per-instance override.
|
|
31
|
+
|
|
32
|
+
If you were computing `small` only to feed this prop, drop the hook call too. `useScreenSize`
|
|
33
|
+
remains exported for its real uses — deciding whether a surface is a `Dialog` or a `Popover`
|
|
34
|
+
at all, sizing touch targets, showing a keyboard shortcut.
|
|
35
|
+
|
|
36
|
+
## 39.0.0 — `PageHeader.leading` is `trailing`, and it renders AFTER the title
|
|
37
|
+
|
|
38
|
+
38.0.0 shipped `leading`, a slot before the title. It is now **`trailing`**, in the same
|
|
39
|
+
place in the props but rendered on the other side of the title text.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// 38.0.0
|
|
43
|
+
<PageHeader title="Members" leading={<IconButton …/>} actions={…} />
|
|
44
|
+
// 39.0.0
|
|
45
|
+
<PageHeader title="Members" trailing={<IconButton …/>} actions={…} />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Why the side changed.** The title is the page's first word. A control ahead of it makes
|
|
49
|
+
the eye parse chrome before it learns where it is, and on a page whose heading is the only
|
|
50
|
+
thing telling you which section you are in, that is the wrong order. After the title the
|
|
51
|
+
control still reads as part of the heading — which is the whole point of not putting it
|
|
52
|
+
with `actions` — without standing in front of it.
|
|
53
|
+
|
|
54
|
+
**Why a rename rather than a second slot.** A slot with no consumer is surface that has to
|
|
55
|
+
be understood by everyone who reads the component and used by no one; the kit's cull rule
|
|
56
|
+
says cut it. One slot for "a control belonging to the title" is the whole contract.
|
|
57
|
+
|
|
58
|
+
The layout law is unchanged and now covers both: the TITLE gives way and wraps, while
|
|
59
|
+
`trailing` and `actions` keep their width.
|
|
60
|
+
|
|
7
61
|
## 38.0.0 — the `lg` avatar is a control (40), not a size (48)
|
|
8
62
|
|
|
9
63
|
`AVATAR_PX.lg` was 48 and `AVATAR_TEXT.lg` was `lg` type. They are now **40** and `md`
|
package/docs/catalog.md
CHANGED
|
@@ -536,13 +536,15 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
536
536
|
- **`container`** — `Container`: centers content at a max width (`ContainerSize` sm|md|lg,
|
|
537
537
|
`CONTAINER_SIZES`).
|
|
538
538
|
- **`page_header`** — `PageHeader`: the page's title band. `actions` puts page-level CTAs on
|
|
539
|
-
the title row (right-aligned) and `
|
|
540
|
-
`left`/`right` form a separate nav row above. Split `
|
|
539
|
+
the title row (right-aligned) and `trailing` puts a control immediately AFTER the title;
|
|
540
|
+
`left`/`right` form a separate nav row above. Split `trailing` from `actions` by what the
|
|
541
541
|
control acts on: `actions` do something to the page's CONTENT (create, sort, export),
|
|
542
|
-
`
|
|
543
|
-
filed under `actions` reads as a peer of "create one of these".
|
|
544
|
-
|
|
545
|
-
|
|
542
|
+
`trailing` changes what is AROUND it (a side-panel toggle, a view switch). A panel toggle
|
|
543
|
+
filed under `actions` reads as a peer of "create one of these". It sits AFTER the title
|
|
544
|
+
because the title is the page's first word and nothing should come between the reader
|
|
545
|
+
and it. Under a title too long for the row the TITLE gives way and wraps while `trailing`
|
|
546
|
+
and `actions` keep their width — a wrapped title is merely taller, a control squeezed
|
|
547
|
+
below its own icon is broken.
|
|
546
548
|
- **`page_content`** — `PageContent` + `PAGE_SIZES`: the page's padded, width-capped content
|
|
547
549
|
region — a centred column with optional `title`/`titleRight`/`description`, `header`/`footer`
|
|
548
550
|
slots and `fullscreen`. **Reach for it before hand-rolling a screen shell**: a scroller, a
|
|
@@ -1245,8 +1247,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1245
1247
|
control both dismisses this popover AND activates that control in one click; clicking outside,
|
|
1246
1248
|
scrolling an ancestor, or Escape dismisses. Interactions inside a layer the popover opened
|
|
1247
1249
|
from within (a nested popover, an `Alert` confirm, a full-screen Modal like the file preview)
|
|
1248
|
-
never dismiss it — clicks, scrolls, and Escape belong to that layer until it closes.
|
|
1249
|
-
`small
|
|
1250
|
+
never dismiss it — clicks, scrolls, and Escape belong to that layer until it closes. In a
|
|
1251
|
+
NARROW VIEWPORT (`small`, under 768) it presents instead as a modal bottom sheet (scrim, close
|
|
1252
|
+
button, slide-up) — decided by `PopoverContent` itself off the screen size, with nothing to pass
|
|
1253
|
+
and no way to answer it differently: an overlay fills the viewport whatever box it was opened
|
|
1254
|
+
from, so the width of the column holding the trigger is never the question. The viewport, not
|
|
1255
|
+
the device — a few-hundred-pixel side panel on a desktop gets the sheet too.
|
|
1256
|
+
`PopoverContent` already insets its
|
|
1250
1257
|
body 12px — put content directly in it, NEVER add your own padding `View` (that double-pads);
|
|
1251
1258
|
title/actions go in `PopoverHeader` / `PopoverFooter`. **A fixed width is the `width` PROP,
|
|
1252
1259
|
never `style`**: `style` reaches only the BODY, and the panel is `max-content`, so a width set
|
|
@@ -1784,8 +1791,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1784
1791
|
width crosses a threshold — a height-only change (the mobile soft keyboard) and a resize
|
|
1785
1792
|
inside one bucket both return the identical value. Correct only for
|
|
1786
1793
|
questions about the device rather than the space: is there a keyboard to show a shortcut
|
|
1787
|
-
for, are these touch-sized targets, and
|
|
1788
|
-
|
|
1794
|
+
for, are these touch-sized targets, and whether a surface is a `Dialog` on a phone and a
|
|
1795
|
+
`Popover` otherwise. A popover's own anchored-vs-sheet presentation is NOT one of them —
|
|
1796
|
+
`PopoverContent` reads this itself and takes no prop. Everything spatial wants
|
|
1797
|
+
`useContainerSize`.
|
|
1789
1798
|
- **`use_async_fn`** — `useAsyncFn`: wrap an async function into a manual-trigger mutation —
|
|
1790
1799
|
`[run, {loading, data, error}]`, unmount-safe, the error lands in state AND rethrows; the
|
|
1791
1800
|
pending-state engine for a submit/download/upload action.
|
package/package.json
CHANGED
package/src/page_header.tsx
CHANGED
|
@@ -10,32 +10,36 @@ interface PageHeaderProps {
|
|
|
10
10
|
/** The nav row ABOVE the title, right-aligned. */
|
|
11
11
|
right?: ReactNode;
|
|
12
12
|
/**
|
|
13
|
-
* A control that belongs to the TITLE, rendered immediately
|
|
14
|
-
* title row — a panel toggle, a
|
|
13
|
+
* A control that belongs to the TITLE, rendered immediately AFTER it on the
|
|
14
|
+
* title row — a panel toggle, a view switch, anything that reframes the page
|
|
15
|
+
* rather than acting on it.
|
|
15
16
|
*
|
|
16
17
|
* The distinction from `actions` is what the control acts ON, not where it
|
|
17
18
|
* looks best. `actions` are things you do to the page's CONTENT (create, sort,
|
|
18
|
-
* export) and collect at the right, where a scanning eye reaches them
|
|
19
|
-
* `
|
|
20
|
-
* it
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* export) and collect at the far right, where a scanning eye reaches them
|
|
20
|
+
* last; `trailing` changes what is AROUND the content and stays with the title
|
|
21
|
+
* it qualifies, so it is read as part of the heading rather than as one more
|
|
22
|
+
* thing you can do to your list.
|
|
23
|
+
*
|
|
24
|
+
* It sits AFTER the title, not before, because the title is the page's first
|
|
25
|
+
* word and nothing should come between the reader and it — a control ahead of
|
|
26
|
+
* the heading makes the eye parse chrome before it learns where it is.
|
|
23
27
|
*/
|
|
24
|
-
|
|
28
|
+
trailing?: ReactNode;
|
|
25
29
|
/** Page-level actions (CTAs), rendered on the title row, right-aligned —
|
|
26
30
|
* distinct from `left`/`right`, which form a separate nav row above. */
|
|
27
31
|
actions?: ReactNode;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export function PageHeader(props: PageHeaderProps) {
|
|
31
|
-
const { title, description, left, right,
|
|
35
|
+
const { title, description, left, right, trailing, actions } = props;
|
|
32
36
|
|
|
33
37
|
const hasNav = !!left || !!right;
|
|
34
38
|
|
|
35
|
-
// ONE shape, whichever slots are filled. Branching the row on
|
|
36
|
-
// the title two different behaviours under a long string — shrinking
|
|
37
|
-
// panel toggle happened to be present and overflowing when it was not —
|
|
38
|
-
// is a layout law decided by an unrelated prop.
|
|
39
|
+
// ONE shape, whichever slots are filled. Branching the row on the title's own
|
|
40
|
+
// slot gave the title two different behaviours under a long string — shrinking
|
|
41
|
+
// when a panel toggle happened to be present and overflowing when it was not —
|
|
42
|
+
// which is a layout law decided by an unrelated prop.
|
|
39
43
|
//
|
|
40
44
|
// The title is what gives way; `actions` keep their width, because a CTA
|
|
41
45
|
// pushed off the row is unreachable while a wrapped title is merely taller.
|
|
@@ -62,10 +66,12 @@ export function PageHeader(props: PageHeaderProps) {
|
|
|
62
66
|
minWidth: 0,
|
|
63
67
|
}}
|
|
64
68
|
>
|
|
65
|
-
{leading}
|
|
66
69
|
<Text size="xxl" weight="semibold" style={{ flexShrink: 1, minWidth: 0 }}>
|
|
67
70
|
{title}
|
|
68
71
|
</Text>
|
|
72
|
+
{/* Never shrinks: the title is what gives way, and a control squeezed
|
|
73
|
+
below its own icon is not a smaller control, it is a broken one. */}
|
|
74
|
+
{trailing !== undefined && <View style={{ flexShrink: 0 }}>{trailing}</View>}
|
|
69
75
|
</View>
|
|
70
76
|
{actions}
|
|
71
77
|
</View>
|
package/src/popover.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { MIN_CONTROL_WIDTH } from "./control_surface";
|
|
|
13
13
|
import { IconButton } from "./icon_button";
|
|
14
14
|
import { Portal } from "./portal";
|
|
15
15
|
import { SizeBoundary } from "./size_boundary";
|
|
16
|
+
import { useScreenSize } from "./use_screen_size";
|
|
16
17
|
import { Divider } from "./divider";
|
|
17
18
|
import { useOverlayScope } from "./overlay_scope";
|
|
18
19
|
import {
|
|
@@ -209,8 +210,6 @@ export interface PopoverContentProps {
|
|
|
209
210
|
* both.
|
|
210
211
|
*/
|
|
211
212
|
width?: number;
|
|
212
|
-
/** When true, renders as bottom sheet on small screens (close button, slide-up animation) */
|
|
213
|
-
small?: boolean;
|
|
214
213
|
/** Accessible name for the bottom-sheet close button. Defaults to the locale's `overlay.close` ("Close" / "Đóng"). */
|
|
215
214
|
closeLabel?: string;
|
|
216
215
|
/** When true (default), the popover moves focus into its content on open and
|
|
@@ -230,10 +229,26 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
230
229
|
contentContainerStyle,
|
|
231
230
|
disableBodyScroll,
|
|
232
231
|
width,
|
|
233
|
-
small = false,
|
|
234
232
|
closeLabel = locale.overlay.close,
|
|
235
233
|
manageFocus = true,
|
|
236
234
|
} = props;
|
|
235
|
+
// The VIEWPORT, never the container, and never a caller's opinion of either.
|
|
236
|
+
// A popover is an overlay: it fills the viewport regardless of the box it was
|
|
237
|
+
// opened from, so the only input to "anchored or bottom sheet" is how wide
|
|
238
|
+
// that viewport is — under `small` (<768) the sheet wins.
|
|
239
|
+
//
|
|
240
|
+
// This was a `small` prop and every caller answered it by hand. One answered
|
|
241
|
+
// with `useContainerSize()`, the width of whatever column held the trigger, so
|
|
242
|
+
// a narrow chat column on a desktop opened a full-bleed sheet; another passed
|
|
243
|
+
// nothing and never became a sheet on a real phone. Reading it here is the
|
|
244
|
+
// only way neither can be written again.
|
|
245
|
+
//
|
|
246
|
+
// Note this is the viewport and NOT "is this a phone": a browser side panel is
|
|
247
|
+
// a few hundred pixels of a desktop and gets the sheet too. That is the rule
|
|
248
|
+
// behaving as defined rather than a bug, but it is the reason a surface that
|
|
249
|
+
// wants a different presentation must change the RULE here, for everyone, and
|
|
250
|
+
// never re-introduce a per-caller override.
|
|
251
|
+
const { small } = useScreenSize();
|
|
237
252
|
const { open, onOpenChange, triggerRef, side, align, offset, inheritTriggerWidth } =
|
|
238
253
|
usePopoverContext();
|
|
239
254
|
|