@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 +8 -4
- package/package.json +1 -7
- package/src/locale.tsx +4 -0
- package/src/page_header.tsx +0 -2
- package/src/popover_nav.tsx +40 -0
- 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
|
@@ -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": "
|
|
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: {
|
package/src/page_header.tsx
CHANGED
|
@@ -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
|
|
package/src/popover_nav.tsx
CHANGED
|
@@ -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
|
+
});
|
package/src/trend_footer.tsx
CHANGED
|
@@ -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 ?
|
|
43
|
+
{up ? labels.up : labels.down} {Math.abs(value)}% {periodLabel}
|
|
42
44
|
</Text>
|
|
43
45
|
</View>
|
|
44
46
|
{detail && (
|
package/src/use_screen_size.ts
CHANGED
|
@@ -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
|
-
}
|
package/src/form_time_picker.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/highlighted_text.tsx
DELETED
|
@@ -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
|
-
});
|
package/src/menu_title.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/pager_view.tsx
DELETED
|
@@ -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
|
-
});
|
package/src/popover_header.tsx
DELETED
|
@@ -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
|
-
});
|