@lotics/ui 36.0.0 → 37.1.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 +27 -0
- package/docs/catalog.md +36 -2
- package/package.json +6 -1
- package/src/file_preview.web.tsx +10 -8
- package/src/media_player.tsx +36 -0
- package/src/media_player.web.tsx +76 -0
- package/src/media_player_types.ts +13 -0
- package/src/page_content.tsx +22 -11
- package/src/spacing.ts +20 -0
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;
|
|
@@ -1507,6 +1519,28 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1507
1519
|
the sandboxed app iframe.
|
|
1508
1520
|
- **`file_preview_types`** — `PreviewLabels` / `FilePreviewProps` / `GalleryLabels` — the
|
|
1509
1521
|
shared label + prop contracts of the file-preview family; types only.
|
|
1522
|
+
- **`media_player`** — `MediaPlayer`: the ONE audio/video playback surface (`FilePreview`
|
|
1523
|
+
composes it; so does the chat's recording message). Transport is the PLATFORM element's
|
|
1524
|
+
— `controls preload="metadata"` — deliberately, because a hand-rolled scrub bar has to
|
|
1525
|
+
re-earn keyboard access, screen-reader semantics, volume, speed, PiP, fullscreen and
|
|
1526
|
+
captions that `<video>`/`<audio>` already get right. What it owns is what the element
|
|
1527
|
+
does not: `kind` picks the element (never inferred from MIME — a screen recording holds
|
|
1528
|
+
both streams and the CALLER decides whether this surface shows a picture), a REQUIRED
|
|
1529
|
+
`accessibilityLabel`, and `objectFit: contain` so a capture letterboxes rather than
|
|
1530
|
+
cropping evidence. It **fills its parent and carries no size of its own** — a lightbox
|
|
1531
|
+
wants the modal's height, a chat bubble wants a fixed 16:9 box that will not resize when
|
|
1532
|
+
metadata arrives, and a `fill`/`compact` prop to switch between them is exactly the
|
|
1533
|
+
branching the kit forbids. Compose the box. Filling is done OUT OF FLOW (the video sits
|
|
1534
|
+
absolutely inside a positioning wrapper the component owns) because a `<video>` is a
|
|
1535
|
+
replaced element: once metadata loads it reports the source's intrinsic size, and any
|
|
1536
|
+
ancestor sized by its content grows to match — with RN-web's `flexShrink: 0` default,
|
|
1537
|
+
nothing pushes back, so a 720p capture measurably widened a 342px chat column to 367px
|
|
1538
|
+
the moment it loaded. **Do not add your own fullscreen/expand control**: the native
|
|
1539
|
+
transport already carries fullscreen, picture-in-picture and download, and a second
|
|
1540
|
+
affordance beside them is both redundant and a claim about where it leads. Native renders
|
|
1541
|
+
the `notAvailable` placeholder — playback there needs a native media dep the package does
|
|
1542
|
+
not carry.
|
|
1543
|
+
- **`media_player_types`** — `MediaPlayerProps`; types only.
|
|
1510
1544
|
- **`file_gallery_modal`** — `FileGalleryModal`: the FULL-SCREEN viewer — toolbar (filename
|
|
1511
1545
|
, counter, download, optional `onOpenExternal`/`onRemove`, close-✕), prev/next, ESC,
|
|
1512
1546
|
rotate (the 90° controls FLOAT as a pill on the image); on a phone the actions collapse
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "37.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
"default": "./src/file_preview.web.tsx"
|
|
40
40
|
},
|
|
41
41
|
"./file_preview_types": "./src/file_preview_types.ts",
|
|
42
|
+
"./media_player": {
|
|
43
|
+
"react-native": "./src/media_player.tsx",
|
|
44
|
+
"default": "./src/media_player.web.tsx"
|
|
45
|
+
},
|
|
46
|
+
"./media_player_types": "./src/media_player_types.ts",
|
|
42
47
|
"./file_gallery_modal": "./src/file_gallery_modal.tsx",
|
|
43
48
|
"./image_gallery": "./src/image_gallery.tsx",
|
|
44
49
|
"./rotatable_image": "./src/rotatable_image.tsx",
|
package/src/file_preview.web.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import { type FilePreviewProps, type PreviewLabels } from "./file_preview_types"
|
|
|
17
17
|
import { useLoticsLocale } from "./locale";
|
|
18
18
|
import { SpreadsheetView } from "./spreadsheet_view";
|
|
19
19
|
import { RotatableImage } from "./rotatable_image";
|
|
20
|
+
import { MediaPlayer } from "./media_player.web";
|
|
20
21
|
import { downloadFileFromUrl } from "./download";
|
|
21
22
|
import { colors } from "./colors";
|
|
22
23
|
|
|
@@ -56,13 +57,19 @@ export function FilePreview({ file, labels, onError, rotation, credentials }: Fi
|
|
|
56
57
|
return <WordPreview file={file} labels={l} onError={onError} credentials={credentials} />;
|
|
57
58
|
}
|
|
58
59
|
if (isVideoMimeType(file.mimeType)) {
|
|
59
|
-
return
|
|
60
|
+
return (
|
|
61
|
+
<div style={videoFillStyle}>
|
|
62
|
+
<MediaPlayer src={file.url} kind="video" accessibilityLabel={file.filename} />
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
60
65
|
}
|
|
61
66
|
if (isAudioMimeType(file.mimeType)) {
|
|
62
67
|
return (
|
|
63
68
|
<div style={audioContainerStyle}>
|
|
64
69
|
<MediaCard mimeType={file.mimeType} filename={file.filename} icon="music" size={160} />
|
|
65
|
-
<
|
|
70
|
+
<div style={audioElementStyle}>
|
|
71
|
+
<MediaPlayer src={file.url} kind="audio" accessibilityLabel={file.filename} />
|
|
72
|
+
</div>
|
|
66
73
|
</div>
|
|
67
74
|
);
|
|
68
75
|
}
|
|
@@ -505,12 +512,7 @@ const pdfOverlayStyle: React.CSSProperties = {
|
|
|
505
512
|
justifyContent: "center",
|
|
506
513
|
pointerEvents: "none",
|
|
507
514
|
};
|
|
508
|
-
const
|
|
509
|
-
width: "100%",
|
|
510
|
-
height: "100%",
|
|
511
|
-
backgroundColor: "rgba(10, 10, 10, 0.8)",
|
|
512
|
-
objectFit: "contain",
|
|
513
|
-
};
|
|
515
|
+
const videoFillStyle: React.CSSProperties = { width: "100%", height: "100%" };
|
|
514
516
|
const audioContainerStyle: React.CSSProperties = {
|
|
515
517
|
display: "flex",
|
|
516
518
|
flexDirection: "column",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { StyleSheet, View } from "react-native";
|
|
2
|
+
import { Text } from "./text";
|
|
3
|
+
import { Icon } from "./icon";
|
|
4
|
+
import { colors } from "./colors";
|
|
5
|
+
import { useLoticsLocale } from "./locale";
|
|
6
|
+
import type { MediaPlayerProps } from "./media_player_types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Native fallback. Playback needs a native media dependency this package does
|
|
10
|
+
* not carry, so it says so rather than rendering an inert box — the same choice
|
|
11
|
+
* `FilePreview` makes for the formats only its web variant can draw.
|
|
12
|
+
*/
|
|
13
|
+
export function MediaPlayer(props: MediaPlayerProps) {
|
|
14
|
+
const { kind, accessibilityLabel, testID } = props;
|
|
15
|
+
const l = useLoticsLocale().gallery;
|
|
16
|
+
return (
|
|
17
|
+
<View testID={testID} accessibilityLabel={accessibilityLabel} style={styles.placeholder}>
|
|
18
|
+
<Icon name={kind === "video" ? "monitor" : "music"} size={20} color={colors.zinc["400"]} />
|
|
19
|
+
<Text size="sm" color="muted">
|
|
20
|
+
{l.notAvailable}
|
|
21
|
+
</Text>
|
|
22
|
+
</View>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const styles = StyleSheet.create({
|
|
27
|
+
placeholder: {
|
|
28
|
+
flexDirection: "row",
|
|
29
|
+
alignItems: "center",
|
|
30
|
+
justifyContent: "center",
|
|
31
|
+
gap: 8,
|
|
32
|
+
padding: 16,
|
|
33
|
+
borderRadius: 8,
|
|
34
|
+
backgroundColor: colors.zinc["100"],
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { colors } from "./colors";
|
|
2
|
+
import type { MediaPlayerProps } from "./media_player_types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Playback surface for one audio or video source.
|
|
6
|
+
*
|
|
7
|
+
* Transport is the PLATFORM's, deliberately. A hand-rolled play/pause/scrub bar
|
|
8
|
+
* would have to re-earn keyboard access, screen-reader semantics, volume, speed,
|
|
9
|
+
* picture-in-picture, fullscreen, download and captions — all of which the
|
|
10
|
+
* native element already does correctly, and which callers should NOT duplicate
|
|
11
|
+
* with a control of their own.
|
|
12
|
+
*
|
|
13
|
+
* It FILLS its parent rather than carrying a size. A lightbox wants the video to
|
|
14
|
+
* take the modal's height; a chat bubble wants a 16:9 box that does not resize
|
|
15
|
+
* when metadata arrives. Those are the caller's boxes, and a `fill`/`compact`
|
|
16
|
+
* prop to switch between them is the branching this codebase forbids — so the
|
|
17
|
+
* caller composes the box and this draws inside it.
|
|
18
|
+
*/
|
|
19
|
+
export function MediaPlayer(props: MediaPlayerProps) {
|
|
20
|
+
const { src, kind, accessibilityLabel, testID } = props;
|
|
21
|
+
|
|
22
|
+
if (kind === "audio") {
|
|
23
|
+
return (
|
|
24
|
+
<audio
|
|
25
|
+
data-testid={testID}
|
|
26
|
+
aria-label={accessibilityLabel}
|
|
27
|
+
controls
|
|
28
|
+
preload="metadata"
|
|
29
|
+
src={src}
|
|
30
|
+
style={audioStyle}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A `<video>` is a REPLACED element: once metadata loads it reports the
|
|
36
|
+
// source's intrinsic size as its content size, and any ancestor sized by its
|
|
37
|
+
// content grows to match. React-native-web views default to `flexShrink: 0`,
|
|
38
|
+
// so nothing pushes back — a 1280x720 capture measurably widened a 342px chat
|
|
39
|
+
// column to 367px the moment it loaded, breaking the very "does not resize"
|
|
40
|
+
// promise above. Taking the media OUT OF FLOW is what makes filling safe, and
|
|
41
|
+
// it lives here rather than in each caller because a caller cannot see the
|
|
42
|
+
// intrinsic size that causes it. The wrapper carries the positioning context
|
|
43
|
+
// so the media cannot escape to some unrelated positioned ancestor.
|
|
44
|
+
return (
|
|
45
|
+
<div style={videoWrapperStyle}>
|
|
46
|
+
<video
|
|
47
|
+
data-testid={testID}
|
|
48
|
+
aria-label={accessibilityLabel}
|
|
49
|
+
controls
|
|
50
|
+
preload="metadata"
|
|
51
|
+
src={src}
|
|
52
|
+
style={videoStyle}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Inline rather than StyleSheet: these land on DOM elements, not RN views.
|
|
59
|
+
const audioStyle = { width: "100%" } as const;
|
|
60
|
+
const videoWrapperStyle = {
|
|
61
|
+
position: "relative",
|
|
62
|
+
width: "100%",
|
|
63
|
+
height: "100%",
|
|
64
|
+
overflow: "hidden",
|
|
65
|
+
} as const;
|
|
66
|
+
const videoStyle = {
|
|
67
|
+
position: "absolute",
|
|
68
|
+
inset: 0,
|
|
69
|
+
width: "100%",
|
|
70
|
+
height: "100%",
|
|
71
|
+
display: "block",
|
|
72
|
+
// Letterbox rather than crop or stretch: a screen capture is evidence, and a
|
|
73
|
+
// cropped one silently hides part of what was on screen.
|
|
74
|
+
objectFit: "contain",
|
|
75
|
+
backgroundColor: colors.zinc["900"],
|
|
76
|
+
} as const;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface MediaPlayerProps {
|
|
2
|
+
/** Already-served URL for the media — signing/auth is the host's problem. */
|
|
3
|
+
src: string;
|
|
4
|
+
/**
|
|
5
|
+
* Which element to render. Not inferred from the MIME type on purpose: a
|
|
6
|
+
* screen recording carries both streams in one file, and the caller — not the
|
|
7
|
+
* container format — decides whether this surface is showing a picture.
|
|
8
|
+
*/
|
|
9
|
+
kind: "audio" | "video";
|
|
10
|
+
/** Accessible name for the player. Required: an unlabelled one is a blank control. */
|
|
11
|
+
accessibilityLabel: string;
|
|
12
|
+
testID?: string;
|
|
13
|
+
}
|
package/src/page_content.tsx
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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);
|