@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,166 @@
|
|
|
1
|
+
# Stats
|
|
2
|
+
|
|
3
|
+
Single value, grouped row, with sparkline, with comparison. Used for dashboards and overview pages.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Stats
|
|
9
|
+
items={[
|
|
10
|
+
{ label: "Active users", value: "71,897", delta: "+12.3% vs. last 30 days" }
|
|
11
|
+
]}
|
|
12
|
+
/>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Variants
|
|
16
|
+
|
|
17
|
+
### Variant - group
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Stats
|
|
21
|
+
items={[
|
|
22
|
+
{ label: "Total users", value: "12,847", delta: "+12.5%" },
|
|
23
|
+
{ label: "Active sessions", value: "1,024", delta: "+3.2%" },
|
|
24
|
+
{ label: "Error rate", value: "0.12%", delta: "+0.03%", down: true }
|
|
25
|
+
]}
|
|
26
|
+
/>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Variant - plain
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<Stats
|
|
33
|
+
plain
|
|
34
|
+
title="Key metrics"
|
|
35
|
+
items={[
|
|
36
|
+
{ label: "Revenue", value: "$48.2k" },
|
|
37
|
+
{ label: "Orders", value: "842" },
|
|
38
|
+
{ label: "Avg. value", value: "$57.24" },
|
|
39
|
+
{ label: "Conversion", value: "3.6%" }
|
|
40
|
+
]}
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Variant - sparkline
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<Stats
|
|
48
|
+
items={[
|
|
49
|
+
{ label: "Requests", value: "24.5k", delta: "+8.2%" },
|
|
50
|
+
{ label: "Latency", value: "142ms", delta: "+12ms", down: true }
|
|
51
|
+
]}
|
|
52
|
+
/>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Do & Don't
|
|
56
|
+
|
|
57
|
+
### Single
|
|
58
|
+
|
|
59
|
+
**Do** — Name the comparison and the period so the delta is unambiguous.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<Stats style={{ maxWidth: 280 }} items={[
|
|
63
|
+
{ label: "Active users", value: "71,897", delta: "+12.3% vs. last 30 days" }
|
|
64
|
+
]} />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Don't** — A bare delta with no baseline leaves the reader asking: up against what, and over what window?
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<Stats style={{ maxWidth: 280 }} items={[
|
|
71
|
+
{ label: "Active users", value: "71,897", delta: "+12.3%" }
|
|
72
|
+
]} />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Group
|
|
76
|
+
|
|
77
|
+
**Do** — Use the auto-fit grid and round headline numbers so cards wrap and stay scannable.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<Stats items={[
|
|
81
|
+
{ label: "Revenue", value: "$48.2k", delta: "+12.5%" },
|
|
82
|
+
{ label: "Orders", value: "842", delta: "+3.2%" },
|
|
83
|
+
{ label: "Conversion", value: "3.6%", delta: "+0.4%" }
|
|
84
|
+
]} />
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Don't** — A fixed flex row of full-precision numbers overflows on narrow viewports and crowds the cards.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<Stats items={[
|
|
91
|
+
{ label: "Revenue", value: "$48,250.00", delta: "+12.5%" },
|
|
92
|
+
{ label: "Orders", value: "842", delta: "+3.2%" },
|
|
93
|
+
{ label: "Conversion", value: "3.6%", delta: "+0.4%" }
|
|
94
|
+
]} />
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Plain (no border)
|
|
98
|
+
|
|
99
|
+
**Do** — On a parent surface drop the border and radius; let the number stacks stand on their own.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<Stats plain items={[
|
|
103
|
+
{ label: "Revenue", value: "$48.2k" },
|
|
104
|
+
{ label: "Orders", value: "842" }
|
|
105
|
+
]} />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Don't** — Bordered cards inside a card surface double the chrome: a box drawn around boxes.
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, ...shadow("sm"), padding: 24 }}>
|
|
112
|
+
<Stats items={[
|
|
113
|
+
{ label: "Revenue", value: "$48.2k" },
|
|
114
|
+
{ label: "Orders", value: "842" }
|
|
115
|
+
]} />
|
|
116
|
+
</View>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### With sparkline
|
|
120
|
+
|
|
121
|
+
**Do** — Pair the sparkline with an explicit delta so the headline reads without decoding the curve.
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, ...shadow("sm"), maxWidth: 220, padding: 20 }}>
|
|
125
|
+
<Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Requests</Text>
|
|
126
|
+
<View style={{ marginTop: 4, flexDirection: "row", alignItems: "baseline", justifyContent: "space-between" }}>
|
|
127
|
+
<Text style={{ fontSize: 24, lineHeight: 32, fontWeight: "600", letterSpacing: -0.4, color: tokens.foreground }}>24.5k</Text>
|
|
128
|
+
<Text style={{ fontFamily: "monospace", fontSize: 11, color: palette["emerald-600"] }}>+8.2%</Text>
|
|
129
|
+
</View>
|
|
130
|
+
<View style={{ marginTop: 12, flexDirection: "row", alignItems: "flex-end", gap: 2, height: 24 }}>
|
|
131
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 4 }} />
|
|
132
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 8 }} />
|
|
133
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 6 }} />
|
|
134
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 12 }} />
|
|
135
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 10 }} />
|
|
136
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 16 }} />
|
|
137
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 14 }} />
|
|
138
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 18 }} />
|
|
139
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 16 }} />
|
|
140
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 20 }} />
|
|
141
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 24 }} />
|
|
142
|
+
</View>
|
|
143
|
+
</View>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Don't** — A trend line with no current delta makes you eyeball the slope to guess the direction.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
<View style={{ borderRadius: 8, borderWidth: 1, borderColor: tokens.border, backgroundColor: tokens.card, ...shadow("sm"), maxWidth: 220, padding: 20 }}>
|
|
150
|
+
<Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Requests</Text>
|
|
151
|
+
<Text style={{ marginTop: 4, fontSize: 24, lineHeight: 32, fontWeight: "600", letterSpacing: -0.4, color: tokens.foreground }}>24.5k</Text>
|
|
152
|
+
<View style={{ marginTop: 12, flexDirection: "row", alignItems: "flex-end", gap: 2, height: 24 }}>
|
|
153
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 4 }} />
|
|
154
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 8 }} />
|
|
155
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 6 }} />
|
|
156
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 12 }} />
|
|
157
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 10 }} />
|
|
158
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 16 }} />
|
|
159
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 14 }} />
|
|
160
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 18 }} />
|
|
161
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 16 }} />
|
|
162
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 20 }} />
|
|
163
|
+
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: "0%", borderRadius: 2, backgroundColor: alpha(tokens.primary, 0.7), height: 24 }} />
|
|
164
|
+
</View>
|
|
165
|
+
</View>
|
|
166
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type ViewStyle, type TextStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens, palette, shadow } from "../../style/index.js";
|
|
3
|
+
|
|
4
|
+
// Co-located Stats styles. Layout-only fragments are static objects; anything
|
|
5
|
+
// that reads a color is a function of the active tokens (so the surfaces follow
|
|
6
|
+
// light/dark and read as glass when the ThemeProvider's surface is "glass",
|
|
7
|
+
// since tokens.card is swapped translucent at the theming level). The delta tone
|
|
8
|
+
// uses the Tailwind palette (a 600 hue in light, a 400 hue in dark), branched on
|
|
9
|
+
// the active scheme.
|
|
10
|
+
|
|
11
|
+
export type Surface = "card" | "plain";
|
|
12
|
+
|
|
13
|
+
// The bordered card surface, mirroring the docs `cardCls` plus its padding
|
|
14
|
+
// (rounded-lg border border-border bg-card shadow-sm p-5).
|
|
15
|
+
export function cardSurface(tokens: ColorTokens): ViewStyle {
|
|
16
|
+
return {
|
|
17
|
+
borderRadius: 8,
|
|
18
|
+
borderWidth: 1,
|
|
19
|
+
borderColor: tokens.border,
|
|
20
|
+
backgroundColor: tokens.card,
|
|
21
|
+
padding: 20,
|
|
22
|
+
...shadow("sm"),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The parent surface the plain variant nests on
|
|
27
|
+
// (rounded-lg border border-border bg-card shadow-sm p-6).
|
|
28
|
+
export function plainContainer(tokens: ColorTokens): ViewStyle {
|
|
29
|
+
return {
|
|
30
|
+
borderRadius: 8,
|
|
31
|
+
borderWidth: 1,
|
|
32
|
+
borderColor: tokens.border,
|
|
33
|
+
backgroundColor: tokens.card,
|
|
34
|
+
padding: 24,
|
|
35
|
+
...shadow("sm"),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A wrapping row: items flow left to right and wrap to the next line when they
|
|
40
|
+
// run out of room (flex-row flex-wrap).
|
|
41
|
+
export const row: ViewStyle = { flexDirection: "row", flexWrap: "wrap" };
|
|
42
|
+
|
|
43
|
+
// Row gap by surface: plain stacks sit roomier (gap-6), cards pack tighter
|
|
44
|
+
// (gap-3.5).
|
|
45
|
+
export const rowGap: Record<Surface, ViewStyle> = {
|
|
46
|
+
card: { gap: 14 },
|
|
47
|
+
plain: { gap: 24 },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Per-item minimum width and growth, by surface. Card items hold a wider floor
|
|
51
|
+
// and grow (flex-1 min-w-48); plain stacks pack tighter (min-w-28).
|
|
52
|
+
export const item: Record<Surface, ViewStyle> = {
|
|
53
|
+
card: { flexGrow: 1, flexShrink: 1, flexBasis: "0%", minWidth: 192 },
|
|
54
|
+
plain: { minWidth: 112 },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Title type by surface (card: text-sm mb-3, plain: text-base mb-4).
|
|
58
|
+
export function title(tokens: ColorTokens, surface: Surface): TextStyle {
|
|
59
|
+
return surface === "card"
|
|
60
|
+
? { fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens.foreground, marginBottom: 12 }
|
|
61
|
+
: { fontSize: 16, lineHeight: 24, fontWeight: "600", color: tokens.foreground, marginBottom: 16 };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- one metric: label, value, optional delta -------------------------------
|
|
65
|
+
|
|
66
|
+
// text-sm text-muted-foreground
|
|
67
|
+
export function labelText(tokens: ColorTokens): TextStyle {
|
|
68
|
+
return { fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// mt-1 text-2xl font-semibold tracking-tight text-foreground
|
|
72
|
+
export function valueText(tokens: ColorTokens): TextStyle {
|
|
73
|
+
return {
|
|
74
|
+
marginTop: 4,
|
|
75
|
+
fontSize: 24,
|
|
76
|
+
lineHeight: 32,
|
|
77
|
+
fontWeight: "600",
|
|
78
|
+
letterSpacing: -0.4,
|
|
79
|
+
color: tokens.foreground,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// mt-1 text-xs font-medium, with the tone color applied on top.
|
|
84
|
+
export const deltaBase: TextStyle = { marginTop: 4, fontSize: 12, lineHeight: 16, fontWeight: "500" };
|
|
85
|
+
|
|
86
|
+
// Delta tone: a rise reads green (text-green-600 dark:text-green-400), a decline
|
|
87
|
+
// reads red (text-red-600 dark:text-red-400).
|
|
88
|
+
export function deltaTone(dark: boolean, down: boolean): TextStyle {
|
|
89
|
+
const hue = down ? "red" : "green";
|
|
90
|
+
return { color: dark ? palette[`${hue}-400`] : palette[`${hue}-600`] };
|
|
91
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { View, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
3
|
+
import * as s from "./stats.styles.js";
|
|
4
|
+
import { type Surface } from "./stats.styles.js";
|
|
5
|
+
|
|
6
|
+
// Stats: a row/grid of metric items, each a small label, a large headline value,
|
|
7
|
+
// and an optional delta (e.g. +12.5% in green, -3% in red). Used for dashboard
|
|
8
|
+
// and overview surfaces.
|
|
9
|
+
//
|
|
10
|
+
// Two surfaces, one axis (`plain` vs the default card):
|
|
11
|
+
//
|
|
12
|
+
// 1. Card (default): each metric sits in its own bordered, shadowed card. Good
|
|
13
|
+
// for a standalone metric or a free-standing group on a bare page.
|
|
14
|
+
// 2. Plain (`plain`): the metric stacks sit borderless on a shared parent
|
|
15
|
+
// surface, so you don't draw boxes inside a box. Pair with a parent card.
|
|
16
|
+
//
|
|
17
|
+
// Layout is responsive by default: items lay out in a wrapping row, each holding
|
|
18
|
+
// a sensible minimum width, so a group reflows from a single row on desktop down
|
|
19
|
+
// to a stack on a phone without any per-breakpoint props.
|
|
20
|
+
//
|
|
21
|
+
// Boolean-prop API: one boolean per option, first-match precedence within an
|
|
22
|
+
// axis (mirrors Button's intentOf). `items` carries the plain data; each item's
|
|
23
|
+
// `down` flag colors its delta red instead of the default green.
|
|
24
|
+
|
|
25
|
+
export interface StatItem {
|
|
26
|
+
/** Small caption above the value (e.g. "Total users"). */
|
|
27
|
+
label: string;
|
|
28
|
+
/** The headline figure, pre-formatted (e.g. "12,847", "$48.2k", "3.6%"). */
|
|
29
|
+
value: string;
|
|
30
|
+
/** Optional change indicator (e.g. "+12.5%"). Omit to hide. */
|
|
31
|
+
delta?: string;
|
|
32
|
+
/** Color the delta red (a decline) instead of the default green (a rise). */
|
|
33
|
+
down?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StatsProps {
|
|
37
|
+
/** The metrics to render, in order. */
|
|
38
|
+
items: StatItem[];
|
|
39
|
+
// Surface (pick one; default is bordered cards).
|
|
40
|
+
/** Borderless metric stacks on a shared surface, for nesting inside a card. */
|
|
41
|
+
plain?: boolean;
|
|
42
|
+
/** Optional heading shown above the metrics (mainly for the plain surface). */
|
|
43
|
+
title?: string;
|
|
44
|
+
/** Escape hatch for layout/positioning composition (mainly width). */
|
|
45
|
+
style?: StyleProp<ViewStyle>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Surface precedence when more than one is passed: first match wins.
|
|
49
|
+
function surfaceOf(p: StatsProps): Surface {
|
|
50
|
+
if (p.plain) return "plain";
|
|
51
|
+
return "card";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// One metric: label, value, optional delta.
|
|
55
|
+
function StatItemView({ item, surface }: { item: StatItem; surface: Surface }): ReactNode {
|
|
56
|
+
const { tokens, dark } = useTheme();
|
|
57
|
+
return (
|
|
58
|
+
<View style={[surface === "card" ? s.cardSurface(tokens) : null, s.item[surface]]}>
|
|
59
|
+
<Text style={s.labelText(tokens)}>{item.label}</Text>
|
|
60
|
+
<Text style={s.valueText(tokens)}>{item.value}</Text>
|
|
61
|
+
{item.delta != null && item.delta !== "" ? (
|
|
62
|
+
<Text style={[s.deltaBase, s.deltaTone(dark, !!item.down)]}>{item.delta}</Text>
|
|
63
|
+
) : null}
|
|
64
|
+
</View>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function Stats(props: StatsProps) {
|
|
69
|
+
const { items, title, style } = props;
|
|
70
|
+
const { tokens } = useTheme();
|
|
71
|
+
const surface = surfaceOf(props);
|
|
72
|
+
|
|
73
|
+
// The card surface lays cards directly in a wrapping, gapped row. The plain
|
|
74
|
+
// surface wraps the stacks in a shared parent card so the borderless metrics
|
|
75
|
+
// have something to sit on.
|
|
76
|
+
const isPlain = surface === "plain";
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<View style={[isPlain ? s.plainContainer(tokens) : null, style]}>
|
|
80
|
+
{title != null && title !== "" ? <Text style={s.title(tokens, surface)}>{title}</Text> : null}
|
|
81
|
+
<View style={[s.row, s.rowGap[surface]]}>
|
|
82
|
+
{items.map((item, i) => (
|
|
83
|
+
<StatItemView key={i} item={item} surface={surface} />
|
|
84
|
+
))}
|
|
85
|
+
</View>
|
|
86
|
+
</View>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createCalendar } from "./calendar.shared.js";
|
|
2
|
+
import { androidSkin } from "./calendar.styles.js";
|
|
3
|
+
|
|
4
|
+
// Material 3 date picker Calendar. Metro resolves this file on Android; the docs import it for preview.
|
|
5
|
+
export const Calendar = createCalendar(androidSkin);
|
|
6
|
+
export type { CalendarProps } from "./calendar.shared.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createCalendar } from "./calendar.shared.js";
|
|
2
|
+
import { iosSkin } from "./calendar.styles.js";
|
|
3
|
+
|
|
4
|
+
// iOS (HIG date picker) Calendar. Metro resolves this file on iOS; the docs import it for preview.
|
|
5
|
+
export const Calendar = createCalendar(iosSkin);
|
|
6
|
+
export type { CalendarProps } from "./calendar.shared.js";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Calendars
|
|
2
|
+
|
|
3
|
+
Date picker, event list. Production: wrap react-day-picker.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Calendar
|
|
9
|
+
month="May 2026"
|
|
10
|
+
today={23}
|
|
11
|
+
selected={24}
|
|
12
|
+
daysInMonth={31}
|
|
13
|
+
startWeekday={4}
|
|
14
|
+
/>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Variants
|
|
18
|
+
|
|
19
|
+
### Variant - events
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", gap: 24 }}>
|
|
23
|
+
<Calendar month="May 2026" today={23} selected={24} daysInMonth={31} startWeekday={4} />
|
|
24
|
+
<Card style={{ minWidth: 240, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
|
|
25
|
+
<View style={{ borderBottomWidth: 1, borderColor: tokens.border, paddingHorizontal: 20, paddingVertical: 12 }}>
|
|
26
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens["card-foreground"] }}>May 24</Text>
|
|
27
|
+
</View>
|
|
28
|
+
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 10, borderBottomWidth: 1, borderColor: tokens.border }}>
|
|
29
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Sprint planning</Text>
|
|
30
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>9:00 AM</Text>
|
|
31
|
+
</View>
|
|
32
|
+
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 10, borderBottomWidth: 1, borderColor: tokens.border }}>
|
|
33
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Design review</Text>
|
|
34
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>11:30 AM</Text>
|
|
35
|
+
</View>
|
|
36
|
+
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 10 }}>
|
|
37
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>1:1 with manager</Text>
|
|
38
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>2:00 PM</Text>
|
|
39
|
+
</View>
|
|
40
|
+
</Card>
|
|
41
|
+
</View>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Do & Don't
|
|
45
|
+
|
|
46
|
+
### Single date
|
|
47
|
+
|
|
48
|
+
**Do** — Exactly one selected day (primary), with today marked separately in the accent tone.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<View style={{ width: "auto", borderRadius: 8, borderWidth: 1, borderColor: tokens.border, padding: 12 }}>
|
|
52
|
+
<View style={{ flexDirection: "row", gap: 2 }}>
|
|
53
|
+
<Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6 }}>
|
|
54
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens.foreground }}>8</Text>
|
|
55
|
+
</Pressable>
|
|
56
|
+
<Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.accent }}>
|
|
57
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens["accent-foreground"] }}>23</Text>
|
|
58
|
+
</Pressable>
|
|
59
|
+
<Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.primary }}>
|
|
60
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>24</Text>
|
|
61
|
+
</Pressable>
|
|
62
|
+
</View>
|
|
63
|
+
</View>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Don't** — Painting several days with the primary selected style makes a single-date picker look like a multi-select.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<View style={{ width: "auto", borderRadius: 8, borderWidth: 1, borderColor: tokens.border, padding: 12 }}>
|
|
70
|
+
<View style={{ flexDirection: "row", gap: 2 }}>
|
|
71
|
+
<Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.primary }}>
|
|
72
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>8</Text>
|
|
73
|
+
</Pressable>
|
|
74
|
+
<Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.primary }}>
|
|
75
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>14</Text>
|
|
76
|
+
</Pressable>
|
|
77
|
+
<Pressable style={{ height: 36, width: 36, alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.primary }}>
|
|
78
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>23</Text>
|
|
79
|
+
</Pressable>
|
|
80
|
+
</View>
|
|
81
|
+
</View>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### With event list
|
|
85
|
+
|
|
86
|
+
**Do** — Keep the panel header and rows in sync with the selected day so the two views always agree.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", gap: 24 }}>
|
|
90
|
+
<Calendar month="May 2026" today={23} selected={24} daysInMonth={31} startWeekday={4} />
|
|
91
|
+
<Card style={{ minWidth: 240, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
|
|
92
|
+
<View style={{ borderBottomWidth: 1, borderColor: tokens.border, paddingHorizontal: 20, paddingVertical: 12 }}>
|
|
93
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "600", color: tokens["card-foreground"] }}>May 24</Text>
|
|
94
|
+
</View>
|
|
95
|
+
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 10 }}>
|
|
96
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Sprint planning</Text>
|
|
97
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>9:00 AM</Text>
|
|
98
|
+
</View>
|
|
99
|
+
</Card>
|
|
100
|
+
</View>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Don't** — Selecting May 24 but leaving the panel on a placeholder breaks the link between the grid and its day.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", gap: 24 }}>
|
|
107
|
+
<Calendar month="May 2026" today={23} selected={24} daysInMonth={31} startWeekday={4} />
|
|
108
|
+
<Card style={{ minWidth: 240, flexGrow: 1, flexShrink: 1, flexBasis: "0%" }}>
|
|
109
|
+
<View style={{ paddingHorizontal: 16, paddingVertical: 12 }}>
|
|
110
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>Pick a date to see events.</Text>
|
|
111
|
+
</View>
|
|
112
|
+
</Card>
|
|
113
|
+
</View>
|
|
114
|
+
```
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type GestureResponderEvent } from "react-native";
|
|
2
|
+
import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
3
|
+
import { type CalendarSkin, type Density } from "./calendar.styles.js";
|
|
4
|
+
|
|
5
|
+
// Shared Calendar shell. The structure (header with prev/next chevrons + month
|
|
6
|
+
// label, a weekday label row, and a 6x7 grid of day cells), the density
|
|
7
|
+
// precedence, the leading-blank padding, the accessibility, and the press
|
|
8
|
+
// handlers live here once; a platform file supplies only its skin (the native
|
|
9
|
+
// container/header/cell shape, weekday-label style, selected/today day treatment,
|
|
10
|
+
// and press feedback) and calls createCalendar.
|
|
11
|
+
//
|
|
12
|
+
// A month calendar: a header (month/year label flanked by prev/next chevrons),
|
|
13
|
+
// a weekday label row, and a 6x7 grid of day cells. Today and the selected day
|
|
14
|
+
// are highlighted; leading blanks pad the first row to the correct weekday.
|
|
15
|
+
//
|
|
16
|
+
// There is no CSS grid, so the day grid is a `flex-row flex-wrap` of fixed-width
|
|
17
|
+
// cells. Seven cells per row times the cell width gives the grid a fixed width,
|
|
18
|
+
// set explicitly so wrapping lands exactly seven-per-row (width supplied by the
|
|
19
|
+
// skin's per-density metrics).
|
|
20
|
+
|
|
21
|
+
export interface CalendarProps {
|
|
22
|
+
/** Month + year label shown in the header, e.g. "June 2026". */
|
|
23
|
+
month?: string;
|
|
24
|
+
/** The day number currently selected (primary highlight). */
|
|
25
|
+
selected?: number;
|
|
26
|
+
/** The day number that is today (primary highlight when unselected). */
|
|
27
|
+
today?: number;
|
|
28
|
+
/** Number of days in the month. */
|
|
29
|
+
daysInMonth?: number;
|
|
30
|
+
/** Weekday (0=Sun .. 6=Sat) the 1st falls on; sets leading blank cells. */
|
|
31
|
+
startWeekday?: number;
|
|
32
|
+
/** Fired with the day number when a day cell is pressed. */
|
|
33
|
+
onSelect?: (day: number) => void;
|
|
34
|
+
/** Fired when the previous-month chevron is pressed. */
|
|
35
|
+
onPrev?: (event: GestureResponderEvent) => void;
|
|
36
|
+
/** Fired when the next-month chevron is pressed. */
|
|
37
|
+
onNext?: (event: GestureResponderEvent) => void;
|
|
38
|
+
|
|
39
|
+
// Density (pick one; default is the comfortable cell).
|
|
40
|
+
/** Tighter cells and smaller type, for dense surfaces. */
|
|
41
|
+
compact?: boolean;
|
|
42
|
+
|
|
43
|
+
/** Escape hatch for layout/positioning composition (width, margins). */
|
|
44
|
+
style?: StyleProp<ViewStyle>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Density precedence: `compact` wins, otherwise the default cell.
|
|
48
|
+
function densityOf(p: CalendarProps): Density {
|
|
49
|
+
if (p.compact) return "compact";
|
|
50
|
+
return "default";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Build a Calendar component from a platform skin. */
|
|
54
|
+
export function createCalendar(skin: CalendarSkin) {
|
|
55
|
+
return function Calendar(props: CalendarProps) {
|
|
56
|
+
const {
|
|
57
|
+
month = "June 2026",
|
|
58
|
+
selected,
|
|
59
|
+
today,
|
|
60
|
+
daysInMonth = 30,
|
|
61
|
+
startWeekday = 0,
|
|
62
|
+
onSelect,
|
|
63
|
+
onPrev,
|
|
64
|
+
onNext,
|
|
65
|
+
style,
|
|
66
|
+
} = props;
|
|
67
|
+
|
|
68
|
+
const { tokens } = useTheme();
|
|
69
|
+
const density = densityOf(props);
|
|
70
|
+
const m = skin.metrics[density];
|
|
71
|
+
const lead = ((startWeekday % 7) + 7) % 7;
|
|
72
|
+
const days = Array.from({ length: daysInMonth }, (_, i) => i + 1);
|
|
73
|
+
const ripple = skin.ripple ? skin.ripple(tokens) : undefined;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<View style={[skin.containerBase, skin.containerSurface(tokens), style]}>
|
|
77
|
+
{/* Header: month label between two ghost chevron buttons. */}
|
|
78
|
+
<View style={skin.header}>
|
|
79
|
+
<Pressable
|
|
80
|
+
style={({ pressed }) => [
|
|
81
|
+
skin.chevron,
|
|
82
|
+
skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
|
|
83
|
+
]}
|
|
84
|
+
android_ripple={ripple ? { ...ripple, borderless: true } : undefined}
|
|
85
|
+
onPress={onPrev}
|
|
86
|
+
accessibilityRole="button"
|
|
87
|
+
accessibilityLabel="Previous month"
|
|
88
|
+
>
|
|
89
|
+
<Text style={skin.chevronText(tokens)}>{"‹"}</Text>
|
|
90
|
+
</Pressable>
|
|
91
|
+
<Text style={skin.monthLabel(tokens)}>{month}</Text>
|
|
92
|
+
<Pressable
|
|
93
|
+
style={({ pressed }) => [
|
|
94
|
+
skin.chevron,
|
|
95
|
+
skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
|
|
96
|
+
]}
|
|
97
|
+
android_ripple={ripple ? { ...ripple, borderless: true } : undefined}
|
|
98
|
+
onPress={onNext}
|
|
99
|
+
accessibilityRole="button"
|
|
100
|
+
accessibilityLabel="Next month"
|
|
101
|
+
>
|
|
102
|
+
<Text style={skin.chevronText(tokens)}>{"›"}</Text>
|
|
103
|
+
</Pressable>
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
{/* Weekday label row. */}
|
|
107
|
+
<View style={[skin.grid, { width: m.gridWidth }]}>
|
|
108
|
+
{skin.weekdays.map((wd, i) => (
|
|
109
|
+
<View key={`wd-${i}`} style={[skin.headCell, m.head]}>
|
|
110
|
+
<Text style={skin.weekdayLabel(tokens)}>{wd}</Text>
|
|
111
|
+
</View>
|
|
112
|
+
))}
|
|
113
|
+
</View>
|
|
114
|
+
|
|
115
|
+
{/* Day grid: leading blanks, then one cell per day. */}
|
|
116
|
+
<View style={[skin.grid, { width: m.gridWidth }]}>
|
|
117
|
+
{Array.from({ length: lead }, (_, i) => (
|
|
118
|
+
<View key={`blank-${i}`} style={[skin.headCell, m.cell]} />
|
|
119
|
+
))}
|
|
120
|
+
{days.map((day) => {
|
|
121
|
+
const isSelected = selected != null && day === selected;
|
|
122
|
+
const isToday = today != null && day === today;
|
|
123
|
+
const state = { selected: isSelected, today: isToday };
|
|
124
|
+
return (
|
|
125
|
+
<Pressable
|
|
126
|
+
key={day}
|
|
127
|
+
style={({ pressed }) => [
|
|
128
|
+
skin.dayCellBase,
|
|
129
|
+
m.cell,
|
|
130
|
+
skin.dayCellState(tokens, state),
|
|
131
|
+
skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
|
|
132
|
+
]}
|
|
133
|
+
android_ripple={ripple}
|
|
134
|
+
onPress={() => onSelect?.(day)}
|
|
135
|
+
accessibilityRole="button"
|
|
136
|
+
accessibilityState={{ selected: isSelected }}
|
|
137
|
+
>
|
|
138
|
+
<Text style={[m.label, skin.dayLabel(tokens, state)]}>{day}</Text>
|
|
139
|
+
</Pressable>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</View>
|
|
143
|
+
</View>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
}
|