@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/card.mdx
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Card
|
|
3
|
+
description: The one card-shaped surface — background, border, elevation and corner decided in a single place.
|
|
4
|
+
order: 20
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Card
|
|
8
|
+
|
|
9
|
+
`Card` is the answer to "what is a card made of". One component owns the `card`
|
|
10
|
+
background role, the border colour and width, the elevation and the corner
|
|
11
|
+
rung — and the platform split those imply, since a shadow is a `box-shadow` on
|
|
12
|
+
web and `elevation` on Android.
|
|
13
|
+
|
|
14
|
+
Five families used to draw that chrome by hand, with five different radii and
|
|
15
|
+
three different elevations. That is what `Card` removes. It does **not** remove
|
|
16
|
+
the variety: a settings group, a hover card and a link preview are legitimately
|
|
17
|
+
different surfaces, and the radius scale has rungs precisely so they can differ.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { Card, CardHeader, CardBody, CardFooter, CardTitle, CardDescription } from '@oxyhq/bloom/card';
|
|
21
|
+
|
|
22
|
+
<Card variant="outlined" radius="radius-16">
|
|
23
|
+
<CardHeader>
|
|
24
|
+
<CardTitle>Weekly digest</CardTitle>
|
|
25
|
+
<CardDescription>Sent every Monday at 09:00.</CardDescription>
|
|
26
|
+
</CardHeader>
|
|
27
|
+
<CardBody>…</CardBody>
|
|
28
|
+
</Card>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Which prop decides what
|
|
32
|
+
|
|
33
|
+
| prop | decides |
|
|
34
|
+
| ----------- | ---------------------------------------------------------------- |
|
|
35
|
+
| `variant` | the preset combination of background, border and elevation |
|
|
36
|
+
| `radius` | the corner, as a **rung of the `RADIUS` scale** — never a number |
|
|
37
|
+
| `border` | `none` · `hairline` (0.5px) · `thin` (1px) |
|
|
38
|
+
| `elevation` | `none` · `s` (a subtle raise) · `m` (the overlay role) |
|
|
39
|
+
|
|
40
|
+
`variant` is only a named default for the other three. An explicit `border` or
|
|
41
|
+
`elevation` wins, in both directions — you can add a shadow to `plain` and take
|
|
42
|
+
one away from `elevated`. That is deliberate: a surface that is "an outlined
|
|
43
|
+
card but flatter" should refine the axes rather than earn a fifth variant name.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
<Card variant="plain" border="hairline" elevation="m" radius="radius-20" />
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Variants
|
|
50
|
+
|
|
51
|
+
- `plain` — the bare `card` surface: no border, no shadow.
|
|
52
|
+
- `elevated` — `shadow-s`. The default.
|
|
53
|
+
- `outlined` — a 1px border in the `border` role.
|
|
54
|
+
- `filled` — paints from `backgroundSecondary` instead of `card`, for a surface
|
|
55
|
+
that sits **on** a card rather than being one.
|
|
56
|
+
|
|
57
|
+
## `radius` is a rung, not a number
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
<Card radius="radius-20" /> // ✅
|
|
61
|
+
<Card radius={20} /> // ✗ does not type-check
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The scale has 2 / 4 / 12 / 16 / 20 / 24 / 28 / max. Taking a rung rather than a
|
|
65
|
+
number is what keeps "this surface is a bit rounder" to eight values instead of
|
|
66
|
+
eight hundred, and it is why a design change to a rung moves every surface that
|
|
67
|
+
uses it.
|
|
68
|
+
|
|
69
|
+
## The mistake it prevents
|
|
70
|
+
|
|
71
|
+
Hand-rolling the chrome means hand-rolling the platform split too, and the
|
|
72
|
+
usual result is a `Platform.OS` branch with slightly different numbers from the
|
|
73
|
+
one next to it. Worse, on web it is tempting to reach for classes —
|
|
74
|
+
`border border-border bg-card` — which are **inert until the consumer wires the
|
|
75
|
+
Tailwind pipeline**. That failure is silent: the card renders as an unbordered
|
|
76
|
+
transparent block, nothing throws, and it looks like a spacing bug.
|
|
77
|
+
|
|
78
|
+
`Card` applies background, border, radius and shadow as resolved inline tokens,
|
|
79
|
+
so the surface paints with or without a consumer's CSS pipeline.
|
|
80
|
+
|
|
81
|
+
## Pressable cards
|
|
82
|
+
|
|
83
|
+
Passing `onPress` makes the card a `Pressable` with a press dip. Say what it
|
|
84
|
+
is: a card that opens a URL is a `link`, a card that performs an in-app action
|
|
85
|
+
is a `button` (the default).
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
<Card onPress={open} accessibilityRole="link" accessibilityLabel="Read the post" />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Clipping
|
|
92
|
+
|
|
93
|
+
`Card` sets `overflow: 'hidden'` so a cover image cannot paint over the corner.
|
|
94
|
+
A surface that must *not* clip — because it has an Android elevation, or
|
|
95
|
+
because a child deliberately overflows — opts out through `style`:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<Card style={{ overflow: 'visible' }} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Parts
|
|
102
|
+
|
|
103
|
+
`CardHeader`, `CardBody`, `CardFooter`, `CardTitle` and `CardDescription` are
|
|
104
|
+
spacing and type conventions, not requirements. A card with entirely custom
|
|
105
|
+
content is a normal thing; use the parts when you want the same rhythm as every
|
|
106
|
+
other card.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Checkbox
|
|
3
|
+
description: A tri-state checkbox with an optional label and description.
|
|
4
|
+
order: 24
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Checkbox
|
|
8
|
+
|
|
9
|
+
A checkbox. Controlled, with an optional label and description, three sizes and a mixed state.
|
|
10
|
+
|
|
11
|
+
## Basic
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Checkbox } from '@oxyhq/bloom/checkbox';
|
|
15
|
+
|
|
16
|
+
const [checked, setChecked] = useState(false);
|
|
17
|
+
|
|
18
|
+
<Checkbox checked={checked} onCheckedChange={setChecked} label="Remember me" />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`label` also becomes the accessible name, so a labelled checkbox needs nothing else. A checkbox with no label needs `accessibilityLabel`.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<Checkbox
|
|
25
|
+
checked={notify}
|
|
26
|
+
onCheckedChange={setNotify}
|
|
27
|
+
label="Email notifications"
|
|
28
|
+
description="Get a message when someone replies to you."
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## The mixed state
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<Checkbox
|
|
36
|
+
checked={allChecked}
|
|
37
|
+
indeterminate={!allChecked && someChecked}
|
|
38
|
+
onCheckedChange={(next) => setAll(next)}
|
|
39
|
+
label="Select all"
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`indeterminate` announces as `aria-checked="mixed"` and draws a rounded bar rather than a second glyph — the icon set has no minus, and a rule is what the state means.
|
|
44
|
+
|
|
45
|
+
## Sizes and colour
|
|
46
|
+
|
|
47
|
+
`size` is `small`, `medium` (default) or `large`. `color` overrides the checked fill; it defaults to the theme primary, and the mark on top of it resolves to that preset's readable foreground rather than a hardcoded white.
|
|
48
|
+
|
|
49
|
+
## Touch target
|
|
50
|
+
|
|
51
|
+
The box is 22dp at the default size, which is half of what a finger needs. The pressable is grown to 44dp with `hitSlop` — the target grows, the drawing does not — and the slack is derived per size, so `large` does not overshoot into a neighbouring row.
|
|
52
|
+
|
|
53
|
+
For a labelled checkbox the pressable already spans the label, so the whole row is the target.
|
|
54
|
+
|
|
55
|
+
## Keyboard focus on web
|
|
56
|
+
|
|
57
|
+
The pressable takes a `:focus-visible` ring, drawn from the same shared recipe every Bloom control's web CSS is built from. It rings the element the browser actually focuses — the box AND its label — because ringing anything else would point at something that does not have focus.
|
|
58
|
+
|
|
59
|
+
`:focus-visible`, not `:focus`: a mouse click never leaves a ring behind.
|
package/docs/chip.mdx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Chip
|
|
3
|
+
description: A labelled pill — a filter, a tag, a removable token.
|
|
4
|
+
order: 23
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Chip
|
|
8
|
+
|
|
9
|
+
A labelled pill. A filter you can toggle, a tag you can remove, or a static token.
|
|
10
|
+
|
|
11
|
+
`Chip` is what shadcn calls `Badge`. Bloom's own [`Badge`](/docs/bloom/main/design-tokens) is a different component — a counter or a dot that rides on another control — so if you are porting a `<Badge>Draft</Badge>` from shadcn, this is the one you want.
|
|
12
|
+
|
|
13
|
+
## Basic
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Chip } from '@oxyhq/bloom/chip';
|
|
17
|
+
|
|
18
|
+
<Chip>Draft</Chip>
|
|
19
|
+
<Chip variant="solid" color="success">Published</Chip>
|
|
20
|
+
<Chip variant="subtle" color="warning">Needs review</Chip>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tone and fill
|
|
24
|
+
|
|
25
|
+
`color` picks the tone (`default`, `primary`, `success`, `warning`, `error`, `info`) and `variant` picks how loudly it is painted (`solid`, `subtle`, `outlined` — default `outlined`). shadcn's four badge variants map onto that pair: `default` is `solid` + `primary`, `secondary` is `subtle` + `default`, `destructive` is `solid` + `error`, and `outline` is `outlined` + `default`.
|
|
26
|
+
|
|
27
|
+
Both come from `theme/accent-colors.ts`, the one resolver `Chip` and `Badge` share. Never derive a tint yourself by appending alpha to a token — an accent resolves to `rgb(...)`, so the appended-hex form parses back fully opaque and paints the label on its own colour at contrast 1.00.
|
|
28
|
+
|
|
29
|
+
## Filter pills
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<Chip variant="subtle" selected={active} onPress={toggle}>
|
|
33
|
+
Music
|
|
34
|
+
</Chip>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`selected` promotes the chip to the brand tone rather than to a colour system of its own, and announces as `aria-pressed`. On web a keyboard user gets a `:focus-visible` ring — keyboard-only, so a mouse click does not leave one behind.
|
|
38
|
+
|
|
39
|
+
## Removable
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<Chip onClose={() => removeTag(tag)}>{tag}</Chip>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The close affordance is its own control inside the pill, with its own label and its own hit slop.
|
|
46
|
+
|
|
47
|
+
## Sizes and icons
|
|
48
|
+
|
|
49
|
+
`size` is `small`, `medium` (default) or `large`. `startIcon` and `endIcon` take any node, and both are sized to the pill rather than to whatever was passed — an icon in a `small` chip and the same icon in a `large` one come out proportional.
|
|
50
|
+
|
|
51
|
+
## A pill does not shrink
|
|
52
|
+
|
|
53
|
+
In a flex row of chips — a filter bar, a tag list — each pill keeps its size and the row overflows, rather than every label squeezing into an ellipsis to make room. Inside one pill, the label yields first: a chip narrower than its own text ellipsises rather than spilling past its radius.
|
package/docs/code.mdx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Code
|
|
3
|
+
description: Inline monospace and code blocks, as real elements rather than styled text.
|
|
4
|
+
order: 53
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Code
|
|
8
|
+
|
|
9
|
+
`Code` is **inline** — an identifier, a flag, a token inside a sentence.
|
|
10
|
+
`Pre` is a **block** — a command, a snippet, a config file.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Code, Pre } from '@oxyhq/bloom/code';
|
|
14
|
+
|
|
15
|
+
<Text>
|
|
16
|
+
Pass <Code>variant="outlined"</Code> to draw the border.
|
|
17
|
+
</Text>
|
|
18
|
+
|
|
19
|
+
<Pre>{`bun add @oxyhq/bloom
|
|
20
|
+
bun run build`}</Pre>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## They are elements, not fonts
|
|
24
|
+
|
|
25
|
+
On web `Code` renders a real `<code>`. That matters for more than styling:
|
|
26
|
+
find-in-page, copy behaviour, and assistive technology all treat it as code
|
|
27
|
+
rather than as decoration. On native it is a `Text` in the monospace family.
|
|
28
|
+
|
|
29
|
+
## `Pre` keeps its whitespace and scrolls
|
|
30
|
+
|
|
31
|
+
A block preserves line breaks and indentation and scrolls horizontally rather
|
|
32
|
+
than wrapping. Wrapping is the wrong default for a command: a wrapped command
|
|
33
|
+
is a command someone will copy wrong, and the break is invisible once pasted.
|
|
34
|
+
|
|
35
|
+
Write the content as a template literal so the source's own indentation does
|
|
36
|
+
not leak into the block.
|
|
37
|
+
|
|
38
|
+
## Use `Code` for things that are typed, not for emphasis
|
|
39
|
+
|
|
40
|
+
`Code` means *this is a literal you would type or paste*: a prop name, a flag,
|
|
41
|
+
a file path, an env var. It is not a way to make a word stand out — monospace
|
|
42
|
+
in the middle of a sentence tells the reader "this is exact", and using it for
|
|
43
|
+
emphasis makes the real literals unfindable.
|
|
44
|
+
|
|
45
|
+
## No syntax highlighting
|
|
46
|
+
|
|
47
|
+
`Pre` renders text. Highlighting means shipping a grammar for every language a
|
|
48
|
+
consumer might use, and getting it wrong is worse than not having it —
|
|
49
|
+
mis-tokenised code reads as broken code. If a surface genuinely needs
|
|
50
|
+
highlighting, it belongs to that surface.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Combobox
|
|
3
|
+
description: A select you can type into — for when the option list is too long to scan.
|
|
4
|
+
order: 42
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Combobox
|
|
8
|
+
|
|
9
|
+
A picker with a search field. Reach for it when there are more options than a
|
|
10
|
+
user can scan — a country, a repository, a person.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Combobox } from '@oxyhq/bloom/combobox';
|
|
14
|
+
|
|
15
|
+
<Combobox
|
|
16
|
+
options={apps.map((a) => ({ value: a.id, label: a.name, description: a.tagline }))}
|
|
17
|
+
value={appId}
|
|
18
|
+
onValueChange={setAppId}
|
|
19
|
+
placeholder="Pick an app"
|
|
20
|
+
emptyText="No apps match that"
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Which prop decides what
|
|
25
|
+
|
|
26
|
+
| prop | decides |
|
|
27
|
+
| ------------------------- | ---------------------------------------------------- |
|
|
28
|
+
| `options` | the choices — `value`, `label`, optional `description` |
|
|
29
|
+
| `value` / `onValueChange` | the selection; `null` means nothing chosen |
|
|
30
|
+
| `placeholder` | the trigger text while nothing is selected |
|
|
31
|
+
| `emptyText` | what an empty result says |
|
|
32
|
+
| `filter` | replaces the default match |
|
|
33
|
+
| `query` / `onQueryChange` | the search text, if you want to control it |
|
|
34
|
+
| `maxListHeight` | how tall the list may grow |
|
|
35
|
+
|
|
36
|
+
## The trigger shows the LABEL
|
|
37
|
+
|
|
38
|
+
`value` is an opaque id to the user. A combobox that renders the value into the
|
|
39
|
+
field shows `usr_8f21c` where the user chose "Ada Lovelace" — which reads as a
|
|
40
|
+
bug rather than as a raw id.
|
|
41
|
+
|
|
42
|
+
## Controlled `query` is for search that leaves the component
|
|
43
|
+
|
|
44
|
+
By default the combobox owns the query. Take control of it when the *filtering*
|
|
45
|
+
happens elsewhere — a debounced server search, a query mirrored into the URL:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<Combobox query={q} onQueryChange={setQ} options={serverResults} filter={() => true} />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Note the `filter={() => true}`: if the server already filtered, filtering again
|
|
52
|
+
locally throws away results that matched for reasons the client cannot see.
|
|
53
|
+
|
|
54
|
+
## `emptyText` earns its place
|
|
55
|
+
|
|
56
|
+
An empty popover reads as broken. Saying "No apps match that" tells the user
|
|
57
|
+
the search worked and there is nothing — a different message from silence.
|
|
58
|
+
|
|
59
|
+
## Combobox, Select or Command
|
|
60
|
+
|
|
61
|
+
- **Combobox** — many values, typing needed to find one, selection persists.
|
|
62
|
+
- **[Select](/docs/bloom/main/select)** — few enough values to scan.
|
|
63
|
+
- **[Command](/docs/bloom/main/command)** — **actions**, not values; runs one
|
|
64
|
+
and closes.
|
package/docs/command.mdx
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Command
|
|
3
|
+
description: A command palette — search across everything the user could do from here.
|
|
4
|
+
order: 41
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Command
|
|
8
|
+
|
|
9
|
+
A searchable list of actions, opened over the page. Type to filter, press to run.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Command } from '@oxyhq/bloom/command';
|
|
13
|
+
|
|
14
|
+
<Command
|
|
15
|
+
visible={paletteOpen}
|
|
16
|
+
onClose={() => setPaletteOpen(false)}
|
|
17
|
+
placeholder="Type a command…"
|
|
18
|
+
items={[
|
|
19
|
+
{
|
|
20
|
+
id: 'new-post',
|
|
21
|
+
label: 'New post',
|
|
22
|
+
group: 'Create',
|
|
23
|
+
shortcut: '⌘N',
|
|
24
|
+
icon: PencilIcon,
|
|
25
|
+
onSelect: newPost,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'settings',
|
|
29
|
+
label: 'Open settings',
|
|
30
|
+
description: 'Account, appearance, notifications',
|
|
31
|
+
keywords: ['preferences', 'account'],
|
|
32
|
+
group: 'Navigate',
|
|
33
|
+
onSelect: () => router.push('/settings'),
|
|
34
|
+
},
|
|
35
|
+
]}
|
|
36
|
+
/>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Which prop decides what
|
|
40
|
+
|
|
41
|
+
| prop | decides |
|
|
42
|
+
| --------------- | ----------------------------------------------------------- |
|
|
43
|
+
| `visible` / `onClose` | whether it is open — controlled |
|
|
44
|
+
| `items` | everything that can be run |
|
|
45
|
+
| `query` / `onQueryChange` | the search text, if you want to control it |
|
|
46
|
+
| `filter` | replaces the default match |
|
|
47
|
+
| `emptyText` | what an empty result says |
|
|
48
|
+
| `maxListHeight` | how tall the list may grow |
|
|
49
|
+
|
|
50
|
+
## `keywords` is what makes a palette usable
|
|
51
|
+
|
|
52
|
+
A palette is only as good as its synonyms. A user looking for "preferences"
|
|
53
|
+
will not type "settings", and a user looking for "dark mode" will not type
|
|
54
|
+
"appearance". `keywords` are matched alongside the label and description and
|
|
55
|
+
are never shown, so they cost nothing on screen.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
{ id: 'theme', label: 'Appearance', keywords: ['dark mode', 'light', 'theme'] }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## `group` is a heading, not a filter
|
|
62
|
+
|
|
63
|
+
Items are grouped under their `group` label in the order the groups first
|
|
64
|
+
appear. Grouping helps a user *scan* a list they did not search; once they type,
|
|
65
|
+
what matters is the match, not the category.
|
|
66
|
+
|
|
67
|
+
## `shortcut` is a reminder, not a binding
|
|
68
|
+
|
|
69
|
+
Setting it draws the key hint on the row. It does **not** register a keyboard
|
|
70
|
+
shortcut — the app owns its key handling, and a palette that silently bound
|
|
71
|
+
keys would collide with the page underneath.
|
|
72
|
+
|
|
73
|
+
## An item that cannot run should not be listed
|
|
74
|
+
|
|
75
|
+
Prefer omitting an inapplicable action to showing it `disabled`. A palette is
|
|
76
|
+
searched, not browsed: a disabled row is a match that answers the query and
|
|
77
|
+
then refuses, which is worse than no match at all. Keep `disabled` for the case
|
|
78
|
+
where the user needs to know the action *exists* here but is unavailable right
|
|
79
|
+
now.
|
|
80
|
+
|
|
81
|
+
## Command or Combobox
|
|
82
|
+
|
|
83
|
+
- **Command** — a list of **actions**, opened over the page, closed after one.
|
|
84
|
+
- **[Combobox](/docs/bloom/main/combobox)** — a list of **values**, inside a
|
|
85
|
+
form, producing a selection that stays.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: CompositionBar
|
|
3
|
+
description: What a total is made of — a segmented bar with a readout you can interrogate.
|
|
4
|
+
order: 62
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# CompositionBar
|
|
8
|
+
|
|
9
|
+
A single bar split into coloured segments, with a readout for the selected one.
|
|
10
|
+
Use it when the question is *what is this total made of* — a portfolio by
|
|
11
|
+
asset, spending by category, storage by file type.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { CompositionBar } from '@oxyhq/bloom/composition-bar';
|
|
15
|
+
|
|
16
|
+
<CompositionBar
|
|
17
|
+
categories={[
|
|
18
|
+
{ key: 'eth', name: 'Ethereum', amount: 4200, color: '#627EEA' },
|
|
19
|
+
{ key: 'btc', name: 'Bitcoin', amount: 3100, color: '#F7931A' },
|
|
20
|
+
{ key: 'other', name: 'Other', amount: 900, color: '#9CA3AF' },
|
|
21
|
+
]}
|
|
22
|
+
selectedKey={selected}
|
|
23
|
+
onSelect={setSelected}
|
|
24
|
+
hintLabel="Tap a segment"
|
|
25
|
+
/>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Which prop decides what
|
|
29
|
+
|
|
30
|
+
| prop | decides |
|
|
31
|
+
| ---------------- | --------------------------------------------------------- |
|
|
32
|
+
| `categories` | the segments — `key`, `name`, `amount`, `color` |
|
|
33
|
+
| `selectedKey` | which segment's readout is shown; `null` for none |
|
|
34
|
+
| `onSelect` | the press |
|
|
35
|
+
| `hintLabel` | the prompt shown while nothing is selected |
|
|
36
|
+
| `formatReadout` | how the selected amount and percentage are worded |
|
|
37
|
+
|
|
38
|
+
## The readout is the point
|
|
39
|
+
|
|
40
|
+
`selectedKey` and `onSelect` are required. A segmented bar with no way to ask
|
|
41
|
+
"which slice is that?" is decoration: the colours are unlabelled and the widths
|
|
42
|
+
are unreadable below about 5%.
|
|
43
|
+
|
|
44
|
+
`null` is a real state — nothing selected — and not the same as selecting the
|
|
45
|
+
first category on the user's behalf. `hintLabel` is what fills that space.
|
|
46
|
+
|
|
47
|
+
## Amounts, not percentages
|
|
48
|
+
|
|
49
|
+
Pass raw `amount`s and let the component compute the shares. Pre-computing
|
|
50
|
+
percentages is how the segments stop summing to 100 after rounding, which shows
|
|
51
|
+
up as a sliver of background at the end of the bar.
|
|
52
|
+
|
|
53
|
+
## Keep the category count low
|
|
54
|
+
|
|
55
|
+
Five or six. Beyond that the small segments are too thin to press and the
|
|
56
|
+
colours stop being distinguishable — group the tail into an "Other" category
|
|
57
|
+
with its own neutral colour, as above.
|
|
58
|
+
|
|
59
|
+
## `formatReadout`
|
|
60
|
+
|
|
61
|
+
The default states the amount and its share. Override it when the unit needs
|
|
62
|
+
saying (`$4,200`, `4.2 GB`) — a bare number in a readout is the same problem as
|
|
63
|
+
a bare number anywhere else.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ConnectionDots
|
|
3
|
+
description: An animated link between two things — an OAuth consent shape, not a loading state.
|
|
4
|
+
order: 68
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ConnectionDots
|
|
8
|
+
|
|
9
|
+
Two elements with a run of animated dots travelling between them. It is the
|
|
10
|
+
"this app is connecting to your account" illustration on a consent screen.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { ConnectionDots } from '@oxyhq/bloom/connection-dots';
|
|
14
|
+
|
|
15
|
+
<ConnectionDots
|
|
16
|
+
left={<AppLogo />}
|
|
17
|
+
right={<Avatar source={user.avatar} size={48} />}
|
|
18
|
+
accessibilityLabel="Notion is connecting to your Oxy account"
|
|
19
|
+
/>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Which prop decides what
|
|
23
|
+
|
|
24
|
+
| prop | decides |
|
|
25
|
+
| -------------------- | --------------------------------------------------- |
|
|
26
|
+
| `left` / `right` | the two endpoints — required |
|
|
27
|
+
| `dotCount` / `dotSize` | the run between them |
|
|
28
|
+
| `accessibilityLabel` | what the whole illustration means |
|
|
29
|
+
| `reducedMotion` | render the dots static instead of animating |
|
|
30
|
+
|
|
31
|
+
## It illustrates a relationship, not progress
|
|
32
|
+
|
|
33
|
+
The dots move continuously; they do not fill up. Nothing here says how far
|
|
34
|
+
along anything is, and using it while an operation runs implies a progress bar
|
|
35
|
+
that never advances. For work in flight use [`Loading`](/docs/bloom/main/loading).
|
|
36
|
+
|
|
37
|
+
## Give it a label
|
|
38
|
+
|
|
39
|
+
The whole point is carried by the picture: two parties and a link between them.
|
|
40
|
+
Without `accessibilityLabel` a screen reader gets an app logo, an avatar, and
|
|
41
|
+
nothing about the relationship — which is exactly the thing the user is being
|
|
42
|
+
asked to consent to.
|
|
43
|
+
|
|
44
|
+
Name both sides in the label, in the direction the consent runs.
|
|
45
|
+
|
|
46
|
+
## `reducedMotion`
|
|
47
|
+
|
|
48
|
+
Honour the OS setting. Perpetual motion is one of the more uncomfortable
|
|
49
|
+
patterns for motion-sensitive users, and this one runs for as long as the
|
|
50
|
+
screen is open. The static form still communicates the link.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ConnectionStatusToasts
|
|
3
|
+
description: Offline and reconnected, announced once — with a delay that hides the flap.
|
|
4
|
+
order: 70
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ConnectionStatusToasts
|
|
8
|
+
|
|
9
|
+
Mount it once and the app announces losing and regaining its connection.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { ConnectionStatusToasts } from '@oxyhq/bloom/connection-status';
|
|
13
|
+
|
|
14
|
+
<ConnectionStatusToasts />
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Which prop decides what
|
|
18
|
+
|
|
19
|
+
| prop | decides |
|
|
20
|
+
| ---------------------- | ----------------------------------------------------- |
|
|
21
|
+
| `offlineMessage` | shown while there is no connection |
|
|
22
|
+
| `reconnectingMessage` | shown after `reconnectingDelayMs` offline |
|
|
23
|
+
| `restoredMessage` | shown when the connection comes back |
|
|
24
|
+
| `reconnectingDelayMs` | how long to wait before escalating |
|
|
25
|
+
| `restoredDurationMs` | how long the restored toast stays |
|
|
26
|
+
|
|
27
|
+
## "Back online" is only shown to someone who saw the outage
|
|
28
|
+
|
|
29
|
+
The restored toast is suppressed unless an outage toast was actually up. A
|
|
30
|
+
"Back online" that appears on its own is confusing — the user never knew
|
|
31
|
+
anything was wrong, and the message implies they missed something.
|
|
32
|
+
|
|
33
|
+
## The delay is the point
|
|
34
|
+
|
|
35
|
+
Connectivity flaps. Announcing every dropped packet produces a toast that
|
|
36
|
+
appears and disappears while the user is reading, which is worse than silence.
|
|
37
|
+
The offline toast is shown immediately because the user is about to hit a
|
|
38
|
+
failure; the *reconnecting* escalation waits, so a two-second blip never
|
|
39
|
+
escalates at all.
|
|
40
|
+
|
|
41
|
+
## Optional peer
|
|
42
|
+
|
|
43
|
+
Connectivity comes from `@react-native-community/netinfo`, loaded through
|
|
44
|
+
Bloom's optional-require boundary. Without the package installed the component
|
|
45
|
+
mounts and does nothing rather than failing the build.
|
|
46
|
+
|
|
47
|
+
That is why the require is written the way it is — a literal specifier, as a
|
|
48
|
+
direct statement of a `try` block. Metro's optionality walk returns at the
|
|
49
|
+
first enclosing block, so one `if` of nesting inside the `try` would lose
|
|
50
|
+
optionality and turn a missing optional peer into a build failure.
|
|
51
|
+
|
|
52
|
+
## Mount it once
|
|
53
|
+
|
|
54
|
+
It presents through the shared [toast](/docs/bloom/main/toast) engine with a
|
|
55
|
+
fixed id, so a second mount does not duplicate the toast — but it does double
|
|
56
|
+
the listeners and the timers. One per app.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ContentPanel
|
|
3
|
+
description: The reading column — a width constraint that optionally becomes a framed surface.
|
|
4
|
+
order: 50
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ContentPanel
|
|
8
|
+
|
|
9
|
+
The column a screen's content lives in. It caps the reading width and, above a
|
|
10
|
+
breakpoint, decides whether that column reads as a framed surface or as the
|
|
11
|
+
page itself.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { ContentPanel } from '@oxyhq/bloom/content-panel';
|
|
15
|
+
|
|
16
|
+
<ContentPanel framed framedFrom={640}>
|
|
17
|
+
<SettingsScreen />
|
|
18
|
+
</ContentPanel>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Which prop decides what
|
|
22
|
+
|
|
23
|
+
| prop | decides |
|
|
24
|
+
| ------------------ | --------------------------------------------------------- |
|
|
25
|
+
| `framed` | whether the panel draws a surface at all |
|
|
26
|
+
| `framedFrom` | the breakpoint it starts framing at — `500` `640` `768` `1024` |
|
|
27
|
+
| `showStickyFrame` | whether the frame persists behind sticky content |
|
|
28
|
+
| `surfaceStyle` / `surfaceClassName` | the surface itself |
|
|
29
|
+
| `contentStyle` / `contentClassName` | the column inside it |
|
|
30
|
+
| `maskColor` | the colour the gutter mask fades to |
|
|
31
|
+
|
|
32
|
+
## `framedFrom` is why the branch is not in your screen
|
|
33
|
+
|
|
34
|
+
A phone wants a plain column: a frame a few pixels from the screen edge reads
|
|
35
|
+
as a rendering bug, not as a card. A desktop wants the framed surface. Handling
|
|
36
|
+
that in the screen means every screen re-derives the same breakpoint, and they
|
|
37
|
+
drift.
|
|
38
|
+
|
|
39
|
+
## Panels are siblings, never nested
|
|
40
|
+
|
|
41
|
+
`ContentPanel` **throws** if you nest one inside another. That is deliberate: a
|
|
42
|
+
panel in a panel would frame inside a frame and inset the reading column twice,
|
|
43
|
+
which looks merely "a bit padded" and would survive review indefinitely. A loud
|
|
44
|
+
failure in development is the cheaper outcome.
|
|
45
|
+
|
|
46
|
+
Two columns side by side are two panels as **siblings**:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<View style={{ flexDirection: 'row', gap: 16 }}>
|
|
50
|
+
<View style={{ flex: 2 }}><ContentPanel framed>…</ContentPanel></View>
|
|
51
|
+
<View style={{ flex: 1 }}><ContentPanel framed>…</ContentPanel></View>
|
|
52
|
+
</View>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A screen picks one panel per column; the sections inside it are plain views.
|
|
56
|
+
|
|
57
|
+
## Do not put a Card immediately inside a framed panel
|
|
58
|
+
|
|
59
|
+
They are both surfaces. A card filling a framed panel is two borders with a few
|
|
60
|
+
pixels between them. Use the panel's frame, and reach for
|
|
61
|
+
[`Card`](/docs/bloom/main/card) for things *within* the column that are
|
|
62
|
+
genuinely separate objects.
|