@lotics/ui 9.0.0 → 11.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 +41 -10
- 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 -7
- package/src/button.tsx +5 -1
- package/src/button_colors.ts +4 -1
- package/src/locale.tsx +4 -0
- package/src/page_content.tsx +1 -1
- package/src/page_header.tsx +2 -4
- package/src/popover_nav.tsx +40 -0
- 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/trend_footer.tsx +3 -1
- package/src/use_screen_size.ts +1 -1
- package/src/animation_horizontal_slide.tsx +0 -75
- package/src/form_time_picker.tsx +0 -22
- package/src/highlighted_text.tsx +0 -92
- package/src/menu_title.tsx +0 -15
- package/src/pager_view.tsx +0 -167
- package/src/popover_header.tsx +0 -38
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 —
|
|
@@ -12,6 +20,10 @@ typeahead, async search, virtualization, and a11y the primitive already ships.
|
|
|
12
20
|
`PortalHost` at the app root.
|
|
13
21
|
- RN-Web only: `View`/`ScrollView` from `react-native`, the `Text` primitive (no raw
|
|
14
22
|
`div`/`span`, no raw `fontSize`/`fontWeight`); styles are RN objects.
|
|
23
|
+
- Fonts: the app entry imports the Inter sheet ONCE — `import "@lotics/ui/fonts.css"` (400/500/600,
|
|
24
|
+
served by absolute URL so it resolves on every origin an app runs from) — or every `Text` falls
|
|
25
|
+
back to system fonts. File picking: `pickFiles` (`@lotics/ui/file_picker`) opens the browser
|
|
26
|
+
picker and resolves the chosen `File[]` — the imperative half behind every Add-file CTA.
|
|
15
27
|
- i18n: most components are string-free. For the rest, set the language ONCE at the root with
|
|
16
28
|
**`LoticsLocaleProvider`** (`@lotics/ui/locale`, the sibling of `LoticsThemeProvider`) — wrap
|
|
17
29
|
`<LoticsLocaleProvider locale={vi}>` and every wired component picks up the pack, no per-instance
|
|
@@ -117,7 +129,12 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
117
129
|
row/column/grand totals; press a cell to drill).
|
|
118
130
|
- **Surfaces & layout** — `Card` (+ `CardHeader`/`CardHeaderTitle`/`CardHeaderMeta`/`CardBody`/
|
|
119
131
|
`CardFooter`), `Section` (the card-less twin of `Card`: bare gap-spaced region + `SectionHeading`/
|
|
120
|
-
`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`,
|
|
121
138
|
`PageHeader` /
|
|
122
139
|
`PageContent`, `Stack`, `Spacer`, `Divider`, `Accordion`, `Tabs`, `SegmentedControl`, `Stepper`,
|
|
123
140
|
`DangerZone` (the destructive section — delete/archive — set apart at the bottom of a record/settings surface).
|
|
@@ -651,19 +668,32 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
651
668
|
- **Canvas**: full-bleed `colors.zinc[50]` ScrollView; content column `maxWidth` 880–1040,
|
|
652
669
|
centered, `padding: 28`. Cards float on the canvas — never white-on-white.
|
|
653
670
|
- **Heading vocabulary — ONE construct per altitude, no drift.**
|
|
654
|
-
· **Page band**: one per screen — `Text size="
|
|
671
|
+
· **Page band**: one per screen — `Text size="xxl" weight="semibold"` title (`#`) + one `sm muted`
|
|
655
672
|
subtitle that says what the screen decides. Right side: the screen's ONE primary action and/or
|
|
656
673
|
a period filter — never a summary Badge (those belong to the KPI strip).
|
|
657
674
|
· **Card header** (a card's own title band, or separate banded cards): `CardHeader` +
|
|
658
675
|
`CardHeaderTitle` (`sm semibold`; `info` when the title alone doesn't define the numbers).
|
|
659
676
|
· **Section title** — ONE construct: `Section` > `SectionHeading` > `SectionHeadingTitle`
|
|
660
677
|
(lg semibold + optional muted `description`/`info`), everything left-aligned at the column
|
|
661
|
-
edge. The `
|
|
662
|
-
one section from the next)
|
|
663
|
-
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
|
|
664
682
|
`SectionHeading` (lg) — its meta + CTA slots carry each section's count and action; a
|
|
665
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
|
|
666
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.
|
|
667
697
|
· **Eyebrow / label** (`<Text size="xs" color="muted" weight="medium">` — **sentence case, NEVER
|
|
668
698
|
`transform="uppercase"`**): a small quiet label above or beside content — an artifact tag
|
|
669
699
|
("Proposed", "Question", "Suggested edit"), a field name, a metric caption, a minor one-line
|
|
@@ -1013,14 +1043,15 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
|
|
|
1013
1043
|
text ·
|
|
1014
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) ·
|
|
1015
1045
|
card (Card · CardHeader · CardHeaderTitle · CardHeaderMeta · CardBody · CardFooter) ·
|
|
1016
|
-
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 ·
|
|
1017
1048
|
option_badge (OptionBadge — a select value as its configured colored badge) ·
|
|
1018
1049
|
member_chip (MemberChip — avatar + name; the universal person render) ·
|
|
1019
1050
|
member_select (MemberSelect — a Picker of MemberChip options; the member picker) ·
|
|
1020
1051
|
status_badge · button · icon_button · link · text_link (TextLink — underlined text that's optionally an `onPress` action or an `href` link, or plain underlined text to wrap in your own pressable; the neutral counterpart to the fixed-blue `Link`) · chip (Chip — the generic pill: pressable when `onPress` (announces as a button; pass `accessibilityLabel` when children aren't self-describing text) + an absolutely-positioned dismiss ✕ sibling when `onDismiss` (its name = `dismissTooltip` ?? the `chip.remove` locale slice). Suggestion pills → `SuggestionChip`) · tabs (Tabs — switch between content sections; WAI-ARIA tablist + roving tabindex; each TabOption takes an optional `status` ColorName → a small attention dot before its label, for a tab whose area needs work) · segmented_control ·
|
|
1021
1052
|
picker (native `<select>`, plain label-only single) · select (Select — rich/custom-rendered, single/multi, select-all, chips via `renderSelected` + `searchable` + `allowCustom` — the tag field is just a multi Select; opens `OptionList`) · option_list (OptionList — the ONE shared searchable listbox body every selector opens: single/multi, optional internal search, create row, keyboard + native-`<select>` typeahead; host it directly in a `Popover`/`Dialog` for a command palette) · combobox (COMPOUND single-select editable search: `Combobox` root + `ComboboxInput` + `ComboboxContent`, optional `ComboboxEmpty`/`ComboboxFooter`, `useCombobox()`; over the shared `useOptionList` engine; browses on focus; no `multi` — multi-value chips → `Select multi`) ·
|
|
1022
1053
|
text_input_field · number_input · search_input · form_field · checkbox · checkbox_input · switch ·
|
|
1023
|
-
radio_picker · counter · range_slider · date_picker · date_range_filter_field · time_picker ·
|
|
1054
|
+
radio_picker · counter · range_slider · date_picker · date_range_filter_field · date_calendar (Calendar — the bare month grid: `mode="single"` or `"range"` ({start,end} — two months side by side on desktop), month/year pickers + arrows, localized weekday/month names via BCP-47 `locale`, `firstDayOfWeek` (default Monday), `ref.navigateToMonth`. The engine DatePicker/DateFilter wrap in field chrome — reach for it bare only when the calendar lives permanently on the surface, not behind a field) · form_switch (FormSwitch — the FormField-labeled twin of `Switch`: toggle + clickable `label` (pressing it flips the value) + optional `description`/`error`; the settings-form boolean row. A bare toggle in a cell/toolbar → `Switch`; a full-row menu toggle → `SwitchButton`) · form_text_input (FormTextInput — `FormField` (label / description / error / optional) wrapping a `TextInputField`; the one-line labeled text field. Controls without a Form* twin just wrap in `FormField`) · switch_button (SwitchButton — the full-ROW toggle: a PressableHighlight row (optional icon + medium title left, `Switch` pinned right) where the whole row IS the switch (`accessibilityRole="switch"`, the inner Switch read-only). The settings-panel/menu row toggle; vs `Switch` (bare control) and `FormSwitch` (form stack)) · use_form (useForm — THE batch draft-form state hook: `values` = `initialValues` + an edits overlay (a revalidation refreshes untouched fields, no sync effect), `validate` (sync/async, gates submit, editing clears the field's error), `onSubmit(values, helpers)` with a re-entry-guarded `submitting`, and `changes`/`changed` — the touched-fields diff that feeds DIFF-writes (send only edited fields); `setFieldValue` (curried or direct) + `setValues`/`setFieldError`/`reset`. Pairs with the Form* family for dialog/settings forms — the draft-validate-COMMIT-together twin of the self-persisting Inline* editors) · time_picker ·
|
|
1024
1055
|
inline_text_input · inline_number_input · inline_select · inline_member_select · inline_date_picker ·
|
|
1025
1056
|
inline_time_picker (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` +
|
|
1026
1057
|
`InlineEditView`; `InlineSelect`/`InlineMemberSelect` render the resting value like its option — `renderOptionContent` by default, `renderSelected` to override — a chip/badge at rest, not just text) ·
|
|
@@ -1031,14 +1062,14 @@ check_circle (CheckCircle — the completion ring: an empty ring that springs to
|
|
|
1031
1062
|
floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
|
|
1032
1063
|
columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
|
|
1033
1064
|
sort_header · table · data_grid (DataGrid — the inline-managed grouped table: a grouped, sortable grid of LIVE inline-editor cells (`columns[].cell` → ANY field) + optional per-row `leading` (a CheckCircle) + `renderGroupFooter` (per-group add, align with the exported `gridRowStyle`) + `labels` (localize the sort-header a11y via `SortHeaderLabels`). Owns header/sections/rows; consumer owns data + sort/group/filter/collapse state + toolbar. Renders ALL rows — MODERATE data; 10k+ → the paginated `Table` register. Example: `tpl_task_board`) · pagination · accordion · stepper (Stepper + Step — done/current/upcoming/warning/complete progress on a track (horizontal) or spine (vertical); compound `<Step status>children` OR data `steps[]`+`current`; **navigable** via `Step.onPress` (both orientations — the whole step is the tap target) + `active` to wash the selected one, so it doubles as a section/phase switcher; the guided-run / agent-feed primitive — subsumes the old StepList) ·
|
|
1034
|
-
step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · modal (Modal + ModalHeader + ModalBody + ModalFooter — the full-bleed, edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog (centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it to dim — NO scrim, NO backdrop. Lays children as a flex column: a pinned ModalHeader (eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody, a pinned ModalFooter (the commit bar, same chrome as DialogFooter/DrawerFooter). Reach for it for a focused capture / multi-step wizard / a console the user steps INTO, where surrounding chrome is a distraction; pick Dialog when the surface is a card the user can see context around) · screen_router (ScreenRouter + Screen + useScreenRouter — the SCREENS compound: a flat navigation stack (`navigate("/case/:id")` pushes, `goBack` pops, `canGoBack`, route `params`; stacked screens stay mounted `display:none` so scroll survives the round trip). Dialog BAKES a router in (`<Dialog><Screen route="">…`); ANY other container hosts the standalone `<ScreenRouter>` — and it wraps AROUND the container so the CHROME can read the stack: a Drawer drilling into a LINKED record swaps its header to a BACK IconButton + the pushed record's id while `canGoBack` (sequence ◀ ▶ hides — stepping the root from inside a linked record disorients), and the pushed `<Screen route="/case/:id">` is a REAL editable workspace with its own footer CTAs. Key the router by record id so stepping ◀ ▶ resets the stack. Worked example: `tpl_item_list` drawer) · popover · tooltip ·
|
|
1065
|
+
step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · modal (Modal + ModalHeader + ModalBody + ModalFooter — the full-bleed, edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog (centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it to dim — NO scrim, NO backdrop. Lays children as a flex column: a pinned ModalHeader (eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody, a pinned ModalFooter (the commit bar, same chrome as DialogFooter/DrawerFooter). Reach for it for a focused capture / multi-step wizard / a console the user steps INTO, where surrounding chrome is a distraction; pick Dialog when the surface is a card the user can see context around) · screen_router (ScreenRouter + Screen + useScreenRouter — the SCREENS compound: a flat navigation stack (`navigate("/case/:id")` pushes, `goBack` pops, `canGoBack`, route `params`; stacked screens stay mounted `display:none` so scroll survives the round trip). Dialog BAKES a router in (`<Dialog><Screen route="">…`); ANY other container hosts the standalone `<ScreenRouter>` — and it wraps AROUND the container so the CHROME can read the stack: a Drawer drilling into a LINKED record swaps its header to a BACK IconButton + the pushed record's id while `canGoBack` (sequence ◀ ▶ hides — stepping the root from inside a linked record disorients), and the pushed `<Screen route="/case/:id">` is a REAL editable workspace with its own footer CTAs. Key the router by record id so stepping ◀ ▶ resets the stack. Worked example: `tpl_item_list` drawer) · popover · popover_nav (usePopoverNav + PopoverScreen + PopoverNavHeader — the popover's built-in mini-router: EVERY `Popover` provides the nav context (`navigate(route)` pushes, `goBack`, `currentRoute`, `canGoBack`; resets on close), `PopoverScreen route=""` is the root and screens render conditionally (unmounted when inactive — no scroll preservation), `PopoverNavHeader` is the title row whose back chevron auto-appears while `canGoBack` (`right` slot, `backLabel`). For a multi-screen menu inside ONE popover (an avatar/settings menu drilling into a sub-panel); route PATTERNS, `params`, and stacked-alive screens are `screen_router`'s job. Distinct from `Popover`'s plain `PopoverHeader` children container) · tooltip ·
|
|
1035
1066
|
alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
|
|
1036
1067
|
CalloutText · CalloutActions) · kpi_card · kpi_strip · summary_line (SummaryLine — the light inline summary of a register/list's FILTERED view, sits below the toolbar; NOT the boxed dashboard `kpi_strip` band) · metric · trend_chip · sparkline ·
|
|
1037
1068
|
bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
|
|
1038
1069
|
funnel (Funnel — conversion funnel: narrowing bars + the step rate as a bold aligned headline row, count below; `orientation` vertical|horizontal, `onSelect`/`selectedKey` press-to-drill; the subset/drop-off sibling of stacked_progress_bar's whole-split) ·
|
|
1039
1070
|
status_grid (StatusGrid + StatusLegend) · heatmap (density cross-tab — colour-only, no numbers) · matrix (Matrix — the PIVOT cross-tab: band-compound `Matrix` root + `Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) + `Matrix.Legend`; the data layer `matrixTotals`/`MatrixAxisItem`/`MatrixCellRef` import from `@lotics/ui/matrix_totals`. Pick over `Heatmap` when the NUMBER and totals matter, not just where it clusters) · legend_item · remainder_meter · allocation_row ·
|
|
1040
1071
|
scan_field · file_dropzone · files_editor (FilesEditor — THE all-in-one attachment field: FileGrid + a toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode (Select all · a Menu of Download/Share/Delete · Done; the per-tile ✕ is select-mode-only, never in the default view; `selectTileRemove={false}` drops even that so delete is menu-only) + built-in gallery (Download + inline preview; no "open in new tab") + Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`, `onShareSelected`, `readOnly`, `selectTileRemove`, `labels`, `galleryLabels`, `gridMaxHeight` — cap the grid height so it scrolls and the toolbar pins, for a popover/drawer); mirrors the frontend cell_files_editor. Use FileGrid/FileRows bare only when you own the chrome) · file_grid (FileGrid — the upload-aware grid: completed files + a live upload queue in one surface; FileUpload/PendingUpload types; the add-files default) · uploading_thumbnail (UploadingThumbnail — the single in-flight upload tile FileGrid renders; reach for it only when hand-rolling a non-grid upload layout) · file_thumbnail · file_thumbnail_grid · file_row · file_rows (FileRows — batteries-included file list: row press → built-in gallery + a ⋯ Download/Open-external/Remove menu; composes FileRow + ActionMenu + FileGalleryModal) · file_preview (FilePreview — the universal inline preview: image/PDF/video/audio + Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines (pdf.js · `@lotics/docx` · `@lotics/xlsx`) are LAZY (dynamic-imported, ~free until a doc of that type is opened) and SHIP AS `@lotics/ui` deps (7.14.0+) — custom-code apps get PDF/Word/Excel preview with ZERO extra install. Renders to canvas/DOM, never a nested iframe — works in the sandboxed app iframe) ·
|
|
1041
|
-
file_gallery_modal · image_gallery · use_selection (useSelection — always-on multi-select state for a register/list: the `selected` Set + `toggle`/`setAll`/`allSelected`/`indeterminate`/`count`/`clear`; selectability gating stays with the caller. The checkbox-always-visible counterpart to `use_selection_mode`) · use_selection_mode · use_section_nav (useSectionNav — scroll-spy for a LONG record surface with a left outline rail: keys in page order → {scrollRef, onScroll, register(key)→onLayout, jumpTo(key), activeKey}; rail items are `MenuButton`s (selected={activeKey===key}); section wrappers must be DIRECT children of the ScrollView content. On NARROW containers the rail becomes a PINNED bar naming the CURRENT section (the spy keeps it honest) that opens a full-page section-picker `Modal` (`MenuButton` list; Escape / the close control dismiss) — never a horizontal tab strip the thumb has to scroll. Worked example: `tpl_record`) · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
|
|
1072
|
+
app_icon (AppIcon — the app's launcher tile: a brand-gradient square from a `themeColor` palette token (unknown → neutral zinc) holding any Lucide icon by runtime name via DynamicIcon; `size` sm|md. One render wherever an app shows — launcher, list, picker, settings) · dynamic_icon (DynamicIcon — any Lucide icon by RUNTIME name (kebab-case string), for user/config-chosen icons a compile-time `IconName` can't express; web lazy-loads per icon, unknown name → blank. Decorative — the enclosing control carries the accessible name. A fixed, code-chosen glyph → `Icon`) · group_avatar (GroupAvatar — the first letter of `name` in a zinc rounded square (`size`, default 40); the avatar for image-less entities — groups, organizations. A person → `Avatar`/`MemberChip`) · rotatable_image (RotatableImage — an image rotating in 90° steps that REFITS: at a quarter turn the box lays out in SWAPPED dimensions then rotates into place, so a rotated landscape fills the frame; the VIEW half of image rotation — `rotate_image` bakes the pixels for re-upload; FilePreview/ImageGallery compose it) · file_preview_types (PreviewLabels / FilePreviewProps / GalleryLabels — the shared label + prop contracts of the file-preview family; types only) · file_gallery_modal · image_gallery · use_selection (useSelection — always-on multi-select state for a register/list: the `selected` Set + `toggle`/`setAll`/`allSelected`/`indeterminate`/`count`/`clear`; selectability gating stays with the caller. The checkbox-always-visible counterpart to `use_selection_mode`) · use_selection_mode · use_section_nav (useSectionNav — scroll-spy for a LONG record surface with a left outline rail: keys in page order → {scrollRef, onScroll, register(key)→onLayout, jumpTo(key), activeKey}; rail items are `MenuButton`s (selected={activeKey===key}); section wrappers must be DIRECT children of the ScrollView content. On NARROW containers the rail becomes a PINNED bar naming the CURRENT section (the spy keeps it honest) that opens a full-page section-picker `Modal` (`MenuButton` list; Escape / the close control dismiss) — never a horizontal tab strip the thumb has to scroll. Worked example: `tpl_record`) · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
|
|
1042
1073
|
spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
|
|
1043
1074
|
comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
|
|
1044
1075
|
expandable form — a composer's working state) · composer (Composer — the adaptive command/chat
|
|
@@ -1059,6 +1090,6 @@ children slot — with FindingComparison, the expected-vs-actual body: labeled s
|
|
|
1059
1090
|
delta; the `finding` locale slice) ·
|
|
1060
1091
|
format_money · format_date · colors (solid · tint · ramp · ColorName ·
|
|
1061
1092
|
isColorName · asColorName — coerce a stored option/status token to a ColorName, neutral fallback) ·
|
|
1062
|
-
use_focus_ring (useFocusRing — keyboard-aware focus state for painting a control's own ring; see Focus rings) ·
|
|
1093
|
+
auto_sizer (AutoSizer — measures its own box via onLayout and renders the render-prop child only once `{width,height}` exist; `autoSizeWidthOnly`/`autoSizeHeightOnly`, `onResize`. For content needing pixel dimensions before first paint — a canvas, a virtualized grid) · separator (Separator — a `Divider` pre-wrapped in vertical padding (`padding: SpacerSize`, default 8): the between-groups rule WITH breathing room — what `Stack useSeparator` inserts and what menus/popovers put between option groups; `Divider` is the bare hairline) · back_button (BackButton — the chevron-left `IconButton` (lg, secondary) heading a screen/panel: `onPress` + translated `accessibilityLabel` (default "Back"); the one go-back glyph — don't hand-roll it) · info_popover (InfoPopover — the ⓘ button opening a 280px popover of explanatory `text`; the middle ground between Tooltip (short hover label) and composing Popover (rich content) — it is what `SectionHeadingTitle.info`/`CardHeaderTitle.info` render) · shortcut_badge (ShortcutBadge — the keycap hint pill: a zinc-50 badge rendering a shortcut from a raw string or `ShortcutDescriptor` (⌘B on Mac, Ctrl+B elsewhere); null on small screens. `TextInputField shortcut` renders it built-in) · trend_footer (TrendFooter — the "Up X% vs last period" caption under a chart card: signed `value` (0 = flat), `periodLabel`, optional `detail`; `goodDirection="down"` flips green/red for metrics where down is good; SKIP it when there's no comparator. Direction words via the `trendFooter` locale slice; goes in `SectionCard footer`) · dots_indicator (DotsIndicator — three looping bouncing dots (`size`/`color`); the indeterminate "working/typing" pulse `Loading` composes; use bare beside a caption while an agent thinks. Determinate work → ProgressBar) · scroll_to_bottom (ScrollToBottom — the floating jump-to-latest circle button for a chat/feed; ONLY the affordance — the caller owns positioning, visibility, and the actual scroll) · animation_fade_in (AnimationFadeIn — the mount transition: children fade (+ optional `translateY` rise) into place on first render, once; the entrance polish Accordion/Timeline/Stepper/AgentRun rows use) · landmark (Landmark — the semantic region wrapper: `kind` banner|navigation|main|complementary|contentinfo|region maps to the matching HTML element on web for screen-reader landmark navigation, `accessibilityRole` on native; `accessibilityLabel` required for `region`) · skip_link (SkipLink — the a11y bypass link: parked off-screen until keyboard focus slides it in; `href="#targetId"` jumps past repeated nav into the main region. One per app shell, FIRST in the tree; web-only) · use_async_fn (useAsyncFn — wrap an async function into a manual-trigger mutation: `[run, {loading, data, error}]`, unmount-safe, the error lands in state AND rethrows; the pending-state engine for a submit/download/upload action) · use_hover (useHover — pointer-hover state `{hovered, hoverProps}` for raw inputs/DOM controls that lack a hovered style state; native stays false. Pressable-based controls use the built-in `hovered` state instead) · use_auto_grow_height (useAutoGrowHeight — the grows-with-content textarea engine ({minLines,maxLines} → container height + `scrollEnabled` at the cap); powers Composer and multiline TextInputField — reach for it only when hand-rolling a growing input) · overlay_scope (isOverlayScopeActive / useOverlayScope — the module-level open-overlay counter every overlay primitive reports into; the host's shortcut registry reads it synchronously to floor page-level shortcuts while any overlay is open. Lives in the primitives — never call it from a screen) · route_matching (pure `:param` route-pattern utilities — routeMatches / parseRouteParams / findBestPattern (exact beats parameterized); the matching core under ScreenRouter/Dialog) · text_utils (text/typography plumbing: `getTextColor` (the TextColor→hex map incl. the AA-cleared valence set), the Inter `fontFamily*` stacks, and `getInputTextStyle`/`getInputLineHeight` — the 16px-mobile/14px-desktop input contract that stops Safari iOS auto-zoom; only for hand-rolled raw inputs) · use_focus_ring (useFocusRing — keyboard-aware focus state for painting a control's own ring; see Focus rings) ·
|
|
1063
1094
|
focus_ring_pressable (FocusRingPressable — a Pressable that rings on keyboard focus; the raw-control default) ·
|
|
1064
1095
|
control_surface (CONTROL_HEIGHT · CONTROL_RADIUS · FOCUS_RING · chipSurfaceStyle — the shared control-surface tokens).
|
|
@@ -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.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -69,7 +69,6 @@
|
|
|
69
69
|
"./theme": "./src/theme.tsx",
|
|
70
70
|
"./progress_bar": "./src/progress_bar.tsx",
|
|
71
71
|
"./form_date_picker": "./src/form_date_picker.tsx",
|
|
72
|
-
"./form_time_picker": "./src/form_time_picker.tsx",
|
|
73
72
|
"./form_picker": "./src/form_picker.tsx",
|
|
74
73
|
"./picker": "./src/picker.tsx",
|
|
75
74
|
"./select": "./src/select.tsx",
|
|
@@ -170,11 +169,11 @@
|
|
|
170
169
|
"./container": "./src/container.tsx",
|
|
171
170
|
"./count": "./src/count.tsx",
|
|
172
171
|
"./dots_indicator": "./src/dots_indicator.tsx",
|
|
173
|
-
"./highlighted_text": "./src/highlighted_text.tsx",
|
|
174
172
|
"./list": "./src/list.tsx",
|
|
175
173
|
"./list_item": "./src/list_item.tsx",
|
|
176
174
|
"./section": "./src/section.tsx",
|
|
177
175
|
"./section_heading": "./src/section_heading.tsx",
|
|
176
|
+
"./section_stack": "./src/section_stack.tsx",
|
|
178
177
|
"./separator": "./src/separator.tsx",
|
|
179
178
|
"./stack": "./src/stack.tsx",
|
|
180
179
|
"./status_badge": "./src/status_badge.tsx",
|
|
@@ -185,11 +184,9 @@
|
|
|
185
184
|
"./tabs": "./src/tabs.tsx",
|
|
186
185
|
"./segmented_control": "./src/segmented_control.tsx",
|
|
187
186
|
"./auto_sizer": "./src/auto_sizer.tsx",
|
|
188
|
-
"./animation_horizontal_slide": "./src/animation_horizontal_slide.tsx",
|
|
189
187
|
"./group_avatar": "./src/group_avatar.tsx",
|
|
190
188
|
"./page_content": "./src/page_content.tsx",
|
|
191
189
|
"./page_header": "./src/page_header.tsx",
|
|
192
|
-
"./pager_view": "./src/pager_view.tsx",
|
|
193
190
|
"./date_picker": "./src/date_picker.tsx",
|
|
194
191
|
"./date_filter": "./src/date_filter.tsx",
|
|
195
192
|
"./date_range_filter_field": "./src/date_range_filter_field.tsx",
|
|
@@ -200,7 +197,6 @@
|
|
|
200
197
|
"./modal": "./src/modal.tsx",
|
|
201
198
|
"./screen_router": "./src/screen_router.tsx",
|
|
202
199
|
"./route_matching": "./src/route_matching.ts",
|
|
203
|
-
"./menu_title": "./src/menu_title.tsx",
|
|
204
200
|
"./avatar": {
|
|
205
201
|
"react-native": "./src/avatar.tsx",
|
|
206
202
|
"default": "./src/avatar.web.tsx"
|
|
@@ -216,7 +212,6 @@
|
|
|
216
212
|
"./json_panel": "./src/json_panel.tsx",
|
|
217
213
|
"./timeline": "./src/timeline.tsx",
|
|
218
214
|
"./loading": "./src/loading.tsx",
|
|
219
|
-
"./popover_header": "./src/popover_header.tsx",
|
|
220
215
|
"./switch_button": "./src/switch_button.tsx",
|
|
221
216
|
"./alert": "./src/alert.tsx",
|
|
222
217
|
"./landmark": "./src/landmark.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
|
}
|