@oxyhq/bloom 1.0.0 → 1.0.2
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 +16 -4
- package/docs/accordion.mdx +69 -0
- package/docs/activity-heatmap.mdx +71 -0
- package/docs/admonition.mdx +77 -0
- package/docs/alert-dialog.mdx +75 -0
- package/docs/alert.mdx +93 -0
- package/docs/animated-check.mdx +53 -0
- package/docs/aspect-ratio.mdx +45 -0
- package/docs/avatar-group.mdx +91 -0
- package/docs/avatar.mdx +126 -0
- package/docs/badge.mdx +74 -0
- package/docs/benefit-list.mdx +56 -0
- package/docs/bottom-sheet.mdx +103 -0
- package/docs/button.mdx +218 -0
- package/docs/card.mdx +106 -0
- package/docs/checkbox.mdx +59 -0
- package/docs/chip.mdx +53 -0
- package/docs/code.mdx +50 -0
- package/docs/combobox.mdx +64 -0
- package/docs/command.mdx +85 -0
- package/docs/composition-bar.mdx +63 -0
- package/docs/connection-dots.mdx +50 -0
- package/docs/connection-status.mdx +56 -0
- package/docs/content-panel.mdx +62 -0
- package/docs/context-menu.mdx +65 -0
- package/docs/design-tokens.mdx +254 -0
- package/docs/dialog.mdx +277 -0
- package/docs/divider.mdx +56 -0
- package/docs/dot-grid-meter.mdx +66 -0
- package/docs/dropdown-menu.mdx +161 -0
- package/docs/error-boundary.mdx +72 -0
- package/docs/fab.mdx +67 -0
- package/docs/field.mdx +70 -0
- package/docs/fill.mdx +58 -0
- package/docs/fonts.mdx +144 -0
- package/docs/frosted-icon-button.mdx +60 -0
- package/docs/getting-started.mdx +75 -0
- package/docs/grid.mdx +64 -0
- package/docs/hooks.mdx +81 -0
- package/docs/icon-circle.mdx +66 -0
- package/docs/icons.mdx +69 -0
- package/docs/image-aspect-ratio-cache.mdx +63 -0
- package/docs/image-resolver.mdx +66 -0
- package/docs/index.mdx +44 -0
- package/docs/input-group.mdx +58 -0
- package/docs/item.mdx +69 -0
- package/docs/kbd.mdx +57 -0
- package/docs/label.mdx +50 -0
- package/docs/link-preview.mdx +66 -0
- package/docs/list.mdx +72 -0
- package/docs/loading.mdx +129 -0
- package/docs/media-inset-border.mdx +52 -0
- package/docs/menubar.mdx +86 -0
- package/docs/migrating-to-1.0.mdx +434 -0
- package/docs/motion.mdx +83 -0
- package/docs/overlay.mdx +75 -0
- package/docs/popover.mdx +73 -0
- package/docs/portal.mdx +63 -0
- package/docs/pressable-scale.mdx +51 -0
- package/docs/profile-card.mdx +66 -0
- package/docs/progressive-blur.mdx +54 -0
- package/docs/prompt-input.mdx +183 -0
- package/docs/provider.mdx +64 -0
- package/docs/radio-indicator.mdx +60 -0
- package/docs/radio.mdx +65 -0
- package/docs/scroll.mdx +66 -0
- package/docs/search.mdx +67 -0
- package/docs/segmented-control.mdx +76 -0
- package/docs/select.mdx +93 -0
- package/docs/settings-list.mdx +186 -0
- package/docs/skeleton.mdx +66 -0
- package/docs/slider.mdx +73 -0
- package/docs/stat-bar.mdx +59 -0
- package/docs/stat-widgets.mdx +33 -0
- package/docs/styles.mdx +61 -0
- package/docs/subtle-hover.mdx +53 -0
- package/docs/surfaces.mdx +72 -0
- package/docs/switch.mdx +90 -0
- package/docs/tab-bar.mdx +386 -0
- package/docs/tabs.mdx +75 -0
- package/docs/text-field.mdx +187 -0
- package/docs/theme.mdx +67 -0
- package/docs/toast.mdx +285 -0
- package/docs/tooltip.mdx +67 -0
- package/docs/typography.mdx +88 -0
- package/docs/user-hover-card.mdx +140 -0
- package/docs/zoomable-image-gallery.mdx +66 -0
- package/lib/commonjs/design-tokens/color-roles.js +21 -1
- package/lib/commonjs/design-tokens/color-roles.js.map +1 -1
- package/lib/commonjs/design-tokens/index.js +6 -0
- package/lib/commonjs/design-tokens/index.js.map +1 -1
- package/lib/commonjs/design-tokens/tailwind-preset.js +3 -1
- package/lib/commonjs/design-tokens/tailwind-preset.js.map +1 -1
- package/lib/commonjs/design-tokens/theme-css.js +1 -0
- package/lib/commonjs/design-tokens/theme-css.js.map +1 -1
- package/lib/commonjs/design-tokens/theme.css +3 -0
- package/lib/commonjs/design-tokens/tokens-json.js +13 -4
- package/lib/commonjs/design-tokens/tokens-json.js.map +1 -1
- package/lib/commonjs/design-tokens/tokens.json +6227 -265
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +7 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/theme/ambient-store.js +16 -1
- package/lib/commonjs/theme/ambient-store.js.map +1 -1
- package/lib/commonjs/theme/color-policy.js +30 -33
- package/lib/commonjs/theme/color-policy.js.map +1 -1
- package/lib/commonjs/theme/color-presets.js +413 -166
- package/lib/commonjs/theme/color-presets.js.map +1 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js +1 -0
- package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/commonjs/theme/glass-colors.js +18 -21
- package/lib/commonjs/theme/glass-colors.js.map +1 -1
- package/lib/commonjs/theme/index.js +30 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/commonjs/theme/index.web.js +30 -0
- package/lib/commonjs/theme/index.web.js.map +1 -1
- package/lib/module/design-tokens/color-roles.js +20 -0
- package/lib/module/design-tokens/color-roles.js.map +1 -1
- package/lib/module/design-tokens/index.js +2 -2
- package/lib/module/design-tokens/index.js.map +1 -1
- package/lib/module/design-tokens/tailwind-preset.js +4 -2
- package/lib/module/design-tokens/tailwind-preset.js.map +1 -1
- package/lib/module/design-tokens/theme-css.js +2 -1
- package/lib/module/design-tokens/theme-css.js.map +1 -1
- package/lib/module/design-tokens/theme.css +3 -0
- package/lib/module/design-tokens/tokens-json.js +14 -5
- package/lib/module/design-tokens/tokens-json.js.map +1 -1
- package/lib/module/design-tokens/tokens.json +6227 -265
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/theme/ambient-store.js +16 -1
- package/lib/module/theme/ambient-store.js.map +1 -1
- package/lib/module/theme/color-policy.js +30 -33
- package/lib/module/theme/color-policy.js.map +1 -1
- package/lib/module/theme/color-presets.js +412 -165
- package/lib/module/theme/color-presets.js.map +1 -1
- package/lib/module/theme/color-scope/seed-scope.js +2 -1
- package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/module/theme/glass-colors.js +18 -21
- package/lib/module/theme/glass-colors.js.map +1 -1
- package/lib/module/theme/index.js +1 -1
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/index.web.js +1 -1
- package/lib/module/theme/index.web.js.map +1 -1
- package/lib/typescript/commonjs/design-tokens/color-roles.d.ts +20 -0
- package/lib/typescript/commonjs/design-tokens/color-roles.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/index.d.ts +3 -3
- package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/theme-css.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +12 -3
- package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/ambient-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-presets.d.ts +386 -62
- package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/glass-colors.d.ts +18 -21
- package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/index.d.ts +2 -2
- package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/theme/index.web.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/color-roles.d.ts +20 -0
- package/lib/typescript/module/design-tokens/color-roles.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/index.d.ts +3 -3
- package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/tailwind-preset.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/theme-css.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/tokens-json.d.ts +12 -3
- package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/theme/ambient-store.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-presets.d.ts +386 -62
- package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/module/theme/glass-colors.d.ts +18 -21
- package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
- package/lib/typescript/module/theme/index.d.ts +2 -2
- package/lib/typescript/module/theme/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/index.web.d.ts +2 -2
- package/lib/typescript/module/theme/index.web.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/__tests__/support/press-host.ts +30 -0
- package/src/__tests__/support/rendered-style.ts +99 -0
- package/src/__tests__/support/unread-hook-fixture.ts +33 -0
- package/src/design-tokens/color-roles.ts +21 -0
- package/src/design-tokens/index.ts +3 -2
- package/src/design-tokens/tailwind-preset.ts +3 -2
- package/src/design-tokens/theme-css.ts +2 -1
- package/src/design-tokens/theme.css +3 -0
- package/src/design-tokens/tokens-json.ts +29 -9
- package/src/design-tokens/tokens.json +6227 -265
- package/src/glass/GlassSurface.tsx +2 -2
- package/src/index.ts +2 -0
- package/src/index.web.ts +2 -0
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +4082 -0
- package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
- package/src/theme/ambient-store.ts +21 -1
- package/src/theme/color-policy.ts +33 -38
- package/src/theme/color-presets.ts +473 -146
- package/src/theme/color-scope/seed-scope.ts +2 -1
- package/src/theme/glass-colors.ts +18 -21
- package/src/theme/index.ts +16 -1
- package/src/theme/index.web.ts +16 -1
package/docs/search.mdx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Search
|
|
3
|
+
description: A pill-shaped search field whose clear button appears only when there is something to clear.
|
|
4
|
+
order: 48
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Search
|
|
8
|
+
|
|
9
|
+
A search input: magnifier, rounded field, and a clear button that appears once
|
|
10
|
+
there is a query.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Search } from '@oxyhq/bloom/search';
|
|
14
|
+
|
|
15
|
+
<Search
|
|
16
|
+
value={query}
|
|
17
|
+
onChangeText={setQuery}
|
|
18
|
+
onClearText={() => setQuery('')}
|
|
19
|
+
label="Search people"
|
|
20
|
+
/>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Which prop decides what
|
|
24
|
+
|
|
25
|
+
| prop | decides |
|
|
26
|
+
| -------------- | ----------------------------------------------------------- |
|
|
27
|
+
| `value` | the query — the field is controlled |
|
|
28
|
+
| `onChangeText` | typing |
|
|
29
|
+
| `onClearText` | the clear button's press |
|
|
30
|
+
| `label` | the accessible name **and** the placeholder; defaults to `Search` |
|
|
31
|
+
|
|
32
|
+
Everything else passes through to the underlying text input.
|
|
33
|
+
|
|
34
|
+
## `onClearText` reports; it does not clear
|
|
35
|
+
|
|
36
|
+
The field is controlled, so clearing itself would fight the caller's state and
|
|
37
|
+
leave the list filtered by a query the field no longer shows. Handle it:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
onClearText={() => { setQuery(''); refetch(); }}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## The clear button is conditional on purpose
|
|
44
|
+
|
|
45
|
+
It appears only when `value` is non-empty. A clear button on an empty field is
|
|
46
|
+
a dead target that looks live, and a missing one on a full field strands the
|
|
47
|
+
user with no way back to the unfiltered list except selecting and deleting.
|
|
48
|
+
|
|
49
|
+
## Searching as you type, or on submit
|
|
50
|
+
|
|
51
|
+
`Search` sets `returnKeyType="search"` but does not decide when the search
|
|
52
|
+
runs. Both are legitimate:
|
|
53
|
+
|
|
54
|
+
- **As you type** — cheap, local filtering. Debounce anything that leaves the
|
|
55
|
+
device.
|
|
56
|
+
- **On submit** — expensive or paginated search. Say so, by leaving the results
|
|
57
|
+
alone until the key is pressed rather than showing stale ones.
|
|
58
|
+
|
|
59
|
+
What is not legitimate is issuing a request per keystroke to a server. That is
|
|
60
|
+
the same failure as a [`Slider`](/docs/bloom/main/slider) that saves in
|
|
61
|
+
`onValueChange`.
|
|
62
|
+
|
|
63
|
+
## Search or Combobox
|
|
64
|
+
|
|
65
|
+
- **Search** — filters something already on the page.
|
|
66
|
+
- **[Combobox](/docs/bloom/main/combobox)** — picks a value into a form, and
|
|
67
|
+
the picked value stays.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SegmentedControl
|
|
3
|
+
description: Two controls that look identical and announce differently — `type` is the decision.
|
|
4
|
+
order: 25
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# SegmentedControl
|
|
8
|
+
|
|
9
|
+
A row of segments with a sliding indicator. Two to four options, all visible at
|
|
10
|
+
once, one selected.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import {
|
|
14
|
+
SegmentedControl,
|
|
15
|
+
SegmentedControlItem,
|
|
16
|
+
SegmentedControlItemText,
|
|
17
|
+
} from '@oxyhq/bloom/segmented-control';
|
|
18
|
+
|
|
19
|
+
<SegmentedControl label="Appearance" type="radio" value={mode} onChange={setMode}>
|
|
20
|
+
<SegmentedControlItem value="light">
|
|
21
|
+
<SegmentedControlItemText>Light</SegmentedControlItemText>
|
|
22
|
+
</SegmentedControlItem>
|
|
23
|
+
<SegmentedControlItem value="dark">
|
|
24
|
+
<SegmentedControlItemText>Dark</SegmentedControlItemText>
|
|
25
|
+
</SegmentedControlItem>
|
|
26
|
+
<SegmentedControlItem value="system">
|
|
27
|
+
<SegmentedControlItemText>System</SegmentedControlItemText>
|
|
28
|
+
</SegmentedControlItem>
|
|
29
|
+
</SegmentedControl>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## `type` is an accessibility decision wearing visual clothes
|
|
33
|
+
|
|
34
|
+
| `type` | means | announces as |
|
|
35
|
+
| ------- | ---------------------------------------------- | ------------------------- |
|
|
36
|
+
| `radio` | the segments **pick a value** — a setting | radio group / radio |
|
|
37
|
+
| `tabs` | the segments **switch the panel below** | tab list / tab |
|
|
38
|
+
|
|
39
|
+
The two render identically. That is exactly why the wrong one is dangerous:
|
|
40
|
+
there is nothing on screen to notice, and it is wrong in every screen reader.
|
|
41
|
+
|
|
42
|
+
Ask what happens when the user picks: if something is now *set*, it is `radio`;
|
|
43
|
+
if the same subject is now shown a different way, it is `tabs`.
|
|
44
|
+
|
|
45
|
+
## The state spelling follows the role
|
|
46
|
+
|
|
47
|
+
ARIA gives a `tab` a **selected** state and a `radio` a **checked** one, so a
|
|
48
|
+
single spelling would be invalid in one of the two modes. `SegmentedControl`
|
|
49
|
+
emits `aria-selected` for tabs and `aria-checked` for radios, and neither uses
|
|
50
|
+
`accessibilityState` — react-native-web drops that entirely, so a control
|
|
51
|
+
relying on it would announce no state at all on web.
|
|
52
|
+
|
|
53
|
+
## `label` is required
|
|
54
|
+
|
|
55
|
+
It names the group. "Light / Dark / System" without a group name announces as
|
|
56
|
+
three unrelated radios; with `label="Appearance"` it announces as a choice
|
|
57
|
+
about appearance.
|
|
58
|
+
|
|
59
|
+
## Which prop decides what
|
|
60
|
+
|
|
61
|
+
| prop | decides |
|
|
62
|
+
| ---------- | ---------------------------------------------- |
|
|
63
|
+
| `label` | the accessible name of the whole group |
|
|
64
|
+
| `type` | radio group vs tab list (see above) |
|
|
65
|
+
| `value` | the selected segment — fully controlled |
|
|
66
|
+
| `onChange` | receives the pressed segment's `value` |
|
|
67
|
+
| `size` | `small` (32px) for a toolbar, `large` (40px) |
|
|
68
|
+
|
|
69
|
+
`SegmentedControlItemText` takes no `value` — it reads whether its segment is
|
|
70
|
+
active from context. Passing one is a type error.
|
|
71
|
+
|
|
72
|
+
## When to reach for something else
|
|
73
|
+
|
|
74
|
+
- More than about four options, or long labels: a [`Select`](/docs/bloom/main/select).
|
|
75
|
+
- A view of the same subject with real panels underneath: [`Tabs`](/docs/bloom/main/tabs).
|
|
76
|
+
- A single on/off: [`Switch`](/docs/bloom/main/switch).
|
package/docs/select.mdx
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Select
|
|
3
|
+
description: A single-choice picker — trigger, value, grouped options.
|
|
4
|
+
order: 15
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Select
|
|
8
|
+
|
|
9
|
+
A single-choice picker. A bottom sheet on native, an anchored dropdown on web. Controlled or uncontrolled.
|
|
10
|
+
|
|
11
|
+
`Select` owns the value, `SelectTrigger` opens it, `SelectContent` renders the choices from an `items` array, and `SelectItem` is one row.
|
|
12
|
+
|
|
13
|
+
## Basic
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import {
|
|
17
|
+
Select,
|
|
18
|
+
SelectContent,
|
|
19
|
+
SelectIcon,
|
|
20
|
+
SelectItem,
|
|
21
|
+
SelectItemIndicator,
|
|
22
|
+
SelectItemText,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
25
|
+
} from '@oxyhq/bloom/select';
|
|
26
|
+
|
|
27
|
+
const [country, setCountry] = useState('us');
|
|
28
|
+
|
|
29
|
+
<Select value={country} onValueChange={setCountry}>
|
|
30
|
+
<SelectTrigger label="Country">
|
|
31
|
+
<SelectValue placeholder="Pick a country" />
|
|
32
|
+
<SelectIcon />
|
|
33
|
+
</SelectTrigger>
|
|
34
|
+
|
|
35
|
+
<SelectContent
|
|
36
|
+
label="Country"
|
|
37
|
+
items={countries}
|
|
38
|
+
renderItem={(item) => (
|
|
39
|
+
<SelectItem value={item.code} label={item.name}>
|
|
40
|
+
<SelectItemIndicator />
|
|
41
|
+
<SelectItemText>{item.name}</SelectItemText>
|
|
42
|
+
</SelectItem>
|
|
43
|
+
)}
|
|
44
|
+
/>
|
|
45
|
+
</Select>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`SelectContent` takes the whole `items` array rather than children, so the native fork can hand it to a `FlatList` and the web fork can measure and scroll it. `renderItem` receives `(item, index, selectedValue)`.
|
|
49
|
+
|
|
50
|
+
`SelectTrigger` renders a pressable around its children, or hands the open handler to your own control with `asChild` — the same escape hatch every anchored Bloom family offers:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<SelectTrigger asChild label="Country">
|
|
54
|
+
<MyField>
|
|
55
|
+
<SelectValue placeholder="Pick a country" />
|
|
56
|
+
<SelectIcon />
|
|
57
|
+
</MyField>
|
|
58
|
+
</SelectTrigger>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
It used to take a render prop instead, which handed its child a `state` object whose `pressed` was a hardcoded `false` and whose `hovered` the native fork never computed. Press and hover styling on a select trigger was therefore dead while the API read as though it worked. Bring your own control through `asChild` and its own states are real.
|
|
62
|
+
|
|
63
|
+
## Groups and labels
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<SelectGroup>
|
|
67
|
+
<SelectLabel>Europe</SelectLabel>
|
|
68
|
+
<SelectItem value="es" label="Spain">…</SelectItem>
|
|
69
|
+
<SelectItem value="fr" label="France">…</SelectItem>
|
|
70
|
+
</SelectGroup>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`SelectGroup` renders `role="group"` so a screen reader announces the run as one; `SelectLabel` is its heading. Return them from `renderItem` on the first row of each run — `SelectContent`'s list stays flat.
|
|
74
|
+
|
|
75
|
+
## Long lists
|
|
76
|
+
|
|
77
|
+
The web dropdown grows to `maxHeight` (default 208, shadcn's `max-h-52`) and then scrolls. When it does, `SelectScrollUpButton` and `SelectScrollDownButton` appear at whichever end still has something to show, and disappear at the ends — a scroll affordance that is always visible tells the user a list scrolls when it does not.
|
|
78
|
+
|
|
79
|
+
`SelectContent` renders both, so you get them by default. They are exported for a caller composing their own content, and on NATIVE they render nothing at all: the sheet's list already scrolls under the finger, and a chevron there is a target with no pointer to hit it.
|
|
80
|
+
|
|
81
|
+
## Selection state
|
|
82
|
+
|
|
83
|
+
Both forks render each option as `role="radio"` with `aria-checked` — the only spelling react-native-web reads, and one React Native folds back into `accessibilityState`. `SelectItemIndicator` draws the mark: the shared `RadioIndicator` on native, a check on web.
|
|
84
|
+
|
|
85
|
+
`SelectSeparator` draws a rule between runs.
|
|
86
|
+
|
|
87
|
+
## Why a row is not `Item`
|
|
88
|
+
|
|
89
|
+
Every menu row in Bloom is an `Item`. A select row is not, and the reason is its layout.
|
|
90
|
+
|
|
91
|
+
The web option reserves a 30px indicator gutter and paints the indicator into it ABSOLUTELY, so the label's left edge does not move between a selected row and an unselected one; `Item` has an in-flow leading slot and no absolute one. On top of that, `Item` couples `selected` — which select needs purely to emit `aria-checked` — to a selected BACKGROUND, and the web select deliberately paints none, because the check mark is its selection signal.
|
|
92
|
+
|
|
93
|
+
Composing would mean flattening one of those two, and both are visible: the first as a label that jumps sideways on selection, the second as a highlighted row where there was none. So select keeps its own row, and `radio-indicator` stays the one indicator both it and `Radio` draw.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SettingsList
|
|
3
|
+
description: iOS-style grouped settings list — items, groups, dividers.
|
|
4
|
+
order: 18
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# SettingsList
|
|
8
|
+
|
|
9
|
+
iOS-style grouped settings list. Items, groups (with optional headers and footers), and dividers. Built for the dense, list-heavy screens common in account / preferences UIs.
|
|
10
|
+
|
|
11
|
+
## Basic
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import {
|
|
15
|
+
SettingsListGroup,
|
|
16
|
+
SettingsListItem,
|
|
17
|
+
SettingsListDivider,
|
|
18
|
+
} from '@oxyhq/bloom/settings-list';
|
|
19
|
+
|
|
20
|
+
<SettingsListGroup title="General">
|
|
21
|
+
<SettingsListItem
|
|
22
|
+
icon={<PersonIcon />}
|
|
23
|
+
title="Account"
|
|
24
|
+
value="ada@oxy.so"
|
|
25
|
+
onPress={() => navigate('Account')}
|
|
26
|
+
/>
|
|
27
|
+
<SettingsListDivider />
|
|
28
|
+
<SettingsListItem
|
|
29
|
+
icon={<GlobeIcon />}
|
|
30
|
+
title="Language"
|
|
31
|
+
value="English"
|
|
32
|
+
onPress={() => navigate('Language')}
|
|
33
|
+
/>
|
|
34
|
+
</SettingsListGroup>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The group renders a rounded card on iOS-style platforms and on web. The first item rounds top corners, the last item rounds bottom corners.
|
|
38
|
+
|
|
39
|
+
## With description
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<SettingsListItem
|
|
43
|
+
icon={<ShieldIcon />}
|
|
44
|
+
title="Two-factor authentication"
|
|
45
|
+
description="Add a second sign-in step with an authenticator app."
|
|
46
|
+
value="On"
|
|
47
|
+
onPress={() => navigate('TwoFactor')}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`description` is multi-line and styled in muted text under the title.
|
|
52
|
+
|
|
53
|
+
## Toggle row
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<SettingsListItem
|
|
57
|
+
icon={<MoonIcon />}
|
|
58
|
+
title="Dark mode"
|
|
59
|
+
rightElement={<Switch value={darkMode} onValueChange={setDarkMode} />}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
When `rightElement` is set, the chevron is hidden automatically (you can override with `showChevron`).
|
|
64
|
+
|
|
65
|
+
## Destructive
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
<SettingsListGroup title="Danger zone">
|
|
69
|
+
<SettingsListItem
|
|
70
|
+
icon={<TrashIcon />}
|
|
71
|
+
title="Delete account"
|
|
72
|
+
destructive
|
|
73
|
+
onPress={confirmDelete}
|
|
74
|
+
/>
|
|
75
|
+
</SettingsListGroup>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`destructive` switches the title to the negative color and uses the destructive icon tint.
|
|
79
|
+
|
|
80
|
+
## Disabled
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<SettingsListItem
|
|
84
|
+
icon={<KeyIcon />}
|
|
85
|
+
title="Reset password"
|
|
86
|
+
description="Coming soon"
|
|
87
|
+
disabled
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Footer / header text
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
<SettingsListGroup
|
|
95
|
+
title="Notifications"
|
|
96
|
+
footer="Push notifications require an enabled device permission."
|
|
97
|
+
>
|
|
98
|
+
<SettingsListItem title="Marketing emails" rightElement={<Switch value={mktg} onValueChange={setMktg} />} />
|
|
99
|
+
<SettingsListDivider />
|
|
100
|
+
<SettingsListItem title="Product updates" rightElement={<Switch value={prod} onValueChange={setProd} />} />
|
|
101
|
+
</SettingsListGroup>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`title` is the group header (uppercase muted label on iOS-style platforms); `footer` is a soft explanatory line under the card.
|
|
105
|
+
|
|
106
|
+
## Components
|
|
107
|
+
|
|
108
|
+
### `SettingsListGroup`
|
|
109
|
+
|
|
110
|
+
| Prop | Type | Description |
|
|
111
|
+
|------|------|-------------|
|
|
112
|
+
| `title?` | `string` | Section header. |
|
|
113
|
+
| `footer?` | `string` | Section footer. |
|
|
114
|
+
| `children` | `React.ReactNode` | `Item`, `Divider`. |
|
|
115
|
+
| `style?` | `StyleProp<ViewStyle>` | Override group container style. |
|
|
116
|
+
|
|
117
|
+
### `SettingsListItem`
|
|
118
|
+
|
|
119
|
+
| Prop | Type | Description |
|
|
120
|
+
|------|------|-------------|
|
|
121
|
+
| `icon?` | `React.ReactNode` | Leading icon. |
|
|
122
|
+
| `title` | `string` | Primary label. |
|
|
123
|
+
| `description?` | `string` | Multi-line secondary text. |
|
|
124
|
+
| `value?` | `string` | Right-side value string. |
|
|
125
|
+
| `rightElement?` | `React.ReactNode` | Replaces value / chevron with a custom node. |
|
|
126
|
+
| `showChevron?` | `boolean` | Defaults to `true` when `onPress` is set. |
|
|
127
|
+
| `destructive?` | `boolean` | Negative palette for title + icon. |
|
|
128
|
+
| `onPress?` | `() => void` | Tap handler. |
|
|
129
|
+
| `disabled?` | `boolean` | |
|
|
130
|
+
| `accessibilityLabel?` | `string` | Override the derived label. |
|
|
131
|
+
| `accessibilityHint?` | `string` | |
|
|
132
|
+
| `accessibilityRole?` | `'button' \| 'link' \| 'none'` | Defaults to `'button'` when pressable. |
|
|
133
|
+
| `leftInset?` | `number` | Left inset to align text when no icon is set. |
|
|
134
|
+
|
|
135
|
+
### `SettingsListDivider`
|
|
136
|
+
|
|
137
|
+
| Prop | Type | Description |
|
|
138
|
+
|------|------|-------------|
|
|
139
|
+
| `inset?` | `number` | Left inset to align with text (default `52`). |
|
|
140
|
+
|
|
141
|
+
## Recipes
|
|
142
|
+
|
|
143
|
+
### Mixed read / link rows
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<SettingsListGroup title="About">
|
|
147
|
+
<SettingsListItem title="Version" value="1.4.2" />
|
|
148
|
+
<SettingsListDivider />
|
|
149
|
+
<SettingsListItem title="Build" value="231" />
|
|
150
|
+
<SettingsListDivider />
|
|
151
|
+
<SettingsListItem title="Terms" onPress={() => navigate('Terms')} />
|
|
152
|
+
<SettingsListDivider />
|
|
153
|
+
<SettingsListItem title="Privacy" onPress={() => navigate('Privacy')} />
|
|
154
|
+
</SettingsListGroup>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Sign-out section
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<SettingsListGroup>
|
|
161
|
+
<SettingsListItem
|
|
162
|
+
title="Sign out"
|
|
163
|
+
onPress={signOut}
|
|
164
|
+
accessibilityRole="button"
|
|
165
|
+
/>
|
|
166
|
+
</SettingsListGroup>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Account switcher
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
<SettingsListGroup title="Accounts">
|
|
173
|
+
{accounts.map((a, i) => (
|
|
174
|
+
<Fragment key={a.id}>
|
|
175
|
+
{i > 0 ? <SettingsListDivider /> : null}
|
|
176
|
+
<SettingsListItem
|
|
177
|
+
icon={<Avatar uri={a.avatarUrl} size={28} />}
|
|
178
|
+
title={a.name}
|
|
179
|
+
description={a.handle}
|
|
180
|
+
rightElement={a.id === activeId ? <CheckIcon /> : null}
|
|
181
|
+
onPress={() => switchTo(a.id)}
|
|
182
|
+
/>
|
|
183
|
+
</Fragment>
|
|
184
|
+
))}
|
|
185
|
+
</SettingsListGroup>
|
|
186
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Skeleton
|
|
3
|
+
description: Shimmering placeholders shaped like the content that is coming.
|
|
4
|
+
order: 32
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Skeleton
|
|
8
|
+
|
|
9
|
+
Placeholders for content that has not arrived. `Skeleton` is a namespace of
|
|
10
|
+
primitives you compose into the shape of the real thing.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import * as Skeleton from '@oxyhq/bloom/skeleton';
|
|
14
|
+
|
|
15
|
+
<Skeleton.Row style={{ gap: 12 }}>
|
|
16
|
+
<Skeleton.Circle size={40} />
|
|
17
|
+
<Skeleton.Col style={{ gap: 6 }}>
|
|
18
|
+
<Skeleton.Text style={{ width: 140 }} />
|
|
19
|
+
<Skeleton.Text style={{ width: 90 }} />
|
|
20
|
+
</Skeleton.Col>
|
|
21
|
+
</Skeleton.Row>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## The parts
|
|
25
|
+
|
|
26
|
+
| part | takes | for |
|
|
27
|
+
| ------------------ | -------------------- | ---------------------------------- |
|
|
28
|
+
| `Skeleton.Text` | `style` (`width`, `lineHeight`) | a line of text |
|
|
29
|
+
| `Skeleton.Circle` | `size` | an avatar |
|
|
30
|
+
| `Skeleton.Pill` | `size` | a chip, a badge, a button |
|
|
31
|
+
| `Skeleton.Box` | `width` / `height` | anything rectangular |
|
|
32
|
+
| `Skeleton.Row` / `Skeleton.Col` | `style` | layout, so a placeholder needs no extra views |
|
|
33
|
+
|
|
34
|
+
`Skeleton.Text` reads its width and line height out of `style` rather than
|
|
35
|
+
taking props for them, so the same style object you would give the real `Text`
|
|
36
|
+
produces a placeholder the right size — which is the point.
|
|
37
|
+
|
|
38
|
+
It is a namespace rather than flat exports because the names are generic
|
|
39
|
+
(`Text`, `Box`, `Row`) and would collide with everything. Each ships as a
|
|
40
|
+
subpath, so `import * as` costs no tree-shaking.
|
|
41
|
+
|
|
42
|
+
## Shape it like the content, not like a grey box
|
|
43
|
+
|
|
44
|
+
The point of a skeleton is that nothing moves when the data arrives. A
|
|
45
|
+
placeholder wider than the real text, or one line where there will be two,
|
|
46
|
+
produces a jump that is more distracting than a spinner would have been. Match
|
|
47
|
+
the line count and the rough widths; do not match them exactly, because
|
|
48
|
+
identical widths on every row read as a pattern rather than as text.
|
|
49
|
+
|
|
50
|
+
## Skeleton or Loading
|
|
51
|
+
|
|
52
|
+
- **Skeleton** — you know the shape of what is coming and it fills the region.
|
|
53
|
+
A feed, a profile header, a table.
|
|
54
|
+
- **[Loading](/docs/bloom/main/loading)** — you do not know the shape, or the
|
|
55
|
+
wait is short, or it is a small inline region. Also the right choice for an
|
|
56
|
+
action in progress, where a skeleton would imply content is arriving.
|
|
57
|
+
|
|
58
|
+
Do not show a skeleton for longer than a couple of seconds. Past that it stops
|
|
59
|
+
reading as "almost there" and starts reading as broken; show progress or an
|
|
60
|
+
explanation instead.
|
|
61
|
+
|
|
62
|
+
## Accessibility
|
|
63
|
+
|
|
64
|
+
A skeleton is decorative — it must not be announced as text. Mark the region as
|
|
65
|
+
busy on the container that will hold the content, so a screen reader user is
|
|
66
|
+
told that something is loading rather than read a grid of empty boxes.
|
package/docs/slider.mdx
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Slider
|
|
3
|
+
description: A value on a continuum — and the difference between `onValueChange` and `onSlidingComplete`.
|
|
4
|
+
order: 35
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Slider
|
|
8
|
+
|
|
9
|
+
For a value where the *approximate* position matters more than the exact number:
|
|
10
|
+
volume, brightness, a zoom level, a price ceiling.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Slider } from '@oxyhq/bloom/slider';
|
|
14
|
+
|
|
15
|
+
<Slider
|
|
16
|
+
value={volume}
|
|
17
|
+
onValueChange={setVolume}
|
|
18
|
+
onSlidingComplete={persistVolume}
|
|
19
|
+
min={0}
|
|
20
|
+
max={100}
|
|
21
|
+
step={1}
|
|
22
|
+
accessibilityLabel="Volume"
|
|
23
|
+
/>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Which prop decides what
|
|
27
|
+
|
|
28
|
+
| prop | decides |
|
|
29
|
+
| ------------------- | --------------------------------------------------------- |
|
|
30
|
+
| `value` | the position — the slider is fully controlled |
|
|
31
|
+
| `onValueChange` | fires **continuously** while dragging |
|
|
32
|
+
| `onSlidingComplete` | fires **once**, when the finger lifts |
|
|
33
|
+
| `min` / `max` | the range |
|
|
34
|
+
| `step` | the granularity; omit for continuous |
|
|
35
|
+
| `disabled` | dims and ignores gestures |
|
|
36
|
+
|
|
37
|
+
## The two callbacks are the whole API
|
|
38
|
+
|
|
39
|
+
`onValueChange` fires on every frame of a drag. Put the *preview* there — the
|
|
40
|
+
number under the thumb, the live volume — and nothing else.
|
|
41
|
+
|
|
42
|
+
`onSlidingComplete` fires once. Put anything expensive or persistent there: a
|
|
43
|
+
network request, a write to storage, a re-render of a large list. A `PATCH` in
|
|
44
|
+
`onValueChange` sends one request per frame, which is the classic way a slider
|
|
45
|
+
takes a settings screen offline.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
onValueChange={setLocalValue} // cheap, immediate
|
|
49
|
+
onSlidingComplete={(v) => save(v)} // once, at the end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Give it a label and a unit
|
|
53
|
+
|
|
54
|
+
A slider announces a number with no meaning attached. `accessibilityLabel`
|
|
55
|
+
names it; if the number is not self-evident (a price, a duration), show the
|
|
56
|
+
formatted value beside the track so it is readable without dragging.
|
|
57
|
+
|
|
58
|
+
The label is not optional in practice, only in the type. ARIA computes no name
|
|
59
|
+
from a `slider`'s contents — a track and a knob render no text — so a caption
|
|
60
|
+
beside it names nothing, and the `aria-value*` props say how far along it is
|
|
61
|
+
and never what it is. Omitting it warns once in development, dev-only and never
|
|
62
|
+
throwing; an empty or whitespace-only string counts as missing, because
|
|
63
|
+
`aria-label=""` is not a name.
|
|
64
|
+
|
|
65
|
+
## When a slider is the wrong control
|
|
66
|
+
|
|
67
|
+
- **The exact value matters.** Use a number input; a slider cannot be typed
|
|
68
|
+
into and is hard to land on a specific value.
|
|
69
|
+
- **There are fewer than about five positions.** Use a
|
|
70
|
+
[`SegmentedControl`](/docs/bloom/main/segmented-control) or a
|
|
71
|
+
[`Radio`](/docs/bloom/main/radio) group — they name the options.
|
|
72
|
+
- **The range is huge and non-linear.** A slider over 1–1,000,000 gives most of
|
|
73
|
+
its travel to values nobody wants.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: StatBar
|
|
3
|
+
description: A labelled bar in two shapes — progress toward a maximum, or a split between two sides.
|
|
4
|
+
order: 63
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# StatBar
|
|
8
|
+
|
|
9
|
+
One bar, two meanings.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { StatBar } from '@oxyhq/bloom/stat-bar';
|
|
13
|
+
|
|
14
|
+
<StatBar label="Storage" value={62} max={100} minLabel="0 GB" maxLabel="100 GB" />
|
|
15
|
+
|
|
16
|
+
<StatBar variant="split" label="Votes" percent={64} leftValue="Yes" rightValue="No" />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Which prop decides what
|
|
20
|
+
|
|
21
|
+
| prop | decides |
|
|
22
|
+
| ------------------------- | --------------------------------------------------- |
|
|
23
|
+
| `variant` | `progress` (default) or `split` |
|
|
24
|
+
| `value` / `max` | progress: how far along |
|
|
25
|
+
| `percent` | split: where the divide sits |
|
|
26
|
+
| `leftValue` / `rightValue`| split: what each side is |
|
|
27
|
+
| `minLabel` / `maxLabel` | progress: the ends of the range |
|
|
28
|
+
| `label` | what the bar is about — required |
|
|
29
|
+
| `icon` | a glyph beside the label |
|
|
30
|
+
| `fillColor` / `trackColor`| the colours |
|
|
31
|
+
|
|
32
|
+
The two variants are one component because they are the same object visually,
|
|
33
|
+
and one prop rather than two components because a row often switches between
|
|
34
|
+
them for the same subject.
|
|
35
|
+
|
|
36
|
+
## `progress` and `split` are different claims
|
|
37
|
+
|
|
38
|
+
`progress` says *how much of a maximum has been used* — there is a ceiling, and
|
|
39
|
+
approaching it means something. `split` says *how a whole divides between two
|
|
40
|
+
named sides* — there is no ceiling and neither side is "full".
|
|
41
|
+
|
|
42
|
+
Using `progress` for a split reads as the left side filling up toward a limit,
|
|
43
|
+
which is a claim you did not intend to make.
|
|
44
|
+
|
|
45
|
+
## `minLabel` / `maxLabel` on a progress bar
|
|
46
|
+
|
|
47
|
+
A bar with no scale is a proportion with no units. "62 of 100" is a different
|
|
48
|
+
statement from "62%", and only one of them tells the user whether to act.
|
|
49
|
+
|
|
50
|
+
## `percent` is a number, the values are strings
|
|
51
|
+
|
|
52
|
+
`leftValue` and `rightValue` are strings because they are labels, not the
|
|
53
|
+
maths: "Yes" / "No", "1,204" / "677", "You" / "Everyone else". The bar's
|
|
54
|
+
geometry comes from `percent` alone.
|
|
55
|
+
|
|
56
|
+
## `label` is required
|
|
57
|
+
|
|
58
|
+
A bar in a stack of stats with no label is a colour. It is also the only thing
|
|
59
|
+
a screen reader gets, since the bar itself conveys nothing.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Choosing a stat widget
|
|
3
|
+
description: Four families, four different questions about a number.
|
|
4
|
+
order: 61
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Choosing a stat widget
|
|
8
|
+
|
|
9
|
+
Bloom has four small data-display families. They are separate because each
|
|
10
|
+
answers a different question, and picking the wrong one makes a number harder
|
|
11
|
+
to read rather than easier.
|
|
12
|
+
|
|
13
|
+
| family | answers |
|
|
14
|
+
| ----------------------------------------------------- | ---------------------------------------- |
|
|
15
|
+
| [`CompositionBar`](/docs/bloom/main/composition-bar) | what is this total **made of**? |
|
|
16
|
+
| [`StatBar`](/docs/bloom/main/stat-bar) | how far along, or how is it split in two? |
|
|
17
|
+
| [`DotGridMeter`](/docs/bloom/main/dot-grid-meter) | how many out of a countable total? |
|
|
18
|
+
| [`ActivityHeatmap`](/docs/bloom/main/activity-heatmap) | when did this happen, over time? |
|
|
19
|
+
|
|
20
|
+
Two rules apply to all four.
|
|
21
|
+
|
|
22
|
+
## Colours are the app's, not the theme's
|
|
23
|
+
|
|
24
|
+
Each takes explicit colours, because the categories belong to the product — a
|
|
25
|
+
token's brand colour is not a Bloom role. Whatever you pass, check it against
|
|
26
|
+
the surface it sits on and in both modes: a colour chosen on a light card is
|
|
27
|
+
routinely invisible in dark mode.
|
|
28
|
+
|
|
29
|
+
## A chart still needs the number in text
|
|
30
|
+
|
|
31
|
+
None of these announces anything useful on its own. Whatever the widget shows,
|
|
32
|
+
the value should also be readable as text nearby — otherwise the information is
|
|
33
|
+
available only to people who can see and interpret the shape.
|