@oxy.so/bloom 2.12.4 → 3.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 +4 -4
- package/docs/button-group.mdx +46 -4
- package/docs/composition.mdx +159 -0
- package/docs/field.mdx +39 -1
- package/docs/glass.mdx +46 -0
- package/docs/layout.mdx +72 -4
- package/docs/migrating-to-3.0.mdx +169 -0
- package/docs/page-header.mdx +192 -36
- package/lib/commonjs/button/Button.js +2 -0
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/button-group/ButtonGroup.js +124 -19
- package/lib/commonjs/button-group/ButtonGroup.js.map +1 -1
- package/lib/commonjs/control-surface/ControlSurface.js +46 -0
- package/lib/commonjs/control-surface/ControlSurface.js.map +1 -0
- package/lib/commonjs/control-surface/context.js +84 -0
- package/lib/commonjs/control-surface/context.js.map +1 -0
- package/lib/commonjs/control-surface/index.js +26 -0
- package/lib/commonjs/control-surface/index.js.map +1 -0
- package/lib/commonjs/control-surface/types.js +6 -0
- package/lib/commonjs/control-surface/types.js.map +1 -0
- package/lib/commonjs/field/Field.js +71 -26
- package/lib/commonjs/field/Field.js.map +1 -1
- package/lib/commonjs/field/context.js +80 -0
- package/lib/commonjs/field/context.js.map +1 -0
- package/lib/commonjs/field/index.js +7 -0
- package/lib/commonjs/field/index.js.map +1 -1
- package/lib/commonjs/floating/TriggerSlot.js +32 -0
- package/lib/commonjs/floating/TriggerSlot.js.map +1 -1
- package/lib/commonjs/glass/GlassIsland.js +107 -0
- package/lib/commonjs/glass/GlassIsland.js.map +1 -0
- package/lib/commonjs/glass/GlassSurface.js +4 -1
- package/lib/commonjs/glass/GlassSurface.js.map +1 -1
- package/lib/commonjs/glass/index.js +7 -0
- package/lib/commonjs/glass/index.js.map +1 -1
- package/lib/commonjs/index.js +564 -517
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +564 -517
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/label/Label.js +4 -1
- package/lib/commonjs/label/Label.js.map +1 -1
- package/lib/commonjs/layout/bottom-edge.js +3 -42
- package/lib/commonjs/layout/bottom-edge.js.map +1 -1
- package/lib/commonjs/layout/edge-store.js +65 -0
- package/lib/commonjs/layout/edge-store.js.map +1 -0
- package/lib/commonjs/layout/index.js +32 -0
- package/lib/commonjs/layout/index.js.map +1 -1
- package/lib/commonjs/layout/scroll-offset.js +83 -0
- package/lib/commonjs/layout/scroll-offset.js.map +1 -0
- package/lib/commonjs/layout/top-edge.js +109 -0
- package/lib/commonjs/layout/top-edge.js.map +1 -0
- package/lib/commonjs/page-header/EdgeScrim.js +136 -0
- package/lib/commonjs/page-header/EdgeScrim.js.map +1 -0
- package/lib/commonjs/page-header/PageHeader.js +236 -61
- package/lib/commonjs/page-header/PageHeader.js.map +1 -1
- package/lib/commonjs/provider/BloomProvider.js +5 -2
- package/lib/commonjs/provider/BloomProvider.js.map +1 -1
- package/lib/commonjs/social-button/OxyMark.js +2 -2
- package/lib/commonjs/social-button/OxyMark.js.map +1 -1
- package/lib/commonjs/social-button/SocialButton.js +2 -2
- package/lib/commonjs/social-button/SocialButton.js.map +1 -1
- package/lib/commonjs/styles/svg-id.js +29 -0
- package/lib/commonjs/styles/svg-id.js.map +1 -0
- package/lib/commonjs/text-field/TextField.js +39 -6
- package/lib/commonjs/text-field/TextField.js.map +1 -1
- package/lib/commonjs/theme/glass-colors.js +81 -1
- package/lib/commonjs/theme/glass-colors.js.map +1 -1
- package/lib/module/button/Button.js +2 -0
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/button-group/ButtonGroup.js +124 -19
- package/lib/module/button-group/ButtonGroup.js.map +1 -1
- package/lib/module/control-surface/ControlSurface.js +41 -0
- package/lib/module/control-surface/ControlSurface.js.map +1 -0
- package/lib/module/control-surface/context.js +77 -0
- package/lib/module/control-surface/context.js.map +1 -0
- package/lib/module/control-surface/index.js +5 -0
- package/lib/module/control-surface/index.js.map +1 -0
- package/lib/module/control-surface/types.js +4 -0
- package/lib/module/control-surface/types.js.map +1 -0
- package/lib/module/field/Field.js +72 -27
- package/lib/module/field/Field.js.map +1 -1
- package/lib/module/field/context.js +74 -0
- package/lib/module/field/context.js.map +1 -0
- package/lib/module/field/index.js +1 -0
- package/lib/module/field/index.js.map +1 -1
- package/lib/module/floating/TriggerSlot.js +33 -1
- package/lib/module/floating/TriggerSlot.js.map +1 -1
- package/lib/module/glass/GlassIsland.js +102 -0
- package/lib/module/glass/GlassIsland.js.map +1 -0
- package/lib/module/glass/GlassSurface.js +5 -2
- package/lib/module/glass/GlassSurface.js.map +1 -1
- package/lib/module/glass/index.js +1 -0
- package/lib/module/glass/index.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/label/Label.js +4 -1
- package/lib/module/label/Label.js.map +1 -1
- package/lib/module/layout/bottom-edge.js +2 -41
- package/lib/module/layout/bottom-edge.js.map +1 -1
- package/lib/module/layout/edge-store.js +58 -0
- package/lib/module/layout/edge-store.js.map +1 -0
- package/lib/module/layout/index.js +8 -3
- package/lib/module/layout/index.js.map +1 -1
- package/lib/module/layout/scroll-offset.js +77 -0
- package/lib/module/layout/scroll-offset.js.map +1 -0
- package/lib/module/layout/top-edge.js +102 -0
- package/lib/module/layout/top-edge.js.map +1 -0
- package/lib/module/page-header/EdgeScrim.js +132 -0
- package/lib/module/page-header/EdgeScrim.js.map +1 -0
- package/lib/module/page-header/PageHeader.js +233 -58
- package/lib/module/page-header/PageHeader.js.map +1 -1
- package/lib/module/provider/BloomProvider.js +5 -2
- package/lib/module/provider/BloomProvider.js.map +1 -1
- package/lib/module/social-button/OxyMark.js +1 -1
- package/lib/module/social-button/OxyMark.js.map +1 -1
- package/lib/module/social-button/SocialButton.js +1 -1
- package/lib/module/social-button/SocialButton.js.map +1 -1
- package/lib/module/styles/svg-id.js +26 -0
- package/lib/module/styles/svg-id.js.map +1 -0
- package/lib/module/text-field/TextField.js +39 -6
- package/lib/module/text-field/TextField.js.map +1 -1
- package/lib/module/theme/glass-colors.js +79 -0
- package/lib/module/theme/glass-colors.js.map +1 -1
- package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
- package/lib/typescript/commonjs/button-group/ButtonGroup.d.ts.map +1 -1
- package/lib/typescript/commonjs/button-group/types.d.ts +61 -5
- package/lib/typescript/commonjs/button-group/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/control-surface/ControlSurface.d.ts +4 -0
- package/lib/typescript/commonjs/control-surface/ControlSurface.d.ts.map +1 -0
- package/lib/typescript/commonjs/control-surface/context.d.ts +21 -0
- package/lib/typescript/commonjs/control-surface/context.d.ts.map +1 -0
- package/lib/typescript/commonjs/control-surface/index.d.ts +4 -0
- package/lib/typescript/commonjs/control-surface/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/control-surface/types.d.ts +37 -0
- package/lib/typescript/commonjs/control-surface/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/field/Field.d.ts +1 -1
- package/lib/typescript/commonjs/field/Field.d.ts.map +1 -1
- package/lib/typescript/commonjs/field/context.d.ts +55 -0
- package/lib/typescript/commonjs/field/context.d.ts.map +1 -0
- package/lib/typescript/commonjs/field/index.d.ts +2 -0
- package/lib/typescript/commonjs/field/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/field/types.d.ts +22 -3
- package/lib/typescript/commonjs/field/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/floating/TriggerSlot.d.ts.map +1 -1
- package/lib/typescript/commonjs/glass/GlassIsland.d.ts +4 -0
- package/lib/typescript/commonjs/glass/GlassIsland.d.ts.map +1 -0
- package/lib/typescript/commonjs/glass/GlassSurface.d.ts.map +1 -1
- package/lib/typescript/commonjs/glass/index.d.ts +2 -1
- package/lib/typescript/commonjs/glass/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/glass/types.d.ts +43 -0
- package/lib/typescript/commonjs/glass/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/label/Label.d.ts.map +1 -1
- package/lib/typescript/commonjs/label/types.d.ts +7 -1
- package/lib/typescript/commonjs/label/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/bottom-edge.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/edge-store.d.ts +31 -0
- package/lib/typescript/commonjs/layout/edge-store.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/index.d.ts +8 -3
- package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/scroll-offset.d.ts +16 -0
- package/lib/typescript/commonjs/layout/scroll-offset.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/top-edge.d.ts +77 -0
- package/lib/typescript/commonjs/layout/top-edge.d.ts.map +1 -0
- package/lib/typescript/commonjs/page-header/EdgeScrim.d.ts +95 -0
- package/lib/typescript/commonjs/page-header/EdgeScrim.d.ts.map +1 -0
- package/lib/typescript/commonjs/page-header/PageHeader.d.ts +1 -1
- package/lib/typescript/commonjs/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/index.d.ts +1 -1
- package/lib/typescript/commonjs/page-header/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/types.d.ts +97 -10
- package/lib/typescript/commonjs/page-header/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/provider/BloomProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/svg-id.d.ts +20 -0
- package/lib/typescript/commonjs/styles/svg-id.d.ts.map +1 -0
- package/lib/typescript/commonjs/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/glass-colors.d.ts +67 -0
- package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.d.ts.map +1 -1
- package/lib/typescript/module/button-group/ButtonGroup.d.ts.map +1 -1
- package/lib/typescript/module/button-group/types.d.ts +61 -5
- package/lib/typescript/module/button-group/types.d.ts.map +1 -1
- package/lib/typescript/module/control-surface/ControlSurface.d.ts +4 -0
- package/lib/typescript/module/control-surface/ControlSurface.d.ts.map +1 -0
- package/lib/typescript/module/control-surface/context.d.ts +21 -0
- package/lib/typescript/module/control-surface/context.d.ts.map +1 -0
- package/lib/typescript/module/control-surface/index.d.ts +4 -0
- package/lib/typescript/module/control-surface/index.d.ts.map +1 -0
- package/lib/typescript/module/control-surface/types.d.ts +37 -0
- package/lib/typescript/module/control-surface/types.d.ts.map +1 -0
- package/lib/typescript/module/field/Field.d.ts +1 -1
- package/lib/typescript/module/field/Field.d.ts.map +1 -1
- package/lib/typescript/module/field/context.d.ts +55 -0
- package/lib/typescript/module/field/context.d.ts.map +1 -0
- package/lib/typescript/module/field/index.d.ts +2 -0
- package/lib/typescript/module/field/index.d.ts.map +1 -1
- package/lib/typescript/module/field/types.d.ts +22 -3
- package/lib/typescript/module/field/types.d.ts.map +1 -1
- package/lib/typescript/module/floating/TriggerSlot.d.ts.map +1 -1
- package/lib/typescript/module/glass/GlassIsland.d.ts +4 -0
- package/lib/typescript/module/glass/GlassIsland.d.ts.map +1 -0
- package/lib/typescript/module/glass/GlassSurface.d.ts.map +1 -1
- package/lib/typescript/module/glass/index.d.ts +2 -1
- package/lib/typescript/module/glass/index.d.ts.map +1 -1
- package/lib/typescript/module/glass/types.d.ts +43 -0
- package/lib/typescript/module/glass/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/label/Label.d.ts.map +1 -1
- package/lib/typescript/module/label/types.d.ts +7 -1
- package/lib/typescript/module/label/types.d.ts.map +1 -1
- package/lib/typescript/module/layout/bottom-edge.d.ts.map +1 -1
- package/lib/typescript/module/layout/edge-store.d.ts +31 -0
- package/lib/typescript/module/layout/edge-store.d.ts.map +1 -0
- package/lib/typescript/module/layout/index.d.ts +8 -3
- package/lib/typescript/module/layout/index.d.ts.map +1 -1
- package/lib/typescript/module/layout/scroll-offset.d.ts +16 -0
- package/lib/typescript/module/layout/scroll-offset.d.ts.map +1 -0
- package/lib/typescript/module/layout/top-edge.d.ts +77 -0
- package/lib/typescript/module/layout/top-edge.d.ts.map +1 -0
- package/lib/typescript/module/page-header/EdgeScrim.d.ts +95 -0
- package/lib/typescript/module/page-header/EdgeScrim.d.ts.map +1 -0
- package/lib/typescript/module/page-header/PageHeader.d.ts +1 -1
- package/lib/typescript/module/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/module/page-header/index.d.ts +1 -1
- package/lib/typescript/module/page-header/index.d.ts.map +1 -1
- package/lib/typescript/module/page-header/types.d.ts +97 -10
- package/lib/typescript/module/page-header/types.d.ts.map +1 -1
- package/lib/typescript/module/provider/BloomProvider.d.ts.map +1 -1
- package/lib/typescript/module/styles/svg-id.d.ts +20 -0
- package/lib/typescript/module/styles/svg-id.d.ts.map +1 -0
- package/lib/typescript/module/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/module/theme/glass-colors.d.ts +67 -0
- package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
- package/package.json +15 -1
- package/src/button/Button.tsx +10 -1
- package/src/button-group/ButtonGroup.tsx +146 -19
- package/src/button-group/types.ts +68 -5
- package/src/control-surface/ControlSurface.tsx +41 -0
- package/src/control-surface/context.ts +81 -0
- package/src/control-surface/index.ts +4 -0
- package/src/control-surface/types.ts +40 -0
- package/src/field/Field.tsx +84 -27
- package/src/field/context.ts +110 -0
- package/src/field/index.ts +3 -0
- package/src/field/types.ts +22 -3
- package/src/floating/TriggerSlot.tsx +35 -1
- package/src/glass/GlassIsland.tsx +109 -0
- package/src/glass/GlassSurface.tsx +26 -2
- package/src/glass/index.ts +3 -0
- package/src/glass/types.ts +45 -0
- package/src/index.ts +6 -0
- package/src/index.web.ts +6 -0
- package/src/label/Label.tsx +3 -1
- package/src/label/types.ts +7 -1
- package/src/layout/bottom-edge.tsx +3 -56
- package/src/layout/edge-store.ts +69 -0
- package/src/layout/index.ts +8 -3
- package/src/layout/scroll-offset.ts +76 -0
- package/src/layout/top-edge.tsx +109 -0
- package/src/page-header/EdgeScrim.tsx +118 -0
- package/src/page-header/PageHeader.tsx +253 -61
- package/src/page-header/index.ts +9 -1
- package/src/page-header/types.ts +103 -10
- package/src/provider/BloomProvider.tsx +12 -1
- package/src/social-button/OxyMark.tsx +1 -1
- package/src/social-button/SocialButton.tsx +1 -1
- package/src/styles/svg-id.ts +23 -0
- package/src/text-field/TextField.tsx +44 -5
- package/src/theme/glass-colors.ts +80 -0
- package/lib/commonjs/social-button/use-svg-id.js +0 -16
- package/lib/commonjs/social-button/use-svg-id.js.map +0 -1
- package/lib/module/social-button/use-svg-id.js +0 -13
- package/lib/module/social-button/use-svg-id.js.map +0 -1
- package/lib/typescript/commonjs/social-button/use-svg-id.d.ts +0 -7
- package/lib/typescript/commonjs/social-button/use-svg-id.d.ts.map +0 -1
- package/lib/typescript/module/social-button/use-svg-id.d.ts +0 -7
- package/lib/typescript/module/social-button/use-svg-id.d.ts.map +0 -1
- package/src/social-button/use-svg-id.ts +0 -10
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
### What it is
|
|
22
22
|
|
|
23
|
-
Components, hooks and design tokens published as `@oxy.so/bloom` across
|
|
23
|
+
Components, hooks and design tokens published as `@oxy.so/bloom` across 168 subpath exports, shipped as `src` for Metro and as compiled CommonJS and ESM for everyone else. Web builds resolve platform forks automatically through export conditions, so a `.web.tsx` fork never reaches a native bundle.
|
|
24
24
|
|
|
25
25
|
Styling is NativeWind classes throughout. There are no colour props and no wrapper components to theme a button, because a second way to set a colour is a second thing that can disagree.
|
|
26
26
|
|
|
@@ -151,11 +151,11 @@ Every palette is generated from a single seed colour by a dependency free colour
|
|
|
151
151
|
|
|
152
152
|
## Components
|
|
153
153
|
|
|
154
|
-
Bloom publishes
|
|
154
|
+
Bloom publishes 168 subpath exports. Importing from the subpath rather than the root keeps a bundle to what it actually renders.
|
|
155
155
|
|
|
156
156
|
| Group | Exports |
|
|
157
157
|
|---|---|
|
|
158
|
-
| Providers and theme | `provider`, `theme`, `theme-toggle`, `color-presets`, `preset-vars`, `design-tokens`, `tailwind-preset`, `styles`, `hooks` |
|
|
158
|
+
| Providers and theme | `provider`, `theme`, `theme-toggle`, `color-presets`, `preset-vars`, `design-tokens`, `tailwind-preset`, `styles`, `control-surface`, `hooks` |
|
|
159
159
|
| App shell and navigation | `app-shell`, `page-header`, `sidebar`, `rail`, `breadcrumb`, `pagination`, `tabs`, `tab-bar`, `layout`, `content-panel` |
|
|
160
160
|
| Overlays | `dialog`, `alert-dialog`, `bottom-sheet`, `popover`, `context-menu`, `dropdown-menu`, `menubar`, `tooltip`, `overlay`, `portal`, `teleport`, `surfaces`, `settings-modal` |
|
|
161
161
|
| Actions | `button`, `button-group`, `social-button`, `fab`, `frosted-icon-button`, `pressable-scale`, `subtle-hover` |
|
|
@@ -175,7 +175,7 @@ Bloom publishes 167 subpath exports. Importing from the subpath rather than the
|
|
|
175
175
|
|
|
176
176
|
## Documentation
|
|
177
177
|
|
|
178
|
-
Component guides live in [`docs/`](./docs), one `.mdx` per family, and **they ship inside the package** at `node_modules/@oxy.so/bloom/docs/`. The copy you read there always describes the version you installed, which matters most for [migrating to
|
|
178
|
+
Component guides live in [`docs/`](./docs), one `.mdx` per family, and **they ship inside the package** at `node_modules/@oxy.so/bloom/docs/`. The copy you read there always describes the version you installed, which matters most for [migrating to 3.0](./docs/migrating-to-3.0.mdx).
|
|
179
179
|
|
|
180
180
|
Start with [getting started](./docs/getting-started.mdx), [theme](./docs/theme.mdx) and [design tokens](./docs/design-tokens.mdx). Then per surface: [app shell](./docs/app-shell.mdx), [sidebar](./docs/sidebar.mdx), [data table](./docs/data-table.mdx), [chart cards](./docs/chart-cards.mdx), [composer panel](./docs/composer-panel.mdx), [dialog](./docs/dialog.mdx), [bottom sheet](./docs/bottom-sheet.mdx), [toast](./docs/toast.mdx), [alert](./docs/alert.mdx), [dropdown menu](./docs/dropdown-menu.mdx), [select](./docs/select.mdx), [context menu](./docs/context-menu.mdx), [button](./docs/button.mdx), [avatar](./docs/avatar.mdx), [text field](./docs/text-field.mdx), [settings list](./docs/settings-list.mdx), [tab bar](./docs/tab-bar.mdx) and [loading](./docs/loading.mdx).
|
|
181
181
|
|
package/docs/button-group.mdx
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: ButtonGroup
|
|
3
|
-
description:
|
|
3
|
+
description: Buttons the consumer declared as one control — a fused bordered pill, or one translucent island — for toolbars, filters, pagers and a floating header's actions.
|
|
4
4
|
order: 11
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# ButtonGroup
|
|
8
8
|
|
|
9
|
-
A row of
|
|
9
|
+
A row of buttons declared as ONE control. Two materials:
|
|
10
|
+
|
|
11
|
+
- **`solid`** (default): secondary-style buttons fused into one bordered, shadowed container, items divided by 1px hairlines, only the outer ends rounded. The container is a full pill, like `Button`. Colours come from the same Bloom-derived ramps as `Button` (`src/button/shared.ts`).
|
|
12
|
+
- **`glass`**: the same group as a floating **island** — one translucent pane the items sit flush on, with no fill and no blur of their own.
|
|
10
13
|
|
|
11
14
|
```tsx
|
|
12
15
|
import { ButtonGroup, ButtonGroupItem } from '@oxy.so/bloom/button-group';
|
|
@@ -18,6 +21,40 @@ import { ButtonGroup, ButtonGroupItem } from '@oxy.so/bloom/button-group';
|
|
|
18
21
|
</ButtonGroup>
|
|
19
22
|
```
|
|
20
23
|
|
|
24
|
+
## Materials
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
<ButtonGroup variant="glass" accessibilityLabel="Page actions">
|
|
28
|
+
<ButtonGroupItem iconOnly leadingIcon={RiSearchLine} accessibilityLabel="Search" />
|
|
29
|
+
<ButtonGroupItem iconOnly leadingIcon={RiShare2Line} accessibilityLabel="Share" />
|
|
30
|
+
</ButtonGroup>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The variant is **inherited** when you do not name it: a `ButtonGroup` inside a floating [`PageHeader`](/docs/page-header)'s `actions` is glass with no prop written on it, and the same group inside a card is solid without one either. An explicit `variant` still wins. The rule and its limits are in [Composition](/docs/composition).
|
|
34
|
+
|
|
35
|
+
On glass:
|
|
36
|
+
|
|
37
|
+
- The GROUP owns the material. Items paint `transparent` at rest, and their hover/press states are an alpha of the theme's own `text` — an ink wash, not a fill. That is the one recipe that survives a backdrop Bloom cannot predict: a fixed neutral tuned against the island's fill disappears the moment a photograph moves the pane 40 levels, while a wash of the label's colour keeps the same relation to the label at every composite.
|
|
38
|
+
- Items round their own press highlight, because the island does not clip. (`overflow: hidden` on a glass box clips the iOS drop shadow away.)
|
|
39
|
+
- `dividers` defaults to **false**. A hairline is how a solid pill says "separate controls, fused"; over a moving backdrop it reads as an artifact of the material. Pass `dividers` to draw them anyway.
|
|
40
|
+
|
|
41
|
+
## Items as triggers
|
|
42
|
+
|
|
43
|
+
`ButtonGroupItem` declares the props an anchored family's `asChild` trigger merges onto it, so a menu button inside a group is an item OF that group rather than a second control that looks like one:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
<ButtonGroup accessibilityLabel="Page actions">
|
|
47
|
+
<Popover>
|
|
48
|
+
<PopoverTrigger asChild label="More">
|
|
49
|
+
<ButtonGroupItem iconOnly leadingIcon={RiMore2Line} accessibilityLabel="More" />
|
|
50
|
+
</PopoverTrigger>
|
|
51
|
+
<PopoverContent label="More">…</PopoverContent>
|
|
52
|
+
</Popover>
|
|
53
|
+
</ButtonGroup>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The item announces its own expanded state on both platforms. A control whose props are a closed list would have dropped `aria-expanded`, `aria-haspopup` and `onLongPress` silently, since all of them are optional — see [Composition](/docs/composition).
|
|
57
|
+
|
|
21
58
|
## Selection
|
|
22
59
|
|
|
23
60
|
`selected` paints an item with its hover fill and announces it as pressed (`aria-pressed` on web, `accessibilityState.selected` on native). Drive it from state for single- or multi-select toolbars; leave every item unselected for a pure action row. For an animated thumb, use `SegmentedControl`.
|
|
@@ -46,11 +83,16 @@ Text is `body-medium` (Inter 14/20 500) through Bloom's typography `Text` — a
|
|
|
46
83
|
|
|
47
84
|
| Prop | Type | Default | Description |
|
|
48
85
|
|------|------|---------|-------------|
|
|
49
|
-
| `ButtonGroup.
|
|
86
|
+
| `ButtonGroup.variant?` | `'solid' \| 'glass'` | inherited, then `'solid'` | The material the group paints. |
|
|
87
|
+
| `ButtonGroup.size?` | `'medium' \| 'small'` | inherited, then `'medium'` | Size for every item. |
|
|
88
|
+
| `ButtonGroup.dividers?` | `boolean` | `solid` → `true`, `glass` → `false` | Hairlines between items. |
|
|
50
89
|
| `ButtonGroup.accessibilityLabel?` | `string` | — | Names the group. |
|
|
51
|
-
| `ButtonGroupItem.onPress?` | `() => void` | — | Press handler. |
|
|
90
|
+
| `ButtonGroupItem.onPress?` | `(event) => void` | — | Press handler. Receives the event, so a composed trigger handler can `preventDefault()` it. |
|
|
52
91
|
| `ButtonGroupItem.selected?` | `boolean` | `false` | Hover fill + pressed state. |
|
|
53
92
|
| `ButtonGroupItem.disabled?` | `boolean` | `false` | Disabled paint, blocks presses. |
|
|
54
93
|
| `ButtonGroupItem.iconOnly?` | `boolean` | `false` | Square icon-only item. |
|
|
55
94
|
| `ButtonGroupItem.leadingIcon?` / `trailingIcon?` | `BloomIconComponent` | — | Icon components. |
|
|
56
95
|
| `ButtonGroupItem.size?` | `'medium' \| 'small'` | group size | Per-item override. |
|
|
96
|
+
| `ButtonGroupItem.variant?` | `'solid' \| 'glass'` | group material | Per-item override. Rarely needed — the group owns it. |
|
|
97
|
+
| `ButtonGroupItem.hitSlop?` | `number \| {top,bottom,left,right}` | — | Extra touch area. NOT defaulted: items in a group are adjacent, so slop on one overlaps its neighbour. Use it for an item that stands alone in its own island. |
|
|
98
|
+
| `ButtonGroupItem.onLongPress?` / `aria-expanded` / `aria-haspopup` / `accessibilityRole` | | — | The trigger contract, set by an anchored family. |
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Composition
|
|
3
|
+
description: How Bloom components work together — the inherited presentation contract, field membership, trigger composition, scroll ownership and edge claims — with what is done and what is not.
|
|
4
|
+
order: 3
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Composition
|
|
8
|
+
|
|
9
|
+
Bloom components share tokens. Sharing tokens is not the same as working together: a control still has to be told what material the container around it is painting, a label still has to be wired to the control it names, and a header still has to know which scroller it is following. Written as props, each of those is a thing the app repeats at every call site and gets wrong at one of them.
|
|
10
|
+
|
|
11
|
+
This page is the set of small, explicit contracts that answer those questions, and the rules that keep them small. Each is a context a container publishes and a component opts into. None of them is a framework, a singleton, or a component that reads its children's types to decide what to do with them.
|
|
12
|
+
|
|
13
|
+
## The precedence rule
|
|
14
|
+
|
|
15
|
+
Every contract on this page resolves the same way:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
an explicit prop > the nearest container > the component's own default
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`undefined` is the only value that defers. Passing `variant="solid"` inside a glass island paints a solid control — the container is a default, not an override, and a caller can always win.
|
|
22
|
+
|
|
23
|
+
**A constraint is not a default.** Anything that RESTRICTS a control — `disabled`, a modal's inertness, the user's reduce-transparency or reduce-motion preference — travels on its own channel and combines with `||`, never `??`. The rule above is exactly wrong for those: a descendant must not be able to re-enable an action its group disabled by passing a prop.
|
|
24
|
+
|
|
25
|
+
**State does not travel either.** `checked`, `selected`, a field's value, a business datum: those belong to the component that holds them. A visual context that also carried state would make every container a place state can leak from.
|
|
26
|
+
|
|
27
|
+
## Control presentation — `ControlSurface`
|
|
28
|
+
|
|
29
|
+
`@oxy.so/bloom/control-surface`
|
|
30
|
+
|
|
31
|
+
What a container tells the controls inside it about presentation, and nothing else. Two fields, both visual configuration:
|
|
32
|
+
|
|
33
|
+
| field | values | meaning |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `material` | `'solid'` \| `'glass'` | the surface a control CONTAINER paints |
|
|
36
|
+
| `density` | `'medium'` \| `'small'` | the size controls default to |
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { ControlSurface } from '@oxy.so/bloom/control-surface';
|
|
40
|
+
|
|
41
|
+
<ControlSurface material="glass">
|
|
42
|
+
<ButtonGroup accessibilityLabel="Playback">…</ButtonGroup>
|
|
43
|
+
</ControlSurface>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
It renders no node of its own, so it can be dropped anywhere without moving anything. Bloom's own containers mount it for you — `glass/GlassIsland` does, which is why a `ButtonGroup` inside a floating `PageHeader` is glass with no prop written on it.
|
|
47
|
+
|
|
48
|
+
A field left `undefined` inherits rather than resetting: `<ControlSurface density="small">` inside a glass island stays glass.
|
|
49
|
+
|
|
50
|
+
**Who reads it today:** `ButtonGroup` and `ButtonGroupItem`. `Button` does not — a `Button` in a floating header keeps its own chrome, which is right for a primary action in its own capsule and wrong for an icon action that should sit flush on an island. Use `ButtonGroup`/`ButtonGroupItem` for the latter. Extending this to more families is adoption work, not a gap in the contract; see [What is not done](#what-is-not-done).
|
|
51
|
+
|
|
52
|
+
Precedence is gated in both directions by `src/__tests__/ControlSurface.test.tsx`, including the assertion that the context carries exactly two fields.
|
|
53
|
+
|
|
54
|
+
## Field membership — `Field`
|
|
55
|
+
|
|
56
|
+
`@oxy.so/bloom/field`
|
|
57
|
+
|
|
58
|
+
A `Field` used to draw a label, a control and a hint, and wire none of them together. It associates them now: the control's id, `aria-describedby` for whichever of description/error is showing, the invalid state, the required state and the disabled state.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<Field label="Email" description="We never share it." error={emailError} required>
|
|
62
|
+
<TextFieldInput label="Email" value={email} onChangeText={setEmail} />
|
|
63
|
+
</Field>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **Ids come from `useId`** when no `nativeID` is given, so they survive SSR and hydration. An explicit `nativeID` still owns every derived id.
|
|
67
|
+
- **The control's accessible NAME is the field's label.** On web a `<label for>` is a real association, but `aria-label` would override it; on native there is no association at all and the control must carry the name. So a control inside a field defers to the field's label text, and the visible label and the announced name cannot disagree.
|
|
68
|
+
- **`disabled` is a constraint.** A control inside a disabled field is disabled, and cannot opt out.
|
|
69
|
+
- **`multiple` is the several-controls case** — a radio set, a date's three boxes. The field becomes a labelled `group`, publishes no control id and describes the GROUP rather than each control: one id on three inputs is invalid HTML, and one error described by three inputs is announced three times.
|
|
70
|
+
- **A floating label and a `Field` label are alternatives, not layers.** `<TextFieldInput floatingLabel>` inside a `Field label="…"` shows the same words above the box and inside it. Pick one.
|
|
71
|
+
|
|
72
|
+
A control opts in by reading `useFieldControl()`:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
const field = useFieldControl();
|
|
76
|
+
<TextInput
|
|
77
|
+
nativeID={field?.controlId ?? nativeID}
|
|
78
|
+
aria-describedby={field?.describedBy}
|
|
79
|
+
aria-invalid={field?.invalid || isInvalid || undefined}
|
|
80
|
+
accessibilityLabel={field?.labelText ?? label}
|
|
81
|
+
editable={!(field?.disabled || disabled)}
|
|
82
|
+
/>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Opt-in, not magic. A child that does not read the context renders exactly as before — a container cannot give an arbitrary child behaviour by wrapping it, and one that inspected its children's types to try would be guessing.
|
|
86
|
+
|
|
87
|
+
**Who reads it today:** `TextFieldInput` (both the default and floating-label renderings). Gate: `src/__tests__/FieldAssociation.test.tsx`.
|
|
88
|
+
|
|
89
|
+
## Trigger composition — `asChild`
|
|
90
|
+
|
|
91
|
+
Every anchored family (`Popover`, `DropdownMenu`, `ContextMenu`, `Menubar`, `Select`) takes `asChild` and merges its open handler and a11y state onto the caller's single element child. Three properties are worth knowing:
|
|
92
|
+
|
|
93
|
+
- **The child's handler runs first, and can CANCEL the open.** `event.preventDefault()` in the child's `onPress` stops the surface from opening — for a trigger that has just found the form dirty, or the row it belongs to gone. It is the event's own vocabulary because both platforms hand `onPress` a React synthetic event. On web the same call also suppresses the browser's default for that click, so cancelling on an anchor child cancels its navigation too.
|
|
94
|
+
- **A fragment child is refused.** `<Trigger asChild><>{control}</></Trigger>` passes every check a clone can make and then drops every prop, because a fragment has no host node. It throws now, naming the fix.
|
|
95
|
+
- **`disabled` is read from BOTH sides** — the trigger's own prop and the child's — in `cloneTrigger`, not left to whatever element the caller passed.
|
|
96
|
+
|
|
97
|
+
**A control used as a trigger must DECLARE the props it will be handed.** A component whose props are a closed list drops the ones it does not name, silently, because all of them are optional. `ButtonGroupItem` declares them, so this works:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<ButtonGroup accessibilityLabel="Page actions">
|
|
101
|
+
<Popover>
|
|
102
|
+
<PopoverTrigger asChild label="More">
|
|
103
|
+
<ButtonGroupItem iconOnly leadingIcon={RiMore2Line} accessibilityLabel="More" />
|
|
104
|
+
</PopoverTrigger>
|
|
105
|
+
<PopoverContent label="More">…</PopoverContent>
|
|
106
|
+
</Popover>
|
|
107
|
+
</ButtonGroup>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Gate: `src/__tests__/TriggerComposition.test.tsx`.
|
|
111
|
+
|
|
112
|
+
## Scroll ownership — `ScrollOffsetProvider`
|
|
113
|
+
|
|
114
|
+
`@oxy.so/bloom/layout`
|
|
115
|
+
|
|
116
|
+
A screen has exactly one scroll owner, and the owner publishes its offset. Chrome reads it. Neither imports the other, so the provider's position in the tree is what pairs them — two panels side by side, each with its own header and its own scroller, resolve correctly.
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
const scrollY = useSharedValue(0);
|
|
120
|
+
const onScroll = useAnimatedScrollHandler(
|
|
121
|
+
{ onScroll: (e) => { scrollY.value = e.contentOffset.y; } },
|
|
122
|
+
[scrollY],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
<ScrollOffsetProvider value={scrollY}>
|
|
126
|
+
<PageHeader title="Inbox" />
|
|
127
|
+
<Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16}>…</Animated.ScrollView>
|
|
128
|
+
</ScrollOffsetProvider>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The context carries the `SharedValue`, not a number. It crosses once, at mount; every subsequent frame reaches the chrome on the UI thread through `useAnimatedStyle`, with no React render. A context whose value changed per frame would re-render every consumer per frame, which is the cost this exists to avoid.
|
|
132
|
+
|
|
133
|
+
`PageHeader` resolves its offset as: the `scrollY` prop, then this context, then `window.scrollY` on web. Without any of the three on native it stays at rest. The context step is the one that matters for a desktop shell, where `window.scrollY` never moves and a header that followed it looked deliberately inert.
|
|
134
|
+
|
|
135
|
+
## Edge claims — `TopEdgeProvider` / `BottomEdgeProvider`
|
|
136
|
+
|
|
137
|
+
`@oxy.so/bloom/layout`
|
|
138
|
+
|
|
139
|
+
A surface that parks at a window edge CLAIMS its footprint; a surface or a scroll container that must clear it READS the total. Claims combine with `max`, not sum — every claim is measured from the same edge, so two surfaces at that edge overlap rather than stack.
|
|
140
|
+
|
|
141
|
+
The bottom edge is the tab bar and the FAB (`docs/fab.mdx`). The top edge is chrome that overlays content:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
<ScrollView contentContainerStyle={{ paddingTop: useTopEdgeInset() }}>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`BloomProvider` mounts both. A nested provider scopes a subtree to its own claims — a full-screen modal with its own header, whose content must clear the MODAL's header rather than the screen's.
|
|
148
|
+
|
|
149
|
+
A claim registers in an effect, so a reader mounted in the same commit sees `0` on its first render. `PageHeader` narrows that to one frame by claiming its resting height (safe area + 56) immediately and replacing it with the measured height when `onLayout` reports.
|
|
150
|
+
|
|
151
|
+
## What is not done
|
|
152
|
+
|
|
153
|
+
This page describes contracts that exist and are gated. Issue #145 asks for more than that, and the rest is honest to name:
|
|
154
|
+
|
|
155
|
+
- **No full inventory of the public API.** The matrix over every family — platforms, contexts, state families, triggers, semantics, styles, tests — has not been produced. Nothing here should be read as "the library was audited"; it was extended where the header integration needed it.
|
|
156
|
+
- **State-family naming is not unified.** `value`/`defaultValue`/`onValueChange`, `checked`/`onCheckedChange` and `open`/`onOpenChange` are used consistently by the families that have them, and `useControllableState` is the shared implementation, but no sweep has been made to rename the exceptions (`onChangeText` among them, which is a React Native contract worth keeping).
|
|
157
|
+
- **Only two families read `ControlSurface`, and one reads `Field`.** Every other control renders as it always did. Adoption is per-family work with its own regressions to check.
|
|
158
|
+
- **`BloomProvider` still imports the Expo Router adapter directly.** Decoupling the core from the router is listed in #145 §8 and is not in this change.
|
|
159
|
+
- **No adaptive overflow.** Actions that do not fit are not collapsed into a menu. #145 says explicitly that this is not required for the first header, and nothing here does it.
|
package/docs/field.mdx
CHANGED
|
@@ -35,7 +35,45 @@ import { TextField, TextFieldInput } from '@oxy.so/bloom/text-field';
|
|
|
35
35
|
| `error` | the message; `null` or omitted means valid |
|
|
36
36
|
| `required` | the required marker and the announced requirement |
|
|
37
37
|
| `disabled` | dims the whole group |
|
|
38
|
-
| `
|
|
38
|
+
| `multiple` | the field holds several controls — a labelled `group` |
|
|
39
|
+
| `nativeID` | the id the label and control are wired through; generated when omitted |
|
|
40
|
+
|
|
41
|
+
## What the wiring actually does
|
|
42
|
+
|
|
43
|
+
The label, description and error are ASSOCIATED with the control, not merely stacked around it. A control opts in by reading `useFieldControl()`; Bloom's `TextFieldInput` does.
|
|
44
|
+
|
|
45
|
+
| the field publishes | the control applies |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `controlId` | its `nativeID`, so the label's `for` points at it |
|
|
48
|
+
| `describedBy` | `aria-describedby` — the description, or the error once there is one |
|
|
49
|
+
| `labelText` | its accessible NAME |
|
|
50
|
+
| `invalid` | `aria-invalid` |
|
|
51
|
+
| `required` | `aria-required` |
|
|
52
|
+
| `disabled` | its own disabled state |
|
|
53
|
+
|
|
54
|
+
Three things worth knowing:
|
|
55
|
+
|
|
56
|
+
- **The ids come from `useId`** when no `nativeID` is given, so they survive SSR and hydration. An explicit `nativeID` still owns every derived id (`<id>-description`, `<id>-error`), so a form already wired by hand keeps working.
|
|
57
|
+
- **The field's label wins over the control's own.** On web `aria-label` OVERRIDES a `<label for>`, and on native there is no association at all and the control must carry the name — so a control inside a field defers to the field's label text, and the visible label and the announced name cannot disagree.
|
|
58
|
+
- **`disabled` is a constraint, not a default.** A control inside a disabled field is disabled and cannot opt out.
|
|
59
|
+
|
|
60
|
+
Opt-in, not magic: a child that does not read the context renders exactly as before. A container cannot give an arbitrary child behaviour by wrapping it, and one that inspected its children's types to try would be guessing. See [Composition](/docs/composition).
|
|
61
|
+
|
|
62
|
+
## One control, or several
|
|
63
|
+
|
|
64
|
+
A field with ONE control gives it the field's id and describes the error BY that control. A field with several — a radio set, a date's three boxes — must not: three inputs sharing one id is invalid HTML, and one error described by three inputs is announced three times.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<Field label="Date of birth" multiple error={dobError}>
|
|
68
|
+
<DayInput /> <MonthInput /> <YearInput />
|
|
69
|
+
</Field>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`multiple` makes the field a `role="group"` labelled by its own label and described by its own hint, and publishes no control id. Each control keeps its own id and its own name.
|
|
73
|
+
|
|
74
|
+
## Floating labels are an alternative, not a layer
|
|
75
|
+
|
|
76
|
+
`<TextFieldInput floatingLabel>` inside a `Field label="…"` shows the same words above the box and inside it. The ids and the described-by still travel, so the error is still announced — but pick one of the two labels.
|
|
39
77
|
|
|
40
78
|
## `error` is a string, not a boolean
|
|
41
79
|
|
package/docs/glass.mdx
CHANGED
|
@@ -92,3 +92,49 @@ Three things the table settles:
|
|
|
92
92
|
|
|
93
93
|
So there is no single alpha that both matches the reference and reads as glass on a flat surface. That is a property of the material — a blur needs a backdrop — not a constant nobody has tuned yet. To make a specific surface read as glass, put something behind it.
|
|
94
94
|
|
|
95
|
+
|
|
96
|
+
## The chrome role
|
|
97
|
+
|
|
98
|
+
Everything above prices a **brand** fill: a `primary`-coloured pane over one of Bloom's own five neutral surfaces, labelled with that fill's on-colour. A page header's island is the other case, and almost nothing about it is the same:
|
|
99
|
+
|
|
100
|
+
| | accent | chrome |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| fill | the caller's brand token | rung 1 of the surface ladder — the fill a card and a menu panel paint |
|
|
103
|
+
| label | the fill's own on-colour | the theme's own `text` |
|
|
104
|
+
| backdrop | Bloom's five neutral surfaces | anything: a photograph, a video still, a map |
|
|
105
|
+
| alpha | 0.85 | 0.72 light · **0.80 dark** |
|
|
106
|
+
|
|
107
|
+
Inside Bloom — `theme/glass-colors.ts` is not a published subpath, for the same reason `resolveGlassColors` is not:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { resolveSurfaceLevel } from '../styles/surface-levels';
|
|
111
|
+
import { resolveChromeGlassColors } from '../theme/glass-colors';
|
|
112
|
+
|
|
113
|
+
const level = resolveSurfaceLevel(theme, 1);
|
|
114
|
+
const glass = resolveChromeGlassColors(level.background, level.border, theme.isDark);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Both arguments are resolved colour strings the caller already holds, because the caller is the only one that knows which rung of the ladder its container sits on.
|
|
118
|
+
|
|
119
|
+
**Dark carries the higher alpha**, which is the opposite of the intuition that a dark UI can afford more transparency. The worst case for each mode is the backdrop it is furthest from: a light pane is washed *down* over black and barely moves, because it is already near white; a dark pane over white has the whole luminance range above it to be washed out into.
|
|
120
|
+
|
|
121
|
+
Because the backdrop is unknown, the range measured is the **whole** range — pure white and pure black. Every composite a real backdrop can produce lies between them on every channel, so a pane that clears AA at both ends clears it everywhere between.
|
|
122
|
+
|
|
123
|
+
Measured over 64 presets × 2 modes × 2 endpoints × 2 platforms (512 rows), with the theme's own `text` as the label:
|
|
124
|
+
|
|
125
|
+
| | failures below AA | worst ratio |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| light | 0 / 256 | **8.23** (over black, web) |
|
|
128
|
+
| dark | 0 / 256 | **6.43** (over white, web) |
|
|
129
|
+
|
|
130
|
+
and the pane still moves **49.98–69.97** of 255 between a black and a white backdrop, so it is glass rather than a tinted slab. Web is the worse of the two platforms in both modes: `expo-blur`'s own tint adds body on native that CSS `backdrop-filter` does not.
|
|
131
|
+
|
|
132
|
+
Gate: `src/theme/__tests__/chrome-glass.test.ts`, which pins the two floors to the hundredth and the translucency range with **literal** bounds. A floor derived from the alpha constant would move with it, so an opaque material would satisfy its own floor.
|
|
133
|
+
|
|
134
|
+
## Islands
|
|
135
|
+
|
|
136
|
+
`glass/GlassIsland` is the assembly: the material, the hairline, the geometry and the drop shadow, in the order that works. `GlassSurface` deliberately owns none of the last three — it clips itself, and a shadow drawn on a clipping node is clipped away on iOS — so every caller that wants a floating pane would otherwise repeat the same four steps.
|
|
137
|
+
|
|
138
|
+
An island also mounts a `ControlSurface` with `material: 'glass'`, so controls inside it paint flush. That is the material's rule rather than a preference: a translucent pane inside a translucent pane composites two alphas and two blurs for one pane's worth of depth, and on Android the second blur is not available at all.
|
|
139
|
+
|
|
140
|
+
Internal (`glass/` is not a published subpath). It is reached through `ButtonGroup`'s `glass` variant and `PageHeader`'s floating presentation.
|
package/docs/layout.mdx
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Layout
|
|
3
|
-
description: Window-edge geometry — how far a floating surface sits from
|
|
3
|
+
description: Window-edge geometry and screen-scope coordination — how far a floating surface sits from an edge, what is already parked there, and which scroller the chrome follows.
|
|
4
4
|
order: 76
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Layout
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
them.
|
|
9
|
+
The questions every floating surface and every piece of scroll-linked chrome has
|
|
10
|
+
to answer, answered once for all of them.
|
|
11
11
|
|
|
12
12
|
```tsx
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
EDGE_GAP,
|
|
15
|
+
windowEdgeGap,
|
|
16
|
+
useBottomEdgeInset,
|
|
17
|
+
useClaimBottomEdge,
|
|
18
|
+
useTopEdgeInset,
|
|
19
|
+
useClaimTopEdge,
|
|
20
|
+
ScrollOffsetProvider,
|
|
21
|
+
useScrollOffset,
|
|
22
|
+
} from '@oxy.so/bloom/layout';
|
|
14
23
|
```
|
|
15
24
|
|
|
16
25
|
## `windowEdgeGap` — how far from the edge
|
|
@@ -59,6 +68,65 @@ FAB with no tab bar, both, or a surface written later.
|
|
|
59
68
|
`BloomProvider` mounts the registry. Outside one, `useBottomEdgeInset()` returns
|
|
60
69
|
`0` and `useClaimBottomEdge` is a no-op, so a surface stays usable standalone.
|
|
61
70
|
|
|
71
|
+
## `useTopEdgeInset` / `useClaimTopEdge` — the same thing, one edge up
|
|
72
|
+
|
|
73
|
+
A header that participates in layout answers this by existing: the content
|
|
74
|
+
starts where the header ends, because they are siblings in a column. A header
|
|
75
|
+
that **overlays** content — [`PageHeader`](/docs/page-header)'s
|
|
76
|
+
`placement="overlay"`, where the page scrolls under translucent islands —
|
|
77
|
+
occupies no layout space at all, and the first item of the content lands
|
|
78
|
+
underneath it.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// The chrome that parks there — PageHeader does this for you.
|
|
82
|
+
useClaimTopEdge(measuredHeight);
|
|
83
|
+
|
|
84
|
+
// The content that has to clear it.
|
|
85
|
+
<ScrollView contentContainerStyle={{ paddingTop: useTopEdgeInset() }}>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Same registry object as the bottom edge, so everything below about `max`,
|
|
89
|
+
about a nested provider and about the one-frame settle applies unchanged.
|
|
90
|
+
|
|
91
|
+
**The height is measured, not computed.** A subtitle, an enlarged font and a
|
|
92
|
+
notch all move it, and a constant plus the insets fails silently — the content
|
|
93
|
+
starts a few points under the islands and looks like a design decision.
|
|
94
|
+
|
|
95
|
+
**Apply the safe area once.** The header pads itself by `insets.top`, and the
|
|
96
|
+
claim already includes that padding. Adding `insets.top` again in the scroll
|
|
97
|
+
container double-counts the notch.
|
|
98
|
+
|
|
99
|
+
## `ScrollOffsetProvider` / `useScrollOffset` — which scroller
|
|
100
|
+
|
|
101
|
+
A screen has exactly one scroll owner, and the owner publishes its offset. Chrome
|
|
102
|
+
reads it. Neither imports the other, so two panels side by side — each with its
|
|
103
|
+
own header and its own scroller — resolve correctly, because the provider's
|
|
104
|
+
position in the tree is what pairs them.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
const scrollY = useSharedValue(0);
|
|
108
|
+
const onScroll = useAnimatedScrollHandler(
|
|
109
|
+
{ onScroll: (e) => { scrollY.value = e.contentOffset.y; } },
|
|
110
|
+
[scrollY],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
<ScrollOffsetProvider value={scrollY}>
|
|
114
|
+
<PageHeader title="Inbox" />
|
|
115
|
+
<Animated.ScrollView onScroll={onScroll} scrollEventThrottle={16}>…</Animated.ScrollView>
|
|
116
|
+
</ScrollOffsetProvider>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The context carries the **`SharedValue`**, not a number: it crosses once, at
|
|
120
|
+
mount, and every subsequent frame reaches the chrome on the UI thread through
|
|
121
|
+
`useAnimatedStyle`, with no React render. A context whose value changed per frame
|
|
122
|
+
would re-render every consumer per frame, which is precisely the cost this
|
|
123
|
+
avoids — the same reasoning as the section below.
|
|
124
|
+
|
|
125
|
+
Without it, chrome falls back to `window.scrollY` on web and to nothing on
|
|
126
|
+
native. That default is right for exactly one layout — a web page whose document
|
|
127
|
+
*is* the scroller — and silently wrong for a desktop panel and for every native
|
|
128
|
+
screen, with no error: the header simply never reacts.
|
|
129
|
+
|
|
62
130
|
## The collapse is not here, on purpose
|
|
63
131
|
|
|
64
132
|
The tab bar minimizes 58 → 44 on scroll. This registry twice tried to publish
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Migrating to 3.0
|
|
3
|
+
description: Every breaking change between @oxy.so/bloom 2.12.4 and 3.0.0 — PageHeader's floating default, the split of `transparent` into two props, and what is additive.
|
|
4
|
+
order: 2
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Migrating to 3.0
|
|
8
|
+
|
|
9
|
+
Bloom 3.0 is a small release with one large default change. `PageHeader` is a
|
|
10
|
+
floating header now — a back capsule, islands of grouped actions and a gradient
|
|
11
|
+
edge — where it used to be an opaque full-width strip. Everything else in this
|
|
12
|
+
release is additive.
|
|
13
|
+
|
|
14
|
+
Bloom keeps its clean-cut rule: there are no `@deprecated` aliases and no shims.
|
|
15
|
+
The flat bar is still here, but you ask for it by name.
|
|
16
|
+
|
|
17
|
+
## The short version
|
|
18
|
+
|
|
19
|
+
1. Install `@oxy.so/bloom@^3.0.0`.
|
|
20
|
+
2. Decide, per header: keep the bar (`presentation="bar"`) or take the islands.
|
|
21
|
+
3. If a header used `transparent` over media, add `titleReveal="onScroll"` —
|
|
22
|
+
`transparent` no longer hides the title.
|
|
23
|
+
4. Nothing else breaks. `tsc` will not catch either of the above, because both
|
|
24
|
+
are default changes rather than API removals; this guide is the checklist.
|
|
25
|
+
|
|
26
|
+
## 1. `PageHeader` defaults to `presentation="floating"`
|
|
27
|
+
|
|
28
|
+
A `<PageHeader>` with no `presentation` used to paint an opaque strip with a
|
|
29
|
+
hairline separator and a scroll-linked shadow. It now paints nothing across the
|
|
30
|
+
width: the back button sits in its own translucent capsule, the `actions` slot
|
|
31
|
+
is a glass control surface, the title floats between them, and a vertical
|
|
32
|
+
gradient of the page colour fades out below the whole thing.
|
|
33
|
+
|
|
34
|
+
**To keep exactly what you had:**
|
|
35
|
+
|
|
36
|
+
```diff
|
|
37
|
+
-<PageHeader title="Order #4821" onBack={router.back} />
|
|
38
|
+
+<PageHeader presentation="bar" title="Order #4821" onBack={router.back} />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That header is byte-for-byte what it was — same background, same separator, same
|
|
42
|
+
shadow, same back button, same geometry.
|
|
43
|
+
|
|
44
|
+
**To take the new one**, decide who reserves the space:
|
|
45
|
+
|
|
46
|
+
- `placement="inline"` (the default) leaves the header in layout, as before.
|
|
47
|
+
Nothing passes under it; the page colour shows through where the strip used to
|
|
48
|
+
be. This is the smallest step, and it is what an existing screen gets for free.
|
|
49
|
+
- `placement="overlay"` floats the header over the content, which is what the
|
|
50
|
+
islands are actually for. The content then has to clear it:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { useTopEdgeInset } from '@oxy.so/bloom/layout';
|
|
54
|
+
|
|
55
|
+
const inset = useTopEdgeInset();
|
|
56
|
+
<ScrollView contentContainerStyle={{ paddingTop: inset }}>…</ScrollView>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`BloomProvider` already mounts the registry. Do not add `insets.top` on top of
|
|
60
|
+
the claim — the header's own safe-area padding is already in it.
|
|
61
|
+
|
|
62
|
+
**Icon actions want `ButtonGroupItem`.** A `Button variant="secondary"` in a
|
|
63
|
+
floating header's `actions` paints its own opaque fill, which reads as a card
|
|
64
|
+
floating over the content. It still works and it is still legible; it is just
|
|
65
|
+
not the shape:
|
|
66
|
+
|
|
67
|
+
```diff
|
|
68
|
+
actions={
|
|
69
|
+
- <>
|
|
70
|
+
- <Button variant="secondary" iconOnly leadingIcon={RiSearchLine} accessibilityLabel="Search" />
|
|
71
|
+
- <Button variant="secondary" iconOnly leadingIcon={RiShare2Line} accessibilityLabel="Share" />
|
|
72
|
+
- </>
|
|
73
|
+
+ <ButtonGroup accessibilityLabel="Page actions">
|
|
74
|
+
+ <ButtonGroupItem iconOnly leadingIcon={RiSearchLine} accessibilityLabel="Search" />
|
|
75
|
+
+ <ButtonGroupItem iconOnly leadingIcon={RiShare2Line} accessibilityLabel="Share" />
|
|
76
|
+
+ </ButtonGroup>
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A primary action keeps its own `Button` — an accent capsule beside the islands is
|
|
81
|
+
the intended shape for Save or Done.
|
|
82
|
+
|
|
83
|
+
## 2. `transparent` no longer hides the title
|
|
84
|
+
|
|
85
|
+
`transparent` did two things: it faded the bar's background in with scroll, and
|
|
86
|
+
it held the title back until the scroll arrived. The first is right for any
|
|
87
|
+
header over media; the second is right only when something else on screen
|
|
88
|
+
already names the page. A header that wanted one inherited both.
|
|
89
|
+
|
|
90
|
+
They are two props now:
|
|
91
|
+
|
|
92
|
+
```diff
|
|
93
|
+
-<PageHeader transparent scrollThreshold={264} title="Lisbon loft" />
|
|
94
|
+
+<PageHeader presentation="bar" transparent titleReveal="onScroll" scrollThreshold={264} title="Lisbon loft" />
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If your header used `transparent` and you were happy for the title to show at
|
|
98
|
+
rest, drop `titleReveal` and you now get what you wanted.
|
|
99
|
+
|
|
100
|
+
## 3. `border` and `transparent` are `presentation="bar"` props
|
|
101
|
+
|
|
102
|
+
The floating presentation has no strip to fade and no separator to draw, so it
|
|
103
|
+
ignores both. Its counterpart is `scrim`:
|
|
104
|
+
|
|
105
|
+
| bar | floating |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `border="auto"` | `scrim="auto"` — the gradient fades in with scroll |
|
|
108
|
+
| `border="always"` | `scrim="always"` — the gradient paints at rest |
|
|
109
|
+
| `border="none"` | `scrim="none"` — no gradient; the islands carry legibility alone |
|
|
110
|
+
| `transparent` | (the floating header is always transparent) |
|
|
111
|
+
|
|
112
|
+
A floating header on a screen that paints its own background colour also wants `scrimColor`: the ramp fades content into the surface it is leaving, and the header cannot read the pixel behind it.
|
|
113
|
+
|
|
114
|
+
## Everything else is additive
|
|
115
|
+
|
|
116
|
+
Nothing below removes or renames anything.
|
|
117
|
+
|
|
118
|
+
- **`@oxy.so/bloom/control-surface`** — the inherited presentation contract. A
|
|
119
|
+
container declares a material and a density once; the controls inside default
|
|
120
|
+
to it. See [Composition](/docs/composition).
|
|
121
|
+
- **`ButtonGroup` gains `variant`, `dividers`, and a `glass` material.** The
|
|
122
|
+
default is `solid`, which is what every existing group renders, unchanged.
|
|
123
|
+
- **`ButtonGroupItem` gains `hitSlop`, `variant` and the trigger contract**
|
|
124
|
+
(`onLongPress`, `aria-expanded`, `aria-haspopup`, `accessibilityRole`), so an
|
|
125
|
+
item can be an anchored family's `asChild` trigger. `onPress` now receives the
|
|
126
|
+
press event; a `() => void` handler still type-checks.
|
|
127
|
+
- **`@oxy.so/bloom/layout` gains `TopEdgeProvider` / `useTopEdgeInset` /
|
|
128
|
+
`useClaimTopEdge`** and **`ScrollOffsetProvider` / `useScrollOffset`**.
|
|
129
|
+
`BloomProvider` mounts `TopEdgeProvider` for you.
|
|
130
|
+
- **`Field` associates rather than only stacking.** It generates SSR-stable ids,
|
|
131
|
+
wires `aria-describedby`, `aria-invalid`, `aria-required` and the control's
|
|
132
|
+
accessible name, and gains `multiple` for a field holding several controls.
|
|
133
|
+
One behaviour change worth naming: **`Field disabled` now disables a control
|
|
134
|
+
that reads the field context** (today, `TextFieldInput`). It previously only
|
|
135
|
+
dimmed the label. If you were relying on a disabled-looking field with a live
|
|
136
|
+
input, that combination no longer exists.
|
|
137
|
+
- **`Label.htmlFor` accepts `null`**, meaning "this label names a group, not a
|
|
138
|
+
control". Omitting it still falls back to `nativeID`.
|
|
139
|
+
- **`asChild` triggers can be cancelled.** `event.preventDefault()` in the
|
|
140
|
+
child's own `onPress` stops the surface opening. A fragment child now throws
|
|
141
|
+
instead of rendering a dead trigger — if you were passing one, the trigger was
|
|
142
|
+
never working.
|
|
143
|
+
- **A neutral `chrome` glass role**, measured over the whole backdrop range. See
|
|
144
|
+
[Glass](/docs/glass).
|
|
145
|
+
|
|
146
|
+
## One fix that changes existing pixels on web
|
|
147
|
+
|
|
148
|
+
`<Svg style={StyleSheet.absoluteFill}>` does not fill anything on web. An
|
|
149
|
+
`<svg>` is a replaced element, so `width: auto` resolves to its intrinsic size
|
|
150
|
+
and an SVG without one falls back to CSS's default 300 × 150 — `left: 0;
|
|
151
|
+
right: 0` does not stretch it. Three surfaces were affected, all of them
|
|
152
|
+
shipping since before 3.0:
|
|
153
|
+
|
|
154
|
+
- **`Button`'s gradient** — a button wider than 300px stopped its gradient dead
|
|
155
|
+
at 300px, and every narrower one scaled the vertical ramp over 150px instead
|
|
156
|
+
of the button's own height, so only the top quarter of the gradient showed.
|
|
157
|
+
- **`GlassSurface`'s sheen** — the same, on every glass surface.
|
|
158
|
+
- The new header's scrim, which is how it was found.
|
|
159
|
+
|
|
160
|
+
All three now declare `width="100%" height="100%"`. **Native was never
|
|
161
|
+
affected** (react-native-svg lays its host view out with flexbox there), so this
|
|
162
|
+
changes web pixels only, in the direction of what the component always meant to
|
|
163
|
+
paint. Gate: `src/__tests__/svg-absolute-fill-size.test.ts`.
|
|
164
|
+
|
|
165
|
+
## What did not change
|
|
166
|
+
|
|
167
|
+
The theme, the tokens, the icon set, the typography, every other family's API,
|
|
168
|
+
and the license. If your app does not render a `PageHeader`, 3.0.0 is a drop-in
|
|
169
|
+
upgrade from 2.12.x.
|