@olympusoss/canvas 3.2.1 → 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 +75 -65
- package/package.json +11 -5
- 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/styles/canvas.css +127 -74
- package/tsconfig.json +4 -2
- package/src/cn.ts +0 -3
- package/styles/atoms/avatar.css +0 -22
- package/styles/atoms/badge.css +0 -83
- package/styles/atoms/breadcrumb.css +0 -35
- package/styles/atoms/button-group.css +0 -23
- package/styles/atoms/button.css +0 -107
- package/styles/atoms/checkbox.css +0 -55
- package/styles/atoms/combobox.css +0 -76
- package/styles/atoms/dropdown.css +0 -54
- package/styles/atoms/icon.css +0 -8
- package/styles/atoms/input-group.css +0 -45
- package/styles/atoms/input.css +0 -56
- package/styles/atoms/kbd.css +0 -15
- package/styles/atoms/pagination.css +0 -48
- package/styles/atoms/popover.css +0 -14
- package/styles/atoms/radio.css +0 -28
- package/styles/atoms/select.css +0 -57
- package/styles/atoms/separator.css +0 -32
- package/styles/atoms/skeleton.css +0 -32
- package/styles/atoms/spinner.css +0 -26
- package/styles/atoms/switch.css +0 -45
- package/styles/atoms/textarea.css +0 -31
- package/styles/atoms/tooltip.css +0 -53
- package/styles/atoms/typography.css +0 -105
- package/styles/base.css +0 -17
- package/styles/molecules/alert.css +0 -66
- package/styles/molecules/card.css +0 -58
- package/styles/molecules/code-block.css +0 -18
- package/styles/molecules/empty-state.css +0 -17
- package/styles/molecules/field.css +0 -27
- package/styles/molecules/form.css +0 -27
- package/styles/molecules/page-header.css +0 -52
- package/styles/molecules/section-card.css +0 -49
- package/styles/molecules/stat-card.css +0 -71
- package/styles/molecules/toast.css +0 -95
- package/styles/organisms/app-shell.css +0 -46
- package/styles/organisms/calendar.css +0 -73
- package/styles/organisms/command.css +0 -95
- package/styles/organisms/data-table.css +0 -142
- package/styles/organisms/dialog.css +0 -72
- package/styles/organisms/filter-panel.css +0 -58
- package/styles/organisms/row-menu.css +0 -69
- package/styles/organisms/sheet.css +0 -70
- package/styles/organisms/sidebar.css +0 -146
- package/styles/organisms/stepper.css +0 -63
- package/styles/organisms/tabs.css +0 -40
- package/styles/organisms/topbar.css +0 -24
- package/styles/patterns/backdrops.css +0 -35
- package/styles/patterns/density.css +0 -66
- package/styles/patterns/focus.css +0 -22
- 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 -108
- 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
- package/styles/utilities/display.css +0 -66
- package/styles/utilities/flexbox.css +0 -240
- package/styles/utilities/gap.css +0 -288
- package/styles/utilities/grid.css +0 -138
- package/styles/utilities/position.css +0 -78
- package/styles/utilities/sizing.css +0 -138
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Skeletons
|
|
2
|
+
|
|
3
|
+
Placeholders for loading content.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Skeleton text animate style={{ width: "60%" }} />
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Variants
|
|
12
|
+
|
|
13
|
+
### Shape - avatar
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Skeleton avatar animate />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Shape - button
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<Skeleton button animate style={{ width: "60%" }} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Shape - card
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<Skeleton card animate />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Shape - list
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<Skeleton list animate />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Shape - table
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<Skeleton table animate />
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Do & Don't
|
|
44
|
+
|
|
45
|
+
### text
|
|
46
|
+
|
|
47
|
+
**Do** — Vary the line widths and shorten the last line so it reads like real wrapped text.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<View style={{ width: 320, flexDirection: "column", gap: 6 }}>
|
|
51
|
+
<Skeleton text animate style={{ width: "100%" }} />
|
|
52
|
+
<Skeleton text animate style={{ width: "95%" }} />
|
|
53
|
+
<Skeleton text animate style={{ width: "60%" }} />
|
|
54
|
+
</View>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Don't** — Three full-width lines read as a solid block, not as a paragraph of prose.
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
<View style={{ width: 320, flexDirection: "column", gap: 6 }}>
|
|
61
|
+
<Skeleton text animate style={{ width: "100%" }} />
|
|
62
|
+
<Skeleton text animate style={{ width: "100%" }} />
|
|
63
|
+
<Skeleton text animate style={{ width: "100%" }} />
|
|
64
|
+
</View>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### avatar
|
|
68
|
+
|
|
69
|
+
**Do** — Match the avatar's circle exactly so the photo drops in with no shift.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<Skeleton avatar animate />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Don't** — A square placeholder for a round avatar snaps shape the instant the image loads.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<View style={{ backgroundColor: tokens.muted, borderRadius: 6, width: 40, height: 40 }} />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### button
|
|
82
|
+
|
|
83
|
+
**Do** — Size the placeholder to the button's real height and width (h-9, content-fit).
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<Skeleton button animate />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Don't** — An oversized bar overstates a button and the layout jumps when the real control mounts.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<View style={{ backgroundColor: tokens.muted, width: 320, height: 72, borderRadius: 6 }} />
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### card
|
|
96
|
+
|
|
97
|
+
**Do** — Mirror the real layout (avatar circle, text lines) so the swap is seamless.
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<Skeleton card animate />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Don't** — A generic block that ignores the content's shape causes a jarring shift when it loads.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<View style={{ backgroundColor: tokens.muted, borderRadius: 6, width: 320, height: 88 }} />
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### list
|
|
110
|
+
|
|
111
|
+
**Do** — Repeat a per-row placeholder so the avatar-and-text rhythm matches the loaded list.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<Skeleton list animate />
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Don't** — One tall block hides the row rhythm, so the list reflows when each item appears.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
<View style={{ backgroundColor: tokens.muted, width: 400, height: 120, borderRadius: 6 }} />
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### table
|
|
124
|
+
|
|
125
|
+
**Do** — Lay placeholders out on the real column grid so each cell stays put when it fills in.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
<Skeleton table animate />
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Don't** — A single rectangle gives no column structure; cells shift sideways once data lands.
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
<View style={{ backgroundColor: tokens.muted, width: 400, height: 120, borderRadius: 6 }} />
|
|
135
|
+
```
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type ViewStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens } from "../../style/index.js";
|
|
3
|
+
|
|
4
|
+
// Co-located Skeleton styles. Every placeholder shares one muted fill (a color,
|
|
5
|
+
// so it reads the active token); the rest is layout-only (line heights, the
|
|
6
|
+
// avatar/button footprints, and the composite card/list/table scaffolds). The
|
|
7
|
+
// component resolves its shape and size axes and composes these fragments.
|
|
8
|
+
|
|
9
|
+
export type Shape = "text" | "avatar" | "button" | "card" | "list" | "table";
|
|
10
|
+
|
|
11
|
+
// The muted fill every placeholder shares (was `bg-muted`).
|
|
12
|
+
export function fill(tokens: ColorTokens): ViewStyle {
|
|
13
|
+
return { backgroundColor: tokens.muted };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// --- line (the building block for text and composite shapes) ----------------
|
|
17
|
+
|
|
18
|
+
// A single muted line base: full width, the default line height (`h-3.5`), the
|
|
19
|
+
// small radius (`rounded`). Was `h-3.5 rounded w-full`.
|
|
20
|
+
export const lineBase: ViewStyle = { height: 14, borderRadius: 4, width: "100%" };
|
|
21
|
+
|
|
22
|
+
// --- text shape: line height per size --------------------------------------
|
|
23
|
+
|
|
24
|
+
// Line height per size; the default line reads like a single row of text.
|
|
25
|
+
// `h-4` -> 16, `h-3` -> 12, `h-3.5` -> 14.
|
|
26
|
+
export function lineHeight(p: { small?: boolean; large?: boolean }): ViewStyle {
|
|
27
|
+
if (p.large) return { height: 16 };
|
|
28
|
+
if (p.small) return { height: 12 };
|
|
29
|
+
return { height: 14 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The text-line radius (`rounded`) plus its full-width default (`w-full`), which
|
|
33
|
+
// the caller's `style` escape hatch can override (e.g. width: "60%").
|
|
34
|
+
export const textBase: ViewStyle = { borderRadius: 4, width: "100%" };
|
|
35
|
+
|
|
36
|
+
// --- avatar shape: diameter per size ----------------------------------------
|
|
37
|
+
|
|
38
|
+
// Avatar diameter per size + the full radius (`rounded-full`).
|
|
39
|
+
// `w-12 h-12` -> 48, `w-8 h-8` -> 32, `w-10 h-10` -> 40.
|
|
40
|
+
export function avatarSize(p: { small?: boolean; large?: boolean }): ViewStyle {
|
|
41
|
+
const d = p.large ? 48 : p.small ? 32 : 40;
|
|
42
|
+
return { width: d, height: d, borderRadius: 9999 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- button shape: footprint per size ---------------------------------------
|
|
46
|
+
|
|
47
|
+
// Button placeholder footprint per size + the medium radius (`rounded-md`);
|
|
48
|
+
// mirrors the real control's height. `h-12 w-32` -> {48,128},
|
|
49
|
+
// `h-8 w-20` -> {32,80}, `h-9 w-28` -> {36,112}.
|
|
50
|
+
export function buttonSize(p: { small?: boolean; large?: boolean }): ViewStyle {
|
|
51
|
+
if (p.large) return { height: 48, width: 128, borderRadius: 6 };
|
|
52
|
+
if (p.small) return { height: 32, width: 80, borderRadius: 6 };
|
|
53
|
+
return { height: 36, width: 112, borderRadius: 6 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- card shape -------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
// The card surface: rounded-lg border on the card fill, capped width, padded.
|
|
59
|
+
// `rounded-lg border border-border bg-card max-w-[320px] p-4`.
|
|
60
|
+
export function cardSurface(tokens: ColorTokens): ViewStyle {
|
|
61
|
+
return {
|
|
62
|
+
borderRadius: 8,
|
|
63
|
+
borderWidth: 1,
|
|
64
|
+
borderColor: tokens.border,
|
|
65
|
+
backgroundColor: tokens.card,
|
|
66
|
+
maxWidth: 320,
|
|
67
|
+
padding: 16,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The card's identity row (avatar + two lines). `flex-row items-center gap-3 mb-4`.
|
|
72
|
+
export const cardRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 12, marginBottom: 16 };
|
|
73
|
+
|
|
74
|
+
// The card avatar pulse. `shrink-0 rounded-full w-10 h-10`.
|
|
75
|
+
export const cardAvatar: ViewStyle = { flexShrink: 0, borderRadius: 9999, width: 40, height: 40 };
|
|
76
|
+
|
|
77
|
+
// The flex column holding the avatar's two lines. `flex-1`.
|
|
78
|
+
export const flexFill: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
|
|
79
|
+
|
|
80
|
+
// Line widths inside the card (`w-[70%]` / `w-[40%]` / `w-[80%]`).
|
|
81
|
+
export const cardLine70: ViewStyle = { width: "70%" };
|
|
82
|
+
export const cardLine40: ViewStyle = { width: "40%", marginTop: 6 };
|
|
83
|
+
export const cardLine80: ViewStyle = { width: "80%", marginTop: 6 };
|
|
84
|
+
|
|
85
|
+
// --- list shape -------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
// The list container. `flex-col gap-4 max-w-[400px]`.
|
|
88
|
+
export const listContainer: ViewStyle = { flexDirection: "column", gap: 16, maxWidth: 400 };
|
|
89
|
+
|
|
90
|
+
// A list row. `flex-row items-center gap-3`.
|
|
91
|
+
export const listRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 12 };
|
|
92
|
+
|
|
93
|
+
// The list row's avatar pulse. `rounded-full w-8 h-8`.
|
|
94
|
+
export const listAvatar: ViewStyle = { borderRadius: 9999, width: 32, height: 32 };
|
|
95
|
+
|
|
96
|
+
// The list row's primary line spacing. `mb-1.5`.
|
|
97
|
+
export const listLineGap: ViewStyle = { marginBottom: 6 };
|
|
98
|
+
|
|
99
|
+
// The list row's trailing meta line. `w-10`.
|
|
100
|
+
export const w10: ViewStyle = { width: 40 };
|
|
101
|
+
|
|
102
|
+
// --- table shape ------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
// The table container. `max-w-[560px]`.
|
|
105
|
+
export const tableContainer: ViewStyle = { maxWidth: 560 };
|
|
106
|
+
|
|
107
|
+
// A table row. `flex-row items-center gap-3 py-3`.
|
|
108
|
+
export const tableRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 12, paddingVertical: 12 };
|
|
109
|
+
|
|
110
|
+
// The hairline divider between table rows (omitted on the last row).
|
|
111
|
+
// `border-b border-border`.
|
|
112
|
+
export function tableDivider(tokens: ColorTokens): ViewStyle {
|
|
113
|
+
return { borderBottomWidth: 1, borderColor: tokens.border };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The table row's trailing cell line. `w-20`.
|
|
117
|
+
export const w20: ViewStyle = { width: 80 };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { Animated } from "react-native";
|
|
3
|
+
import { View, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
4
|
+
import * as s from "./skeleton.styles.js";
|
|
5
|
+
|
|
6
|
+
// Skeleton: muted placeholder blocks shown while content loads. A single shape
|
|
7
|
+
// (text line, avatar, button, or a composite card/list/table scaffold) built
|
|
8
|
+
// from one muted fill, optionally pulsing. The size axis scales the line height
|
|
9
|
+
// and the avatar/button footprint.
|
|
10
|
+
//
|
|
11
|
+
// Boolean-prop API: one boolean per option, grouped by axis, first-match
|
|
12
|
+
// precedence within an axis (mirrors Button's intentOf).
|
|
13
|
+
|
|
14
|
+
export interface SkeletonProps {
|
|
15
|
+
// Shape (pick one; default is a single text line).
|
|
16
|
+
text?: boolean;
|
|
17
|
+
avatar?: boolean;
|
|
18
|
+
button?: boolean;
|
|
19
|
+
card?: boolean;
|
|
20
|
+
list?: boolean;
|
|
21
|
+
table?: boolean;
|
|
22
|
+
// Size (pick one). Scales the line height and the avatar/button footprint.
|
|
23
|
+
small?: boolean;
|
|
24
|
+
large?: boolean;
|
|
25
|
+
/** Subtle opacity pulse while content loads. */
|
|
26
|
+
animate?: boolean;
|
|
27
|
+
/** Escape hatch for layout/positioning composition (mainly sizing, e.g. width). */
|
|
28
|
+
style?: StyleProp<ViewStyle>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Shape = s.Shape;
|
|
32
|
+
|
|
33
|
+
// Shape precedence when more than one is passed: first match wins.
|
|
34
|
+
function shapeOf(p: SkeletonProps): Shape {
|
|
35
|
+
if (p.text) return "text";
|
|
36
|
+
if (p.avatar) return "avatar";
|
|
37
|
+
if (p.button) return "button";
|
|
38
|
+
if (p.card) return "card";
|
|
39
|
+
if (p.list) return "list";
|
|
40
|
+
if (p.table) return "table";
|
|
41
|
+
return "text";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A pulsing or static muted block. The resolved width/height/fill go on the
|
|
45
|
+
* Animated.View itself so percentage widths resolve against the real parent
|
|
46
|
+
* (a nested View would collapse `w-[60%]` against an auto-width wrapper). */
|
|
47
|
+
function Pulse({ animate, style }: { animate?: boolean; style: StyleProp<ViewStyle> }) {
|
|
48
|
+
const opacity = useRef(new Animated.Value(1)).current;
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!animate) {
|
|
52
|
+
opacity.setValue(1);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const loop = Animated.loop(
|
|
56
|
+
Animated.sequence([
|
|
57
|
+
Animated.timing(opacity, { toValue: 0.5, duration: 600, useNativeDriver: false }),
|
|
58
|
+
Animated.timing(opacity, { toValue: 1, duration: 600, useNativeDriver: false }),
|
|
59
|
+
]),
|
|
60
|
+
);
|
|
61
|
+
loop.start();
|
|
62
|
+
return () => loop.stop();
|
|
63
|
+
}, [animate, opacity]);
|
|
64
|
+
|
|
65
|
+
return <Animated.View style={[style, { opacity: animate ? opacity : 1 }]} />;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A single muted line; the building block for text and the composite shapes.
|
|
69
|
+
// The muted fill + the line base (`h-3.5 rounded w-full`), then any width/margin
|
|
70
|
+
// overrides the caller layers on.
|
|
71
|
+
function Line({ animate, style }: { animate?: boolean; style?: StyleProp<ViewStyle> }) {
|
|
72
|
+
const { tokens } = useTheme();
|
|
73
|
+
return <Pulse animate={animate} style={[s.fill(tokens), s.lineBase, style]} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function Skeleton(props: SkeletonProps) {
|
|
77
|
+
const { animate, style } = props;
|
|
78
|
+
const { tokens } = useTheme();
|
|
79
|
+
const shape = shapeOf(props);
|
|
80
|
+
|
|
81
|
+
if (shape === "avatar") {
|
|
82
|
+
return <Pulse animate={animate} style={[s.fill(tokens), s.avatarSize(props), style]} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (shape === "button") {
|
|
86
|
+
return <Pulse animate={animate} style={[s.fill(tokens), s.buttonSize(props), style]} />;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (shape === "card") {
|
|
90
|
+
return (
|
|
91
|
+
<View style={[s.cardSurface(tokens), style]}>
|
|
92
|
+
<View style={s.cardRow}>
|
|
93
|
+
<Pulse animate={animate} style={[s.fill(tokens), s.cardAvatar]} />
|
|
94
|
+
<View style={s.flexFill}>
|
|
95
|
+
<Line animate={animate} style={s.cardLine70} />
|
|
96
|
+
<Line animate={animate} style={s.cardLine40} />
|
|
97
|
+
</View>
|
|
98
|
+
</View>
|
|
99
|
+
<Line animate={animate} />
|
|
100
|
+
<Line animate={animate} style={s.cardLine80} />
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (shape === "list") {
|
|
106
|
+
const Row = ({ a, b }: { a: StyleProp<ViewStyle>; b: StyleProp<ViewStyle> }) => (
|
|
107
|
+
<View style={s.listRow}>
|
|
108
|
+
<Pulse animate={animate} style={[s.fill(tokens), s.listAvatar]} />
|
|
109
|
+
<View style={s.flexFill}>
|
|
110
|
+
<Line animate={animate} style={[s.listLineGap, a]} />
|
|
111
|
+
<Line animate={animate} style={b} />
|
|
112
|
+
</View>
|
|
113
|
+
<Line animate={animate} style={s.w10} />
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
return (
|
|
117
|
+
<View style={[s.listContainer, style]}>
|
|
118
|
+
<Row a={{ width: "70%" }} b={{ width: "50%" }} />
|
|
119
|
+
<Row a={{ width: "55%" }} b={{ width: "35%" }} />
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (shape === "table") {
|
|
125
|
+
const Row = ({ a, b, last }: { a: StyleProp<ViewStyle>; b: StyleProp<ViewStyle>; last?: boolean }) => (
|
|
126
|
+
<View style={[s.tableRow, !last ? s.tableDivider(tokens) : null]}>
|
|
127
|
+
<Line animate={animate} style={s.w10} />
|
|
128
|
+
<Line animate={animate} style={[s.flexFill, a]} />
|
|
129
|
+
<Line animate={animate} style={[s.flexFill, b]} />
|
|
130
|
+
<Line animate={animate} style={s.w20} />
|
|
131
|
+
</View>
|
|
132
|
+
);
|
|
133
|
+
return (
|
|
134
|
+
<View style={[s.tableContainer, style]}>
|
|
135
|
+
<Row a={{ width: "70%" }} b={{ width: "50%" }} />
|
|
136
|
+
<Row a={{ width: "80%" }} b={{ width: "60%" }} />
|
|
137
|
+
<Row a={{ width: "65%" }} b={{ width: "45%" }} last />
|
|
138
|
+
</View>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Default: a single text line, full width by default; style carries the width
|
|
143
|
+
// override (e.g. width: "60%") and any other layout.
|
|
144
|
+
return <Pulse animate={animate} style={[s.fill(tokens), s.lineHeight(props), s.textBase, style]} />;
|
|
145
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createSpinner } from "./spinner.shared.js";
|
|
2
|
+
import { androidSkin } from "./spinner.styles.js";
|
|
3
|
+
|
|
4
|
+
// Material 3 Spinner: the indeterminate CircularProgressIndicator, a single
|
|
5
|
+
// sweeping arc. Metro resolves this file on Android; the docs import it for preview.
|
|
6
|
+
export const Spinner = createSpinner(androidSkin);
|
|
7
|
+
export type { SpinnerProps } from "./spinner.shared.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createSpinner } from "./spinner.shared.js";
|
|
2
|
+
import { iosSkin } from "./spinner.styles.js";
|
|
3
|
+
|
|
4
|
+
// iOS (HIG) Spinner: the UIActivityIndicatorView ring of fading spokes. Metro
|
|
5
|
+
// resolves this file on iOS; the docs import it for preview.
|
|
6
|
+
export const Spinner = createSpinner(iosSkin);
|
|
7
|
+
export type { SpinnerProps } from "./spinner.shared.js";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Spinner
|
|
2
|
+
|
|
3
|
+
Animated loading spinner in three sizes.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Spinner />
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Variants
|
|
12
|
+
|
|
13
|
+
### Size - sm
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Spinner small />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Size - lg
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<Spinner large />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Do & Don't
|
|
26
|
+
|
|
27
|
+
**Do** — Pair longer waits with a short label so the spinner has context.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
31
|
+
<Spinner small />
|
|
32
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>Loading…</Text>
|
|
33
|
+
</View>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Don't** — A bare spinner with no label leaves users guessing what is happening and for how long.
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<Spinner />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### sm
|
|
43
|
+
|
|
44
|
+
**Do** — Use the small size inline: inside a button or beside a line of text where its scale matches the type.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<Button loading disabled>Saving…</Button>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Don't** — The 4×4 spinner is too small to anchor a full panel; alone in open space it reads as a stray dot.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<View style={{ height: 128, alignItems: "center", justifyContent: "center", borderRadius: 8, borderWidth: 1, borderStyle: "dashed", borderColor: tokens.border }}>
|
|
54
|
+
<Spinner small />
|
|
55
|
+
</View>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### default
|
|
59
|
+
|
|
60
|
+
**Do** — Keep the default square and centered with a label for small content panels and cards.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<View style={{ flexDirection: "column", alignItems: "center", gap: 8, borderRadius: 8, borderWidth: 1, borderColor: tokens.border, padding: 24 }}>
|
|
64
|
+
<Spinner />
|
|
65
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>Loading…</Text>
|
|
66
|
+
</View>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Don't** — Don't stretch it with conflicting w/h utilities; a spinner must stay a perfect circle to spin cleanly.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<View style={{ borderRadius: 6, backgroundColor: tokens.muted, padding: 12 }}>
|
|
73
|
+
<View style={{ width: 48, height: 20, borderRadius: 9999, borderWidth: 2, borderColor: tokens.muted, borderTopColor: tokens.foreground }} />
|
|
74
|
+
</View>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### lg
|
|
78
|
+
|
|
79
|
+
**Do** — Reserve the large size for section- or page-level loading, centered in the empty content area.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<View style={{ height: 160, flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, borderRadius: 8, borderWidth: 1, borderColor: tokens.border }}>
|
|
83
|
+
<Spinner large />
|
|
84
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] }}>Loading dashboard…</Text>
|
|
85
|
+
</View>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Don't** — The 8×8 spinner overflows a small control; cramming the large size into a button breaks its height.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<Pressable style={{ flexDirection: "row", alignItems: "center", justifyContent: "center", height: 32, borderRadius: 6, paddingHorizontal: 12, backgroundColor: tokens.primary, opacity: 0.5 }}>
|
|
92
|
+
<Spinner large />
|
|
93
|
+
</Pressable>
|
|
94
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type ReactElement, useEffect, useRef } from "react";
|
|
2
|
+
import { Animated, Easing } from "react-native";
|
|
3
|
+
import { useTheme, type ColorTokens } from "../../style/index.js";
|
|
4
|
+
import { type Tone, TONE_TOKEN } from "./spinner.styles.js";
|
|
5
|
+
|
|
6
|
+
// Shared Spinner shell. Uses React Native's primitives DIRECTLY (no engine
|
|
7
|
+
// className layer) and reads the active brand tokens via useTheme, so the arc/
|
|
8
|
+
// spoke color follows light/dark and the glass surface. The shared structure
|
|
9
|
+
// (size/tone precedence, the continuous-rotation animation, accessibility) lives
|
|
10
|
+
// here once; a platform file supplies only its rendered shape (a SpinnerSkin) and
|
|
11
|
+
// calls createSpinner. The web skin keeps the current ActivityIndicator look; the
|
|
12
|
+
// iOS skin draws the ring of fading spokes (UIActivityIndicatorView); the Android
|
|
13
|
+
// skin draws the single sweeping arc (M3 CircularProgressIndicator).
|
|
14
|
+
|
|
15
|
+
export interface SpinnerProps {
|
|
16
|
+
// Size (pick one; default sits between small and large).
|
|
17
|
+
small?: boolean;
|
|
18
|
+
large?: boolean;
|
|
19
|
+
// Tone (pick one; default is the foreground arc color).
|
|
20
|
+
primary?: boolean;
|
|
21
|
+
muted?: boolean;
|
|
22
|
+
foreground?: boolean;
|
|
23
|
+
/** Accessible description of what is loading. */
|
|
24
|
+
accessibilityLabel?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Tone precedence when more than one is passed: first match wins.
|
|
28
|
+
function toneOf(p: SpinnerProps): Tone {
|
|
29
|
+
if (p.primary) return "primary";
|
|
30
|
+
if (p.muted) return "muted";
|
|
31
|
+
if (p.foreground) return "foreground";
|
|
32
|
+
return "foreground";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Three distinct diameters (px) so each size axis value renders a different
|
|
36
|
+
// spinner. Precedence within the size axis: large > small > default (first
|
|
37
|
+
// match wins). Kept identical to the original component.
|
|
38
|
+
function sizeOf(p: SpinnerProps): number {
|
|
39
|
+
if (p.large) return 32;
|
|
40
|
+
if (p.small) return 16;
|
|
41
|
+
return 20;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// What a platform skin owns: how it draws the spinner for a given diameter and
|
|
45
|
+
// color. `rotate` is the shared Animated spin value (0..1, mapped to 0..360deg by
|
|
46
|
+
// the skin if it spins the whole shape). The skin returns a ready-to-mount node.
|
|
47
|
+
export interface SpinnerSkin {
|
|
48
|
+
render: (args: {
|
|
49
|
+
size: number;
|
|
50
|
+
color: string;
|
|
51
|
+
rotate: Animated.Value;
|
|
52
|
+
tokens: ColorTokens;
|
|
53
|
+
}) => ReactElement;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build a Spinner component from a platform skin. */
|
|
57
|
+
export function createSpinner(skin: SpinnerSkin) {
|
|
58
|
+
return function Spinner(props: SpinnerProps) {
|
|
59
|
+
const { accessibilityLabel } = props;
|
|
60
|
+
const { tokens } = useTheme();
|
|
61
|
+
const tone = toneOf(props);
|
|
62
|
+
const size = sizeOf(props);
|
|
63
|
+
const color = tokens[TONE_TOKEN[tone]];
|
|
64
|
+
|
|
65
|
+
// One continuous rotation per ~900ms, looping forever. The skins that spin a
|
|
66
|
+
// drawn shape (iOS spokes, Android arc) interpolate this 0..1 value to
|
|
67
|
+
// 0..360deg; the web ActivityIndicator animates itself and ignores it.
|
|
68
|
+
const rotate = useRef(new Animated.Value(0)).current;
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const loop = Animated.loop(
|
|
71
|
+
Animated.timing(rotate, {
|
|
72
|
+
toValue: 1,
|
|
73
|
+
duration: 900,
|
|
74
|
+
easing: Easing.linear,
|
|
75
|
+
useNativeDriver: true,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
loop.start();
|
|
79
|
+
return () => loop.stop();
|
|
80
|
+
}, [rotate]);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Animated.View
|
|
84
|
+
accessibilityRole="progressbar"
|
|
85
|
+
accessibilityLabel={accessibilityLabel ?? "Loading"}
|
|
86
|
+
style={{ width: size, height: size, alignItems: "center", justifyContent: "center" }}
|
|
87
|
+
>
|
|
88
|
+
{skin.render({ size, color, rotate, tokens })}
|
|
89
|
+
</Animated.View>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
}
|