@oxyhq/bloom 0.79.1 → 0.81.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/lib/commonjs/scroll/index.web.js +52 -18
- package/lib/commonjs/scroll/index.web.js.map +1 -1
- package/lib/commonjs/scroll/scrollable.web.js +9 -3
- package/lib/commonjs/scroll/scrollable.web.js.map +1 -1
- package/lib/commonjs/tabs/Tabs.js +409 -71
- package/lib/commonjs/tabs/Tabs.js.map +1 -1
- package/lib/commonjs/tabs/expo-router/RouterTabs.js +215 -0
- package/lib/commonjs/tabs/expo-router/RouterTabs.js.map +1 -0
- package/lib/commonjs/tabs/expo-router/index.js +13 -0
- package/lib/commonjs/tabs/expo-router/index.js.map +1 -0
- package/lib/commonjs/theme/color-engine/index.js +6 -0
- package/lib/commonjs/theme/color-engine/index.js.map +1 -1
- package/lib/commonjs/theme/color-engine/scheme-variants.js +29 -1
- package/lib/commonjs/theme/color-engine/scheme-variants.js.map +1 -1
- package/lib/commonjs/theme/color-policy.js +54 -10
- package/lib/commonjs/theme/color-policy.js.map +1 -1
- package/lib/commonjs/theme/color-presets.js +14 -2
- package/lib/commonjs/theme/color-presets.js.map +1 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js +5 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/module/scroll/index.web.js +52 -18
- package/lib/module/scroll/index.web.js.map +1 -1
- package/lib/module/scroll/scrollable.web.js +9 -3
- package/lib/module/scroll/scrollable.web.js.map +1 -1
- package/lib/module/tabs/Tabs.js +417 -73
- package/lib/module/tabs/Tabs.js.map +1 -1
- package/lib/module/tabs/expo-router/RouterTabs.js +210 -0
- package/lib/module/tabs/expo-router/RouterTabs.js.map +1 -0
- package/lib/module/tabs/expo-router/index.js +10 -0
- package/lib/module/tabs/expo-router/index.js.map +1 -0
- package/lib/module/theme/color-engine/index.js +1 -1
- package/lib/module/theme/color-engine/index.js.map +1 -1
- package/lib/module/theme/color-engine/scheme-variants.js +28 -1
- package/lib/module/theme/color-engine/scheme-variants.js.map +1 -1
- package/lib/module/theme/color-policy.js +53 -10
- package/lib/module/theme/color-policy.js.map +1 -1
- package/lib/module/theme/color-presets.js +14 -2
- package/lib/module/theme/color-presets.js.map +1 -1
- package/lib/module/theme/color-scope/seed-scope.js +6 -2
- package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/typescript/commonjs/scroll/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/scrollable.web.d.ts +18 -5
- package/lib/typescript/commonjs/scroll/scrollable.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tabs/Tabs.d.ts +41 -1
- package/lib/typescript/commonjs/tabs/Tabs.d.ts.map +1 -1
- package/lib/typescript/commonjs/tabs/expo-router/RouterTabs.d.ts +80 -0
- package/lib/typescript/commonjs/tabs/expo-router/RouterTabs.d.ts.map +1 -0
- package/lib/typescript/commonjs/tabs/expo-router/index.d.ts +9 -0
- package/lib/typescript/commonjs/tabs/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/tabs/types.d.ts +48 -4
- package/lib/typescript/commonjs/tabs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-engine/index.d.ts +1 -1
- package/lib/typescript/commonjs/theme/color-engine/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts +13 -1
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-policy.d.ts +6 -0
- package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-presets.d.ts +1 -1
- package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/module/scroll/index.web.d.ts.map +1 -1
- package/lib/typescript/module/scroll/scrollable.web.d.ts +18 -5
- package/lib/typescript/module/scroll/scrollable.web.d.ts.map +1 -1
- package/lib/typescript/module/tabs/Tabs.d.ts +41 -1
- package/lib/typescript/module/tabs/Tabs.d.ts.map +1 -1
- package/lib/typescript/module/tabs/expo-router/RouterTabs.d.ts +80 -0
- package/lib/typescript/module/tabs/expo-router/RouterTabs.d.ts.map +1 -0
- package/lib/typescript/module/tabs/expo-router/index.d.ts +9 -0
- package/lib/typescript/module/tabs/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/module/tabs/types.d.ts +48 -4
- package/lib/typescript/module/tabs/types.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-engine/index.d.ts +1 -1
- package/lib/typescript/module/theme/color-engine/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts +13 -1
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-policy.d.ts +6 -0
- package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-presets.d.ts +1 -1
- package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/package.json +15 -1
- package/src/__tests__/Tabs.test.tsx +470 -7
- package/src/__tests__/optional-peer-imports.test.ts +5 -0
- package/src/__tests__/scroll-web.test.tsx +255 -2
- package/src/__tests__/theme.test.ts +2 -1
- package/src/scroll/index.web.tsx +55 -19
- package/src/scroll/scrollable.web.ts +30 -8
- package/src/tabs/Tabs.tsx +518 -83
- package/src/tabs/expo-router/RouterTabs.tsx +268 -0
- package/src/tabs/expo-router/index.ts +8 -0
- package/src/tabs/types.ts +48 -4
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +120 -0
- package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +70 -0
- package/src/theme/__tests__/policy-legibility.test.ts +12 -4
- package/src/theme/color-engine/index.ts +1 -1
- package/src/theme/color-engine/scheme-variants.ts +38 -1
- package/src/theme/color-policy.ts +68 -14
- package/src/theme/color-presets.ts +11 -2
- package/src/theme/color-scope/seed-scope.ts +6 -2
|
@@ -131,6 +131,29 @@ const TEXT_CHROMA = 60;
|
|
|
131
131
|
const SUBTLE_SOURCE_TONE = 60;
|
|
132
132
|
const SUBTLE_ALPHA = { light: 0.13, dark: 0.24 } as const;
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* A seed at or below this chroma has no colour to derive from, so the whole
|
|
136
|
+
* palette goes greyscale — the black-and-white theme. Keyed on the seed rather
|
|
137
|
+
* than on a flag, so picking any grey in a colour wheel lands there too instead
|
|
138
|
+
* of on a nearly-grey approximation of it.
|
|
139
|
+
*/
|
|
140
|
+
const MONOCHROME_MAX_CHROMA = 6;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Whether a seed carries no colour to derive a theme from — the one authority for
|
|
144
|
+
* that question, so the policy and the scheme choice can never disagree about a
|
|
145
|
+
* given seed and produce a grey palette wearing chromatic surfaces.
|
|
146
|
+
*/
|
|
147
|
+
export function isColourlessSeed(seedHex: string): boolean {
|
|
148
|
+
return Hct.fromInt(argbFromHex(seedHex)).chroma <= MONOCHROME_MAX_CHROMA;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Where a monochrome brand fill sits: near-black on a white page, near-white on a black one. */
|
|
152
|
+
const MONOCHROME_FILL_TONE = { light: 12, dark: 94 } as const;
|
|
153
|
+
|
|
154
|
+
/** The quiet member of a monochrome trio — the only one that is actually grey. */
|
|
155
|
+
const MONOCHROME_SECONDARY_TONE = { light: 36, dark: 72 } as const;
|
|
156
|
+
|
|
134
157
|
/** Split-complementary: the two accents sit either side of the seed's complement. */
|
|
135
158
|
const SPLIT_ROTATIONS = [150, 210] as const;
|
|
136
159
|
|
|
@@ -211,7 +234,8 @@ function vividHueNear(hue: number): number {
|
|
|
211
234
|
* ceasing to be itself: yellow turns to brown and lime to moss. Those keep their
|
|
212
235
|
* own tone and take a black label instead, which is what a bright yellow button
|
|
213
236
|
* wants anyway. The 25-tone budget is what separates the two cases, and it is
|
|
214
|
-
* why 11 of the 13 presets carry white and exactly the two light ones
|
|
237
|
+
* why 11 of the 13 chromatic presets carry white and exactly the two light ones
|
|
238
|
+
* do not. (`mono` sits outside this rule — see MONOCHROME_FILL_TONE.)
|
|
215
239
|
*/
|
|
216
240
|
function whiteLabelTone(palette: TonalPalette, seedTone: number, isDark: boolean): number {
|
|
217
241
|
// LIGHT: every fill comes down, no exemption. On a near-white page a bright
|
|
@@ -306,12 +330,24 @@ export function buildPolicyTokens(
|
|
|
306
330
|
// tone <= 49, so that is the ceiling — in dark as much as in light. Letting the
|
|
307
331
|
// dark fill sit at the seed's own tone instead makes it brighter, but every one
|
|
308
332
|
// of those labels turns black, which costs more than the brightness buys.
|
|
333
|
+
const monochrome = seed.chroma <= MONOCHROME_MAX_CHROMA;
|
|
309
334
|
// DARK keys off the seed itself so the fill IS the brand hex; LIGHT uses the
|
|
310
335
|
// max-chroma palette, which is what stops a deepened fill from going pastel.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
336
|
+
// A monochrome seed has no chroma to preserve either way, and its fill goes to
|
|
337
|
+
// the far end of the scale rather than the white-label ceiling: a mid-grey
|
|
338
|
+
// button reads as disabled, where near-black on white reads as the primary
|
|
339
|
+
// action.
|
|
340
|
+
const brandPalette = monochrome
|
|
341
|
+
? TonalPalette.fromHueAndChroma(0, 0)
|
|
342
|
+
: isDark
|
|
343
|
+
? TonalPalette.fromInt(argbFromHex(seedHex))
|
|
344
|
+
: TonalPalette.fromHueAndChroma(seed.hue, 200);
|
|
345
|
+
const primary = fillPair(
|
|
346
|
+
brandPalette,
|
|
347
|
+
monochrome
|
|
348
|
+
? (isDark ? MONOCHROME_FILL_TONE.dark : MONOCHROME_FILL_TONE.light)
|
|
349
|
+
: whiteLabelTone(brandPalette, seed.tone, isDark),
|
|
350
|
+
);
|
|
315
351
|
|
|
316
352
|
const tokens: PolicyTokens = {
|
|
317
353
|
'--primary': primary.fill,
|
|
@@ -320,7 +356,7 @@ export function buildPolicyTokens(
|
|
|
320
356
|
// does correctly, and which the fill must stop trying to do at the same time.
|
|
321
357
|
'--primary-text': roles.primary,
|
|
322
358
|
'--primary-subtle': rgba(
|
|
323
|
-
TonalPalette.fromHueAndChroma(seed.hue, 200).tone(SUBTLE_SOURCE_TONE),
|
|
359
|
+
TonalPalette.fromHueAndChroma(seed.hue, monochrome ? 0 : 200).tone(SUBTLE_SOURCE_TONE),
|
|
324
360
|
isDark ? SUBTLE_ALPHA.dark : SUBTLE_ALPHA.light,
|
|
325
361
|
),
|
|
326
362
|
'--ring': roles.primary,
|
|
@@ -332,7 +368,9 @@ export function buildPolicyTokens(
|
|
|
332
368
|
'--card': isDark ? roles.surfaceContainer : roles.surfaceContainerLowest,
|
|
333
369
|
// A touch darker than M3's page background: near-white reads as unfinished
|
|
334
370
|
// next to the card, and the extra step gives the surface ramp somewhere to sit.
|
|
335
|
-
'--background': rgb(
|
|
371
|
+
'--background': rgb(
|
|
372
|
+
TonalPalette.fromHueAndChroma(seed.hue, monochrome ? 0 : 10).tone(isDark ? 4 : 96),
|
|
373
|
+
),
|
|
336
374
|
// `--accent` stays what every consumer actually uses it for: a hover surface.
|
|
337
375
|
'--accent': roles.surfaceContainerHigh,
|
|
338
376
|
'--accent-foreground': roles.onSurfaceVariant,
|
|
@@ -344,9 +382,11 @@ export function buildPolicyTokens(
|
|
|
344
382
|
const hue = pin !== undefined ? Hct.fromInt(argbFromHex(pin)).hue : hues[role];
|
|
345
383
|
// One hue, three palettes. Every member shares it, so the family still reads
|
|
346
384
|
// as one colour; only how loudly each speaks differs.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
385
|
+
// A monochrome theme has no accents to rotate to; the trio becomes a ladder
|
|
386
|
+
// of greys, which still separates a standout button from a quiet one.
|
|
387
|
+
const fillPalette = TonalPalette.fromHueAndChroma(hue, monochrome ? 0 : ACCENT_CHROMA);
|
|
388
|
+
const textPalette = TonalPalette.fromHueAndChroma(hue, monochrome ? 0 : TEXT_CHROMA);
|
|
389
|
+
const vividPalette = TonalPalette.fromHueAndChroma(hue, monochrome ? 0 : 200);
|
|
350
390
|
// DARK sits at the hue's peak tone, floored by the same rule the brand fill
|
|
351
391
|
// uses: some hues peak dark (violet at 35), and rendering them there gives a
|
|
352
392
|
// slab rather than a highlight. LIGHT goes deep like every other fill, but
|
|
@@ -362,7 +402,20 @@ export function buildPolicyTokens(
|
|
|
362
402
|
// black. The accent chroma cap already keeps these hues out of neon territory,
|
|
363
403
|
// and the hue itself is the caller's brand rather than ours to move. The
|
|
364
404
|
// analyzer still runs where it was designed to, inside `color-roles`.
|
|
365
|
-
|
|
405
|
+
// Monochrome keeps `tertiary` AT the brand fill rather than a step off it:
|
|
406
|
+
// tertiary is what a compose button and a FAB paint with, and those are the
|
|
407
|
+
// primary action — a step of grey between them and `primary` reads as two
|
|
408
|
+
// buttons disagreeing, not as hierarchy. `secondary` carries the whole
|
|
409
|
+
// demotion instead.
|
|
410
|
+
const tone = monochrome
|
|
411
|
+
? role === 'tertiary'
|
|
412
|
+
? isDark
|
|
413
|
+
? MONOCHROME_FILL_TONE.dark
|
|
414
|
+
: MONOCHROME_FILL_TONE.light
|
|
415
|
+
: isDark
|
|
416
|
+
? MONOCHROME_SECONDARY_TONE.dark
|
|
417
|
+
: MONOCHROME_SECONDARY_TONE.light
|
|
418
|
+
: accentTone(hue, isDark);
|
|
366
419
|
const pair = fillPair(fillPalette, tone);
|
|
367
420
|
tokens[`--${role}`] = pair.fill;
|
|
368
421
|
tokens[`--${role}-foreground`] = pair.foreground;
|
|
@@ -377,9 +430,10 @@ export function buildPolicyTokens(
|
|
|
377
430
|
// The old ramp drew all five from the primary/secondary/tertiary trio, which
|
|
378
431
|
// spans ~35 degrees, so adjacent series were indistinguishable.
|
|
379
432
|
for (let i = 0; i < 5; i += 1) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
433
|
+
// Greyscale charts spread by TONE instead of hue — the only axis left.
|
|
434
|
+
tokens[`--chart-${i + 1}`] = monochrome
|
|
435
|
+
? rgb(TonalPalette.fromHueAndChroma(0, 0).tone(isDark ? 40 + i * 13 : 78 - i * 13))
|
|
436
|
+
: rgb(TonalPalette.fromHueAndChroma((seed.hue + i * 72) % 360, 60).tone(isDark ? 72 : 48));
|
|
383
437
|
}
|
|
384
438
|
|
|
385
439
|
// The status family, themed per mode. The four frozen hexes it replaces all
|
|
@@ -13,7 +13,8 @@ export type AppColorName =
|
|
|
13
13
|
| 'orange'
|
|
14
14
|
| 'mint'
|
|
15
15
|
| 'oxy'
|
|
16
|
-
| 'faircoin'
|
|
16
|
+
| 'faircoin'
|
|
17
|
+
| 'mono';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* A single colour preset: a brand SEED colour plus the tonal scheme variant to
|
|
@@ -56,7 +57,7 @@ export interface AppColorPreset {
|
|
|
56
57
|
*/
|
|
57
58
|
export type PresetTokens = Record<string, string>;
|
|
58
59
|
|
|
59
|
-
export const APP_COLOR_NAMES: readonly AppColorName[] = ['teal', 'blue', 'green', 'amber', 'yellow', 'red', 'purple', 'pink', 'sky', 'orange', 'mint', 'oxy', 'faircoin'];
|
|
60
|
+
export const APP_COLOR_NAMES: readonly AppColorName[] = ['teal', 'blue', 'green', 'amber', 'yellow', 'red', 'purple', 'pink', 'sky', 'orange', 'mint', 'oxy', 'faircoin', 'mono'];
|
|
60
61
|
|
|
61
62
|
/** Premium-exclusive presets, hidden from the standard color picker. */
|
|
62
63
|
export const PREMIUM_COLOR_NAMES: readonly AppColorName[] = ['oxy', 'faircoin'];
|
|
@@ -75,6 +76,7 @@ export const HEX_TO_APP_COLOR: Record<string, AppColorName> = {
|
|
|
75
76
|
'#14b8a6': 'mint',
|
|
76
77
|
'#c46ede': 'oxy',
|
|
77
78
|
'#9ffb50': 'faircoin',
|
|
79
|
+
'#000000': 'mono',
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
export function hexToAppColorName(hex: string): AppColorName {
|
|
@@ -101,4 +103,11 @@ export const APP_COLOR_PRESETS: Record<AppColorName, AppColorPreset> = {
|
|
|
101
103
|
mint: { name: 'mint', hex: '#14b8a6', variant: 'vivid' },
|
|
102
104
|
oxy: { name: 'oxy', hex: '#c46ede', variant: 'vivid' },
|
|
103
105
|
faircoin: { name: 'faircoin', hex: '#9ffb50', variant: 'vivid' },
|
|
106
|
+
/**
|
|
107
|
+
* No colour at all — the black-and-white theme. Its seed carries zero chroma,
|
|
108
|
+
* which is the whole mechanism: the policy derives a greyscale palette from any
|
|
109
|
+
* seed with none, so a user who picks a grey in the colour wheel lands here too
|
|
110
|
+
* rather than on a nearly-grey approximation.
|
|
111
|
+
*/
|
|
112
|
+
mono: { name: 'mono', hex: '#000000', variant: 'monochrome' },
|
|
104
113
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateRoleColors, type RoleColors, type SchemeVariant } from '../color-engine';
|
|
2
|
-
import { buildPolicyTokens } from '../color-policy';
|
|
2
|
+
import { buildPolicyTokens, isColourlessSeed } from '../color-policy';
|
|
3
3
|
import { CANONICAL_TOKENS } from '../token-registry';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -104,7 +104,11 @@ export interface SeedScopeOptions {
|
|
|
104
104
|
export function buildSeedScopeVars(options: SeedScopeOptions): Record<string, string> {
|
|
105
105
|
const roles = generateRoleColors({
|
|
106
106
|
seed: options.seed,
|
|
107
|
-
|
|
107
|
+
// A seed with no chroma has no colour for a chromatic variant to work with,
|
|
108
|
+
// so it takes the greyscale scheme unless the caller asked for something else.
|
|
109
|
+
// This is what makes a grey picked in a colour wheel produce the SAME
|
|
110
|
+
// black-and-white theme as the `mono` preset instead of a tinted near-grey.
|
|
111
|
+
variant: options.variant ?? (isColourlessSeed(options.seed) ? 'monochrome' : 'vivid'),
|
|
108
112
|
isDark: options.mode === 'dark',
|
|
109
113
|
contrastLevel: options.contrastLevel ?? 0,
|
|
110
114
|
secondarySeed: options.secondarySeed,
|