@lotics/ui 39.0.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 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. `useScreenSize()` answers a question about the DEVICE — is there a keyboard
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,35 @@ 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
+
7
36
  ## 39.0.0 — `PageHeader.leading` is `trailing`, and it renders AFTER the title
8
37
 
9
38
  38.0.0 shipped `leading`, a slot before the title. It is now **`trailing`**, in the same
package/docs/catalog.md CHANGED
@@ -1247,8 +1247,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1247
1247
  control both dismisses this popover AND activates that control in one click; clicking outside,
1248
1248
  scrolling an ancestor, or Escape dismisses. Interactions inside a layer the popover opened
1249
1249
  from within (a nested popover, an `Alert` confirm, a full-screen Modal like the file preview)
1250
- never dismiss it — clicks, scrolls, and Escape belong to that layer until it closes. Only the
1251
- `small` (bottom-sheet) presentation is modal (scrim). `PopoverContent` already insets its
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
1252
1257
  body 12px — put content directly in it, NEVER add your own padding `View` (that double-pads);
1253
1258
  title/actions go in `PopoverHeader` / `PopoverFooter`. **A fixed width is the `width` PROP,
1254
1259
  never `style`**: `style` reaches only the BODY, and the panel is `max-content`, so a width set
@@ -1786,8 +1791,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1786
1791
  width crosses a threshold — a height-only change (the mobile soft keyboard) and a resize
1787
1792
  inside one bucket both return the identical value. Correct only for
1788
1793
  questions about the device rather than the space: is there a keyboard to show a shortcut
1789
- for, are these touch-sized targets, and an overlay deciding whether it is a full-screen
1790
- sheet. Everything spatial wants `useContainerSize`.
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`.
1791
1798
  - **`use_async_fn`** — `useAsyncFn`: wrap an async function into a manual-trigger mutation —
1792
1799
  `[run, {loading, data, error}]`, unmount-safe, the error lands in state AND rethrows; the
1793
1800
  pending-state engine for a submit/download/upload action.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "39.0.0",
3
+ "version": "40.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
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