@oxy.so/bloom 2.12.3 → 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/message-bubble.mdx +37 -3
- package/docs/message-media.mdx +6 -0
- 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/message-bubble/MessageBubble.js +16 -5
- package/lib/commonjs/message-bubble/MessageBubble.js.map +1 -1
- 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/message-bubble/MessageBubble.js +16 -5
- package/lib/module/message-bubble/MessageBubble.js.map +1 -1
- 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 +3 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +3 -2
- 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/message-bubble/MessageBubble.d.ts +1 -1
- package/lib/typescript/commonjs/message-bubble/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/commonjs/message-bubble/index.d.ts +1 -1
- package/lib/typescript/commonjs/message-bubble/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/message-bubble/types.d.ts +27 -5
- package/lib/typescript/commonjs/message-bubble/types.d.ts.map +1 -1
- 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 +3 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +3 -2
- 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/message-bubble/MessageBubble.d.ts +1 -1
- package/lib/typescript/module/message-bubble/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/module/message-bubble/index.d.ts +1 -1
- package/lib/typescript/module/message-bubble/index.d.ts.map +1 -1
- package/lib/typescript/module/message-bubble/types.d.ts +27 -5
- package/lib/typescript/module/message-bubble/types.d.ts.map +1 -1
- 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 +7 -0
- package/src/index.web.ts +7 -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/message-bubble/MessageBubble.tsx +19 -5
- package/src/message-bubble/index.ts +1 -0
- package/src/message-bubble/types.ts +28 -5
- 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
|
@@ -192,4 +192,71 @@ export interface GlassColors {
|
|
|
192
192
|
* and pins both on every alpha or surface change.
|
|
193
193
|
*/
|
|
194
194
|
export declare function resolveGlassColors(fill: string): GlassColors;
|
|
195
|
+
/**
|
|
196
|
+
* The CHROME alpha — the neutral counterpart of {@link GLASS_FILL_ALPHA}.
|
|
197
|
+
*
|
|
198
|
+
* ── WHY A SECOND ALPHA AND NOT THE SAME ONE ─────────────────────────────────
|
|
199
|
+
*
|
|
200
|
+
* {@link GLASS_FILL_ALPHA} prices a BRAND fill: a `primary` button whose label
|
|
201
|
+
* is that fill's own on-colour, sitting on one of Bloom's five neutral
|
|
202
|
+
* surfaces. A page header's island is neither of those things. It is a NEUTRAL
|
|
203
|
+
* surface off the existing ladder (`styles/surface-levels`, rung 1 — the same
|
|
204
|
+
* fill a card and a menu panel paint) and it floats over content Bloom does not
|
|
205
|
+
* own: a photograph, a video still, a map, a list that scrolls under it. Its
|
|
206
|
+
* label is the theme's own `text`, not an on-fill token.
|
|
207
|
+
*
|
|
208
|
+
* So the two materials are priced against different backdrops, and the number
|
|
209
|
+
* that is right for one is wrong for the other in a measurable direction:
|
|
210
|
+
*
|
|
211
|
+
* brand fill backdrop ∈ Bloom's own five surfaces worst case: light mode,
|
|
212
|
+
* a white-ish page LIFTS
|
|
213
|
+
* the pane under a white
|
|
214
|
+
* label
|
|
215
|
+
* chrome fill backdrop ∈ [black, white] — anything worst case: DARK mode
|
|
216
|
+
* over a WHITE backdrop,
|
|
217
|
+
* which lifts a dark pane
|
|
218
|
+
* under a white label
|
|
219
|
+
*
|
|
220
|
+
* That is why dark carries the HIGHER alpha here and light the lower one, which
|
|
221
|
+
* is the opposite of the intuition that a dark UI can afford more transparency.
|
|
222
|
+
* A dark island has the whole luminance range above it to be washed out INTO; a
|
|
223
|
+
* light island over the same white backdrop barely moves, because it is already
|
|
224
|
+
* near white.
|
|
225
|
+
*
|
|
226
|
+
* ── WHAT THESE TWO VALUES BUY ───────────────────────────────────────────────
|
|
227
|
+
*
|
|
228
|
+
* Measured over 64 presets x the two extremes of the backdrop range, with the
|
|
229
|
+
* theme's own `text` as the label, in `theme/__tests__/glass-colors.test.ts`.
|
|
230
|
+
* The gate pins the floors EXACTLY, in both directions, so a hundredth in
|
|
231
|
+
* either direction has to be a decision:
|
|
232
|
+
*
|
|
233
|
+
* light 0.72 worst AA ratio over the range …pinned by the gate
|
|
234
|
+
* dark 0.80 worst AA ratio over the range …pinned by the gate
|
|
235
|
+
*
|
|
236
|
+
* and both remain genuinely translucent: the gate also measures how far the
|
|
237
|
+
* PAINTED pane moves between a black and a white backdrop, asserts that
|
|
238
|
+
* movement is non-zero, and asserts it is monotonic in the alpha — so an alpha
|
|
239
|
+
* quietly raised to 1.0 to buy contrast fails on the translucency side rather
|
|
240
|
+
* than passing on the legibility side.
|
|
241
|
+
*/
|
|
242
|
+
export declare const GLASS_CHROME_ALPHA: {
|
|
243
|
+
readonly light: 0.72;
|
|
244
|
+
readonly dark: 0.8;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Resolve the pane around a NEUTRAL chrome fill — an island of controls
|
|
248
|
+
* floating over content.
|
|
249
|
+
*
|
|
250
|
+
* Both arguments are resolved colour strings the caller already holds, for the
|
|
251
|
+
* same reason {@link resolveGlassColors} takes one: the caller is the only one
|
|
252
|
+
* who knows which rung of the surface ladder its container sits on. In practice
|
|
253
|
+
* that is `resolveSurfaceLevel(theme, 1)` — `.background` for `fill`, `.border`
|
|
254
|
+
* for `hairline` — which is the fill a card and a menu panel already paint, so
|
|
255
|
+
* an island reads as Bloom chrome rather than as a fourth material.
|
|
256
|
+
*
|
|
257
|
+
* The LABEL is not returned, exactly as in `resolveGlassColors`: a neutral pane
|
|
258
|
+
* carries the theme's own `text`, and the rungs under it come from
|
|
259
|
+
* `surfaceTextOn(theme, fill)` — the caller is already holding both.
|
|
260
|
+
*/
|
|
261
|
+
export declare function resolveChromeGlassColors(fill: string, hairline: string, isDark: boolean): GlassColors;
|
|
195
262
|
//# sourceMappingURL=glass-colors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glass-colors.d.ts","sourceRoot":"","sources":["../../../../src/theme/glass-colors.ts"],"names":[],"mappings":"AAyEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;IAItB,0EAA0E;;CAElE,CAAC;AAEX,wFAAwF;AACxF,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAElF,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAE1D;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,6CAA6C,CAAC;AAE9E,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,eAAoC,CAAC;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,QAA4K,CAAC;AAE9M;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAE/D,MAAM,WAAW,WAAW;IAC1B,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAM5D"}
|
|
1
|
+
{"version":3,"file":"glass-colors.d.ts","sourceRoot":"","sources":["../../../../src/theme/glass-colors.ts"],"names":[],"mappings":"AAyEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;IAItB,0EAA0E;;CAElE,CAAC;AAEX,wFAAwF;AACxF,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAElF,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAE1D;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,6CAA6C,CAAC;AAE9E,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,eAAoC,CAAC;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,QAA4K,CAAC;AAE9M;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAE/D,MAAM,WAAW,WAAW;IAC1B,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAM5D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,eAAO,MAAM,kBAAkB;;;CAAsC,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,GACd,WAAW,CAUb"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/bloom",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"packageManager": "bun@1.3.14",
|
|
5
5
|
"description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
|
|
6
6
|
"main": "lib/commonjs/index.js",
|
|
@@ -272,6 +272,20 @@
|
|
|
272
272
|
"default": "./lib/commonjs/button-group/index.js"
|
|
273
273
|
}
|
|
274
274
|
},
|
|
275
|
+
"./control-surface": {
|
|
276
|
+
"react-native": {
|
|
277
|
+
"types": "./lib/typescript/module/control-surface/index.d.ts",
|
|
278
|
+
"default": "./src/control-surface/index.ts"
|
|
279
|
+
},
|
|
280
|
+
"import": {
|
|
281
|
+
"types": "./lib/typescript/module/control-surface/index.d.ts",
|
|
282
|
+
"default": "./lib/module/control-surface/index.js"
|
|
283
|
+
},
|
|
284
|
+
"require": {
|
|
285
|
+
"types": "./lib/typescript/commonjs/control-surface/index.d.ts",
|
|
286
|
+
"default": "./lib/commonjs/control-surface/index.js"
|
|
287
|
+
}
|
|
288
|
+
},
|
|
275
289
|
"./layout": {
|
|
276
290
|
"react-native": {
|
|
277
291
|
"types": "./lib/typescript/module/layout/index.d.ts",
|
package/src/button/Button.tsx
CHANGED
|
@@ -135,7 +135,16 @@ const ButtonGradientFill = memo(function ButtonGradientFill({
|
|
|
135
135
|
pointerEvents="none"
|
|
136
136
|
style={[StyleSheet.absoluteFill, { borderRadius: radius, overflow: 'hidden' }]}
|
|
137
137
|
>
|
|
138
|
-
|
|
138
|
+
{/*
|
|
139
|
+
`width`/`height` are load-bearing beside `absoluteFill`: an `<svg>` is a
|
|
140
|
+
REPLACED element, so `width: auto` resolves to its intrinsic size and an
|
|
141
|
+
SVG without one falls back to CSS's 300 x 150 — `left: 0; right: 0` does
|
|
142
|
+
not stretch it. A button wider than 300px stopped its gradient dead at
|
|
143
|
+
300, and every narrower one scaled the vertical ramp over 150px instead
|
|
144
|
+
of its own 36, so only the top quarter of the gradient was ever visible.
|
|
145
|
+
Native is unaffected. Gate: `src/__tests__/svg-absolute-fill-size.test.ts`.
|
|
146
|
+
*/}
|
|
147
|
+
<Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
|
|
139
148
|
<Defs>
|
|
140
149
|
<LinearGradient id={id} x1="0" y1="0" x2="0" y2="1">
|
|
141
150
|
<Stop offset="0" stopColor={gradient[0]} />
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import React, { Children, createContext, Fragment, isValidElement, memo, useContext, useMemo } from 'react';
|
|
2
2
|
import { Platform, Pressable, View, type TextStyle } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import { useInheritedControl } from '../control-surface';
|
|
5
|
+
import { GlassIsland } from '../glass';
|
|
4
6
|
import { useTheme } from '../theme/use-theme';
|
|
5
7
|
import { Text } from '../typography/Typography';
|
|
6
8
|
import { useInteractionState } from '../hooks/use-interaction-state';
|
|
9
|
+
import { resolveSurfaceLevel } from '../styles/surface-levels';
|
|
7
10
|
import { NOT_DISABLED, interactiveWebCss, useInteractiveWebCss } from '../styles/interactive-web-css';
|
|
11
|
+
import { withAlpha } from '../theme/color-utils';
|
|
8
12
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
9
13
|
import {
|
|
10
14
|
BUTTON_RADIUS,
|
|
@@ -13,14 +17,30 @@ import {
|
|
|
13
17
|
mixColor,
|
|
14
18
|
resolveButtonRamps,
|
|
15
19
|
} from '../button/shared';
|
|
20
|
+
import type { ControlMaterial } from '../control-surface/types';
|
|
16
21
|
import type { Theme } from '../theme/types';
|
|
17
22
|
import type { ButtonGroupItemProps, ButtonGroupProps, ButtonGroupSize } from './types';
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
|
-
* A group of
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
25
|
+
* A group of buttons the consumer has declared as ONE control.
|
|
26
|
+
*
|
|
27
|
+
* ── TWO MATERIALS, ONE GROUP ────────────────────────────────────────────────
|
|
28
|
+
*
|
|
29
|
+
* `solid` is the original: one bordered, shadowed container, items divided by
|
|
30
|
+
* 1px hairlines, only the outer ends rounded, colours from the same
|
|
31
|
+
* Bloom-derived ramps as `Button` (`button/shared.ts`).
|
|
32
|
+
*
|
|
33
|
+
* `glass` is the same group as a floating ISLAND — one translucent pane
|
|
34
|
+
* (`glass/GlassIsland`) that the items sit FLUSH on. The group owns the
|
|
35
|
+
* material and the items paint no fill of their own at rest; a second fill
|
|
36
|
+
* under a translucent pane is the thing that makes a row of controls read as a
|
|
37
|
+
* row of cards rather than as one island, and a second BLUR is not available at
|
|
38
|
+
* all on Android.
|
|
39
|
+
*
|
|
40
|
+
* Neither is a decoration a caller has to ask for by name. The variant is
|
|
41
|
+
* INHERITED from the nearest `ControlSurface`, so `<ButtonGroup>` inside a
|
|
42
|
+
* floating `PageHeader` is glass and the same group inside a card is solid,
|
|
43
|
+
* with no prop either way. An explicit `variant` still wins.
|
|
24
44
|
*
|
|
25
45
|
* medium small
|
|
26
46
|
* item h 34 30 (plus the container's 1px border = 36 / 32)
|
|
@@ -75,6 +95,43 @@ function resolveGroupPalette(theme: Theme): GroupPalette {
|
|
|
75
95
|
};
|
|
76
96
|
}
|
|
77
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The items' paint on a GLASS island.
|
|
100
|
+
*
|
|
101
|
+
* Every state that is not "at rest" is an alpha of the theme's own `text` — an
|
|
102
|
+
* ink wash, not a fill. That is the one recipe that works on a pane whose
|
|
103
|
+
* composited colour Bloom cannot predict: a fixed neutral tuned against the
|
|
104
|
+
* island's own fill goes invisible the moment a photograph behind it moves the
|
|
105
|
+
* pane 40 levels, while a wash of the label's colour keeps the same relation to
|
|
106
|
+
* the label at every composite.
|
|
107
|
+
*
|
|
108
|
+
* `transparent` at rest is load-bearing rather than tidy. It is what makes the
|
|
109
|
+
* island ONE pane: give the items a fill and the group is a row of chips inside
|
|
110
|
+
* a capsule, which is the shape this variant exists to stop.
|
|
111
|
+
*
|
|
112
|
+
* The disabled label is read off the island's own fill with `surfaceTextOn`
|
|
113
|
+
* rather than picked from the ramp, because a ramp stop measured against the
|
|
114
|
+
* page is not measured against this surface.
|
|
115
|
+
*/
|
|
116
|
+
function resolveGlassItemPalette(theme: Theme): GroupPalette {
|
|
117
|
+
const { accent } = resolveButtonRamps(theme);
|
|
118
|
+
const level = resolveSurfaceLevel(theme, 1);
|
|
119
|
+
const ink = theme.colors.text;
|
|
120
|
+
const wash = theme.isDark ? { hover: 0.12, active: 0.2 } : { hover: 0.08, active: 0.14 };
|
|
121
|
+
return {
|
|
122
|
+
// The island owns the box: the group draws neither border nor divider.
|
|
123
|
+
groupBorder: 'transparent',
|
|
124
|
+
divider: withAlpha(ink, theme.isDark ? 0.16 : 0.12),
|
|
125
|
+
background: 'transparent',
|
|
126
|
+
hover: withAlpha(ink, wash.hover),
|
|
127
|
+
active: withAlpha(ink, wash.active),
|
|
128
|
+
disabledBackground: 'transparent',
|
|
129
|
+
foreground: ink,
|
|
130
|
+
disabledForeground: level.textGraphical,
|
|
131
|
+
ring: accent[500],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
78
135
|
// ---------------------------------------------------------------------------
|
|
79
136
|
// Keyboard focus on web — an inset `ring-2`. Items are react-native-web
|
|
80
137
|
// `Pressable`s, so the rules hang off a `dataSet` attribute (a class never
|
|
@@ -110,10 +167,18 @@ ${SELECTOR}${NOT_DISABLED}:focus-visible {
|
|
|
110
167
|
|
|
111
168
|
const IS_WEB = Platform.OS === 'web';
|
|
112
169
|
|
|
113
|
-
|
|
170
|
+
interface GroupContextValue {
|
|
171
|
+
size: ButtonGroupSize;
|
|
172
|
+
material: ControlMaterial;
|
|
173
|
+
palette: GroupPalette;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const GroupContext = createContext<GroupContextValue | null>(null);
|
|
114
177
|
|
|
115
178
|
const ButtonGroupComponent: React.FC<ButtonGroupProps> = ({
|
|
116
|
-
|
|
179
|
+
variant,
|
|
180
|
+
size: sizeProp,
|
|
181
|
+
dividers,
|
|
117
182
|
children,
|
|
118
183
|
accessibilityLabel,
|
|
119
184
|
style,
|
|
@@ -121,9 +186,48 @@ const ButtonGroupComponent: React.FC<ButtonGroupProps> = ({
|
|
|
121
186
|
}) => {
|
|
122
187
|
const theme = useTheme();
|
|
123
188
|
useInteractiveWebCss(STYLE_ID, BUTTON_GROUP_CSS);
|
|
124
|
-
const
|
|
125
|
-
const
|
|
189
|
+
const material = useInheritedControl('material', variant, 'solid');
|
|
190
|
+
const size = useInheritedControl('density', sizeProp, 'medium');
|
|
191
|
+
const palette = useMemo(
|
|
192
|
+
() => (material === 'glass' ? resolveGlassItemPalette(theme) : resolveGroupPalette(theme)),
|
|
193
|
+
[material, theme],
|
|
194
|
+
);
|
|
195
|
+
const context = useMemo<GroupContextValue>(
|
|
196
|
+
() => ({ size, material, palette }),
|
|
197
|
+
[size, material, palette],
|
|
198
|
+
);
|
|
126
199
|
const items = Children.toArray(children).filter(isValidElement);
|
|
200
|
+
const showDividers = dividers ?? material === 'solid';
|
|
201
|
+
|
|
202
|
+
const row = items.map((item, index) => (
|
|
203
|
+
<Fragment key={item.key ?? index}>
|
|
204
|
+
{/* `divide-x`: a hairline BETWEEN items, never at the ends. */}
|
|
205
|
+
{showDividers && index > 0 ? (
|
|
206
|
+
<View style={{ width: 1, backgroundColor: palette.divider }} />
|
|
207
|
+
) : null}
|
|
208
|
+
{item}
|
|
209
|
+
</Fragment>
|
|
210
|
+
));
|
|
211
|
+
|
|
212
|
+
if (material === 'glass') {
|
|
213
|
+
return (
|
|
214
|
+
<GroupContext.Provider value={context}>
|
|
215
|
+
{/*
|
|
216
|
+
The island mounts its own `ControlSurface` with `material: 'glass'`,
|
|
217
|
+
so an item rendered through a trigger's `asChild` — outside this
|
|
218
|
+
group's own children, in a caller's tree — still paints flush.
|
|
219
|
+
*/}
|
|
220
|
+
<GlassIsland
|
|
221
|
+
role="group"
|
|
222
|
+
accessibilityLabel={accessibilityLabel}
|
|
223
|
+
style={style}
|
|
224
|
+
testID={testID}
|
|
225
|
+
>
|
|
226
|
+
{row}
|
|
227
|
+
</GlassIsland>
|
|
228
|
+
</GroupContext.Provider>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
127
231
|
|
|
128
232
|
return (
|
|
129
233
|
<GroupContext.Provider value={context}>
|
|
@@ -146,13 +250,7 @@ const ButtonGroupComponent: React.FC<ButtonGroupProps> = ({
|
|
|
146
250
|
style,
|
|
147
251
|
]}
|
|
148
252
|
>
|
|
149
|
-
{
|
|
150
|
-
<Fragment key={item.key ?? index}>
|
|
151
|
-
{/* `divide-x`: a hairline BETWEEN items, never at the ends. */}
|
|
152
|
-
{index > 0 ? <View style={{ width: 1, backgroundColor: palette.divider }} /> : null}
|
|
153
|
-
{item}
|
|
154
|
-
</Fragment>
|
|
155
|
-
))}
|
|
253
|
+
{row}
|
|
156
254
|
</View>
|
|
157
255
|
</GroupContext.Provider>
|
|
158
256
|
);
|
|
@@ -160,21 +258,42 @@ const ButtonGroupComponent: React.FC<ButtonGroupProps> = ({
|
|
|
160
258
|
|
|
161
259
|
const ButtonGroupItemComponent: React.FC<ButtonGroupItemProps> = ({
|
|
162
260
|
onPress,
|
|
261
|
+
onLongPress,
|
|
163
262
|
children,
|
|
164
263
|
size: sizeProp,
|
|
264
|
+
variant,
|
|
165
265
|
selected = false,
|
|
166
266
|
disabled = false,
|
|
167
267
|
iconOnly = false,
|
|
168
268
|
leadingIcon: LeadingIcon,
|
|
169
269
|
trailingIcon: TrailingIcon,
|
|
170
270
|
accessibilityLabel,
|
|
271
|
+
accessibilityRole = 'button',
|
|
272
|
+
hitSlop,
|
|
273
|
+
'aria-expanded': ariaExpanded,
|
|
274
|
+
'aria-haspopup': ariaHasPopup,
|
|
171
275
|
style,
|
|
172
276
|
testID,
|
|
173
277
|
}) => {
|
|
174
278
|
const theme = useTheme();
|
|
175
279
|
const group = useContext(GroupContext);
|
|
176
|
-
|
|
177
|
-
|
|
280
|
+
// An item can be rendered OUTSIDE its group's children — an anchored family's
|
|
281
|
+
// `asChild` trigger clones it into the caller's tree — so the material falls
|
|
282
|
+
// back to the inherited `ControlSurface` (which `GlassIsland` mounts) before
|
|
283
|
+
// it falls back to `solid`.
|
|
284
|
+
const inheritedMaterial = useInheritedControl('material', undefined, 'solid');
|
|
285
|
+
const inheritedDensity = useInheritedControl('density', undefined, 'medium');
|
|
286
|
+
const material = variant ?? group?.material ?? inheritedMaterial;
|
|
287
|
+
const palette = useMemo(
|
|
288
|
+
() =>
|
|
289
|
+
group && group.material === material
|
|
290
|
+
? group.palette
|
|
291
|
+
: material === 'glass'
|
|
292
|
+
? resolveGlassItemPalette(theme)
|
|
293
|
+
: resolveGroupPalette(theme),
|
|
294
|
+
[group, material, theme],
|
|
295
|
+
);
|
|
296
|
+
const size = sizeProp ?? group?.size ?? inheritedDensity;
|
|
178
297
|
const geometry = GEOMETRY[size];
|
|
179
298
|
const { state: hovered, onIn: onHoverIn, onOut: onHoverOut } = useInteractionState();
|
|
180
299
|
const { state: pressed, onIn: onPressIn, onOut: onPressOut } = useInteractionState();
|
|
@@ -198,6 +317,10 @@ const ButtonGroupItemComponent: React.FC<ButtonGroupItemProps> = ({
|
|
|
198
317
|
justifyContent: 'center',
|
|
199
318
|
gap: 4,
|
|
200
319
|
backgroundColor: background,
|
|
320
|
+
// On glass the island does not clip (a clipping node loses its iOS drop
|
|
321
|
+
// shadow), so the item rounds its OWN highlight. On solid the container
|
|
322
|
+
// clips and a square item is what makes the fused ends read as one pill.
|
|
323
|
+
borderRadius: material === 'glass' ? BUTTON_RADIUS : undefined,
|
|
201
324
|
'--bloom-button-group-ring': palette.ring,
|
|
202
325
|
};
|
|
203
326
|
|
|
@@ -208,17 +331,21 @@ const ButtonGroupItemComponent: React.FC<ButtonGroupItemProps> = ({
|
|
|
208
331
|
{...(IS_WEB ? ({ dataSet: { bloomButtonGroupItem: '' } } as Record<string, unknown>) : {})}
|
|
209
332
|
style={[containerStyle, style]}
|
|
210
333
|
onPress={disabled ? undefined : onPress}
|
|
334
|
+
onLongPress={disabled ? undefined : onLongPress}
|
|
211
335
|
onPressIn={disabled ? undefined : onPressIn}
|
|
212
336
|
onPressOut={disabled ? undefined : onPressOut}
|
|
213
337
|
onHoverIn={onHoverIn}
|
|
214
338
|
onHoverOut={onHoverOut}
|
|
339
|
+
hitSlop={hitSlop}
|
|
215
340
|
disabled={disabled}
|
|
216
|
-
accessibilityRole=
|
|
341
|
+
accessibilityRole={accessibilityRole}
|
|
217
342
|
accessibilityLabel={accessibilityLabel}
|
|
218
343
|
// Both spellings: react-native-web reads only `aria-pressed`, React Native
|
|
219
344
|
// has no `aria-pressed` and reads `accessibilityState`.
|
|
220
|
-
accessibilityState={{ disabled, selected }}
|
|
345
|
+
accessibilityState={{ disabled, selected, expanded: ariaExpanded }}
|
|
221
346
|
aria-pressed={selected}
|
|
347
|
+
aria-expanded={ariaExpanded}
|
|
348
|
+
aria-haspopup={ariaHasPopup}
|
|
222
349
|
testID={testID}
|
|
223
350
|
>
|
|
224
351
|
{LeadingIcon ? (
|
|
@@ -1,27 +1,59 @@
|
|
|
1
|
-
import type { StyleProp, ViewStyle } from 'react-native';
|
|
1
|
+
import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native';
|
|
2
2
|
|
|
3
|
+
import type { ControlMaterial } from '../control-surface/types';
|
|
3
4
|
import type { ButtonIconComponent } from '../button/types';
|
|
5
|
+
import type { WebAriaProps } from '../styles/styled-primitives';
|
|
4
6
|
|
|
5
7
|
/** The two group sizes: 34px (`medium`) and 30px (`small`) items. */
|
|
6
8
|
export type ButtonGroupSize = 'medium' | 'small';
|
|
7
9
|
|
|
8
10
|
export interface ButtonGroupProps {
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The material the group paints: `solid` (an opaque bordered pill, Bloom's
|
|
13
|
+
* default chrome) or `glass` (one translucent island the items sit flush on).
|
|
14
|
+
*
|
|
15
|
+
* Omitted, it is INHERITED from the nearest `ControlSurface` — which is what a
|
|
16
|
+
* `PageHeader`'s action slot mounts — and falls back to `solid`. So a group in
|
|
17
|
+
* a floating header needs no variant written on it, and the same group in a
|
|
18
|
+
* card is solid without one either. Precedence and its limits:
|
|
19
|
+
* `docs/composition.mdx`.
|
|
20
|
+
*/
|
|
21
|
+
variant?: ControlMaterial;
|
|
22
|
+
/**
|
|
23
|
+
* Size for every item that does not set its own. Inherited from the nearest
|
|
24
|
+
* `ControlSurface` when omitted, then `medium`.
|
|
25
|
+
*/
|
|
10
26
|
size?: ButtonGroupSize;
|
|
11
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Whether to draw the 1px hairlines BETWEEN items.
|
|
29
|
+
*
|
|
30
|
+
* Defaults to `true` for `solid` and `false` for `glass`. A divider is how a
|
|
31
|
+
* solid pill says "these are separate controls, fused"; on a translucent
|
|
32
|
+
* island the press highlight already says it, and a hairline over a moving
|
|
33
|
+
* backdrop reads as an artifact of the material.
|
|
34
|
+
*/
|
|
35
|
+
dividers?: boolean;
|
|
36
|
+
/** `ButtonGroupItem` children. */
|
|
12
37
|
children: React.ReactNode;
|
|
13
|
-
/** Names the group
|
|
38
|
+
/** Names the group. */
|
|
14
39
|
accessibilityLabel?: string;
|
|
15
40
|
style?: StyleProp<ViewStyle>;
|
|
16
41
|
testID?: string;
|
|
17
42
|
}
|
|
18
43
|
|
|
19
44
|
export interface ButtonGroupItemProps {
|
|
20
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Press handler. Receives the event, so a handler composed onto this item by
|
|
47
|
+
* an anchored family's `asChild` trigger can `preventDefault()` it — see
|
|
48
|
+
* `floating/TriggerSlot.tsx`.
|
|
49
|
+
*/
|
|
50
|
+
onPress?: (event: GestureResponderEvent) => void;
|
|
21
51
|
/** Label. Ignored when `iconOnly`. */
|
|
22
52
|
children?: React.ReactNode;
|
|
23
53
|
/** Overrides the group's size. */
|
|
24
54
|
size?: ButtonGroupSize;
|
|
55
|
+
/** Overrides the group's material. Rarely needed; the group owns it. */
|
|
56
|
+
variant?: ControlMaterial;
|
|
25
57
|
/** Highlights the item like its hover state and announces it as pressed. */
|
|
26
58
|
selected?: boolean;
|
|
27
59
|
disabled?: boolean;
|
|
@@ -32,6 +64,37 @@ export interface ButtonGroupItemProps {
|
|
|
32
64
|
/** Icon component after the label. */
|
|
33
65
|
trailingIcon?: ButtonIconComponent;
|
|
34
66
|
accessibilityLabel?: string;
|
|
67
|
+
|
|
68
|
+
// ── The trigger contract ────────────────────────────────────────────────
|
|
69
|
+
//
|
|
70
|
+
// Declared, not spread. An anchored family's `asChild` trigger merges
|
|
71
|
+
// `TriggerHandleProps` onto its child, and a child whose props are a closed
|
|
72
|
+
// list DROPS the ones it does not declare — silently, since every one of them
|
|
73
|
+
// is optional. Naming them is what makes `<PopoverTrigger asChild>` around an
|
|
74
|
+
// item announce its own expanded state instead of looking like a plain
|
|
75
|
+
// button. Gate: `button-group-trigger.test.tsx`.
|
|
76
|
+
|
|
77
|
+
/** Long-press handler, composed by a `ContextMenu` trigger. */
|
|
78
|
+
onLongPress?: (event: GestureResponderEvent) => void;
|
|
79
|
+
/** Set by an anchored family: whether the surface this opens is showing. */
|
|
80
|
+
'aria-expanded'?: boolean;
|
|
81
|
+
/** Set by an anchored family: what kind of surface this opens. */
|
|
82
|
+
'aria-haspopup'?: WebAriaProps['aria-haspopup'];
|
|
83
|
+
/** Set by an anchored family. The item is a `button` either way. */
|
|
84
|
+
accessibilityRole?: 'button';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extra touch area around the item, in px.
|
|
88
|
+
*
|
|
89
|
+
* NOT set by the group, and deliberately not defaulted: items in a group are
|
|
90
|
+
* adjacent, so slop on one overlaps its neighbour and the press lands on
|
|
91
|
+
* whichever the platform happens to hit-test first. It is for an item that
|
|
92
|
+
* stands ALONE in its own island — a page header's back capsule, where 4px on
|
|
93
|
+
* each side turns the 36pt capsule into a 44pt target with nothing beside it
|
|
94
|
+
* to steal from.
|
|
95
|
+
*/
|
|
96
|
+
hitSlop?: number | { top?: number; bottom?: number; left?: number; right?: number };
|
|
97
|
+
|
|
35
98
|
style?: StyleProp<ViewStyle>;
|
|
36
99
|
testID?: string;
|
|
37
100
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ControlSurfaceProvider, useControlSurface } from './context';
|
|
4
|
+
import type { ControlSurfaceProps } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Declare the presentation the controls inside should default to.
|
|
8
|
+
*
|
|
9
|
+
* Renders nothing of its own — no view, no layout node — so it can be dropped
|
|
10
|
+
* anywhere without moving anything. Bloom's own containers (`ButtonGroup`'s
|
|
11
|
+
* glass variant, `PageHeader`'s islands) mount it for you; this is the explicit
|
|
12
|
+
* entry point for an app that wants the same effect around its own controls.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <ControlSurface material="glass">
|
|
16
|
+
* <ButtonGroup accessibilityLabel="Playback">…</ButtonGroup>
|
|
17
|
+
* </ControlSurface>
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* A field left `undefined` INHERITS rather than resetting, so a nested
|
|
21
|
+
* `<ControlSurface density="small">` inside a glass island stays glass. The
|
|
22
|
+
* full precedence rule is in `control-surface/context.ts`.
|
|
23
|
+
*/
|
|
24
|
+
const ControlSurfaceComponent: React.FC<ControlSurfaceProps> = ({
|
|
25
|
+
material,
|
|
26
|
+
density,
|
|
27
|
+
children,
|
|
28
|
+
}) => {
|
|
29
|
+
const inherited = useControlSurface();
|
|
30
|
+
const value = useMemo(
|
|
31
|
+
() => ({
|
|
32
|
+
material: material ?? inherited?.material ?? 'solid',
|
|
33
|
+
density: density ?? inherited?.density ?? 'medium',
|
|
34
|
+
}),
|
|
35
|
+
[material, density, inherited?.material, inherited?.density],
|
|
36
|
+
);
|
|
37
|
+
return <ControlSurfaceProvider value={value}>{children}</ControlSurfaceProvider>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const ControlSurface = memo(ControlSurfaceComponent);
|
|
41
|
+
ControlSurface.displayName = 'ControlSurface';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inherited CONTROL PRESENTATION contract — the third of Bloom's visual
|
|
3
|
+
* contexts, beside the theme (`theme/use-theme`) and the surface ladder
|
|
4
|
+
* (`styles/surface-levels`).
|
|
5
|
+
*
|
|
6
|
+
* ## What it is for
|
|
7
|
+
*
|
|
8
|
+
* A container that owns a material has to be able to say so once, rather than
|
|
9
|
+
* have every control inside it repeat the material as a prop. A page header's
|
|
10
|
+
* action island owns its glass; the `ButtonGroup` inside it should not need
|
|
11
|
+
* `variant="glass"` written on it, and the items inside THAT should not need it
|
|
12
|
+
* either. Written as props it is three places to keep in step, and the app is
|
|
13
|
+
* the one that pays when they drift.
|
|
14
|
+
*
|
|
15
|
+
* ## Precedence, in one rule
|
|
16
|
+
*
|
|
17
|
+
* an explicit prop > the nearest container > the component's default
|
|
18
|
+
*
|
|
19
|
+
* {@link useInheritedControl} is that rule, and it is a function rather than a
|
|
20
|
+
* convention so the three consumers cannot each interpret it slightly
|
|
21
|
+
* differently. `undefined` is the only value that defers: passing
|
|
22
|
+
* `material="solid"` inside a glass island paints a solid control, which is a
|
|
23
|
+
* decision the caller is allowed to make.
|
|
24
|
+
*
|
|
25
|
+
* ## What deliberately does NOT travel through it
|
|
26
|
+
*
|
|
27
|
+
* Only VISUAL CONFIGURATION. A constraint — `disabled`, a modal's inertness,
|
|
28
|
+
* the user's reduce-transparency preference — is not a default, and the rule
|
|
29
|
+
* above is exactly wrong for one: a descendant must not be able to re-enable an
|
|
30
|
+
* action its group disabled by passing a prop. Constraints keep their own
|
|
31
|
+
* channels (a group's `disabled` is applied by the group when it renders its
|
|
32
|
+
* items; accessibility preferences are read from the OS at the point of use).
|
|
33
|
+
*
|
|
34
|
+
* Nor does STATE. `checked`, `selected`, a field's value and any business datum
|
|
35
|
+
* are owned by the component that holds them; a visual context that also
|
|
36
|
+
* carried state would make every container a place state could leak from.
|
|
37
|
+
*
|
|
38
|
+
* ## Scope
|
|
39
|
+
*
|
|
40
|
+
* One context, two fields, both of which change rarely (a container sets them
|
|
41
|
+
* once at mount). Scroll offsets, focus, form state and the overlay stack each
|
|
42
|
+
* have their own context for the opposite reason — they change often, and a
|
|
43
|
+
* consumer of one must not re-render for a change in another.
|
|
44
|
+
*/
|
|
45
|
+
import { createContext, useContext } from 'react';
|
|
46
|
+
|
|
47
|
+
import type { ControlSurfaceValue } from './types';
|
|
48
|
+
|
|
49
|
+
const ControlSurfaceContext = createContext<ControlSurfaceValue | null>(null);
|
|
50
|
+
ControlSurfaceContext.displayName = 'BloomControlSurfaceContext';
|
|
51
|
+
|
|
52
|
+
/** @internal The provider `ControlSurface` renders. */
|
|
53
|
+
export const ControlSurfaceProvider = ControlSurfaceContext.Provider;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The nearest container's presentation, or `null` when there is none.
|
|
57
|
+
*
|
|
58
|
+
* `null` rather than a default object on purpose: a control's own default is
|
|
59
|
+
* the control's to choose (`ButtonGroup` defaults to `solid`, a media overlay's
|
|
60
|
+
* toolbar could default to `glass`), and a context that invented one here would
|
|
61
|
+
* take that choice away by looking like an answer.
|
|
62
|
+
*/
|
|
63
|
+
export function useControlSurface(): ControlSurfaceValue | null {
|
|
64
|
+
return useContext(ControlSurfaceContext);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve one field by the precedence rule: `explicit` → container → `fallback`.
|
|
69
|
+
*
|
|
70
|
+
* ```ts
|
|
71
|
+
* const material = useInheritedControl('material', variant, 'solid');
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function useInheritedControl<K extends keyof ControlSurfaceValue>(
|
|
75
|
+
key: K,
|
|
76
|
+
explicit: ControlSurfaceValue[K] | undefined,
|
|
77
|
+
fallback: ControlSurfaceValue[K],
|
|
78
|
+
): ControlSurfaceValue[K] {
|
|
79
|
+
const inherited = useContext(ControlSurfaceContext);
|
|
80
|
+
return explicit ?? inherited?.[key] ?? fallback;
|
|
81
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The MATERIAL a control container paints itself in.
|
|
5
|
+
*
|
|
6
|
+
* `solid` is Bloom's default chrome: an opaque fill off the surface ladder, a
|
|
7
|
+
* hairline and a shadow. `glass` is the translucent chrome a container paints
|
|
8
|
+
* when it floats over content the container does not own — a page header's
|
|
9
|
+
* islands, a media overlay's toolbar.
|
|
10
|
+
*
|
|
11
|
+
* It names the container's SURFACE, never the action's meaning. A destructive
|
|
12
|
+
* action stays destructive in either material; see `docs/composition.mdx`.
|
|
13
|
+
*/
|
|
14
|
+
export type ControlMaterial = 'solid' | 'glass';
|
|
15
|
+
|
|
16
|
+
/** The size a container asks the controls inside it for. */
|
|
17
|
+
export type ControlDensity = 'medium' | 'small';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a container tells the controls inside it about presentation — and
|
|
21
|
+
* nothing else.
|
|
22
|
+
*
|
|
23
|
+
* Both fields are VISUAL CONFIGURATION: a descendant may override either with
|
|
24
|
+
* an explicit prop. Restrictions do not travel here, deliberately. `disabled`,
|
|
25
|
+
* a modal's inertness and the effective accessibility preferences are
|
|
26
|
+
* constraints rather than defaults, and a descendant must not be able to
|
|
27
|
+
* re-enable itself by passing a prop — so they stay on their own channels.
|
|
28
|
+
*/
|
|
29
|
+
export interface ControlSurfaceValue {
|
|
30
|
+
material: ControlMaterial;
|
|
31
|
+
density: ControlDensity;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ControlSurfaceProps {
|
|
35
|
+
/** The material controls inside should default to. Inherited when omitted. */
|
|
36
|
+
material?: ControlMaterial;
|
|
37
|
+
/** The density controls inside should default to. Inherited when omitted. */
|
|
38
|
+
density?: ControlDensity;
|
|
39
|
+
children?: ReactNode;
|
|
40
|
+
}
|