@olympusoss/canvas 4.0.0 → 5.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/README.md +108 -0
- package/package.json +14 -3
- package/src/atoms/avatar/avatar.md +185 -0
- package/src/atoms/avatar/avatar.styles.ts +48 -0
- package/src/atoms/avatar/avatar.tsx +99 -0
- package/src/atoms/badge/badge.md +237 -0
- package/src/atoms/badge/badge.styles.ts +79 -0
- package/src/atoms/badge/badge.tsx +86 -0
- package/src/atoms/breadcrumb/breadcrumb.md +233 -0
- package/src/atoms/breadcrumb/breadcrumb.styles.ts +40 -0
- package/src/atoms/breadcrumb/breadcrumb.tsx +130 -0
- package/src/atoms/button/button.android.tsx +6 -0
- package/src/atoms/button/button.ios.tsx +6 -0
- package/src/atoms/button/button.md +184 -0
- package/src/atoms/button/button.shared.tsx +79 -0
- package/src/atoms/button/button.styles.ts +152 -0
- package/src/atoms/button/button.tsx +6 -0
- package/src/atoms/button-group/button-group.android.tsx +6 -0
- package/src/atoms/button-group/button-group.ios.tsx +6 -0
- package/src/atoms/button-group/button-group.md +120 -0
- package/src/atoms/button-group/button-group.shared.tsx +398 -0
- package/src/atoms/button-group/button-group.styles.ts +483 -0
- package/src/atoms/button-group/button-group.tsx +6 -0
- package/src/atoms/checkbox/checkbox.android.tsx +6 -0
- package/src/atoms/checkbox/checkbox.ios.tsx +6 -0
- package/src/atoms/checkbox/checkbox.md +150 -0
- package/src/atoms/checkbox/checkbox.shared.tsx +103 -0
- package/src/atoms/checkbox/checkbox.styles.ts +106 -0
- package/src/atoms/checkbox/checkbox.tsx +6 -0
- package/src/atoms/combobox/combobox.android.tsx +6 -0
- package/src/atoms/combobox/combobox.ios.tsx +6 -0
- package/src/atoms/combobox/combobox.md +213 -0
- package/src/atoms/combobox/combobox.shared.tsx +160 -0
- package/src/atoms/combobox/combobox.styles.ts +270 -0
- package/src/atoms/combobox/combobox.tsx +6 -0
- package/src/atoms/divider/divider.md +140 -0
- package/src/atoms/divider/divider.styles.ts +35 -0
- package/src/atoms/divider/divider.tsx +67 -0
- package/src/atoms/dropdown/dropdown.android.tsx +6 -0
- package/src/atoms/dropdown/dropdown.ios.tsx +6 -0
- package/src/atoms/dropdown/dropdown.md +221 -0
- package/src/atoms/dropdown/dropdown.shared.tsx +190 -0
- package/src/atoms/dropdown/dropdown.styles.ts +233 -0
- package/src/atoms/dropdown/dropdown.tsx +6 -0
- package/src/atoms/icon/icon.md +131 -0
- package/src/atoms/icon/icon.styles.ts +30 -0
- package/src/atoms/icon/icon.tsx +328 -0
- package/src/atoms/index.ts +24 -0
- package/src/atoms/input/input.android.tsx +6 -0
- package/src/atoms/input/input.ios.tsx +6 -0
- package/src/atoms/input/input.md +118 -0
- package/src/atoms/input/input.shared.tsx +203 -0
- package/src/atoms/input/input.styles.ts +286 -0
- package/src/atoms/input/input.tsx +6 -0
- package/src/atoms/kbd/kbd.md +91 -0
- package/src/atoms/kbd/kbd.styles.ts +33 -0
- package/src/atoms/kbd/kbd.tsx +27 -0
- package/src/atoms/listbox/listbox.md +177 -0
- package/src/atoms/listbox/listbox.styles.ts +60 -0
- package/src/atoms/listbox/listbox.tsx +113 -0
- package/src/atoms/pagination/pagination.android.tsx +6 -0
- package/src/atoms/pagination/pagination.ios.tsx +6 -0
- package/src/atoms/pagination/pagination.md +133 -0
- package/src/atoms/pagination/pagination.shared.tsx +289 -0
- package/src/atoms/pagination/pagination.styles.ts +245 -0
- package/src/atoms/pagination/pagination.tsx +6 -0
- package/src/atoms/popover/popover.android.tsx +8 -0
- package/src/atoms/popover/popover.ios.tsx +6 -0
- package/src/atoms/popover/popover.md +87 -0
- package/src/atoms/popover/popover.shared.tsx +124 -0
- package/src/atoms/popover/popover.styles.ts +144 -0
- package/src/atoms/popover/popover.tsx +6 -0
- package/src/atoms/radio/radio.android.tsx +6 -0
- package/src/atoms/radio/radio.ios.tsx +6 -0
- package/src/atoms/radio/radio.md +173 -0
- package/src/atoms/radio/radio.shared.tsx +98 -0
- package/src/atoms/radio/radio.styles.ts +109 -0
- package/src/atoms/radio/radio.tsx +6 -0
- package/src/atoms/select/select.android.tsx +6 -0
- package/src/atoms/select/select.ios.tsx +6 -0
- package/src/atoms/select/select.md +156 -0
- package/src/atoms/select/select.shared.tsx +143 -0
- package/src/atoms/select/select.styles.ts +310 -0
- package/src/atoms/select/select.tsx +6 -0
- package/src/atoms/skeleton/skeleton.md +135 -0
- package/src/atoms/skeleton/skeleton.styles.ts +117 -0
- package/src/atoms/skeleton/skeleton.tsx +145 -0
- package/src/atoms/spinner/spinner.android.tsx +7 -0
- package/src/atoms/spinner/spinner.ios.tsx +7 -0
- package/src/atoms/spinner/spinner.md +94 -0
- package/src/atoms/spinner/spinner.shared.tsx +92 -0
- package/src/atoms/spinner/spinner.styles.tsx +115 -0
- package/src/atoms/spinner/spinner.tsx +7 -0
- package/src/atoms/switch/switch.android.tsx +6 -0
- package/src/atoms/switch/switch.ios.tsx +6 -0
- package/src/atoms/switch/switch.md +91 -0
- package/src/atoms/switch/switch.shared.tsx +97 -0
- package/src/atoms/switch/switch.styles.ts +79 -0
- package/src/atoms/switch/switch.tsx +6 -0
- package/src/atoms/textarea/textarea.android.tsx +6 -0
- package/src/atoms/textarea/textarea.ios.tsx +6 -0
- package/src/atoms/textarea/textarea.md +140 -0
- package/src/atoms/textarea/textarea.shared.tsx +74 -0
- package/src/atoms/textarea/textarea.styles.ts +116 -0
- package/src/atoms/textarea/textarea.tsx +6 -0
- package/src/atoms/tooltip/tooltip.android.tsx +6 -0
- package/src/atoms/tooltip/tooltip.ios.tsx +7 -0
- package/src/atoms/tooltip/tooltip.md +122 -0
- package/src/atoms/tooltip/tooltip.shared.tsx +113 -0
- package/src/atoms/tooltip/tooltip.styles.ts +113 -0
- package/src/atoms/tooltip/tooltip.tsx +6 -0
- package/src/atoms/typography/typography.md +330 -0
- package/src/atoms/typography/typography.styles.ts +95 -0
- package/src/atoms/typography/typography.tsx +76 -0
- package/src/index.ts +12 -2
- package/src/molecules/action-panels/action-panels.md +133 -0
- package/src/molecules/action-panels/action-panels.styles.ts +39 -0
- package/src/molecules/action-panels/action-panels.tsx +113 -0
- package/src/molecules/alert/alert.md +119 -0
- package/src/molecules/alert/alert.styles.ts +88 -0
- package/src/molecules/alert/alert.tsx +74 -0
- package/src/molecules/alert-dialog/alert-dialog.android.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.ios.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.md +177 -0
- package/src/molecules/alert-dialog/alert-dialog.shared.tsx +187 -0
- package/src/molecules/alert-dialog/alert-dialog.styles.ts +248 -0
- package/src/molecules/alert-dialog/alert-dialog.tsx +6 -0
- package/src/molecules/card/card.md +190 -0
- package/src/molecules/card/card.styles.ts +67 -0
- package/src/molecules/card/card.tsx +176 -0
- package/src/molecules/code-block/code-block.md +159 -0
- package/src/molecules/code-block/code-block.styles.ts +167 -0
- package/src/molecules/code-block/code-block.tsx +176 -0
- package/src/molecules/description-lists/description-lists.md +129 -0
- package/src/molecules/description-lists/description-lists.styles.ts +102 -0
- package/src/molecules/description-lists/description-lists.tsx +133 -0
- package/src/molecules/empty-state/empty-state.md +218 -0
- package/src/molecules/empty-state/empty-state.styles.ts +63 -0
- package/src/molecules/empty-state/empty-state.tsx +77 -0
- package/src/molecules/feeds/feeds.md +102 -0
- package/src/molecules/feeds/feeds.styles.ts +120 -0
- package/src/molecules/feeds/feeds.tsx +167 -0
- package/src/molecules/field/field.md +117 -0
- package/src/molecules/field/field.styles.ts +85 -0
- package/src/molecules/field/field.tsx +175 -0
- package/src/molecules/fieldset/fieldset.md +141 -0
- package/src/molecules/fieldset/fieldset.styles.ts +79 -0
- package/src/molecules/fieldset/fieldset.tsx +182 -0
- package/src/molecules/form/form.md +137 -0
- package/src/molecules/form/form.styles.ts +39 -0
- package/src/molecules/form/form.tsx +246 -0
- package/src/molecules/grid-lists/grid-lists.md +114 -0
- package/src/molecules/grid-lists/grid-lists.styles.ts +79 -0
- package/src/molecules/grid-lists/grid-lists.tsx +157 -0
- package/src/molecules/index.ts +16 -0
- package/src/molecules/media-objects/media-objects.md +87 -0
- package/src/molecules/media-objects/media-objects.styles.ts +94 -0
- package/src/molecules/media-objects/media-objects.tsx +128 -0
- package/src/molecules/stacked-lists/stacked-lists.md +116 -0
- package/src/molecules/stacked-lists/stacked-lists.styles.ts +111 -0
- package/src/molecules/stacked-lists/stacked-lists.tsx +195 -0
- package/src/molecules/stats/stats.md +166 -0
- package/src/molecules/stats/stats.styles.ts +91 -0
- package/src/molecules/stats/stats.tsx +88 -0
- package/src/organisms/calendar/calendar.android.tsx +6 -0
- package/src/organisms/calendar/calendar.ios.tsx +6 -0
- package/src/organisms/calendar/calendar.md +114 -0
- package/src/organisms/calendar/calendar.shared.tsx +146 -0
- package/src/organisms/calendar/calendar.styles.ts +315 -0
- package/src/organisms/calendar/calendar.tsx +6 -0
- package/src/organisms/charts/charts.md +326 -0
- package/src/organisms/charts/charts.styles.ts +135 -0
- package/src/organisms/charts/charts.tsx +124 -0
- package/src/organisms/command/command.md +117 -0
- package/src/organisms/command/command.styles.ts +179 -0
- package/src/organisms/command/command.tsx +164 -0
- package/src/organisms/data-table/data-table.md +182 -0
- package/src/organisms/data-table/data-table.styles.ts +103 -0
- package/src/organisms/data-table/data-table.tsx +105 -0
- package/src/organisms/dialog/dialog.android.tsx +6 -0
- package/src/organisms/dialog/dialog.ios.tsx +6 -0
- package/src/organisms/dialog/dialog.md +271 -0
- package/src/organisms/dialog/dialog.shared.tsx +230 -0
- package/src/organisms/dialog/dialog.styles.ts +272 -0
- package/src/organisms/dialog/dialog.tsx +6 -0
- package/src/organisms/filter-panel/filter-panel.md +116 -0
- package/src/organisms/filter-panel/filter-panel.styles.ts +83 -0
- package/src/organisms/filter-panel/filter-panel.tsx +91 -0
- package/src/organisms/index.ts +13 -0
- package/src/organisms/navbars/navbars.android.tsx +6 -0
- package/src/organisms/navbars/navbars.ios.tsx +6 -0
- package/src/organisms/navbars/navbars.md +144 -0
- package/src/organisms/navbars/navbars.shared.tsx +137 -0
- package/src/organisms/navbars/navbars.styles.ts +251 -0
- package/src/organisms/navbars/navbars.tsx +6 -0
- package/src/organisms/overlays/overlays.android.tsx +6 -0
- package/src/organisms/overlays/overlays.ios.tsx +6 -0
- package/src/organisms/overlays/overlays.md +123 -0
- package/src/organisms/overlays/overlays.shared.tsx +175 -0
- package/src/organisms/overlays/overlays.styles.ts +309 -0
- package/src/organisms/overlays/overlays.tsx +6 -0
- package/src/organisms/row-menu/row-menu.android.tsx +6 -0
- package/src/organisms/row-menu/row-menu.ios.tsx +6 -0
- package/src/organisms/row-menu/row-menu.md +102 -0
- package/src/organisms/row-menu/row-menu.shared.tsx +105 -0
- package/src/organisms/row-menu/row-menu.styles.ts +262 -0
- package/src/organisms/row-menu/row-menu.tsx +6 -0
- package/src/organisms/sidebar/sidebar.android.tsx +6 -0
- package/src/organisms/sidebar/sidebar.ios.tsx +6 -0
- package/src/organisms/sidebar/sidebar.md +188 -0
- package/src/organisms/sidebar/sidebar.shared.tsx +167 -0
- package/src/organisms/sidebar/sidebar.styles.ts +262 -0
- package/src/organisms/sidebar/sidebar.tsx +6 -0
- package/src/organisms/stepper/stepper.android.tsx +6 -0
- package/src/organisms/stepper/stepper.ios.tsx +6 -0
- package/src/organisms/stepper/stepper.md +150 -0
- package/src/organisms/stepper/stepper.shared.tsx +158 -0
- package/src/organisms/stepper/stepper.styles.ts +280 -0
- package/src/organisms/stepper/stepper.tsx +6 -0
- package/src/organisms/tabs/tabs.android.tsx +6 -0
- package/src/organisms/tabs/tabs.ios.tsx +6 -0
- package/src/organisms/tabs/tabs.md +127 -0
- package/src/organisms/tabs/tabs.shared.tsx +281 -0
- package/src/organisms/tabs/tabs.styles.ts +398 -0
- package/src/organisms/tabs/tabs.tsx +6 -0
- package/src/style/color.ts +17 -0
- package/src/style/index.ts +14 -0
- package/src/style/primitives.ts +26 -0
- package/src/style/responsive.ts +45 -0
- package/src/style/shadow.ts +21 -0
- package/src/style/theme.tsx +56 -0
- package/src/style/tokens.ts +487 -0
- package/src/theme.ts +21 -0
- package/styles/canvas.css +128 -67
- package/tsconfig.json +4 -2
- package/src/cn.ts +0 -3
- package/styles/base.css +0 -17
- package/styles/components/alert.css +0 -66
- package/styles/components/app-shell.css +0 -46
- package/styles/components/avatar.css +0 -15
- package/styles/components/badge.css +0 -83
- package/styles/components/breadcrumb.css +0 -35
- package/styles/components/button-group.css +0 -23
- package/styles/components/button.css +0 -107
- package/styles/components/calendar.css +0 -73
- package/styles/components/card.css +0 -58
- package/styles/components/checkbox.css +0 -55
- package/styles/components/code-block.css +0 -18
- package/styles/components/combobox.css +0 -75
- package/styles/components/command.css +0 -94
- package/styles/components/data-table.css +0 -142
- package/styles/components/dialog.css +0 -72
- package/styles/components/dropdown.css +0 -54
- package/styles/components/empty-state.css +0 -17
- package/styles/components/field.css +0 -27
- package/styles/components/filter-panel.css +0 -58
- package/styles/components/form.css +0 -27
- package/styles/components/icon.css +0 -8
- package/styles/components/input-group.css +0 -45
- package/styles/components/input.css +0 -56
- package/styles/components/kbd.css +0 -15
- package/styles/components/page-header.css +0 -52
- package/styles/components/pagination.css +0 -48
- package/styles/components/popover.css +0 -14
- package/styles/components/radio.css +0 -28
- package/styles/components/row-menu.css +0 -69
- package/styles/components/section-card.css +0 -49
- package/styles/components/select.css +0 -57
- package/styles/components/separator.css +0 -32
- package/styles/components/sheet.css +0 -70
- package/styles/components/sidebar.css +0 -146
- package/styles/components/skeleton.css +0 -32
- package/styles/components/spinner.css +0 -26
- package/styles/components/stat-card.css +0 -71
- package/styles/components/stepper.css +0 -63
- package/styles/components/switch.css +0 -45
- package/styles/components/tabs.css +0 -40
- package/styles/components/textarea.css +0 -31
- package/styles/components/toast.css +0 -95
- package/styles/components/tooltip.css +0 -53
- package/styles/components/topbar.css +0 -24
- package/styles/components/typography.css +0 -105
- package/styles/patterns/backdrops.css +0 -35
- package/styles/patterns/density.css +0 -66
- package/styles/patterns/focus.css +0 -38
- package/styles/patterns/glass.css +0 -85
- package/styles/patterns/high-contrast.css +0 -70
- package/styles/patterns/reduced-motion.css +0 -12
- package/styles/patterns/scrollbar.css +0 -10
- package/styles/reset.css +0 -89
- package/styles/tokens/colors.css +0 -106
- package/styles/tokens/motion.css +0 -33
- package/styles/tokens/radius.css +0 -10
- package/styles/tokens/shadows.css +0 -35
- package/styles/tokens/spacing.css +0 -19
- package/styles/tokens/typography.css +0 -6
- package/styles/tokens/z-index.css +0 -12
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle, type ColorTokens } from "../../style/index.js";
|
|
2
|
+
import * as s from "./pagination.styles.js";
|
|
3
|
+
import { type Size, type PaginationSkin } from "./pagination.styles.js";
|
|
4
|
+
|
|
5
|
+
// Shared Pagination shell. The structure (numbered / compact / with-size
|
|
6
|
+
// variants, the windowing math, the clamp + go handler, the size-selector
|
|
7
|
+
// cycling), the accessibility, and the variant/size precedence live here once; a
|
|
8
|
+
// platform file supplies only its skin (the cell shape, the active-page fill,
|
|
9
|
+
// the label color, and the press feedback) and calls createPagination.
|
|
10
|
+
//
|
|
11
|
+
// Pagination is page-of-N navigation for tables and lists: a horizontal row of
|
|
12
|
+
// page-number buttons flanked by Prev/Next controls, with the current page
|
|
13
|
+
// highlighted. When there are too many pages to show at once, the middle is
|
|
14
|
+
// truncated with an ellipsis glyph, keeping the first page, the last page, and a
|
|
15
|
+
// small window around the current page.
|
|
16
|
+
//
|
|
17
|
+
// The brand survives on every platform (the indigo `primary` token fills the
|
|
18
|
+
// active page); only the native SHAPE (cell radius) and press feedback change:
|
|
19
|
+
// - iOS (HIG page controls): pill-rounded cells (radius ~8), the active page
|
|
20
|
+
// filled `primary`; press = opacity dim 0.8.
|
|
21
|
+
// - Android (M3): flat cells (radius ~8), active page a tonal alpha(primary)
|
|
22
|
+
// fill with a brand label; press = android_ripple.
|
|
23
|
+
// - Web: the established Canvas look (bordered boxes, radius 6, solid primary
|
|
24
|
+
// fill on the active page), lifted verbatim.
|
|
25
|
+
//
|
|
26
|
+
// Boolean-prop API across two axes (mirrors Button's intentOf precedence; first
|
|
27
|
+
// match wins within an axis, axes are orthogonal):
|
|
28
|
+
// - Variant: `withSize` prepends a "Rows per page" size selector to the
|
|
29
|
+
// compact Prev/Next + "Page X of N" layout; `compact` collapses the number
|
|
30
|
+
// row to just Prev/Next plus a "Page X of N" label; the default is the full
|
|
31
|
+
// numbered row. Precedence when more than one is passed: withSize, then
|
|
32
|
+
// compact, then the numbered default.
|
|
33
|
+
// - Size: `small`, `large` (omit for the default, medium size).
|
|
34
|
+
//
|
|
35
|
+
// There is no icon utility at this layer, so Prev/Next use reading-direction
|
|
36
|
+
// single guillemet glyphs ("‹" / "›") rendered as Text rather than SVG chevrons,
|
|
37
|
+
// the size selector uses a "▾" caret glyph, and the truncation gap is an ellipsis
|
|
38
|
+
// glyph ("…").
|
|
39
|
+
|
|
40
|
+
export interface PaginationProps {
|
|
41
|
+
/** Current page, 1-based. Clamped into the 1..total range before rendering. */
|
|
42
|
+
page?: number;
|
|
43
|
+
/** Total number of pages. */
|
|
44
|
+
total?: number;
|
|
45
|
+
/** Fired with the next 1-based page when a control or number is pressed. */
|
|
46
|
+
onChange?: (page: number) => void;
|
|
47
|
+
|
|
48
|
+
// Variant (pick one; default is the full numbered row).
|
|
49
|
+
/** Collapse to Prev/Next plus a "Page X of N" label, no number buttons. */
|
|
50
|
+
compact?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Prepend a "Rows per page" size selector to the compact Prev/Next +
|
|
53
|
+
* "Page X of N" layout. Takes precedence over `compact`.
|
|
54
|
+
*/
|
|
55
|
+
withSize?: boolean;
|
|
56
|
+
|
|
57
|
+
// Content for the `withSize` selector.
|
|
58
|
+
/** Currently selected rows-per-page value shown in the selector. */
|
|
59
|
+
pageSize?: number;
|
|
60
|
+
/** Selectable rows-per-page values; pressing the selector advances through them. */
|
|
61
|
+
pageSizes?: number[];
|
|
62
|
+
/** Fired with the next rows-per-page value when the selector is pressed. */
|
|
63
|
+
onPageSizeChange?: (size: number) => void;
|
|
64
|
+
|
|
65
|
+
// Size (pick one; default is the medium size).
|
|
66
|
+
small?: boolean;
|
|
67
|
+
large?: boolean;
|
|
68
|
+
|
|
69
|
+
disabled?: boolean;
|
|
70
|
+
/** Escape hatch for layout/positioning composition (margins, alignment). */
|
|
71
|
+
style?: StyleProp<ViewStyle>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Size precedence when more than one is passed: first match wins.
|
|
75
|
+
function sizeOf(p: PaginationProps): Size {
|
|
76
|
+
if (p.small) return "small";
|
|
77
|
+
if (p.large) return "large";
|
|
78
|
+
return "default";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type Variant = "withSize" | "compact" | "numbered";
|
|
82
|
+
|
|
83
|
+
// Variant precedence when more than one is passed: first match wins.
|
|
84
|
+
function variantOf(p: PaginationProps): Variant {
|
|
85
|
+
if (p.withSize) return "withSize";
|
|
86
|
+
if (p.compact) return "compact";
|
|
87
|
+
return "numbered";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sentinel inserted into the page list for a truncation gap.
|
|
91
|
+
const GAP = -1;
|
|
92
|
+
|
|
93
|
+
// Compute the windowed page list: first page, last page, and a one-page window
|
|
94
|
+
// around the current page, with GAP sentinels marking truncated stretches. For
|
|
95
|
+
// small totals (<= 7) every page is shown.
|
|
96
|
+
function pageWindow(current: number, total: number): number[] {
|
|
97
|
+
if (total <= 7) {
|
|
98
|
+
const all: number[] = [];
|
|
99
|
+
for (let p = 1; p <= total; p++) all.push(p);
|
|
100
|
+
return all;
|
|
101
|
+
}
|
|
102
|
+
const list: number[] = [];
|
|
103
|
+
const add = (p: number) => {
|
|
104
|
+
if (!list.includes(p)) list.push(p);
|
|
105
|
+
};
|
|
106
|
+
add(1);
|
|
107
|
+
if (current > 3) list.push(GAP);
|
|
108
|
+
for (let p = Math.max(2, current - 1); p <= Math.min(total - 1, current + 1); p++) add(p);
|
|
109
|
+
if (current < total - 2) list.push(GAP);
|
|
110
|
+
add(total);
|
|
111
|
+
return list;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Build a Pagination component from a platform skin. */
|
|
115
|
+
export function createPagination(skin: PaginationSkin) {
|
|
116
|
+
const ripple = skin.ripple;
|
|
117
|
+
|
|
118
|
+
interface ControlProps {
|
|
119
|
+
glyph: string;
|
|
120
|
+
size: Size;
|
|
121
|
+
tokens: ColorTokens;
|
|
122
|
+
disabled: boolean;
|
|
123
|
+
accessibilityLabel: string;
|
|
124
|
+
onPress: () => void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A Prev/Next chevron control. Reads as a square page button without a number.
|
|
128
|
+
function Control({ glyph, size, tokens, disabled, accessibilityLabel, onPress }: ControlProps) {
|
|
129
|
+
return (
|
|
130
|
+
<Pressable
|
|
131
|
+
style={({ pressed }) => [
|
|
132
|
+
skin.controlBox(tokens),
|
|
133
|
+
s.itemSize[size],
|
|
134
|
+
disabled ? { opacity: 0.5 } : null,
|
|
135
|
+
skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
|
|
136
|
+
]}
|
|
137
|
+
onPress={onPress}
|
|
138
|
+
disabled={disabled}
|
|
139
|
+
android_ripple={ripple ? ripple(tokens, false) : undefined}
|
|
140
|
+
accessibilityRole="button"
|
|
141
|
+
accessibilityLabel={accessibilityLabel}
|
|
142
|
+
accessibilityState={{ disabled }}
|
|
143
|
+
>
|
|
144
|
+
<Text style={[skin.controlLabel(tokens), s.labelSize[size]]}>{glyph}</Text>
|
|
145
|
+
</Pressable>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return function Pagination(props: PaginationProps) {
|
|
150
|
+
const { onChange, disabled, style } = props;
|
|
151
|
+
const size = sizeOf(props);
|
|
152
|
+
const variant = variantOf(props);
|
|
153
|
+
const { tokens } = useTheme();
|
|
154
|
+
|
|
155
|
+
// Clamp inputs so the control never renders an out-of-range current page.
|
|
156
|
+
const total = Math.max(1, Math.floor(props.total ?? 1));
|
|
157
|
+
const current = Math.min(Math.max(1, Math.floor(props.page ?? 1)), total);
|
|
158
|
+
|
|
159
|
+
const atStart = current <= 1;
|
|
160
|
+
const atEnd = current >= total;
|
|
161
|
+
|
|
162
|
+
const go = (next: number) => {
|
|
163
|
+
if (disabled) return;
|
|
164
|
+
const clamped = Math.min(Math.max(1, next), total);
|
|
165
|
+
if (clamped !== current) onChange?.(clamped);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const prev = (
|
|
169
|
+
<Control
|
|
170
|
+
glyph="‹"
|
|
171
|
+
size={size}
|
|
172
|
+
tokens={tokens}
|
|
173
|
+
disabled={disabled || atStart}
|
|
174
|
+
accessibilityLabel="Previous page"
|
|
175
|
+
onPress={() => go(current - 1)}
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
const next = (
|
|
179
|
+
<Control
|
|
180
|
+
glyph="›"
|
|
181
|
+
size={size}
|
|
182
|
+
tokens={tokens}
|
|
183
|
+
disabled={disabled || atEnd}
|
|
184
|
+
accessibilityLabel="Next page"
|
|
185
|
+
onPress={() => go(current + 1)}
|
|
186
|
+
/>
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Compact: Prev/Next bracketing a "Page X of N" indicator, no number buttons.
|
|
190
|
+
if (variant === "compact") {
|
|
191
|
+
return (
|
|
192
|
+
<View style={[s.compactRow, style]}>
|
|
193
|
+
{prev}
|
|
194
|
+
<Text style={[skin.mutedLabel(tokens), s.labelSize[size]]}>
|
|
195
|
+
{`Page ${current} of ${total}`}
|
|
196
|
+
</Text>
|
|
197
|
+
{next}
|
|
198
|
+
</View>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// With-size: a "Rows per page" selector ahead of the compact indicator and the
|
|
203
|
+
// Prev/Next controls. There is no native select, so the selector is a closed
|
|
204
|
+
// trigger (value + caret) that advances through `pageSizes` on press.
|
|
205
|
+
if (variant === "withSize") {
|
|
206
|
+
const sizes = props.pageSizes ?? [10, 25, 50];
|
|
207
|
+
const pageSize = props.pageSize ?? sizes[0] ?? 10;
|
|
208
|
+
const cycleSize = () => {
|
|
209
|
+
if (disabled) return;
|
|
210
|
+
const i = sizes.indexOf(pageSize);
|
|
211
|
+
const nextSize = sizes[(i + 1) % sizes.length];
|
|
212
|
+
if (nextSize !== undefined && nextSize !== pageSize) props.onPageSizeChange?.(nextSize);
|
|
213
|
+
};
|
|
214
|
+
return (
|
|
215
|
+
<View style={[s.withSizeRow, style]}>
|
|
216
|
+
<View style={s.selectorCluster}>
|
|
217
|
+
<Text style={[skin.mutedLabel(tokens), s.labelSize[size]]}>Rows per page</Text>
|
|
218
|
+
<Pressable
|
|
219
|
+
style={({ pressed }) => [
|
|
220
|
+
skin.selectorBox(tokens),
|
|
221
|
+
s.itemSize[size],
|
|
222
|
+
disabled ? { opacity: 0.5 } : null,
|
|
223
|
+
skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
|
|
224
|
+
]}
|
|
225
|
+
onPress={cycleSize}
|
|
226
|
+
disabled={disabled}
|
|
227
|
+
android_ripple={ripple ? ripple(tokens, false) : undefined}
|
|
228
|
+
accessibilityRole="button"
|
|
229
|
+
accessibilityLabel="Rows per page"
|
|
230
|
+
>
|
|
231
|
+
<Text style={[skin.controlLabel(tokens), s.labelSize[size]]}>{pageSize}</Text>
|
|
232
|
+
<Text style={[skin.mutedLabel(tokens), s.labelSize[size]]}>▾</Text>
|
|
233
|
+
</Pressable>
|
|
234
|
+
</View>
|
|
235
|
+
<Text style={[skin.mutedLabel(tokens), s.labelSize[size]]}>
|
|
236
|
+
{`Page ${current} of ${total}`}
|
|
237
|
+
</Text>
|
|
238
|
+
<View style={s.controlPair}>
|
|
239
|
+
{prev}
|
|
240
|
+
{next}
|
|
241
|
+
</View>
|
|
242
|
+
</View>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Numbered (default): a windowed row of page buttons with ellipsis gaps.
|
|
247
|
+
const window = pageWindow(current, total);
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<View style={[s.numberedRow, style]}>
|
|
251
|
+
{prev}
|
|
252
|
+
{window.map((p, i) => {
|
|
253
|
+
if (p === GAP) {
|
|
254
|
+
return (
|
|
255
|
+
<Text
|
|
256
|
+
key={`gap-${i}`}
|
|
257
|
+
style={[skin.gapLabel(tokens), s.labelSize[size]]}
|
|
258
|
+
accessibilityElementsHidden
|
|
259
|
+
>
|
|
260
|
+
…
|
|
261
|
+
</Text>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const selected = p === current;
|
|
265
|
+
return (
|
|
266
|
+
<Pressable
|
|
267
|
+
key={`page-${p}`}
|
|
268
|
+
style={({ pressed }) => [
|
|
269
|
+
skin.pageBox(tokens, selected),
|
|
270
|
+
s.itemSize[size],
|
|
271
|
+
disabled ? { opacity: 0.5 } : null,
|
|
272
|
+
skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
|
|
273
|
+
]}
|
|
274
|
+
onPress={() => go(p)}
|
|
275
|
+
disabled={disabled}
|
|
276
|
+
android_ripple={ripple ? ripple(tokens, selected) : undefined}
|
|
277
|
+
accessibilityRole="button"
|
|
278
|
+
accessibilityLabel={`Page ${p}`}
|
|
279
|
+
accessibilityState={{ selected, disabled: !!disabled }}
|
|
280
|
+
>
|
|
281
|
+
<Text style={[skin.pageLabel(tokens, selected), s.labelSize[size]]}>{p}</Text>
|
|
282
|
+
</Pressable>
|
|
283
|
+
);
|
|
284
|
+
})}
|
|
285
|
+
{next}
|
|
286
|
+
</View>
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { type ViewStyle, type TextStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens, alpha } from "../../style/index.js";
|
|
3
|
+
|
|
4
|
+
// Co-located Pagination skins, one per platform. The page buttons, Prev/Next
|
|
5
|
+
// controls, and the size selector are square-ish boxes whose footprint scales
|
|
6
|
+
// with the size axis; labels scale on a smaller type ramp. The BRAND survives on
|
|
7
|
+
// every platform (the active page reads the indigo `primary` token, never a
|
|
8
|
+
// platform default), and only the native SHAPE (cell radius / border) and press
|
|
9
|
+
// feedback change per OS. This is a LIGHT platform touch: neither iOS nor
|
|
10
|
+
// Material 3 ships a true numbered pagination, so the prev/next/numbered
|
|
11
|
+
// structure stays identical and each platform applies only its shape + feedback.
|
|
12
|
+
// iOS (HIG page controls): pill-rounded cells (radius 8), no cell border, the
|
|
13
|
+
// ACTIVE page filled `primary`; press = opacity dim 0.8.
|
|
14
|
+
// Android (M3): flat cells (radius 8), no border, the ACTIVE page a tonal
|
|
15
|
+
// alpha(primary, .12) fill with a brand-indigo label; press = android_ripple.
|
|
16
|
+
// Web: the established Canvas look (1px bordered boxes, radius 6, the active
|
|
17
|
+
// page a solid `primary` fill/border), lifted verbatim.
|
|
18
|
+
|
|
19
|
+
export type Size = "small" | "default" | "large";
|
|
20
|
+
|
|
21
|
+
// The platform-varying surface. Everything color/shape-bearing the cells need
|
|
22
|
+
// lives here, built from the active tokens (so fills/borders follow light/dark
|
|
23
|
+
// and the glass surface). Layout (gaps, rows) is shared and stays in this file's
|
|
24
|
+
// static fragments below.
|
|
25
|
+
export interface PaginationSkin {
|
|
26
|
+
/** A Prev/Next chevron control: the bordered/filled box behind the glyph. */
|
|
27
|
+
controlBox: (t: ColorTokens) => ViewStyle;
|
|
28
|
+
/** A numbered page cell. Selected reads the brand fill; the rest are plain. */
|
|
29
|
+
pageBox: (t: ColorTokens, selected: boolean) => ViewStyle;
|
|
30
|
+
/** The rows-per-page selector trigger box (value + caret). */
|
|
31
|
+
selectorBox: (t: ColorTokens) => ViewStyle;
|
|
32
|
+
/** Control glyph / selector value color (foreground). */
|
|
33
|
+
controlLabel: (t: ColorTokens) => TextStyle;
|
|
34
|
+
/** A numbered page label; brand-on-fill when selected, foreground otherwise. */
|
|
35
|
+
pageLabel: (t: ColorTokens, selected: boolean) => TextStyle;
|
|
36
|
+
/** Muted supporting text: the "Page X of N" indicator, "Rows per page", caret. */
|
|
37
|
+
mutedLabel: (t: ColorTokens) => TextStyle;
|
|
38
|
+
/** The truncation ellipsis: muted, with a small horizontal inset. */
|
|
39
|
+
gapLabel: (t: ColorTokens) => TextStyle;
|
|
40
|
+
/** iOS/web dim the cell on press; Android uses a ripple instead (null). */
|
|
41
|
+
pressedOpacity: number | null;
|
|
42
|
+
/** Android ripple over a pressed cell; null on iOS/web. */
|
|
43
|
+
ripple?: (t: ColorTokens, selected: boolean) => { color: string; borderless: boolean };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- shared size scales (brand type/sizing, identical across platforms) ------
|
|
47
|
+
|
|
48
|
+
// Square-ish button footprint per size: height + matching min width + the
|
|
49
|
+
// horizontal pad (`h-8 min-w-8 px-2` / `h-9 min-w-9 px-2.5` / `h-10 min-w-10 px-3`).
|
|
50
|
+
export const itemSize: Record<Size, ViewStyle> = {
|
|
51
|
+
small: { height: 32, minWidth: 32, paddingHorizontal: 8 },
|
|
52
|
+
default: { height: 36, minWidth: 36, paddingHorizontal: 10 },
|
|
53
|
+
large: { height: 40, minWidth: 40, paddingHorizontal: 12 },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Label type per size (`text-xs` for small, `text-sm` otherwise).
|
|
57
|
+
export const labelSize: Record<Size, TextStyle> = {
|
|
58
|
+
small: { fontSize: 12, lineHeight: 16 },
|
|
59
|
+
default: { fontSize: 14, lineHeight: 20 },
|
|
60
|
+
large: { fontSize: 14, lineHeight: 20 },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// --- shared layout fragments (color-free; identical across platforms) --------
|
|
64
|
+
|
|
65
|
+
// Row of [prev, numbers, next] for the numbered default (`gap-1`).
|
|
66
|
+
export const numberedRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 4 };
|
|
67
|
+
|
|
68
|
+
// Compact row: Prev/Next bracketing the "Page X of N" label (`gap-2`).
|
|
69
|
+
export const compactRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 8 };
|
|
70
|
+
|
|
71
|
+
// With-size outer row: the selector group, the indicator, and the controls (`gap-4`).
|
|
72
|
+
export const withSizeRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 16 };
|
|
73
|
+
|
|
74
|
+
// The "Rows per page" label + selector cluster (`gap-2`).
|
|
75
|
+
export const selectorCluster: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 8 };
|
|
76
|
+
|
|
77
|
+
// The Prev/Next pair inside the with-size row (`gap-1`).
|
|
78
|
+
export const controlPair: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 4 };
|
|
79
|
+
|
|
80
|
+
// The centered-row base every cell shares (the skin layers fill/border/radius on top).
|
|
81
|
+
const CELL_ROW: ViewStyle = { flexDirection: "row", alignItems: "center", justifyContent: "center" };
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Web: the established Canvas look (lifted verbatim from the original file).
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
export const webSkin: PaginationSkin = {
|
|
88
|
+
// A Prev/Next chevron control: a square bordered box on the background fill
|
|
89
|
+
// (`rounded-md border border-input bg-background`).
|
|
90
|
+
controlBox(t) {
|
|
91
|
+
return {
|
|
92
|
+
...CELL_ROW,
|
|
93
|
+
borderRadius: 6,
|
|
94
|
+
borderWidth: 1,
|
|
95
|
+
borderColor: t.input,
|
|
96
|
+
backgroundColor: t.background,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
// A numbered page button. Selected uses the primary fill/border; the rest match
|
|
100
|
+
// the bordered background box (`border-primary bg-primary` vs `border-input bg-background`).
|
|
101
|
+
pageBox(t, selected) {
|
|
102
|
+
return {
|
|
103
|
+
...CELL_ROW,
|
|
104
|
+
borderRadius: 6,
|
|
105
|
+
borderWidth: 1,
|
|
106
|
+
borderColor: selected ? t.primary : t.input,
|
|
107
|
+
backgroundColor: selected ? t.primary : t.background,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
// The size selector trigger: value + caret pushed apart in a bordered box
|
|
111
|
+
// (`flex-row items-center justify-between gap-1 rounded-md border border-input bg-background`).
|
|
112
|
+
selectorBox(t) {
|
|
113
|
+
return {
|
|
114
|
+
flexDirection: "row",
|
|
115
|
+
alignItems: "center",
|
|
116
|
+
justifyContent: "space-between",
|
|
117
|
+
gap: 4,
|
|
118
|
+
borderRadius: 6,
|
|
119
|
+
borderWidth: 1,
|
|
120
|
+
borderColor: t.input,
|
|
121
|
+
backgroundColor: t.background,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
// Control glyph / selector value color (`font-medium text-foreground`).
|
|
125
|
+
controlLabel(t) {
|
|
126
|
+
return { fontWeight: "500", color: t.foreground };
|
|
127
|
+
},
|
|
128
|
+
// A numbered page label: medium weight, primary-foreground when selected
|
|
129
|
+
// (`font-medium` + `text-primary-foreground` / `text-foreground`).
|
|
130
|
+
pageLabel(t, selected) {
|
|
131
|
+
return { fontWeight: "500", color: selected ? t["primary-foreground"] : t.foreground };
|
|
132
|
+
},
|
|
133
|
+
// Muted supporting text (`text-muted-foreground`).
|
|
134
|
+
mutedLabel(t) {
|
|
135
|
+
return { color: t["muted-foreground"] };
|
|
136
|
+
},
|
|
137
|
+
// The truncation ellipsis: muted, with a small horizontal inset (`px-1`).
|
|
138
|
+
gapLabel(t) {
|
|
139
|
+
return { paddingHorizontal: 4, color: t["muted-foreground"] };
|
|
140
|
+
},
|
|
141
|
+
pressedOpacity: 0.9,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// iOS (HIG page controls): pill-rounded cells, the active page filled primary.
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
export const iosSkin: PaginationSkin = {
|
|
149
|
+
// Pill-rounded (radius 8), no border; the chevron reads as a plain glyph on the
|
|
150
|
+
// background so the numbered cells carry the shape.
|
|
151
|
+
controlBox(t) {
|
|
152
|
+
return {
|
|
153
|
+
...CELL_ROW,
|
|
154
|
+
borderRadius: 8,
|
|
155
|
+
backgroundColor: t.background,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
// The active page is a filled `primary` pill (radius 8); inactive pages are
|
|
159
|
+
// plain background pills with no border (HIG dots: filled current, hollow rest).
|
|
160
|
+
pageBox(t, selected) {
|
|
161
|
+
return {
|
|
162
|
+
...CELL_ROW,
|
|
163
|
+
borderRadius: 8,
|
|
164
|
+
backgroundColor: selected ? t.primary : t.background,
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
// The rows-per-page selector: a muted-filled pill trigger (radius 8), value +
|
|
168
|
+
// caret pushed apart, no border (iOS controls favor fills over outlines).
|
|
169
|
+
selectorBox(t) {
|
|
170
|
+
return {
|
|
171
|
+
flexDirection: "row",
|
|
172
|
+
alignItems: "center",
|
|
173
|
+
justifyContent: "space-between",
|
|
174
|
+
gap: 4,
|
|
175
|
+
borderRadius: 8,
|
|
176
|
+
backgroundColor: t.muted,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
controlLabel(t) {
|
|
180
|
+
// SF-scale control glyph reads slightly heavier on iOS.
|
|
181
|
+
return { fontWeight: "600", color: t.foreground };
|
|
182
|
+
},
|
|
183
|
+
pageLabel(t, selected) {
|
|
184
|
+
return { fontWeight: "600", color: selected ? t["primary-foreground"] : t.foreground };
|
|
185
|
+
},
|
|
186
|
+
mutedLabel(t) {
|
|
187
|
+
return { color: t["muted-foreground"] };
|
|
188
|
+
},
|
|
189
|
+
gapLabel(t) {
|
|
190
|
+
return { paddingHorizontal: 4, color: t["muted-foreground"] };
|
|
191
|
+
},
|
|
192
|
+
pressedOpacity: 0.8,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// Android (Material 3): flat cells, the active page a tonal primary fill.
|
|
197
|
+
// =============================================================================
|
|
198
|
+
|
|
199
|
+
export const androidSkin: PaginationSkin = {
|
|
200
|
+
// Flat cell (radius 8), no border, transparent over the surface; the ripple is
|
|
201
|
+
// the press feedback.
|
|
202
|
+
controlBox() {
|
|
203
|
+
return {
|
|
204
|
+
...CELL_ROW,
|
|
205
|
+
borderRadius: 8,
|
|
206
|
+
backgroundColor: "transparent",
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
// The active page is a tonal fill (secondaryContainer ≈ alpha(primary, .12)),
|
|
210
|
+
// M3's selected-cell treatment; inactive pages are flat/transparent.
|
|
211
|
+
pageBox(t, selected) {
|
|
212
|
+
return {
|
|
213
|
+
...CELL_ROW,
|
|
214
|
+
borderRadius: 8,
|
|
215
|
+
backgroundColor: selected ? alpha(t.primary, 0.12) : "transparent",
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
// The selector trigger: a flat tonal pill (alpha(primary, .08)) with the value
|
|
219
|
+
// and caret pushed apart; the ripple supplies the press feedback.
|
|
220
|
+
selectorBox(t) {
|
|
221
|
+
return {
|
|
222
|
+
flexDirection: "row",
|
|
223
|
+
alignItems: "center",
|
|
224
|
+
justifyContent: "space-between",
|
|
225
|
+
gap: 4,
|
|
226
|
+
borderRadius: 8,
|
|
227
|
+
backgroundColor: alpha(t.primary, 0.08),
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
controlLabel(t) {
|
|
231
|
+
return { fontWeight: "500", color: t.foreground };
|
|
232
|
+
},
|
|
233
|
+
pageLabel(t, selected) {
|
|
234
|
+
// labelMedium; the active page reads in brand indigo (onSecondaryContainer ≈ primary).
|
|
235
|
+
return { fontWeight: "500", color: selected ? t.primary : t.foreground };
|
|
236
|
+
},
|
|
237
|
+
mutedLabel(t) {
|
|
238
|
+
return { color: t["muted-foreground"] };
|
|
239
|
+
},
|
|
240
|
+
gapLabel(t) {
|
|
241
|
+
return { paddingHorizontal: 4, color: t["muted-foreground"] };
|
|
242
|
+
},
|
|
243
|
+
pressedOpacity: null, // Android uses a ripple instead
|
|
244
|
+
ripple: (t) => ({ color: alpha(t.primary, 0.12), borderless: false }),
|
|
245
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createPagination } from "./pagination.shared.js";
|
|
2
|
+
import { webSkin } from "./pagination.styles.js";
|
|
3
|
+
|
|
4
|
+
// Web Pagination (the base; Metro falls back to it on native, web bundlers resolve it).
|
|
5
|
+
export const Pagination = createPagination(webSkin);
|
|
6
|
+
export type { PaginationProps } from "./pagination.shared.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createPopover } from "./popover.shared.js";
|
|
2
|
+
import { androidSkin } from "./popover.styles.js";
|
|
3
|
+
|
|
4
|
+
// Android (elevated surface) Popover. Material 3 has no native popover, so this
|
|
5
|
+
// uses an elevated menu/dialog-style surface. Metro resolves this file on Android;
|
|
6
|
+
// the docs import it for preview.
|
|
7
|
+
export const Popover = createPopover(androidSkin);
|
|
8
|
+
export type { PopoverProps } from "./popover.shared.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createPopover } from "./popover.shared.js";
|
|
2
|
+
import { iosSkin } from "./popover.styles.js";
|
|
3
|
+
|
|
4
|
+
// iOS (HIG popover) Popover. Metro resolves this file on iOS; the docs import it for preview.
|
|
5
|
+
export const Popover = createPopover(iosSkin);
|
|
6
|
+
export type { PopoverProps } from "./popover.shared.js";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
Floating panel for rich content triggered by a click.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Popover
|
|
9
|
+
trigger="Open popover"
|
|
10
|
+
title="Popover"
|
|
11
|
+
description="Place your rich content, form fields, or secondary actions here."
|
|
12
|
+
actionLabel="Close"
|
|
13
|
+
/>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Do & Don't
|
|
17
|
+
|
|
18
|
+
**Do** — Keep popovers compact: a focused prompt with one input and a clear action.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<View style={{ borderRadius: 6, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.popover, padding: 16, ...shadow("md"), alignSelf: "flex-start", minWidth: 240 }}>
|
|
22
|
+
<Text style={{ marginBottom: 8, fontSize: 14, lineHeight: 20, color: tokens["popover-foreground"] }}>Rename this project?</Text>
|
|
23
|
+
<Input value="Identity Platform" style={{ marginBottom: 8 }} />
|
|
24
|
+
<View style={{ flexDirection: "row", justifyContent: "flex-end", gap: 8 }}>
|
|
25
|
+
<Button outline small>Cancel</Button>
|
|
26
|
+
<Button primary small>Rename</Button>
|
|
27
|
+
</View>
|
|
28
|
+
</View>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Don't** — A full form belongs in a dialog; in a floating popover it is cramped and easy to dismiss by accident.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<View style={{ borderRadius: 6, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.popover, padding: 16, ...shadow("md"), alignSelf: "flex-start", minWidth: 260 }}>
|
|
35
|
+
<Field label="Name" placeholder="Ada Lovelace" style={{ marginBottom: 8 }} />
|
|
36
|
+
<Field label="Email" placeholder="ada@canvas.dev" style={{ marginBottom: 8 }} />
|
|
37
|
+
<View style={{ marginBottom: 8 }}>
|
|
38
|
+
<Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Role</Text>
|
|
39
|
+
<Select value="Engineer" options={["Engineer", "Designer", "Manager"]} />
|
|
40
|
+
</View>
|
|
41
|
+
<Field label="Team" placeholder="Identity Platform" style={{ marginBottom: 8 }} />
|
|
42
|
+
<View style={{ flexDirection: "row", justifyContent: "flex-end", gap: 8 }}>
|
|
43
|
+
<Button outline small>Cancel</Button>
|
|
44
|
+
<Button primary small>Save</Button>
|
|
45
|
+
</View>
|
|
46
|
+
</View>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Triggered
|
|
50
|
+
|
|
51
|
+
**Do** — Wrap the trigger in a relative anchor and dismiss on outside click so the panel positions and closes predictably.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<Popover trigger="Open popover" open description="Anchored to the trigger, closes on outside click." actionLabel="Close" />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Don't** — A trigger with no relative anchor and no way to dismiss leaves the panel floating loose and stuck open.
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
<View style={{ alignSelf: "flex-start" }}>
|
|
61
|
+
<Button outline small>Open popover</Button>
|
|
62
|
+
<View style={{ borderRadius: 6, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.popover, padding: 16, ...shadow("md"), marginTop: 8, minWidth: 240 }}>
|
|
63
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["popover-foreground"] }}>No anchor, no dismiss, no Close.</Text>
|
|
64
|
+
</View>
|
|
65
|
+
</View>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Inline
|
|
69
|
+
|
|
70
|
+
**Do** — Reserve the static panel for a brief always-on message with a single follow-up action.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<Popover inline description="Saved to drafts. Publish when ready." actionLabel="Publish" />
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Don't** — An always-visible panel that scrolls internally is doing a card's or section's job; use the panel chrome only for short content.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
<View style={{ borderRadius: 6, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.popover, padding: 16, ...shadow("md"), maxHeight: 120, minWidth: 260, overflow: "hidden" }}>
|
|
80
|
+
<ScrollView style={{ maxHeight: 88 }}>
|
|
81
|
+
<Field label="Street" placeholder="100 Market St" style={{ marginBottom: 8 }} />
|
|
82
|
+
<Field label="City" placeholder="San Francisco" style={{ marginBottom: 8 }} />
|
|
83
|
+
<Field label="Region" placeholder="California" style={{ marginBottom: 8 }} />
|
|
84
|
+
<Field label="Postal code" placeholder="94105" style={{ marginBottom: 8 }} />
|
|
85
|
+
</ScrollView>
|
|
86
|
+
</View>
|
|
87
|
+
```
|