@lotics/ui 36.0.0 → 37.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/MIGRATION.md CHANGED
@@ -4,6 +4,33 @@ 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
+ ## 37.0.0 — `PageContent`'s side gutter is a token, and responsive
8
+
9
+ `PageContent` padded `16` at every width. It now uses `pagePad` from
10
+ `@lotics/ui/spacing` — **16 on a phone, 32 wider**. Nothing to pass; the only visible
11
+ change is that a wide page breathes where it used to sit 16px off the chrome.
12
+
13
+ ```ts
14
+ import { pagePad } from "@lotics/ui/spacing";
15
+ pagePad(small); // SPACE.md | SPACE.xl
16
+ ```
17
+
18
+ **Why a token rather than a prop.** A screen that cannot use this shell — one with a
19
+ rail, a side panel, or a scroller it must hold a ref to — still has to land on the
20
+ same left edge, and a list and the record it opens are one reading column seen twice:
21
+ a gutter that differs between them slides the content sideways as the reader crosses.
22
+ Left to each app this lands on hand-picked numbers; the case that prompted this had
23
+ 14 and 28, one of them not even a multiple of 4, against a scale whose own doc says
24
+ that reads as broken.
25
+
26
+ It is also COUPLED to `ROW_WASH_BLEED`: a register row bleeds its wash outward to
27
+ `pagePad − ROW_WASH_BLEED`. Two numbers that must be read together should not live in
28
+ two repositories.
29
+
30
+ **If you hand-roll a page shell, import `pagePad` instead of choosing.** And check
31
+ whether `PageContent` would do — it carries the centred `maxWidth` column and the
32
+ title band most screens re-type by hand.
33
+
7
34
  ## 36.0.0 — seven components stopped announcing English; their strings moved to the pack
8
35
 
9
36
  **Nothing to change if you pass a shipped pack** (`en` / `vi`) — both carry every
package/docs/catalog.md CHANGED
@@ -418,7 +418,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
418
418
  scales and `getCssVariables()` — an OPT-IN serializer to `--lotics-*` CSS variables for
419
419
  hand-rolled plain DOM/CSS (nothing injects them automatically; `@lotics/ui` components
420
420
  don't need them).
421
- - **`spacing`** — the `SPACE` scale + `SpaceToken`.
421
+ - **`spacing`** — the `SPACE` scale + `SpaceToken`, and **`pagePad(small)`** — the page's
422
+ side gutter (`SPACE.md` on a phone, `SPACE.xl` wider). `PageContent` applies it; a screen
423
+ that cannot use that shell — one with a rail, a side panel or its own scroller — imports it
424
+ rather than picking a number, because a list and the record it opens are one reading column
425
+ seen twice and must not shift sideways between them. It is also COUPLED to `ROW_WASH_BLEED`:
426
+ a register row bleeds its wash outward to `pagePad − ROW_WASH_BLEED`, so the two are read
427
+ together and neither belongs to the app.
422
428
  - **`control_surface`** — `CONTROL_HEIGHT` (40), `CONTROL_CONTENT_HEIGHT` (28 — the tallest
423
429
  FIXED-height node a 40px band seats, 6 per side of air; what `InlineButton` and a `md`
424
430
  `IconButton` already used, now named so an avatar or any other child lands on it too. Text
@@ -513,7 +519,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
513
519
  `CONTAINER_SIZES`).
514
520
  - **`page_header`** — `PageHeader`: the page's title band; `actions` puts page-level CTAs on the title row (right-aligned), while `left`/`right` form a separate nav row above.
515
521
  - **`page_content`** — `PageContent` + `PAGE_SIZES`: the page's padded, width-capped content
516
- region.
522
+ region — a centred column with optional `title`/`titleRight`/`description`, `header`/`footer`
523
+ slots and `fullscreen`. **Reach for it before hand-rolling a screen shell**: a scroller, a
524
+ centred `maxWidth` column and a title band re-typed per screen is how two pages of one app end
525
+ up on different gutters. Its side padding is `pagePad` (`@lotics/ui/spacing`), so a screen that
526
+ genuinely cannot use it — one with a rail, a side panel, or a scroller it must hold a ref to —
527
+ still lands on the same edge by importing that token. It OWNS its `ScrollView` and exposes no
528
+ scroll props, which is the real limit on adoption.
517
529
  - **`accordion`** — `Accordion` + `AccordionHeader`/`AccordionTitle`/`AccordionMeta`:
518
530
  expandable section rows.
519
531
  - **`tabs`** — `Tabs`: switch between content sections; WAI-ARIA tablist + roving tabindex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "36.0.0",
3
+ "version": "37.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
@@ -4,6 +4,7 @@ import { colors } from "@lotics/ui/colors";
4
4
  import { Spacer } from "@lotics/ui/spacer";
5
5
  import { ReactNode } from "react";
6
6
  import { useContainerSize } from "@lotics/ui/size_boundary";
7
+ import { pagePad } from "@lotics/ui/spacing";
7
8
 
8
9
  interface PageContentProps {
9
10
  children: ReactNode;
@@ -26,6 +27,10 @@ export function PageContent(props: PageContentProps) {
26
27
  const screenSize = useContainerSize();
27
28
 
28
29
  const maxWidth = !screenSize.small && size ? PAGE_SIZES[size] : undefined;
30
+ // The gutter is a TOKEN, not this component's own number — a screen that cannot
31
+ // use this shell (one with a rail, a side panel, its own scroller) still has to
32
+ // land on the same edge, and it can only do that if the number has one home.
33
+ const pad = pagePad(screenSize.small);
29
34
 
30
35
  if (fullscreen) {
31
36
  return (
@@ -33,7 +38,7 @@ export function PageContent(props: PageContentProps) {
33
38
  {!!header && (
34
39
  <View
35
40
  style={{
36
- paddingHorizontal: 16,
41
+ paddingHorizontal: pad,
37
42
  paddingTop: 16,
38
43
  width: "100%",
39
44
  }}
@@ -51,7 +56,7 @@ export function PageContent(props: PageContentProps) {
51
56
  {!!header && (
52
57
  <View
53
58
  style={{
54
- paddingHorizontal: 16,
59
+ paddingHorizontal: pad,
55
60
  paddingTop: 16,
56
61
  maxWidth,
57
62
  width: "100%",
@@ -73,7 +78,7 @@ export function PageContent(props: PageContentProps) {
73
78
  maxWidth,
74
79
  width: "100%",
75
80
  marginHorizontal: "auto",
76
- paddingHorizontal: 16,
81
+ paddingHorizontal: pad,
77
82
  }}
78
83
  >
79
84
  <View
@@ -101,19 +106,25 @@ export function PageContent(props: PageContentProps) {
101
106
  </View>
102
107
  </ScrollView>
103
108
  {!!footer && (
104
- <View
105
- style={{
106
- borderTopWidth: 1,
107
- borderTopColor: colors.border,
108
- padding: 16,
109
- paddingBottom: 24,
110
- }}
111
- >
109
+ // The RULE spans the page; the CONTENT sits on the body's column. Those
110
+ // are two different widths, which is why they are two elements.
111
+ <View style={{ borderTopWidth: 1, borderTopColor: colors.border }}>
112
+ {/* ONE box carries the cap AND the gutter, exactly as the body's does.
113
+ Split across two — the padding outside, the cap inside — they
114
+ compute different edges the moment a cap is in play and the viewport
115
+ is wider than it: the body's content starts `pad` inside a centred
116
+ 1280 box, while the footer's starts wherever 1280 centres inside a
117
+ padded full-width box. That put a footer's totals 32px off the column
118
+ they total, and it is invisible at phone width, where nothing is
119
+ capped. */}
112
120
  <View
113
121
  style={{
114
122
  maxWidth,
115
123
  width: "100%",
116
124
  marginHorizontal: "auto",
125
+ paddingHorizontal: pad,
126
+ paddingTop: 16,
127
+ paddingBottom: 24,
117
128
  }}
118
129
  >
119
130
  {footer}
package/src/spacing.ts CHANGED
@@ -21,3 +21,23 @@ export const SPACE = {
21
21
  } as const;
22
22
 
23
23
  export type SpaceToken = keyof typeof SPACE;
24
+
25
+ /**
26
+ * The page's side gutter, by width — what a full-page surface pads by.
27
+ *
28
+ * ONE number, because a list and the record it opens are the same reading column
29
+ * seen twice: a gutter that differs between them slides the content sideways at
30
+ * the moment the reader changes screens. `PageContent` applies it; a screen that
31
+ * hand-rolls its own shell imports it rather than picking again.
32
+ *
33
+ * ON THE LADDER above, deliberately. Left to each app this lands on numbers like
34
+ * 14 and 28 — one of them not even a multiple of 4 — which is the "reads as
35
+ * broken" case this scale exists to prevent. `md` on a phone, where 28 a side
36
+ * spends 15% of a 375px screen on margin and 16 is the platform norm besides;
37
+ * `xl` wider, where 16 leaves a capped column nearly touching the chrome.
38
+ *
39
+ * COUPLED to `ROW_WASH_BLEED` (`control_surface`): a register row bleeds its wash
40
+ * OUTWARD past this, landing at `pagePad − ROW_WASH_BLEED`. Two numbers that must
41
+ * be read together is exactly why this one does not belong to the app.
42
+ */
43
+ export const pagePad = (small: boolean): number => (small ? SPACE.md : SPACE.xl);