@lotics/ui 9.0.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -12,6 +12,10 @@ typeahead, async search, virtualization, and a11y the primitive already ships.
12
12
  `PortalHost` at the app root.
13
13
  - RN-Web only: `View`/`ScrollView` from `react-native`, the `Text` primitive (no raw
14
14
  `div`/`span`, no raw `fontSize`/`fontWeight`); styles are RN objects.
15
+ - Fonts: the app entry imports the Inter sheet ONCE — `import "@lotics/ui/fonts.css"` (400/500/600,
16
+ served by absolute URL so it resolves on every origin an app runs from) — or every `Text` falls
17
+ back to system fonts. File picking: `pickFiles` (`@lotics/ui/file_picker`) opens the browser
18
+ picker and resolves the chosen `File[]` — the imperative half behind every Add-file CTA.
15
19
  - i18n: most components are string-free. For the rest, set the language ONCE at the root with
16
20
  **`LoticsLocaleProvider`** (`@lotics/ui/locale`, the sibling of `LoticsThemeProvider`) — wrap
17
21
  `<LoticsLocaleProvider locale={vi}>` and every wired component picks up the pack, no per-instance
@@ -1020,7 +1024,7 @@ member_select (MemberSelect — a Picker of MemberChip options; the member picke
1020
1024
  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
1025
  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
1026
  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 ·
1027
+ 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
1028
  inline_text_input · inline_number_input · inline_select · inline_member_select · inline_date_picker ·
1025
1029
  inline_time_picker (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` +
1026
1030
  `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 +1035,14 @@ check_circle (CheckCircle — the completion ring: an empty ring that springs to
1031
1035
  floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
1032
1036
  columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
1033
1037
  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 ·
1038
+ 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
1039
  alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
1036
1040
  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
1041
  bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
1038
1042
  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
1043
  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
1044
  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 ·
1045
+ 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
1046
  spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
1043
1047
  comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
1044
1048
  expandable form — a composer's working state) · composer (Composer — the adaptive command/chat
@@ -1059,6 +1063,6 @@ children slot — with FindingComparison, the expected-vs-actual body: labeled s
1059
1063
  delta; the `finding` locale slice) ·
1060
1064
  format_money · format_date · colors (solid · tint · ramp · ColorName ·
1061
1065
  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) ·
1066
+ 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
1067
  focus_ring_pressable (FocusRingPressable — a Pressable that rings on keyboard focus; the raw-control default) ·
1064
1068
  control_surface (CONTROL_HEIGHT · CONTROL_RADIUS · FOCUS_RING · chipSurfaceStyle — the shared control-surface tokens).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "9.0.0",
3
+ "version": "10.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,7 +169,6 @@
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",
@@ -185,11 +183,9 @@
185
183
  "./tabs": "./src/tabs.tsx",
186
184
  "./segmented_control": "./src/segmented_control.tsx",
187
185
  "./auto_sizer": "./src/auto_sizer.tsx",
188
- "./animation_horizontal_slide": "./src/animation_horizontal_slide.tsx",
189
186
  "./group_avatar": "./src/group_avatar.tsx",
190
187
  "./page_content": "./src/page_content.tsx",
191
188
  "./page_header": "./src/page_header.tsx",
192
- "./pager_view": "./src/pager_view.tsx",
193
189
  "./date_picker": "./src/date_picker.tsx",
194
190
  "./date_filter": "./src/date_filter.tsx",
195
191
  "./date_range_filter_field": "./src/date_range_filter_field.tsx",
@@ -200,7 +196,6 @@
200
196
  "./modal": "./src/modal.tsx",
201
197
  "./screen_router": "./src/screen_router.tsx",
202
198
  "./route_matching": "./src/route_matching.ts",
203
- "./menu_title": "./src/menu_title.tsx",
204
199
  "./avatar": {
205
200
  "react-native": "./src/avatar.tsx",
206
201
  "default": "./src/avatar.web.tsx"
@@ -216,7 +211,6 @@
216
211
  "./json_panel": "./src/json_panel.tsx",
217
212
  "./timeline": "./src/timeline.tsx",
218
213
  "./loading": "./src/loading.tsx",
219
- "./popover_header": "./src/popover_header.tsx",
220
214
  "./switch_button": "./src/switch_button.tsx",
221
215
  "./alert": "./src/alert.tsx",
222
216
  "./landmark": "./src/landmark.tsx",
package/src/locale.tsx CHANGED
@@ -45,6 +45,8 @@ export interface LoticsLocale {
45
45
  sectionHeading: { info: string };
46
46
  /** `Chip`: the ✕ default name when no `dismissTooltip` is given. */
47
47
  chip: { remove: string };
48
+ /** `TrendFooter`: the direction words of the trend sentence. */
49
+ trendFooter: { up: string; down: string };
48
50
  /** `SuggestionChip`: the press target's default name and the ✕ tooltip. */
49
51
  suggestionChip: { add: (label: string) => string; dismiss: string };
50
52
  /** `Confidence`: the full level phrase ("High confidence" …). */
@@ -105,6 +107,7 @@ export const en: LoticsLocale = {
105
107
  ledger: { rowDetails: (label) => `${label} details` },
106
108
  sectionHeading: { info: "About this data" },
107
109
  chip: { remove: "Remove" },
110
+ trendFooter: { up: "Up", down: "Down" },
108
111
  suggestionChip: { add: (label) => `Add: ${label}`, dismiss: "Dismiss suggestion" },
109
112
  confidence: { high: "High confidence", medium: "Medium confidence", low: "Low confidence" },
110
113
  remainderMeter: {
@@ -181,6 +184,7 @@ export const vi: LoticsLocale = {
181
184
  ledger: { rowDetails: (label) => `Chi tiết ${label}` },
182
185
  sectionHeading: { info: "Giải thích dữ liệu" },
183
186
  chip: { remove: "Xóa" },
187
+ trendFooter: { up: "Tăng", down: "Giảm" },
184
188
  suggestionChip: { add: (label) => `Thêm: ${label}`, dismiss: "Bỏ gợi ý" },
185
189
  confidence: { high: "Độ tin cậy cao", medium: "Độ tin cậy trung bình", low: "Độ tin cậy thấp" },
186
190
  remainderMeter: {
@@ -1,7 +1,6 @@
1
1
  import { View } from "react-native";
2
2
  import { Text } from "@lotics/ui/text";
3
3
  import { ReactNode } from "react";
4
- import { useScreenSize } from "@lotics/ui/use_screen_size";
5
4
 
6
5
  interface PageHeaderProps {
7
6
  title: string;
@@ -12,7 +11,6 @@ interface PageHeaderProps {
12
11
 
13
12
  export function PageHeader(props: PageHeaderProps) {
14
13
  const { title, description, left, right } = props;
15
- const screenSize = useScreenSize();
16
14
 
17
15
  const hasNav = !!left || !!right;
18
16
 
@@ -1,4 +1,7 @@
1
1
  import { createContext, ReactNode, useContext } from "react";
2
+ import { IconButton } from "./icon_button";
3
+ import { View, StyleSheet } from "react-native";
4
+ import { Text } from "./text";
2
5
 
3
6
  export interface PopoverNavContextValue {
4
7
  currentRoute: string;
@@ -30,3 +33,40 @@ export function PopoverScreen(props: PopoverScreenProps) {
30
33
 
31
34
  return <>{children}</>;
32
35
  }
36
+
37
+ export interface PopoverNavHeaderProps {
38
+ title: string;
39
+ right?: ReactNode;
40
+ /** Accessible name for the back button. Default: "Back". Pass a translated string from the consumer. */
41
+ backLabel?: string;
42
+ }
43
+
44
+ /** The routed-popover title row: a back chevron auto-appears while `canGoBack`
45
+ * (reads `usePopoverNav`), sm-medium title fills, `right` a trailing action.
46
+ * Pairs with `PopoverScreen`. Distinct from `Popover`'s own plain
47
+ * `PopoverHeader` children container. */
48
+ export function PopoverNavHeader(props: PopoverNavHeaderProps) {
49
+ const { title, right, backLabel = "Back" } = props;
50
+ const { goBack, canGoBack } = usePopoverNav();
51
+
52
+ return (
53
+ <View style={navHeaderStyles.container}>
54
+ {canGoBack && (
55
+ <IconButton icon="chevron-left" size="lg" color="secondary" accessibilityLabel={backLabel} onPress={goBack} />
56
+ )}
57
+ <Text size="sm" weight="medium" style={{ flex: 1 }}>
58
+ {title}
59
+ </Text>
60
+ {right}
61
+ </View>
62
+ );
63
+ }
64
+
65
+ const navHeaderStyles = StyleSheet.create({
66
+ container: {
67
+ paddingLeft: 8,
68
+ flexDirection: "row",
69
+ alignItems: "center",
70
+ gap: 8,
71
+ },
72
+ });
@@ -1,4 +1,5 @@
1
1
  import { TrendingDown, TrendingUp } from "lucide-react";
2
+ import { useLoticsLocale } from "./locale";
2
3
  import { View, StyleSheet } from "react-native";
3
4
  import { Text } from "./text";
4
5
  import { colors } from "./colors";
@@ -32,13 +33,14 @@ export function TrendFooter(props: TrendFooterProps) {
32
33
  const flat = value === 0;
33
34
  const isGood = flat ? null : (up && goodDirection === "up") || (!up && goodDirection === "down");
34
35
  const color = isGood === null ? colors.zinc[600] : isGood ? colors.green[700] : colors.red[700];
36
+ const labels = useLoticsLocale().trendFooter;
35
37
  const Icon = up ? TrendingUp : TrendingDown;
36
38
  return (
37
39
  <View style={styles.container}>
38
40
  <View style={styles.row}>
39
41
  <Icon size={16} color={color} />
40
42
  <Text size="sm" weight="medium" style={{ color }}>
41
- {up ? "Up" : "Down"} {Math.abs(value)}% {periodLabel}
43
+ {up ? labels.up : labels.down} {Math.abs(value)}% {periodLabel}
42
44
  </Text>
43
45
  </View>
44
46
  {detail && (
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from "react";
2
- import { Dimensions, Platform } from "react-native";
2
+ import { Dimensions } from "react-native";
3
3
 
4
4
  const breakpoints = {
5
5
  // small (mobile)
@@ -1,75 +0,0 @@
1
- import React, { useEffect, useRef } from "react";
2
- import { Animated, StyleProp, ViewStyle } from "react-native";
3
-
4
- interface AnimationHorizontalSlideProps {
5
- width: number;
6
- visible: boolean;
7
- children?: React.ReactNode;
8
- onSlideAnimationEnd?: () => void;
9
- onCollapseAnimationEnd?: () => void;
10
- style?: StyleProp<ViewStyle>;
11
- contentContainerStyle?: StyleProp<ViewStyle>;
12
- }
13
-
14
- export function AnimationHorizontalSlide(props: AnimationHorizontalSlideProps) {
15
- const {
16
- width: intrinsicWidth,
17
- visible,
18
- children,
19
- onSlideAnimationEnd,
20
- onCollapseAnimationEnd,
21
- style,
22
- contentContainerStyle,
23
- } = props;
24
- const widthAnim = useRef(new Animated.Value(visible ? intrinsicWidth : 0)).current;
25
- const opacityAnim = useRef(new Animated.Value(visible ? 1 : 0)).current;
26
-
27
- useEffect(() => {
28
- Animated.parallel([
29
- Animated.spring(widthAnim, {
30
- toValue: visible ? intrinsicWidth : 0,
31
- bounciness: 0,
32
- useNativeDriver: false,
33
- }),
34
- Animated.spring(opacityAnim, {
35
- toValue: visible ? 1 : 0,
36
- bounciness: 0,
37
- useNativeDriver: false,
38
- }),
39
- ]).start((animation) => {
40
- if (animation.finished) {
41
- if (visible) {
42
- onSlideAnimationEnd?.();
43
- } else {
44
- onCollapseAnimationEnd?.();
45
- }
46
- }
47
- });
48
- }, [
49
- widthAnim,
50
- opacityAnim,
51
- visible,
52
- intrinsicWidth,
53
- onSlideAnimationEnd,
54
- onCollapseAnimationEnd,
55
- ]);
56
-
57
- return (
58
- <Animated.View style={[{ width: widthAnim, opacity: opacityAnim }, style]}>
59
- <Animated.View
60
- style={[
61
- {
62
- display: visible ? "flex" : "none",
63
- position: "absolute",
64
- top: 0,
65
- bottom: 0,
66
- width: intrinsicWidth,
67
- },
68
- contentContainerStyle,
69
- ]}
70
- >
71
- {children}
72
- </Animated.View>
73
- </Animated.View>
74
- );
75
- }
@@ -1,22 +0,0 @@
1
- import { FormField, FormFieldProps } from "./form_field";
2
- import { TimePicker, TimePickerProps } from "./time_picker";
3
-
4
- export interface FormTimePickerProps
5
- extends Omit<FormFieldProps, "style">,
6
- TimePickerProps {}
7
-
8
- export function FormTimePicker(props: FormTimePickerProps) {
9
- const { label, description, optional, optionalLabel, error, ...timePickerProps } = props;
10
-
11
- return (
12
- <FormField
13
- label={label}
14
- description={description}
15
- error={error}
16
- optional={optional}
17
- optionalLabel={optionalLabel}
18
- >
19
- <TimePicker {...timePickerProps} />
20
- </FormField>
21
- );
22
- }
@@ -1,92 +0,0 @@
1
- import React from "react";
2
- import { Text, TextProps } from "./text";
3
- import { colors } from "./colors";
4
- import { View, StyleSheet } from "react-native";
5
-
6
- interface HighlightedTextProps extends Omit<TextProps, "children"> {
7
- text: string;
8
- searchTerm: string;
9
- singleLine?: boolean;
10
- }
11
-
12
- export function HighlightedText({
13
- text,
14
- searchTerm,
15
- singleLine = false,
16
- ...textProps
17
- }: HighlightedTextProps) {
18
- const parts = highlightSearchTerm(text || "", searchTerm || "");
19
-
20
- // Use numberOfLines prop for single line display instead of custom CSS
21
- const numberOfLines = singleLine ? 1 : undefined;
22
-
23
- return (
24
- <Text {...textProps} numberOfLines={numberOfLines}>
25
- {parts.map((part, index) => (
26
- <Text key={index} style={part.isHighlighted ? styles.highlightedText : undefined}>
27
- {part.text}
28
- </Text>
29
- ))}
30
- </Text>
31
- );
32
- }
33
-
34
- interface TextPart {
35
- text: string;
36
- isHighlighted: boolean;
37
- }
38
-
39
- function highlightSearchTerm(text: string, searchTerm: string): TextPart[] {
40
- if (!searchTerm.trim()) {
41
- return [{ text, isHighlighted: false }];
42
- }
43
-
44
- const searchRegex = new RegExp(`(${escapeRegExp(searchTerm.trim())})`, "gi");
45
- const parts: TextPart[] = [];
46
- let lastIndex = 0;
47
-
48
- text.replace(searchRegex, (match, ...args) => {
49
- const index = args[args.length - 2] as number; // offset index from regex replace
50
-
51
- // Add text before the match
52
- if (index > lastIndex) {
53
- parts.push({
54
- text: text.slice(lastIndex, index),
55
- isHighlighted: false,
56
- });
57
- }
58
-
59
- // Add the highlighted match
60
- parts.push({
61
- text: match,
62
- isHighlighted: true,
63
- });
64
-
65
- lastIndex = index + match.length;
66
- return match;
67
- });
68
-
69
- // Add any remaining text after the last match
70
- if (lastIndex < text.length) {
71
- parts.push({
72
- text: text.slice(lastIndex),
73
- isHighlighted: false,
74
- });
75
- }
76
-
77
- return parts.length > 0 ? parts : [{ text, isHighlighted: false }];
78
- }
79
-
80
- function escapeRegExp(string: string): string {
81
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82
- }
83
-
84
- const styles = StyleSheet.create({
85
- highlightedText: {
86
- backgroundColor: colors.yellow[100],
87
- color: colors.yellow[900],
88
- fontWeight: "bold" as const,
89
- paddingHorizontal: 2,
90
- borderRadius: 2,
91
- },
92
- });
@@ -1,15 +0,0 @@
1
- import { Text } from "./text";
2
-
3
- export interface MenuTitleProps {
4
- title: string;
5
- }
6
-
7
- export function MenuTitle(props: MenuTitleProps) {
8
- const { title } = props;
9
-
10
- return (
11
- <Text size="lg" weight="semibold" style={{ paddingLeft: 8, lineHeight: 32, paddingBottom: 16 }}>
12
- {title}
13
- </Text>
14
- );
15
- }
@@ -1,167 +0,0 @@
1
- import React, { Ref, useCallback, useEffect, useImperativeHandle, useRef } from "react";
2
- import { Animated, View, PanResponder, StyleSheet, NativeSyntheticEvent } from "react-native";
3
- import type RNPagerView from "react-native-pager-view";
4
- import type { PagerViewProps } from "react-native-pager-view";
5
- import { AutoSizer } from "@lotics/ui/auto_sizer";
6
-
7
- type PagerViewHandle = Pick<
8
- RNPagerView,
9
- "setPage" | "setPageWithoutAnimation" | "setScrollEnabled"
10
- >;
11
-
12
- export function PagerView(props: PagerViewProps & { ref?: Ref<PagerViewHandle> }) {
13
- const {
14
- ref,
15
- children,
16
- initialPage = 0,
17
- scrollEnabled = true,
18
- overdrag = false,
19
- onPageSelected,
20
- } = props;
21
- const pageWidthRef = useRef(0);
22
- const pan = useRef(new Animated.Value(0)).current;
23
- const offset = useRef(0);
24
- const pageRef = useRef(initialPage);
25
- const scroll = useRef(!!scrollEnabled);
26
-
27
- const setPage = useCallback(
28
- (newPage: number) => {
29
- if (newPage < 0 || newPage >= React.Children.count(children)) {
30
- return; // Ignore invalid pageRef
31
- }
32
-
33
- pageRef.current = newPage;
34
-
35
- Animated.spring(pan, {
36
- toValue: -newPage * pageWidthRef.current,
37
- useNativeDriver: false,
38
- bounciness: 0,
39
- }).start();
40
- },
41
- [children, pan],
42
- );
43
-
44
- const setPageWithoutAnimation = useCallback(
45
- (newPage: number) => {
46
- if (newPage < 0 || newPage >= React.Children.count(children)) {
47
- return; // Ignore invalid pageRef
48
- }
49
-
50
- pageRef.current = newPage;
51
- pan.setValue(-newPage * pageWidthRef.current);
52
- },
53
- [children, pan],
54
- );
55
-
56
- const setScrollEnabled = useCallback((enabled: boolean) => {
57
- scroll.current = enabled;
58
- }, []);
59
-
60
- useEffect(() => {
61
- // Update the scroll when the scrollEnabled prop changes
62
- scroll.current = !!scrollEnabled;
63
- }, [scrollEnabled]);
64
-
65
- useImperativeHandle(ref, () => ({
66
- setPage,
67
- setPageWithoutAnimation,
68
- setScrollEnabled,
69
- }));
70
-
71
- const panResponder = useRef(
72
- PanResponder.create({
73
- onStartShouldSetPanResponder: () => scroll.current,
74
- onPanResponderMove: (_evt, gestureState) => {
75
- let newOffset = gestureState.dx - pageRef.current * pageWidthRef.current;
76
-
77
- // Prevent overdragging at the start
78
- if (overdrag === false && pageRef.current === 0 && gestureState.dx > 0) {
79
- newOffset = 0;
80
- }
81
-
82
- // Prevent overdragging at the end
83
- if (
84
- overdrag === false &&
85
- pageRef.current === React.Children.count(children) - 1 &&
86
- gestureState.dx < 0
87
- ) {
88
- newOffset = -pageRef.current * pageWidthRef.current;
89
- }
90
-
91
- offset.current = newOffset;
92
- pan.setValue(newOffset);
93
- },
94
- onPanResponderRelease: (evt, gestureState) => {
95
- // Consider both the offset and the swipe velocity for the snap. Adjust according to preference
96
- const swipeVelocityThreshold = 0.5;
97
- const swipeDistanceThreshold = pageWidthRef.current / 4;
98
-
99
- let newPage = pageRef.current;
100
-
101
- if (
102
- Math.abs(gestureState.vx) > swipeVelocityThreshold ||
103
- Math.abs(gestureState.dx) > swipeDistanceThreshold
104
- ) {
105
- newPage =
106
- gestureState.dx > 0
107
- ? Math.max(pageRef.current - 1, 0)
108
- : Math.min(pageRef.current + 1, React.Children.count(children) - 1);
109
- }
110
-
111
- pageRef.current = newPage;
112
-
113
- Animated.spring(pan, {
114
- toValue: -newPage * pageWidthRef.current,
115
- useNativeDriver: false,
116
- bounciness: 0,
117
- }).start(() => {
118
- // Update the offset after the animation completes
119
- offset.current = -newPage * pageWidthRef.current;
120
- });
121
-
122
- onPageSelected?.({
123
- nativeEvent: { position: newPage },
124
- } as NativeSyntheticEvent<Readonly<{ position: number }>>);
125
- },
126
- }),
127
- ).current;
128
-
129
- const handleResize = useCallback(
130
- (size: { width: number; height: number }) => {
131
- const width = size.width;
132
-
133
- pan.setValue(-pageRef.current * width);
134
- offset.current = -pageRef.current * width;
135
- pageWidthRef.current = width;
136
- },
137
- [pan],
138
- );
139
-
140
- return (
141
- <AutoSizer onResize={handleResize}>
142
- {({ width }) => (
143
- <View style={styles.container}>
144
- <Animated.View
145
- style={{
146
- flex: 1,
147
- flexDirection: "row",
148
- transform: [{ translateX: pan }],
149
- }}
150
- {...panResponder.panHandlers}
151
- >
152
- {React.Children.map(children, (child) => (
153
- <View style={{ width }}>{child}</View>
154
- ))}
155
- </Animated.View>
156
- </View>
157
- )}
158
- </AutoSizer>
159
- );
160
- }
161
-
162
- const styles = StyleSheet.create({
163
- container: {
164
- flex: 1,
165
- overflow: "hidden",
166
- },
167
- });
@@ -1,38 +0,0 @@
1
- import { ReactNode } from "react";
2
- import { View, StyleSheet } from "react-native";
3
- import { IconButton } from "./icon_button";
4
- import { Text } from "./text";
5
- import { usePopoverNav } from "./popover_nav";
6
-
7
- interface PopoverHeaderProps {
8
- title: string;
9
- right?: ReactNode;
10
- /** Accessible name for the back button. Default: "Back". Pass a translated string from the consumer. */
11
- backLabel?: string;
12
- }
13
-
14
- export function PopoverHeader(props: PopoverHeaderProps) {
15
- const { title, right, backLabel = "Back" } = props;
16
- const { goBack, canGoBack } = usePopoverNav();
17
-
18
- return (
19
- <View style={styles.container}>
20
- {canGoBack && (
21
- <IconButton icon="chevron-left" size="lg" color="secondary" accessibilityLabel={backLabel} onPress={goBack} />
22
- )}
23
- <Text size="sm" weight="medium" style={{ flex: 1 }}>
24
- {title}
25
- </Text>
26
- {right}
27
- </View>
28
- );
29
- }
30
-
31
- const styles = StyleSheet.create({
32
- container: {
33
- paddingLeft: 8,
34
- flexDirection: "row",
35
- alignItems: "center",
36
- gap: 8,
37
- },
38
- });