@lotics/ui 10.0.0 → 11.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/AGENTS.md +33 -6
- package/examples/tpl_allocate.tsx +2 -2
- package/examples/tpl_attendance.tsx +1 -1
- package/examples/tpl_calendar.tsx +1 -1
- package/examples/tpl_dashboard.tsx +1 -1
- package/examples/tpl_documents.tsx +1 -1
- package/examples/tpl_item_list.tsx +1 -1
- package/examples/tpl_lookup.tsx +1 -1
- package/examples/tpl_pick.tsx +3 -3
- package/examples/tpl_pivot.tsx +1 -1
- package/examples/tpl_record.tsx +24 -30
- package/examples/tpl_report.tsx +1 -1
- package/examples/tpl_rollup.tsx +1 -1
- package/examples/tpl_shifts.tsx +1 -1
- package/examples/tpl_statements.tsx +1 -1
- package/examples/tpl_stock.tsx +1 -1
- package/examples/tpl_task_board.tsx +1 -1
- package/examples/tpl_tasks.tsx +1 -1
- package/examples/tpl_tower.tsx +1 -1
- package/package.json +2 -1
- package/src/button.tsx +5 -1
- package/src/button_colors.ts +4 -1
- package/src/page_content.tsx +1 -1
- package/src/page_header.tsx +2 -2
- package/src/record_summary.tsx +3 -1
- package/src/ring_gauge.tsx +1 -1
- package/src/section_heading.tsx +70 -9
- package/src/section_stack.tsx +67 -0
- package/src/text.css +13 -2
- package/src/text.tsx +8 -2
- package/src/text_utils.ts +6 -1
- package/src/use_section_nav.ts +25 -5
package/AGENTS.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @lotics/ui — the UI reference
|
|
2
2
|
|
|
3
|
+
> **Upgrading to v11 from ≤10.x — the type scale was re-slotted.** The display sizes gained the
|
|
4
|
+
> missing `##` step: `xl` is now 22/24 (section title), `xxl` is 28/32 (page/record title — the
|
|
5
|
+
> OLD `xl` look), and the new `xxxl` is 32/48 (hero numbers — the OLD `xxl` look). Migrate an
|
|
6
|
+
> app with two renames IN THIS ORDER: first `size="xxl"` → `size="xxxl"`, then `size="xl"` →
|
|
7
|
+
> `size="xxl"` — every existing usage keeps its exact look. Also: `SectionHeadingTitle` is now
|
|
8
|
+
> ALWAYS `##` (22/24), and disabled `Button`s fade their label to the zinc-400 disabled ink on
|
|
9
|
+
> every variant.
|
|
10
|
+
|
|
3
11
|
A React-Native-Web component kit (renders on web **and** native). This file is the design
|
|
4
12
|
law + the "reach for what" guide. The **exact API** of any component is its source, which
|
|
5
13
|
ships with the package: read `node_modules/@lotics/ui/src/<name>.tsx`. **Never guess a prop —
|
|
@@ -121,7 +129,12 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
121
129
|
row/column/grand totals; press a cell to drill).
|
|
122
130
|
- **Surfaces & layout** — `Card` (+ `CardHeader`/`CardHeaderTitle`/`CardHeaderMeta`/`CardBody`/
|
|
123
131
|
`CardFooter`), `Section` (the card-less twin of `Card`: bare gap-spaced region + `SectionHeading`/
|
|
124
|
-
`SectionHeadingTitle`/`SectionHeadingMeta`; no body component — children are the body
|
|
132
|
+
`SectionHeadingTitle`/`SectionHeadingMeta`; no body component — children are the body;
|
|
133
|
+
), `Subsection` (+ `SubsectionHeading`/
|
|
134
|
+
`SubsectionHeadingTitle` — the named group INSIDE a section, `###` lg-semibold title),
|
|
135
|
+
`SectionStack` (the flat page's content column — owns the fixed 56px beat + hairline between
|
|
136
|
+
top-level blocks), `SubsectionStack` (the same law one step tighter — fixed 24px beat +
|
|
137
|
+
hairline between a section's `Subsection` groups), `SectionCard`,
|
|
125
138
|
`PageHeader` /
|
|
126
139
|
`PageContent`, `Stack`, `Spacer`, `Divider`, `Accordion`, `Tabs`, `SegmentedControl`, `Stepper`,
|
|
127
140
|
`DangerZone` (the destructive section — delete/archive — set apart at the bottom of a record/settings surface).
|
|
@@ -655,19 +668,32 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
655
668
|
- **Canvas**: full-bleed `colors.zinc[50]` ScrollView; content column `maxWidth` 880–1040,
|
|
656
669
|
centered, `padding: 28`. Cards float on the canvas — never white-on-white.
|
|
657
670
|
- **Heading vocabulary — ONE construct per altitude, no drift.**
|
|
658
|
-
· **Page band**: one per screen — `Text size="
|
|
671
|
+
· **Page band**: one per screen — `Text size="xxl" weight="semibold"` title (`#`) + one `sm muted`
|
|
659
672
|
subtitle that says what the screen decides. Right side: the screen's ONE primary action and/or
|
|
660
673
|
a period filter — never a summary Badge (those belong to the KPI strip).
|
|
661
674
|
· **Card header** (a card's own title band, or separate banded cards): `CardHeader` +
|
|
662
675
|
`CardHeaderTitle` (`sm semibold`; `info` when the title alone doesn't define the numbers).
|
|
663
676
|
· **Section title** — ONE construct: `Section` > `SectionHeading` > `SectionHeadingTitle`
|
|
664
677
|
(lg semibold + optional muted `description`/`info`), everything left-aligned at the column
|
|
665
|
-
edge. The `
|
|
666
|
-
one section from the next)
|
|
667
|
-
its own content. Inside a DRAWER, a full
|
|
678
|
+
edge. The page column is a `SectionStack` — it owns the between-section law (`gap: 32` +
|
|
679
|
+
a bare hairline separating one section from the next); the `Divider` NEVER goes directly
|
|
680
|
+
under the heading, which orphans the title from its own content. Inside a DRAWER, a full
|
|
681
|
+
multi-section WORKSPACE uses the real
|
|
668
682
|
`SectionHeading` (lg) — its meta + CTA slots carry each section's count and action; a
|
|
669
683
|
`Text size="sm" weight="semibold"` stand-in is only for small sub-groups. Do NOT wrap each section in its own `CardHeader`, do NOT
|
|
670
684
|
hand-roll sm-semibold lead lines on a flat page. NEVER a bare eyebrow as a section title.
|
|
685
|
+
· **The heading ramp is FIXED — the markdown ladder, no size knobs.** `#` = xxl
|
|
686
|
+
(`RecordSummary` / `PageHeader` title) → `##` = xl (`SectionHeadingTitle`, always) →
|
|
687
|
+
`###` = lg (`SubsectionHeadingTitle`, always) → body sm. One outline on every surface;
|
|
688
|
+
never restyle a heading level per-page.
|
|
689
|
+
· **Subsection title** — the level BELOW a section on a long record surface: `Subsection` >
|
|
690
|
+
`SubsectionHeading` > `SubsectionHeadingTitle` (`###` lg semibold; siblings — a `Badge`, a
|
|
691
|
+
`SectionHeadingMeta`, an action — ride the heading row's right edge). Sibling subsections
|
|
692
|
+
stack in a `SubsectionStack` (24 + hairline between groups — no margins, no hand-rolled
|
|
693
|
+
dividers); a headingless `Subsection` is the section's lead group. Group leads INSIDE a
|
|
694
|
+
subsection's rows are `md medium` at most. Do NOT hand-roll `Text weight="semibold"` group
|
|
695
|
+
leads inside a section, and never promote a subsection to its own section-level heading
|
|
696
|
+
just to separate it.
|
|
671
697
|
· **Eyebrow / label** (`<Text size="xs" color="muted" weight="medium">` — **sentence case, NEVER
|
|
672
698
|
`transform="uppercase"`**): a small quiet label above or beside content — an artifact tag
|
|
673
699
|
("Proposed", "Question", "Suggested edit"), a field name, a metric caption, a minor one-line
|
|
@@ -1017,7 +1043,8 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
|
|
|
1017
1043
|
text ·
|
|
1018
1044
|
markdown (Markdown — the single canonical markdown renderer for chat, apps, and `AgentRun`; rich on web via react-markdown/remark-gfm with copyable tables, plain-text on native; takes a markdown `children` string; import `@lotics/ui/markdown.css` once for styling) ·
|
|
1019
1045
|
card (Card · CardHeader · CardHeaderTitle · CardHeaderMeta · CardBody · CardFooter) ·
|
|
1020
|
-
section_heading (Section · SectionHeading · SectionHeadingTitle · SectionHeadingMeta — the card-less twin of the Card family, compound, owns no margin; spacing via the Section gap, no body component. `SectionHeadingTitle`
|
|
1046
|
+
section_heading (Section · SectionHeading · SectionHeadingTitle · SectionHeadingMeta · Subsection · SubsectionHeading · SubsectionHeadingTitle — the card-less twin of the Card family, compound, owns no margin; spacing via the Section gap (12, fixed), no body component. `SectionHeadingTitle` is ALWAYS `##` (xl semibold; `weight="medium"` opt-down only) + `info` for an ⓘ provenance popover after the title, same as `CardHeaderTitle.info`. `SubsectionHeadingTitle` is the `###` lg-semibold level-3 title of a named group inside a section — heading-row siblings ride its right edge; the heading ramp is FIXED: # xxl / ## xl / ### lg, no size props) ·
|
|
1047
|
+
section_stack (SectionStack · SubsectionStack — divided stacks that own the between-block law: a fixed beat + hairline Divider BETWEEN blocks, skipping null children (56 for the flat page's Sections, 24 for a section's Subsections); stop hand-rolling gap + `<Divider />` pairs) · badge ·
|
|
1021
1048
|
option_badge (OptionBadge — a select value as its configured colored badge) ·
|
|
1022
1049
|
member_chip (MemberChip — avatar + name; the universal person render) ·
|
|
1023
1050
|
member_select (MemberSelect — a Picker of MemberChip options; the member picker) ·
|
|
@@ -67,7 +67,7 @@ export function TplAllocate() {
|
|
|
67
67
|
<View style={{ width: "100%", maxWidth: 1000, alignSelf: "center", gap: 16 }}>
|
|
68
68
|
{/* header */}
|
|
69
69
|
<View style={{ gap: 2 }}>
|
|
70
|
-
<Text size="
|
|
70
|
+
<Text size="xxl" weight="semibold">Apply payment</Text>
|
|
71
71
|
<Text size="sm" color="muted">Split one received payment across the customer's open invoices — oldest first, or line by line</Text>
|
|
72
72
|
</View>
|
|
73
73
|
|
|
@@ -76,7 +76,7 @@ export function TplAllocate() {
|
|
|
76
76
|
<View style={{ padding: 20, flexDirection: "row", alignItems: "flex-start", gap: 24, flexWrap: "wrap" }}>
|
|
77
77
|
<View style={{ gap: 2, flexGrow: 1, flexBasis: 220 }}>
|
|
78
78
|
<Text size="xs" color="muted" weight="medium">Payment received</Text>
|
|
79
|
-
<Text size="
|
|
79
|
+
<Text size="xxxl" weight="semibold" tabular>{formatMoney(PAYMENT.amount)}</Text>
|
|
80
80
|
<Text size="sm" color="muted">{`${PAYMENT.from} · ref ${PAYMENT.ref} · ${PAYMENT.date}`}</Text>
|
|
81
81
|
</View>
|
|
82
82
|
<View style={{ gap: 10, flexGrow: 1, flexBasis: 300 }}>
|
|
@@ -274,7 +274,7 @@ export function TplAttendance() {
|
|
|
274
274
|
{/* header band */}
|
|
275
275
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
|
|
276
276
|
<View style={{ gap: 2, flex: 1 }}>
|
|
277
|
-
<Text size="
|
|
277
|
+
<Text size="xxl" weight="semibold">Attendance</Text>
|
|
278
278
|
<Text size="sm" color="muted">Today, Friday 12/06 — in/out times for 9 employees</Text>
|
|
279
279
|
</View>
|
|
280
280
|
<Picker
|
|
@@ -209,7 +209,7 @@ export function TplCalendar() {
|
|
|
209
209
|
{/* header band */}
|
|
210
210
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
|
|
211
211
|
<View style={{ gap: 2, flex: 1 }}>
|
|
212
|
-
<Text size="
|
|
212
|
+
<Text size="xxl" weight="semibold">Calendar & planning</Text>
|
|
213
213
|
<Text size="sm" color="muted">This week — deliveries, reconciliations and customer visits</Text>
|
|
214
214
|
</View>
|
|
215
215
|
<Text size="sm" color="muted" tabular>
|
|
@@ -313,7 +313,7 @@ export function TplDashboard() {
|
|
|
313
313
|
screen needs it) */}
|
|
314
314
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
|
|
315
315
|
<View style={{ gap: 2, flex: 1 }}>
|
|
316
|
-
<Text size="
|
|
316
|
+
<Text size="xxl" weight="semibold">Executive dashboard</Text>
|
|
317
317
|
<Text size="sm" color="muted">Revenue, the sales pipeline and what needs handling — refreshed every morning</Text>
|
|
318
318
|
</View>
|
|
319
319
|
<DateRangeFilterField value={period} onValueChange={setPeriod} locale="en-GB" labels={NHAN_BO_LOC} />
|
|
@@ -351,7 +351,7 @@ export function TplDocuments() {
|
|
|
351
351
|
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28, paddingBottom: 120 }}>
|
|
352
352
|
<View style={{ maxWidth: 720, width: "100%", alignSelf: "center", gap: 24 }}>
|
|
353
353
|
<View style={{ gap: 2 }}>
|
|
354
|
-
<Text size="
|
|
354
|
+
<Text size="xxl" weight="semibold">PO-2481 — Nordic Furniture AB</Text>
|
|
355
355
|
<Text size="sm" color="muted">Sea freight · Gothenburg → Ho Chi Minh City · FOB · ETD 18 Jul 2026</Text>
|
|
356
356
|
</View>
|
|
357
357
|
|
|
@@ -1212,7 +1212,7 @@ export function TplItemList() {
|
|
|
1212
1212
|
<View style={{ width: "100%", maxWidth: 1040, alignSelf: "center", gap: 16 }}>
|
|
1213
1213
|
{/* header — title only; the New case action lives on the toolbar row */}
|
|
1214
1214
|
<View style={{ gap: 2 }}>
|
|
1215
|
-
<Text size="
|
|
1215
|
+
<Text size="xxl" weight="semibold">Records</Text>
|
|
1216
1216
|
<Text size="sm" color="muted">Track each record's status and print the right document for its stage</Text>
|
|
1217
1217
|
</View>
|
|
1218
1218
|
|
package/examples/tpl_lookup.tsx
CHANGED
|
@@ -255,7 +255,7 @@ export function TplLookup() {
|
|
|
255
255
|
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Suggested HS code</Text>
|
|
256
256
|
<Confidence level={active.confidence} />
|
|
257
257
|
</View>
|
|
258
|
-
<Text size="
|
|
258
|
+
<Text size="xxxl" weight="semibold" tabular>{active.code}</Text>
|
|
259
259
|
<Text size="sm" color="muted">{active.heading}</Text>
|
|
260
260
|
</View>
|
|
261
261
|
|
package/examples/tpl_pick.tsx
CHANGED
|
@@ -101,7 +101,7 @@ function CurrentTask({ line, index, total, onConfirm, onShort }: {
|
|
|
101
101
|
{/* the bin is the biggest thing on screen — it's where you walk */}
|
|
102
102
|
<View style={{ gap: 2 }}>
|
|
103
103
|
<Text size="xs" color="muted" weight="medium">Go to bin</Text>
|
|
104
|
-
<Text size="
|
|
104
|
+
<Text size="xxxl" weight="semibold" tabular>{line.bin}</Text>
|
|
105
105
|
</View>
|
|
106
106
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 16, flexWrap: "wrap" }}>
|
|
107
107
|
<View style={{ gap: 2, flexGrow: 1, flexBasis: 200 }}>
|
|
@@ -110,7 +110,7 @@ function CurrentTask({ line, index, total, onConfirm, onShort }: {
|
|
|
110
110
|
</View>
|
|
111
111
|
<View style={{ flexDirection: "row", alignItems: "baseline", gap: 8 }}>
|
|
112
112
|
<Text size="sm" color="muted">Pick</Text>
|
|
113
|
-
<Text size="
|
|
113
|
+
<Text size="xxl" weight="semibold" tabular>{line.qty}</Text>
|
|
114
114
|
<Text size="sm" color="muted">units</Text>
|
|
115
115
|
</View>
|
|
116
116
|
</View>
|
|
@@ -199,7 +199,7 @@ export function TplPick() {
|
|
|
199
199
|
{/* header — the wave + the cart it's filling */}
|
|
200
200
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12, flexWrap: "wrap" }}>
|
|
201
201
|
<View style={{ gap: 2, flex: 1 }}>
|
|
202
|
-
<Text size="
|
|
202
|
+
<Text size="xxl" weight="semibold">Queue W-0142</Text>
|
|
203
203
|
<Text size="sm" color="muted">7 lines for 3 outbound orders · Cart C-07</Text>
|
|
204
204
|
</View>
|
|
205
205
|
<Button title="Pause wave" color="muted" onPress={() => {}} />
|
package/examples/tpl_pivot.tsx
CHANGED
|
@@ -128,7 +128,7 @@ export function TplPivot() {
|
|
|
128
128
|
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
129
129
|
<View style={{ width: "100%", maxWidth: 960, alignSelf: "center", gap: 16 }}>
|
|
130
130
|
<View style={{ gap: 2 }}>
|
|
131
|
-
<Text size="
|
|
131
|
+
<Text size="xxl" weight="semibold">Stock distribution</Text>
|
|
132
132
|
<Text size="sm" color="muted">Where each category sits across the network — press a cell to list what's behind it</Text>
|
|
133
133
|
</View>
|
|
134
134
|
|
package/examples/tpl_record.tsx
CHANGED
|
@@ -12,7 +12,8 @@ import type { PickerOption } from "@lotics/ui/picker";
|
|
|
12
12
|
import { Combobox, ComboboxInput, ComboboxContent } from "@lotics/ui/combobox";
|
|
13
13
|
import { DetailRow, DetailTable } from "@lotics/ui/detail_row";
|
|
14
14
|
import { Callout, CalloutText } from "@lotics/ui/callout";
|
|
15
|
-
import { Section, SectionHeading, SectionHeadingMeta, SectionHeadingTitle } from "@lotics/ui/section_heading";
|
|
15
|
+
import { Section, SectionHeading, SectionHeadingMeta, SectionHeadingTitle, Subsection, SubsectionHeading, SubsectionHeadingTitle } from "@lotics/ui/section_heading";
|
|
16
|
+
import { SectionStack, SubsectionStack } from "@lotics/ui/section_stack";
|
|
16
17
|
import { MenuButton } from "@lotics/ui/menu_button";
|
|
17
18
|
import { Modal, ModalBody, ModalHeader } from "@lotics/ui/modal";
|
|
18
19
|
import { useSectionNav } from "@lotics/ui/use_section_nav";
|
|
@@ -653,7 +654,12 @@ export function TplRecord() {
|
|
|
653
654
|
</View>
|
|
654
655
|
)}
|
|
655
656
|
<ScrollView ref={nav.scrollRef} onScroll={nav.onScroll} scrollEventThrottle={16} style={{ flex: 1 }} contentContainerStyle={{ padding: 28, paddingBottom: 96 }}>
|
|
656
|
-
|
|
657
|
+
{/* the page column is a SectionStack — it owns the 56px beat + the
|
|
658
|
+
hairline BETWEEN top-level blocks (a conditional block that renders
|
|
659
|
+
null never leaves a stray divider) */}
|
|
660
|
+
<SectionStack style={{ width: "100%", maxWidth: 720, alignSelf: "center" }}>
|
|
661
|
+
{/* header band — app bar + summary + key facts read as ONE block */}
|
|
662
|
+
<View style={{ gap: 32 }}>
|
|
657
663
|
{/* app bar — CREATE is one click; the surface is where everything is refined */}
|
|
658
664
|
<View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", columnGap: 10, rowGap: 8 }}>
|
|
659
665
|
<Text size="sm" color="muted">Records <Text size="sm" color="muted">/</Text> <Text size="sm" weight="medium" color="default" tabular>{code}</Text></Text>
|
|
@@ -691,8 +697,7 @@ export function TplRecord() {
|
|
|
691
697
|
<InlineSelect value={terms} options={TERMS} onSave={persist(setTerms)} accessibilityLabel="Payment terms" />
|
|
692
698
|
</DetailRow>
|
|
693
699
|
</DetailTable>
|
|
694
|
-
|
|
695
|
-
<Divider />
|
|
700
|
+
</View>
|
|
696
701
|
|
|
697
702
|
{/* ONE DetailTable = the record's field grid: label · editor · trailing
|
|
698
703
|
columns set once on the parent, every row aligned like a table —
|
|
@@ -743,8 +748,6 @@ export function TplRecord() {
|
|
|
743
748
|
</Section>
|
|
744
749
|
</View>
|
|
745
750
|
|
|
746
|
-
<Divider />
|
|
747
|
-
|
|
748
751
|
{/* CUSTOMER — TWO states, no swap mode. Attached: the read-only-first
|
|
749
752
|
card (identity + editable fields — Tax ID with the registry Fetch
|
|
750
753
|
that fills Contact + City, which are inline-editable too); Remove
|
|
@@ -857,8 +860,6 @@ export function TplRecord() {
|
|
|
857
860
|
</Section>
|
|
858
861
|
</View>
|
|
859
862
|
|
|
860
|
-
<Divider />
|
|
861
|
-
|
|
862
863
|
{/* BILLING — invoice documents on the record, in the inline vocabulary.
|
|
863
864
|
The issue action lives in each band's header; ONE section-level
|
|
864
865
|
callout explains what blocks issuing (no per-band totals, no
|
|
@@ -884,6 +885,9 @@ export function TplRecord() {
|
|
|
884
885
|
</Callout>
|
|
885
886
|
) : null}
|
|
886
887
|
|
|
888
|
+
{/* the section's GROUPS — bands · collect · deposit — divide via the
|
|
889
|
+
SubsectionStack law (24 + hairline), no hand-rolled dividers */}
|
|
890
|
+
<SubsectionStack>
|
|
887
891
|
<View style={{ gap: 10 }}>
|
|
888
892
|
{invoices.map((inv) => (
|
|
889
893
|
<InvoiceBand
|
|
@@ -905,9 +909,8 @@ export function TplRecord() {
|
|
|
905
909
|
))}
|
|
906
910
|
</View>
|
|
907
911
|
|
|
908
|
-
{/* collect band */}
|
|
909
|
-
<
|
|
910
|
-
<View style={{ gap: 10 }}>
|
|
912
|
+
{/* collect band — a headingless Subsection: the total owns the number */}
|
|
913
|
+
<Subsection>
|
|
911
914
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
912
915
|
<Text weight="semibold" style={{ flex: 1 }}>Total to collect</Text>
|
|
913
916
|
<Text weight="semibold" size="lg" tabular>{formatMoney(grandTotal)}</Text>
|
|
@@ -923,15 +926,15 @@ export function TplRecord() {
|
|
|
923
926
|
<Text size="xs" color="muted" style={{ flex: 1 }}>{issuedCount} of {invoices.length} issued</Text>
|
|
924
927
|
<Button title="Print receipt" color="primary" disabled={grandTotal <= 0} onPress={printReceipt} />
|
|
925
928
|
</View>
|
|
926
|
-
</
|
|
929
|
+
</Subsection>
|
|
927
930
|
|
|
928
|
-
{/* deposit band — labelled separate, never part
|
|
929
|
-
|
|
930
|
-
<
|
|
931
|
-
<
|
|
932
|
-
<
|
|
931
|
+
{/* deposit band — a named Subsection: labelled separate, never part
|
|
932
|
+
of the total (the badge rides the heading row's right edge) */}
|
|
933
|
+
<Subsection>
|
|
934
|
+
<SubsectionHeading>
|
|
935
|
+
<SubsectionHeadingTitle>Deposit</SubsectionHeadingTitle>
|
|
933
936
|
<Badge label="Collected separately" color="zinc" />
|
|
934
|
-
</
|
|
937
|
+
</SubsectionHeading>
|
|
935
938
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, flexWrap: "wrap", minHeight: 40 }}>
|
|
936
939
|
<Text size="sm" color="muted" style={{ width: 150 }}>Refundable</Text>
|
|
937
940
|
<View style={{ flexGrow: 1, flexBasis: 160 }}>
|
|
@@ -951,12 +954,11 @@ export function TplRecord() {
|
|
|
951
954
|
onPress={() => Alert.alert("Deposit receipt", `Printing deposit receipt for ${formatMoney(deposit)}.`, [{ text: "OK" }])}
|
|
952
955
|
/>
|
|
953
956
|
</View>
|
|
954
|
-
</
|
|
957
|
+
</Subsection>
|
|
958
|
+
</SubsectionStack>
|
|
955
959
|
</Section>
|
|
956
960
|
</View>
|
|
957
961
|
|
|
958
|
-
<Divider />
|
|
959
|
-
|
|
960
962
|
{/* FILES — the record's document CRUD: drop to add, click a thumbnail
|
|
961
963
|
to preview in the gallery, ✕ to delete. */}
|
|
962
964
|
<View onLayout={nav.register("files")}>
|
|
@@ -996,8 +998,6 @@ export function TplRecord() {
|
|
|
996
998
|
</Section>
|
|
997
999
|
</View>
|
|
998
1000
|
|
|
999
|
-
<Divider />
|
|
1000
|
-
|
|
1001
1001
|
{/* TASKS — the handoff checklist, grouped by the desk that owns
|
|
1002
1002
|
them; every row edits (the stage gates the handoff and Billing,
|
|
1003
1003
|
never task editing). The CURRENT desk's open tasks warn beside
|
|
@@ -1159,8 +1159,6 @@ export function TplRecord() {
|
|
|
1159
1159
|
</Section>
|
|
1160
1160
|
</View>
|
|
1161
1161
|
|
|
1162
|
-
<Divider />
|
|
1163
|
-
|
|
1164
1162
|
{/* THE HANDOFF GATE — the atomic boundary between departments, not a
|
|
1165
1163
|
form and not a message: the record itself changes desks. Each stage
|
|
1166
1164
|
names what it owes (the why-hint) before its CTA un-gates; the
|
|
@@ -1191,8 +1189,6 @@ export function TplRecord() {
|
|
|
1191
1189
|
</View>
|
|
1192
1190
|
)}
|
|
1193
1191
|
|
|
1194
|
-
<Divider />
|
|
1195
|
-
|
|
1196
1192
|
{/* the settings shape: title + description left, a live control right,
|
|
1197
1193
|
applied on change */}
|
|
1198
1194
|
<View onLayout={nav.register("preferences")}>
|
|
@@ -1217,15 +1213,13 @@ export function TplRecord() {
|
|
|
1217
1213
|
</Section>
|
|
1218
1214
|
</View>
|
|
1219
1215
|
|
|
1220
|
-
<Divider />
|
|
1221
|
-
|
|
1222
1216
|
<DangerZone
|
|
1223
1217
|
title="Delete record"
|
|
1224
1218
|
description="Permanently remove this record and its invoices, charges, and history. This can't be undone."
|
|
1225
1219
|
>
|
|
1226
1220
|
<Button title="Delete record" color="danger" onPress={() => {}} />
|
|
1227
1221
|
</DangerZone>
|
|
1228
|
-
</
|
|
1222
|
+
</SectionStack>
|
|
1229
1223
|
|
|
1230
1224
|
{/* the narrow-mode section picker — a full-page takeover: pick a section
|
|
1231
1225
|
to jump there; Escape or the close control dismisses */}
|
package/examples/tpl_report.tsx
CHANGED
|
@@ -222,7 +222,7 @@ export function TplReport() {
|
|
|
222
222
|
{/* header band — title + the persistent period + the one primary action */}
|
|
223
223
|
<View style={{ flexDirection: "row", gap: 16, alignItems: "flex-start", flexWrap: "wrap", zIndex: 20 }}>
|
|
224
224
|
<View style={{ gap: 2, flex: 1, minWidth: 260 }}>
|
|
225
|
-
<Text size="
|
|
225
|
+
<Text size="xxl" weight="semibold">Payments</Text>
|
|
226
226
|
<Text size="sm" color="muted">Look up payments by customer, reference, or operator across a period — totals, facets, and the matching lines</Text>
|
|
227
227
|
</View>
|
|
228
228
|
<View style={{ flexDirection: "row", gap: 12, alignItems: "center", flexWrap: "wrap" }}>
|
package/examples/tpl_rollup.tsx
CHANGED
|
@@ -147,7 +147,7 @@ export function TplRollup() {
|
|
|
147
147
|
<View style={{ width: "100%", maxWidth: 980, alignSelf: "center", gap: 16 }}>
|
|
148
148
|
{/* header band */}
|
|
149
149
|
<View style={{ gap: 2 }}>
|
|
150
|
-
<Text size="
|
|
150
|
+
<Text size="xxl" weight="semibold">Network rollup</Text>
|
|
151
151
|
<Text size="sm" color="muted">This month by region, site and lane — expand the branch that drags the total</Text>
|
|
152
152
|
</View>
|
|
153
153
|
|
package/examples/tpl_shifts.tsx
CHANGED
|
@@ -185,7 +185,7 @@ export function TplShifts() {
|
|
|
185
185
|
<View style={{ width: "100%", maxWidth: 1100, alignSelf: "center", gap: 16 }}>
|
|
186
186
|
{/* header band */}
|
|
187
187
|
<View style={{ gap: 2 }}>
|
|
188
|
-
<Text size="
|
|
188
|
+
<Text size="xxl" weight="semibold">Shift staffing · week 15–21/06</Text>
|
|
189
189
|
<Text size="sm" color="muted">Members sign up, you allocate — fill every slot without burning anyone out</Text>
|
|
190
190
|
</View>
|
|
191
191
|
|
|
@@ -119,7 +119,7 @@ function StatementPage({ title, meta, children }: { title: string; meta: string;
|
|
|
119
119
|
<View style={{ width: "100%", maxWidth: 680, alignSelf: "center", gap: 20 }}>
|
|
120
120
|
<View style={{ flexDirection: "row", alignItems: "flex-start", flexWrap: "wrap", columnGap: 12, rowGap: 8 }}>
|
|
121
121
|
<View style={{ flexGrow: 1, flexBasis: 240, gap: 2 }}>
|
|
122
|
-
<Text size="
|
|
122
|
+
<Text size="xxl" weight="semibold">{title}</Text>
|
|
123
123
|
<Text size="sm" color="muted">{meta}</Text>
|
|
124
124
|
</View>
|
|
125
125
|
<Button title="Export" color="secondary" icon="file-down" onPress={() => {}} />
|
package/examples/tpl_stock.tsx
CHANGED
|
@@ -193,7 +193,7 @@ export function TplStock() {
|
|
|
193
193
|
{/* header band */}
|
|
194
194
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
|
|
195
195
|
<View style={{ gap: 2, flex: 1 }}>
|
|
196
|
-
<Text size="
|
|
196
|
+
<Text size="xxl" weight="semibold">Stock overview</Text>
|
|
197
197
|
<Text size="sm" color="muted">The whole fleet at a glance — drill into any slice, then walk the units one by one</Text>
|
|
198
198
|
</View>
|
|
199
199
|
<Button title="Register units" color="primary" onPress={() => {}} />
|
|
@@ -328,7 +328,7 @@ export function TplTaskBoard() {
|
|
|
328
328
|
<>
|
|
329
329
|
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ paddingVertical: 28, paddingHorizontal: 24, paddingBottom: 96 }}>
|
|
330
330
|
<View style={{ maxWidth: 1240, width: "100%", alignSelf: "center", gap: 16 }}>
|
|
331
|
-
<Text size="
|
|
331
|
+
<Text size="xxl" weight="semibold">Task board</Text>
|
|
332
332
|
|
|
333
333
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
334
334
|
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 8, flex: 1 }}>
|
package/examples/tpl_tasks.tsx
CHANGED
|
@@ -235,7 +235,7 @@ export function TplTasks() {
|
|
|
235
235
|
<>
|
|
236
236
|
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ paddingVertical: 28, paddingHorizontal: 20, paddingBottom: 96 }}>
|
|
237
237
|
<View style={{ maxWidth: 680, width: "100%", alignSelf: "center", gap: 14 }}>
|
|
238
|
-
<Text size="
|
|
238
|
+
<Text size="xxl" weight="semibold">Tasks</Text>
|
|
239
239
|
|
|
240
240
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
241
241
|
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 8, flex: 1 }}>
|
package/examples/tpl_tower.tsx
CHANGED
|
@@ -166,7 +166,7 @@ export function TplTower() {
|
|
|
166
166
|
<View style={{ width: "100%", maxWidth: 1100, alignSelf: "center", gap: 16 }}>
|
|
167
167
|
{/* header band */}
|
|
168
168
|
<View style={{ gap: 2 }}>
|
|
169
|
-
<Text size="
|
|
169
|
+
<Text size="xxl" weight="semibold">Control tower</Text>
|
|
170
170
|
<Text size="sm" color="muted">Every unit's live state — scan for red, work the exceptions, read the pattern</Text>
|
|
171
171
|
</View>
|
|
172
172
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -173,6 +173,7 @@
|
|
|
173
173
|
"./list_item": "./src/list_item.tsx",
|
|
174
174
|
"./section": "./src/section.tsx",
|
|
175
175
|
"./section_heading": "./src/section_heading.tsx",
|
|
176
|
+
"./section_stack": "./src/section_stack.tsx",
|
|
176
177
|
"./separator": "./src/separator.tsx",
|
|
177
178
|
"./stack": "./src/stack.tsx",
|
|
178
179
|
"./status_badge": "./src/status_badge.tsx",
|
package/src/button.tsx
CHANGED
|
@@ -151,6 +151,10 @@ export function Button(props: ButtonProps) {
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
export function getButtonTextColor(color?: ButtonColor, disabled?: boolean): TextColor {
|
|
154
|
+
// Disabled fades the label to the zinc-400 disabled ink on EVERY variant —
|
|
155
|
+
// the washed background alone is a one-step tint nobody reads as disabled.
|
|
156
|
+
// (The primary keeps inverted ink; its zinc-300 surface carries the wash.)
|
|
157
|
+
if (disabled) return color === "primary" ? "inverted" : "zinc-400";
|
|
154
158
|
switch (color) {
|
|
155
159
|
case "primary":
|
|
156
160
|
return "inverted";
|
|
@@ -162,7 +166,7 @@ export function getButtonTextColor(color?: ButtonColor, disabled?: boolean): Tex
|
|
|
162
166
|
case "muted":
|
|
163
167
|
return "muted";
|
|
164
168
|
default:
|
|
165
|
-
return
|
|
169
|
+
return "default";
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
|
package/src/button_colors.ts
CHANGED
|
@@ -6,6 +6,9 @@ import { colors } from "./colors";
|
|
|
6
6
|
* same colour. Lives in its own module (not `button.tsx`) so a test that mocks
|
|
7
7
|
* `@lotics/ui/button` can't leave `IconButton` without it. */
|
|
8
8
|
export function getButtonIconColor(color?: ButtonColor, disabled?: boolean): string {
|
|
9
|
+
// Disabled mutes the glyph on EVERY variant (mirrors `getButtonTextColor`) —
|
|
10
|
+
// the primary keeps white ink on its washed zinc-300 disabled surface.
|
|
11
|
+
if (disabled) return color === "primary" ? colors.white : colors.zinc["400"];
|
|
9
12
|
switch (color) {
|
|
10
13
|
case "primary":
|
|
11
14
|
return colors.white;
|
|
@@ -18,6 +21,6 @@ export function getButtonIconColor(color?: ButtonColor, disabled?: boolean): str
|
|
|
18
21
|
case "muted":
|
|
19
22
|
return colors.zinc["600"];
|
|
20
23
|
default:
|
|
21
|
-
return
|
|
24
|
+
return colors.zinc["950"];
|
|
22
25
|
}
|
|
23
26
|
}
|
package/src/page_content.tsx
CHANGED
package/src/page_header.tsx
CHANGED
|
@@ -22,12 +22,12 @@ export function PageHeader(props: PageHeaderProps) {
|
|
|
22
22
|
{left}
|
|
23
23
|
{right}
|
|
24
24
|
</View>
|
|
25
|
-
<Text size="
|
|
25
|
+
<Text size="xxl" weight="semibold">
|
|
26
26
|
{title}
|
|
27
27
|
</Text>
|
|
28
28
|
</>
|
|
29
29
|
) : (
|
|
30
|
-
<Text size="
|
|
30
|
+
<Text size="xxl" weight="semibold">
|
|
31
31
|
{title}
|
|
32
32
|
</Text>
|
|
33
33
|
)}
|
package/src/record_summary.tsx
CHANGED
|
@@ -47,7 +47,9 @@ export function RecordSummary(props: RecordSummaryProps) {
|
|
|
47
47
|
<View style={styles.identityRow}>
|
|
48
48
|
<View style={styles.identity}>
|
|
49
49
|
<View style={styles.titleRow}>
|
|
50
|
-
|
|
50
|
+
{/* xxl — the `#` of the heading ramp (# xxl → ## xl section →
|
|
51
|
+
### lg subsection); the identity always tops the outline */}
|
|
52
|
+
<Text size="xxl" weight="semibold" tabular level={level}>
|
|
51
53
|
{title}
|
|
52
54
|
</Text>
|
|
53
55
|
{status ?? null}
|
package/src/ring_gauge.tsx
CHANGED
package/src/section_heading.tsx
CHANGED
|
@@ -38,13 +38,72 @@ export interface SectionProps {
|
|
|
38
38
|
*/
|
|
39
39
|
export function Section(props: SectionProps) {
|
|
40
40
|
// gap 12 spaces the heading from its body. Space BETWEEN sections belongs to
|
|
41
|
-
// the page column
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// section).
|
|
41
|
+
// the page column (`SectionStack`, 56 + hairline); a body of `Subsection`
|
|
42
|
+
// groups divides via a nested `SubsectionStack` (24 + hairline) —
|
|
43
|
+
// never a hairline directly under the heading (it belongs to its content).
|
|
45
44
|
return <View style={[{ gap: 12 }, props.style]}>{props.children}</View>;
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
export interface SubsectionProps {
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
style?: StyleProp<ViewStyle>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One named group INSIDE a `Section` — the level below `Section` on a long
|
|
54
|
+
* record surface (`###` in the heading ramp). Compose a `SubsectionHeading`
|
|
55
|
+
* then the group's rows; sibling subsections stack in a `SubsectionStack`
|
|
56
|
+
* (24 + hairline between groups — no margins, no hand-rolled dividers). A
|
|
57
|
+
* headingless `Subsection` is fine for the section's lead group.
|
|
58
|
+
*
|
|
59
|
+
* <Section>
|
|
60
|
+
* <SectionHeading><SectionHeadingTitle>Delivery</SectionHeadingTitle></SectionHeading>
|
|
61
|
+
* <SubsectionStack>
|
|
62
|
+
* <Subsection>{lead rows}</Subsection>
|
|
63
|
+
* <Subsection>
|
|
64
|
+
* <SubsectionHeading><SubsectionHeadingTitle>Carrier</SubsectionHeadingTitle></SubsectionHeading>
|
|
65
|
+
* {rows}
|
|
66
|
+
* </Subsection>
|
|
67
|
+
* </SubsectionStack>
|
|
68
|
+
* </Section>
|
|
69
|
+
*/
|
|
70
|
+
export function Subsection(props: SubsectionProps) {
|
|
71
|
+
return <View style={[{ gap: 12 }, props.style]}>{props.children}</View>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SubsectionHeadingProps {
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
style?: StyleProp<ViewStyle>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function SubsectionHeading(props: SubsectionHeadingProps) {
|
|
80
|
+
return (
|
|
81
|
+
<View style={[{ flexDirection: "row", alignItems: "center", gap: 10 }, props.style]}>
|
|
82
|
+
{props.children}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SubsectionHeadingTitleProps {
|
|
88
|
+
children: React.ReactNode;
|
|
89
|
+
/** Heading rank. Defaults to 3 — one level under `SectionHeadingTitle`'s 2. */
|
|
90
|
+
level?: HeadingLevel;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The subsection title — ALWAYS `###` (lg semibold), the fixed step under the
|
|
94
|
+
* xl section title and above the md/sm body. Grows to push siblings (a
|
|
95
|
+
* `SectionHeadingMeta`, a `Badge`, an action) to the right edge. */
|
|
96
|
+
export function SubsectionHeadingTitle(props: SubsectionHeadingTitleProps) {
|
|
97
|
+
const { children, level = 3 } = props;
|
|
98
|
+
return (
|
|
99
|
+
<View style={{ flex: 1 }}>
|
|
100
|
+
<Text level={level} size="lg" weight="semibold">
|
|
101
|
+
{children}
|
|
102
|
+
</Text>
|
|
103
|
+
</View>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
48
107
|
export interface SectionHeadingProps {
|
|
49
108
|
children: React.ReactNode;
|
|
50
109
|
style?: StyleProp<ViewStyle>;
|
|
@@ -73,16 +132,18 @@ export interface SectionHeadingTitleProps {
|
|
|
73
132
|
info?: string;
|
|
74
133
|
}
|
|
75
134
|
|
|
76
|
-
/** The title block —
|
|
77
|
-
*
|
|
78
|
-
*
|
|
135
|
+
/** The title block — ALWAYS `##` (xl semibold): the heading ramp is the fixed
|
|
136
|
+
* markdown ladder `#` xxl (`RecordSummary`/`PageHeader`) → `##` xl (section) →
|
|
137
|
+
* `###` lg (`SubsectionHeadingTitle`) over the sm body — no per-surface size
|
|
138
|
+
* knobs, one outline everywhere. Optional muted `description` line; grows to
|
|
139
|
+
* push any sibling actions to the right edge. */
|
|
79
140
|
export function SectionHeadingTitle(props: SectionHeadingTitleProps) {
|
|
80
141
|
const { children, description, icon, level = 2, weight = "semibold", info } = props;
|
|
81
142
|
return (
|
|
82
143
|
<View style={{ flex: 1, gap: 2 }}>
|
|
83
144
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
84
|
-
{icon ? <Icon name={icon} size={
|
|
85
|
-
<Text level={level} weight={weight} size="
|
|
145
|
+
{icon ? <Icon name={icon} size={20} /> : null}
|
|
146
|
+
<Text level={level} weight={weight} size="xl">
|
|
86
147
|
{children}
|
|
87
148
|
</Text>
|
|
88
149
|
{info ? <InfoPopover text={info} accessibilityLabel={useLoticsLocale().sectionHeading.info} /> : null}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Children, Fragment } from "react";
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { Divider } from "./divider";
|
|
4
|
+
|
|
5
|
+
interface StackProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/** Hairline between blocks (default true). Turn off for a short surface
|
|
8
|
+
* where the beat alone is enough. */
|
|
9
|
+
divided?: boolean;
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Interleave a hairline between the non-null blocks of a gap column. */
|
|
14
|
+
function DividedStack({ children, divided = true, gap, style }: StackProps & { gap: number }) {
|
|
15
|
+
const blocks = Children.toArray(children); // drops null/undefined/false
|
|
16
|
+
return (
|
|
17
|
+
<View style={[{ gap }, style]}>
|
|
18
|
+
{blocks.map((block, i) => (
|
|
19
|
+
// Children.toArray keys are stable for conditional lists; the fragment
|
|
20
|
+
// pairs each block with the hairline that separates it from the last.
|
|
21
|
+
<Fragment key={`sec_${i}`}>
|
|
22
|
+
{divided && i > 0 ? <Divider /> : null}
|
|
23
|
+
{block}
|
|
24
|
+
</Fragment>
|
|
25
|
+
))}
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SectionStackProps = StackProps;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The content column of a flat record/form page — it OWNS the between-section
|
|
34
|
+
* law: a fixed 56px beat with a hairline `Divider` between top-level blocks
|
|
35
|
+
* (never under a heading; the hairline separates a block from the PREVIOUS
|
|
36
|
+
* one). Null/false children are skipped, so a conditional section never
|
|
37
|
+
* leaves a stray hairline. No knobs — every flat page shares one rhythm.
|
|
38
|
+
*
|
|
39
|
+
* <SectionStack>
|
|
40
|
+
* {headerBand}
|
|
41
|
+
* <Section>…</Section>
|
|
42
|
+
* {canTransfer ? <Section>…</Section> : null}
|
|
43
|
+
* </SectionStack>
|
|
44
|
+
*/
|
|
45
|
+
export function SectionStack(props: SectionStackProps) {
|
|
46
|
+
return <DividedStack {...props} gap={56} />;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type SubsectionStackProps = StackProps;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The divided stack of `Subsection` groups INSIDE a `Section` — the same
|
|
53
|
+
* hairline law one step tighter: a fixed 24px beat. Compose it as the
|
|
54
|
+
* section's body (after the `SectionHeading`); a section's groups divide
|
|
55
|
+
* without hand-rolled gaps or dividers.
|
|
56
|
+
*
|
|
57
|
+
* <Section>
|
|
58
|
+
* <SectionHeading><SectionHeadingTitle>Delivery</SectionHeadingTitle></SectionHeading>
|
|
59
|
+
* <SubsectionStack>
|
|
60
|
+
* <Subsection>{lead rows}</Subsection>
|
|
61
|
+
* {carrier ? <Subsection>…</Subsection> : null}
|
|
62
|
+
* </SubsectionStack>
|
|
63
|
+
* </Section>
|
|
64
|
+
*/
|
|
65
|
+
export function SubsectionStack(props: SubsectionStackProps) {
|
|
66
|
+
return <DividedStack {...props} gap={24} />;
|
|
67
|
+
}
|
package/src/text.css
CHANGED
|
@@ -31,12 +31,19 @@
|
|
|
31
31
|
line-height: 24px;
|
|
32
32
|
letter-spacing: -0.005em;
|
|
33
33
|
}
|
|
34
|
+
/* Display ramp — the heading ladder: xl = `##` (section), xxl = `#`
|
|
35
|
+
(page/record title), xxxl = hero numbers. */
|
|
34
36
|
[data-text-size="xl"] {
|
|
37
|
+
font-size: 22px;
|
|
38
|
+
line-height: 28px;
|
|
39
|
+
letter-spacing: -0.01em;
|
|
40
|
+
}
|
|
41
|
+
[data-text-size="xxl"] {
|
|
35
42
|
font-size: 28px;
|
|
36
43
|
line-height: 34px;
|
|
37
44
|
letter-spacing: -0.015em;
|
|
38
45
|
}
|
|
39
|
-
[data-text-size="
|
|
46
|
+
[data-text-size="xxxl"] {
|
|
40
47
|
font-size: 32px;
|
|
41
48
|
line-height: 38px;
|
|
42
49
|
letter-spacing: -0.02em;
|
|
@@ -61,10 +68,14 @@
|
|
|
61
68
|
line-height: 28px;
|
|
62
69
|
}
|
|
63
70
|
[data-text-size="xl"] {
|
|
71
|
+
font-size: 24px;
|
|
72
|
+
line-height: 32px;
|
|
73
|
+
}
|
|
74
|
+
[data-text-size="xxl"] {
|
|
64
75
|
font-size: 32px;
|
|
65
76
|
line-height: 44px;
|
|
66
77
|
}
|
|
67
|
-
[data-text-size="
|
|
78
|
+
[data-text-size="xxxl"] {
|
|
68
79
|
font-size: 48px;
|
|
69
80
|
line-height: 52px;
|
|
70
81
|
}
|
package/src/text.tsx
CHANGED
|
@@ -50,7 +50,7 @@ export interface TextProps {
|
|
|
50
50
|
|
|
51
51
|
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
52
52
|
|
|
53
|
-
export type TextSize = "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
|
|
53
|
+
export type TextSize = "xs" | "sm" | "md" | "lg" | "xl" | "xxl" | "xxxl";
|
|
54
54
|
type TextAlign = "left" | "right" | "center";
|
|
55
55
|
type TextDecorationLine = "underline" | "lineThrough" | "underline lineThrough";
|
|
56
56
|
type TextWeight = "regular" | "medium" | "semibold";
|
|
@@ -146,11 +146,17 @@ const styles = StyleSheet.create({
|
|
|
146
146
|
fontSize: 18,
|
|
147
147
|
lineHeight: 30,
|
|
148
148
|
},
|
|
149
|
+
// Display ramp — the heading ladder: xl = `##` (section), xxl = `#`
|
|
150
|
+
// (page/record title), xxxl = hero numbers.
|
|
149
151
|
xl: {
|
|
152
|
+
fontSize: 22,
|
|
153
|
+
lineHeight: 28,
|
|
154
|
+
},
|
|
155
|
+
xxl: {
|
|
150
156
|
fontSize: 28,
|
|
151
157
|
lineHeight: 34,
|
|
152
158
|
},
|
|
153
|
-
|
|
159
|
+
xxxl: {
|
|
154
160
|
fontSize: 32,
|
|
155
161
|
lineHeight: 38,
|
|
156
162
|
},
|
package/src/text_utils.ts
CHANGED
|
@@ -10,7 +10,8 @@ export type TextColor =
|
|
|
10
10
|
| "success"
|
|
11
11
|
| "zinc-900"
|
|
12
12
|
| "zinc-700"
|
|
13
|
-
| "zinc-500"
|
|
13
|
+
| "zinc-500"
|
|
14
|
+
| "zinc-400";
|
|
14
15
|
|
|
15
16
|
export function getTextColor(color?: TextColor): string {
|
|
16
17
|
switch (color) {
|
|
@@ -24,6 +25,10 @@ export function getTextColor(color?: TextColor): string {
|
|
|
24
25
|
return colors.zinc["700"];
|
|
25
26
|
case "zinc-500":
|
|
26
27
|
return colors.zinc["500"];
|
|
28
|
+
// The disabled ink — light enough that a control reads inert at a glance
|
|
29
|
+
// (muted/600 still reads as body ink on a zinc-50 chip).
|
|
30
|
+
case "zinc-400":
|
|
31
|
+
return colors.zinc["400"];
|
|
27
32
|
case "inverted":
|
|
28
33
|
return colors.white;
|
|
29
34
|
// Valence set — the lightest status weight: a state WORD whose meaning is in
|
package/src/use_section_nav.ts
CHANGED
|
@@ -31,20 +31,40 @@ export interface SectionNavHandle<K extends string> {
|
|
|
31
31
|
*
|
|
32
32
|
* Contract: every key's section stays MOUNTED (an unmounted section leaves its
|
|
33
33
|
* last offset registered — conditional sections belong inside an always-mounted
|
|
34
|
-
* wrapper that carries the `onLayout`)
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* wrapper that carries the `onLayout`). On WEB, `jumpTo` re-measures the live
|
|
35
|
+
* DOM position, so async content growing ABOVE a section (a photo grid, a
|
|
36
|
+
* late-loading table) can never stale a jump; the cached offsets only drive
|
|
37
|
+
* the scroll-spy highlight, which self-corrects as jumps refresh them. On
|
|
38
|
+
* native there is no re-measure — content above should settle before precision
|
|
39
|
+
* matters (`onLayout` won't refire on a pure position shift).
|
|
38
40
|
*/
|
|
39
41
|
export function useSectionNav<K extends string>(keys: readonly [K, ...K[]]): SectionNavHandle<K> {
|
|
40
42
|
const scrollRef = useRef<ScrollView>(null);
|
|
41
43
|
const sectionY = useRef<Partial<Record<K, number>>>({});
|
|
44
|
+
// The registered wrapper's DOM node (web only) — RN-web's layout event
|
|
45
|
+
// exposes it as `nativeEvent.target`, which lets jumpTo re-measure live.
|
|
46
|
+
const sectionEl = useRef<Partial<Record<K, Element>>>({});
|
|
42
47
|
const [activeKey, setActiveKey] = useState<K>(keys[0]);
|
|
43
48
|
const register = (key: K) => (e: LayoutChangeEvent) => {
|
|
44
49
|
sectionY.current[key] = e.nativeEvent.layout.y;
|
|
50
|
+
const target: unknown = (e.nativeEvent as { target?: unknown }).target;
|
|
51
|
+
if (typeof Element !== "undefined" && target instanceof Element) sectionEl.current[key] = target;
|
|
52
|
+
};
|
|
53
|
+
/** The section's CURRENT content-relative offset — measured live on web
|
|
54
|
+
* (immune to async content shifting the page), cached `layout.y` otherwise. */
|
|
55
|
+
const currentY = (key: K): number => {
|
|
56
|
+
const el = sectionEl.current[key];
|
|
57
|
+
const sv = scrollRef.current as (ScrollView & { getScrollableNode?: () => unknown }) | null;
|
|
58
|
+
const sc = typeof sv?.getScrollableNode === "function" ? sv.getScrollableNode() : null;
|
|
59
|
+
if (typeof Element !== "undefined" && el instanceof Element && sc instanceof Element) {
|
|
60
|
+
const y = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop;
|
|
61
|
+
sectionY.current[key] = y; // refresh the spy's cache while we're here
|
|
62
|
+
return y;
|
|
63
|
+
}
|
|
64
|
+
return sectionY.current[key] ?? 0;
|
|
45
65
|
};
|
|
46
66
|
const jumpTo = (key: K) => {
|
|
47
|
-
scrollRef.current?.scrollTo({ y: Math.max(0, (
|
|
67
|
+
scrollRef.current?.scrollTo({ y: Math.max(0, currentY(key) - 12), animated: true });
|
|
48
68
|
};
|
|
49
69
|
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
50
70
|
const y = e.nativeEvent.contentOffset.y + 80;
|