@oxyhq/bloom 0.79.0 → 0.79.1
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 +3 -3
- package/lib/commonjs/provider/index.js +11 -7
- package/lib/commonjs/provider/index.js.map +1 -1
- package/lib/commonjs/scroll/context.js +96 -0
- package/lib/commonjs/scroll/context.js.map +1 -0
- package/lib/commonjs/scroll/expo-router/index.js +133 -0
- package/lib/commonjs/scroll/expo-router/index.js.map +1 -0
- package/lib/commonjs/scroll/index.js +85 -21
- package/lib/commonjs/scroll/index.js.map +1 -1
- package/lib/commonjs/scroll/index.web.js +184 -118
- package/lib/commonjs/scroll/index.web.js.map +1 -1
- package/lib/commonjs/scroll/scrollable.native.js +58 -0
- package/lib/commonjs/scroll/scrollable.native.js.map +1 -0
- package/lib/commonjs/scroll/store.js +47 -25
- package/lib/commonjs/scroll/store.js.map +1 -1
- package/lib/commonjs/theme/color-policy.js +11 -10
- package/lib/commonjs/theme/color-policy.js.map +1 -1
- package/lib/module/provider/index.js +10 -6
- package/lib/module/provider/index.js.map +1 -1
- package/lib/module/scroll/context.js +90 -0
- package/lib/module/scroll/context.js.map +1 -0
- package/lib/module/scroll/expo-router/index.js +128 -0
- package/lib/module/scroll/expo-router/index.js.map +1 -0
- package/lib/module/scroll/index.js +80 -21
- package/lib/module/scroll/index.js.map +1 -1
- package/lib/module/scroll/index.web.js +181 -119
- package/lib/module/scroll/index.web.js.map +1 -1
- package/lib/module/scroll/scrollable.native.js +54 -0
- package/lib/module/scroll/scrollable.native.js.map +1 -0
- package/lib/module/scroll/store.js +47 -25
- package/lib/module/scroll/store.js.map +1 -1
- package/lib/module/theme/color-policy.js +11 -10
- package/lib/module/theme/color-policy.js.map +1 -1
- package/lib/typescript/commonjs/provider/index.d.ts +7 -5
- package/lib/typescript/commonjs/provider/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/context.d.ts +14 -0
- package/lib/typescript/commonjs/scroll/context.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/expo-router/index.d.ts +7 -0
- package/lib/typescript/commonjs/scroll/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/index.d.ts +14 -23
- package/lib/typescript/commonjs/scroll/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/index.web.d.ts +17 -19
- package/lib/typescript/commonjs/scroll/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/scrollable.native.d.ts +23 -0
- package/lib/typescript/commonjs/scroll/scrollable.native.d.ts.map +1 -0
- package/lib/typescript/commonjs/scroll/store.d.ts +41 -22
- package/lib/typescript/commonjs/scroll/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/scroll/types.d.ts +82 -12
- package/lib/typescript/commonjs/scroll/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/module/provider/index.d.ts +7 -5
- package/lib/typescript/module/provider/index.d.ts.map +1 -1
- package/lib/typescript/module/scroll/context.d.ts +14 -0
- package/lib/typescript/module/scroll/context.d.ts.map +1 -0
- package/lib/typescript/module/scroll/expo-router/index.d.ts +7 -0
- package/lib/typescript/module/scroll/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/module/scroll/index.d.ts +14 -23
- package/lib/typescript/module/scroll/index.d.ts.map +1 -1
- package/lib/typescript/module/scroll/index.web.d.ts +17 -19
- package/lib/typescript/module/scroll/index.web.d.ts.map +1 -1
- package/lib/typescript/module/scroll/scrollable.native.d.ts +23 -0
- package/lib/typescript/module/scroll/scrollable.native.d.ts.map +1 -0
- package/lib/typescript/module/scroll/store.d.ts +41 -22
- package/lib/typescript/module/scroll/store.d.ts.map +1 -1
- package/lib/typescript/module/scroll/types.d.ts +82 -12
- package/lib/typescript/module/scroll/types.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
- package/package.json +15 -1
- package/src/__tests__/BloomProvider.web.test.tsx +8 -15
- package/src/__tests__/optional-peer-imports.test.ts +2 -2
- package/src/__tests__/scroll-expo-router-adapter.test.tsx +164 -0
- package/src/__tests__/scroll-native.test.tsx +465 -0
- package/src/__tests__/scroll-store.test.ts +41 -17
- package/src/__tests__/scroll-web.test.tsx +547 -111
- package/src/provider/index.tsx +10 -7
- package/src/scroll/context.tsx +107 -0
- package/src/scroll/expo-router/index.ts +140 -0
- package/src/scroll/index.ts +92 -25
- package/src/scroll/index.web.tsx +219 -162
- package/src/scroll/scrollable.native.ts +64 -0
- package/src/scroll/store.ts +47 -25
- package/src/scroll/types.ts +82 -12
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +5 -5
- package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +11 -11
- package/src/theme/__tests__/policy-legibility.test.ts +10 -7
- package/src/theme/color-policy.ts +11 -10
- package/lib/commonjs/provider/scroll-provider.js +0 -13
- package/lib/commonjs/provider/scroll-provider.js.map +0 -1
- package/lib/commonjs/provider/scroll-provider.web.js +0 -13
- package/lib/commonjs/provider/scroll-provider.web.js.map +0 -1
- package/lib/module/provider/scroll-provider.js +0 -16
- package/lib/module/provider/scroll-provider.js.map +0 -1
- package/lib/module/provider/scroll-provider.web.js +0 -5
- package/lib/module/provider/scroll-provider.web.js.map +0 -1
- package/lib/typescript/commonjs/provider/scroll-provider.d.ts +0 -14
- package/lib/typescript/commonjs/provider/scroll-provider.d.ts.map +0 -1
- package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts +0 -3
- package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts.map +0 -1
- package/lib/typescript/module/provider/scroll-provider.d.ts +0 -14
- package/lib/typescript/module/provider/scroll-provider.d.ts.map +0 -1
- package/lib/typescript/module/provider/scroll-provider.web.d.ts +0 -3
- package/lib/typescript/module/provider/scroll-provider.web.d.ts.map +0 -1
- package/src/__tests__/scroll-native.test.ts +0 -25
- package/src/provider/scroll-provider.ts +0 -13
- package/src/provider/scroll-provider.web.ts +0 -2
package/src/scroll/store.ts
CHANGED
|
@@ -3,16 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This file deliberately contains NO React and NO DOM/native imports so the
|
|
5
5
|
* logic can be unit-tested in isolation and shared verbatim by the web and
|
|
6
|
-
* native barrels.
|
|
7
|
-
* native barrel never instantiates it (native-stack already restores scroll).
|
|
6
|
+
* native barrels.
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
|
-
* Separator between the
|
|
12
|
-
* sub-key. A route may host more than one independently-scrolling list (e.g.
|
|
13
|
-
* a tabbed profile screen), so each list contributes its own sub-key.
|
|
9
|
+
* Separator between the two components of a storage key.
|
|
14
10
|
*
|
|
15
|
-
* `\0` (NUL) can never appear in a
|
|
11
|
+
* `\0` (NUL) can never appear in a router-derived content id or in a
|
|
16
12
|
* developer-authored sub-key, so it is collision-free as a delimiter. It is
|
|
17
13
|
* written as an escape sequence (not a literal byte) so the source stays
|
|
18
14
|
* text-diffable.
|
|
@@ -20,31 +16,51 @@
|
|
|
20
16
|
const COMPOSITE_KEY_SEPARATOR = '\0';
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
|
-
* Derive the storage key for a scrollable from its
|
|
24
|
-
* optional caller sub-key.
|
|
19
|
+
* Derive the storage key for a scrollable from WHAT its screen is showing plus
|
|
20
|
+
* an optional caller sub-key.
|
|
21
|
+
*
|
|
22
|
+
* The key is CONTENT identity, deliberately not the navigation entry. An offset
|
|
23
|
+
* belongs to what the user was looking at, so returning to it restores however
|
|
24
|
+
* they got there — browser Back or Forward, a tab press, an in-app link — and
|
|
25
|
+
* only content never seen this session opens at the top. Keying on the entry
|
|
26
|
+
* would defeat that: a tab press and `router.replace` both mint a fresh entry,
|
|
27
|
+
* so both would open at the top even though the user has been there.
|
|
28
|
+
*
|
|
29
|
+
* The trade, chosen rather than overlooked: the same content occupying two LIVE
|
|
30
|
+
* entries now shares one offset. Pushing `/@alice` on top of `/@alice` in a
|
|
31
|
+
* native stack restores the first one's position instead of opening at the top,
|
|
32
|
+
* and while both are mounted each writes to the same key. That is the price of
|
|
33
|
+
* a tab press restoring, which is the behaviour that was asked for.
|
|
25
34
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
35
|
+
* The key is always the same two-field composite, with an absent sub-key
|
|
36
|
+
* written as empty, so two different identities can never collapse onto one key
|
|
37
|
+
* by omission.
|
|
28
38
|
*
|
|
29
|
-
* Returns `null` when there is no
|
|
30
|
-
* is not inside a navigator
|
|
39
|
+
* Returns `null` when there is no content id to anchor against (the scrollable
|
|
40
|
+
* is not inside a navigator, or the adapter cannot answer), which every caller
|
|
41
|
+
* treats as "do not persist and do not restore".
|
|
31
42
|
*/
|
|
32
43
|
export function deriveScrollKey(
|
|
33
|
-
|
|
44
|
+
contentId: string | null,
|
|
34
45
|
subKey?: string,
|
|
35
46
|
): string | null {
|
|
36
|
-
if (!
|
|
37
|
-
|
|
38
|
-
return `${routeKey}${COMPOSITE_KEY_SEPARATOR}${subKey}`;
|
|
47
|
+
if (!contentId) return null;
|
|
48
|
+
return [contentId, subKey ?? ''].join(COMPOSITE_KEY_SEPARATOR);
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
/**
|
|
42
52
|
* In-memory map of `scrollKey -> last-known scroll offset`.
|
|
43
53
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
54
|
+
* Offsets live only for the lifetime of the document/session. We never persist
|
|
55
|
+
* them — a full reload should start at the top — and we never auto-evict,
|
|
56
|
+
* because an entry can be revisited via browser Forward/Back long after it
|
|
57
|
+
* blurred.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately an instance held in React context rather than a module-level
|
|
60
|
+
* singleton: a dependency tree can legitimately contain two copies of Bloom
|
|
61
|
+
* (a hoisted one plus a nested one under another Oxy package), and a
|
|
62
|
+
* module-global would then exist twice with no way for either half to see the
|
|
63
|
+
* other's offsets.
|
|
48
64
|
*/
|
|
49
65
|
export class ScrollOffsetStore {
|
|
50
66
|
private readonly offsets = new Map<string, number>();
|
|
@@ -55,19 +71,25 @@ export class ScrollOffsetStore {
|
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
/**
|
|
58
|
-
* Read the saved offset for a key. Returns `0` when nothing was saved,
|
|
59
|
-
*
|
|
74
|
+
* Read the saved offset for a key. Returns `0` when nothing was saved, which
|
|
75
|
+
* is the same value a caller writes when {@link has} is false — an unseen
|
|
76
|
+
* list restores to the top.
|
|
60
77
|
*/
|
|
61
78
|
read(key: string): number {
|
|
62
79
|
return this.offsets.get(key) ?? 0;
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Whether an offset was ever saved for this key. This — not `read() > 0` —
|
|
84
|
+
* is what separates "restore" from "reset": a key that was never seen and a
|
|
85
|
+
* key deliberately saved at the top both read 0, and only the store knows
|
|
86
|
+
* which is which.
|
|
87
|
+
*/
|
|
66
88
|
has(key: string): boolean {
|
|
67
89
|
return this.offsets.has(key);
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
/** Drop a saved offset (e.g. when
|
|
92
|
+
/** Drop a saved offset (e.g. when an entry is permanently removed). */
|
|
71
93
|
forget(key: string): void {
|
|
72
94
|
this.offsets.delete(key);
|
|
73
95
|
}
|
package/src/scroll/types.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import type { ReactNode, RefObject } from 'react';
|
|
2
|
+
import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Anything `useScrollRestoration` knows how to read/write a vertical scroll
|
|
5
6
|
* offset from. The hook accepts a ref to one of these:
|
|
6
7
|
*
|
|
7
|
-
* - a React Native `ScrollView` / `FlatList` ref (which on web is
|
|
8
|
-
* DOM node and exposes `getScrollableNode()`),
|
|
8
|
+
* - a React Native `ScrollView` / `FlatList` / `FlashList` ref (which on web is
|
|
9
|
+
* backed by a DOM node and exposes `getScrollableNode()`),
|
|
9
10
|
* - a raw DOM element ref (when a component renders its own scroll container),
|
|
10
|
-
* - the literal `'window'` sentinel, for the
|
|
11
|
-
*
|
|
11
|
+
* - the literal `'window'` sentinel, for the layout where the list IS the
|
|
12
|
+
* document scroller.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* The two imperative methods are the native write path; web writes `scrollTop`
|
|
15
|
+
* on the resolved DOM node instead. Both are optional because a given ref only
|
|
16
|
+
* ever exposes one of them (list vs. scroll view).
|
|
15
17
|
*/
|
|
16
18
|
export interface ScrollableHandle {
|
|
17
|
-
/**
|
|
19
|
+
/** `ScrollView`'s imperative scroll API. */
|
|
18
20
|
scrollTo?: (options: { x?: number; y?: number; animated?: boolean }) => void;
|
|
21
|
+
/** `FlatList`/`FlashList`'s imperative scroll API. */
|
|
22
|
+
scrollToOffset?: (options: { offset: number; animated?: boolean }) => void;
|
|
19
23
|
/** Web/RNW path: returns the underlying DOM node. */
|
|
20
24
|
getScrollableNode?: () => unknown;
|
|
21
25
|
}
|
|
@@ -28,21 +32,87 @@ export type ScrollRestorationTarget =
|
|
|
28
32
|
| RefObject<unknown>
|
|
29
33
|
| 'window';
|
|
30
34
|
|
|
35
|
+
/** The effect shape `useScreenFocusEffect` runs — mirrors `useEffect`'s. */
|
|
36
|
+
export type ScreenFocusEffect = () => (() => void) | void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The router bindings the scroll core needs, and the ONLY thing it knows about
|
|
40
|
+
* routing. Keeping this to two hooks is what lets the core ship without a
|
|
41
|
+
* router import, so a Vite/SPA consumer can supply its own adapter (or none).
|
|
42
|
+
*
|
|
43
|
+
* Both members are HOOKS: the core calls them from `useScrollRestoration`, so
|
|
44
|
+
* the adapter value must be referentially stable for the lifetime of the
|
|
45
|
+
* provider (a module-level constant, which is what every adapter Bloom ships
|
|
46
|
+
* is). Swapping in a different adapter at runtime would change the hook order
|
|
47
|
+
* of every consumer below it.
|
|
48
|
+
*/
|
|
49
|
+
export interface ScrollRouterAdapter {
|
|
50
|
+
/**
|
|
51
|
+
* Identify WHAT the calling screen is showing — its route and params, not the
|
|
52
|
+
* history slot it occupies. Offsets are remembered per content, so returning
|
|
53
|
+
* to something already seen restores it however the user got there.
|
|
54
|
+
*
|
|
55
|
+
* It must describe the screen the CALLER is rendered inside, never the
|
|
56
|
+
* globally focused one: a background screen has to keep reporting its own
|
|
57
|
+
* content, or it would save its offset under the foreground screen's key.
|
|
58
|
+
*
|
|
59
|
+
* `null` when the adapter cannot answer (a scrollable rendered outside any
|
|
60
|
+
* navigator), which makes the hook inert rather than guessing.
|
|
61
|
+
*/
|
|
62
|
+
useScreenContentId(): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Run `effect` while the calling screen is focused, and its cleanup on blur.
|
|
65
|
+
* Re-runs when `effect`'s identity changes AND the screen is focused — that
|
|
66
|
+
* is what makes an in-screen key change (a tab or folder swap) visible to a
|
|
67
|
+
* hook that never blurred.
|
|
68
|
+
*/
|
|
69
|
+
useScreenFocusEffect(effect: ScreenFocusEffect): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
31
72
|
export interface UseScrollRestorationOptions {
|
|
32
73
|
/**
|
|
33
|
-
* Sub-key
|
|
34
|
-
*
|
|
35
|
-
*
|
|
74
|
+
* Sub-key distinguishing scrollables that share one screen AND one content
|
|
75
|
+
* id — the tabs of a profile, the folders of a saved-items screen. Changing
|
|
76
|
+
* it while the screen stays focused saves the outgoing list's offset and
|
|
77
|
+
* restores (or resets) the incoming one.
|
|
36
78
|
*/
|
|
37
79
|
key?: string;
|
|
38
80
|
/**
|
|
39
|
-
* When `false
|
|
40
|
-
*
|
|
81
|
+
* When `false` the hook is completely inert: no save, no restore, no reset.
|
|
82
|
+
*
|
|
83
|
+
* On NATIVE this is load-bearing rather than a feature flag. Restoring into a
|
|
84
|
+
* list whose rows have not been laid out yet lands at the wrong place (or is
|
|
85
|
+
* clamped away), and native has no equivalent of the web path's re-apply
|
|
86
|
+
* loop — so the caller gates on its own content being present, e.g.
|
|
87
|
+
* `enabled: rows.length > 0`. Flipping it from `false` to `true` is what
|
|
88
|
+
* triggers the restore.
|
|
89
|
+
*
|
|
41
90
|
* Defaults to `true`.
|
|
42
91
|
*/
|
|
43
92
|
enabled?: boolean;
|
|
44
93
|
}
|
|
45
94
|
|
|
95
|
+
/**
|
|
96
|
+
* What `useScrollRestoration` hands back. Identical on both platforms so call
|
|
97
|
+
* sites are written once.
|
|
98
|
+
*/
|
|
99
|
+
export interface ScrollRestorationBinding {
|
|
100
|
+
/**
|
|
101
|
+
* Wire onto the list being restored: `<FlashList onScroll={scroll.onScroll}>`.
|
|
102
|
+
*
|
|
103
|
+
* Native has no way to observe a list's offset from outside it, so this is
|
|
104
|
+
* how offsets are recorded there. On web it is a stable no-op — the web path
|
|
105
|
+
* subscribes to the resolved DOM node's own `scroll` event, which needs
|
|
106
|
+
* nothing from the caller — and passing it costs nothing.
|
|
107
|
+
*/
|
|
108
|
+
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
109
|
+
}
|
|
110
|
+
|
|
46
111
|
export interface ScrollRestorationProviderProps {
|
|
47
112
|
children: ReactNode;
|
|
113
|
+
/**
|
|
114
|
+
* Binds the store to a router. Must be referentially stable — see
|
|
115
|
+
* {@link ScrollRouterAdapter}.
|
|
116
|
+
*/
|
|
117
|
+
adapter: ScrollRouterAdapter;
|
|
48
118
|
}
|
|
@@ -366,7 +366,7 @@
|
|
|
366
366
|
"--surface-foreground": "rgb(36 26 14)",
|
|
367
367
|
"--popover": "rgb(255 234 215)",
|
|
368
368
|
"--popover-foreground": "rgb(36 26 14)",
|
|
369
|
-
"--primary": "rgb(
|
|
369
|
+
"--primary": "rgb(150 94 0)",
|
|
370
370
|
"--primary-foreground": "rgb(255 255 255)",
|
|
371
371
|
"--secondary": "rgb(0 117 141)",
|
|
372
372
|
"--secondary-foreground": "rgb(255 255 255)",
|
|
@@ -486,8 +486,8 @@
|
|
|
486
486
|
"--surface-foreground": "rgb(33 27 13)",
|
|
487
487
|
"--popover": "rgb(250 236 213)",
|
|
488
488
|
"--popover-foreground": "rgb(33 27 13)",
|
|
489
|
-
"--primary": "rgb(
|
|
490
|
-
"--primary-foreground": "rgb(
|
|
489
|
+
"--primary": "rgb(135 102 0)",
|
|
490
|
+
"--primary-foreground": "rgb(255 255 255)",
|
|
491
491
|
"--secondary": "rgb(0 114 158)",
|
|
492
492
|
"--secondary-foreground": "rgb(255 255 255)",
|
|
493
493
|
"--tertiary": "rgb(117 66 250)",
|
|
@@ -1446,8 +1446,8 @@
|
|
|
1446
1446
|
"--surface-foreground": "rgb(24 29 18)",
|
|
1447
1447
|
"--popover": "rgb(234 240 222)",
|
|
1448
1448
|
"--popover-foreground": "rgb(24 29 18)",
|
|
1449
|
-
"--primary": "rgb(
|
|
1450
|
-
"--primary-foreground": "rgb(
|
|
1449
|
+
"--primary": "rgb(63 120 0)",
|
|
1450
|
+
"--primary-foreground": "rgb(255 255 255)",
|
|
1451
1451
|
"--secondary": "rgb(79 79 255)",
|
|
1452
1452
|
"--secondary-foreground": "rgb(255 255 255)",
|
|
1453
1453
|
"--tertiary": "rgb(190 36 152)",
|
|
@@ -47,14 +47,14 @@ exports[`theme.colors snapshot for all presets (catches unintended palette shift
|
|
|
47
47
|
"contrast50": "rgb(249 229 209)",
|
|
48
48
|
"error": "rgb(203 41 46)",
|
|
49
49
|
"icon": "rgb(84 68 50)",
|
|
50
|
-
"iconActive": "rgb(
|
|
50
|
+
"iconActive": "rgb(150 94 0)",
|
|
51
51
|
"info": "rgb(5 102 217)",
|
|
52
52
|
"negative": "rgb(186 26 26)",
|
|
53
53
|
"negativeForeground": "rgb(255 255 255)",
|
|
54
54
|
"negativeSubtle": "rgba(239, 68, 68, 0.13)",
|
|
55
55
|
"negativeSubtleForeground": "rgb(167 55 53)",
|
|
56
56
|
"overlay": "rgba(0, 0, 0, 0.5)",
|
|
57
|
-
"primary": "rgb(
|
|
57
|
+
"primary": "rgb(150 94 0)",
|
|
58
58
|
"primaryDark": "rgb(255 241 229)",
|
|
59
59
|
"primaryForeground": "rgb(255 255 255)",
|
|
60
60
|
"primaryLight": "rgb(255 241 229)",
|
|
@@ -69,7 +69,7 @@ exports[`theme.colors snapshot for all presets (catches unintended palette shift
|
|
|
69
69
|
"text": "rgb(36 26 14)",
|
|
70
70
|
"textSecondary": "rgb(84 68 50)",
|
|
71
71
|
"textTertiary": "rgb(135 116 96)",
|
|
72
|
-
"tint": "rgb(
|
|
72
|
+
"tint": "rgb(150 94 0)",
|
|
73
73
|
"warning": "rgb(150 94 0)",
|
|
74
74
|
},
|
|
75
75
|
"blue/dark": {
|
|
@@ -187,16 +187,16 @@ exports[`theme.colors snapshot for all presets (catches unintended palette shift
|
|
|
187
187
|
"contrast50": "rgb(228 235 216)",
|
|
188
188
|
"error": "rgb(203 41 46)",
|
|
189
189
|
"icon": "rgb(65 73 57)",
|
|
190
|
-
"iconActive": "rgb(
|
|
190
|
+
"iconActive": "rgb(63 120 0)",
|
|
191
191
|
"info": "rgb(5 102 217)",
|
|
192
192
|
"negative": "rgb(186 26 26)",
|
|
193
193
|
"negativeForeground": "rgb(255 255 255)",
|
|
194
194
|
"negativeSubtle": "rgba(239, 68, 68, 0.13)",
|
|
195
195
|
"negativeSubtleForeground": "rgb(167 55 53)",
|
|
196
196
|
"overlay": "rgba(0, 0, 0, 0.5)",
|
|
197
|
-
"primary": "rgb(
|
|
197
|
+
"primary": "rgb(63 120 0)",
|
|
198
198
|
"primaryDark": "rgb(240 246 227)",
|
|
199
|
-
"primaryForeground": "rgb(
|
|
199
|
+
"primaryForeground": "rgb(255 255 255)",
|
|
200
200
|
"primaryLight": "rgb(240 246 227)",
|
|
201
201
|
"primarySubtle": "rgba(87, 163, 0, 0.13)",
|
|
202
202
|
"primarySubtleForeground": "rgb(40 80 0)",
|
|
@@ -209,7 +209,7 @@ exports[`theme.colors snapshot for all presets (catches unintended palette shift
|
|
|
209
209
|
"text": "rgb(24 29 18)",
|
|
210
210
|
"textSecondary": "rgb(65 73 57)",
|
|
211
211
|
"textTertiary": "rgb(114 122 103)",
|
|
212
|
-
"tint": "rgb(
|
|
212
|
+
"tint": "rgb(63 120 0)",
|
|
213
213
|
"warning": "rgb(150 94 0)",
|
|
214
214
|
},
|
|
215
215
|
"green/dark": {
|
|
@@ -887,16 +887,16 @@ exports[`theme.colors snapshot for all presets (catches unintended palette shift
|
|
|
887
887
|
"contrast50": "rgb(244 231 208)",
|
|
888
888
|
"error": "rgb(203 41 46)",
|
|
889
889
|
"icon": "rgb(79 70 49)",
|
|
890
|
-
"iconActive": "rgb(
|
|
890
|
+
"iconActive": "rgb(135 102 0)",
|
|
891
891
|
"info": "rgb(5 102 217)",
|
|
892
892
|
"negative": "rgb(186 26 26)",
|
|
893
893
|
"negativeForeground": "rgb(255 255 255)",
|
|
894
894
|
"negativeSubtle": "rgba(239, 68, 68, 0.13)",
|
|
895
895
|
"negativeSubtleForeground": "rgb(167 55 53)",
|
|
896
896
|
"overlay": "rgba(0, 0, 0, 0.5)",
|
|
897
|
-
"primary": "rgb(
|
|
897
|
+
"primary": "rgb(135 102 0)",
|
|
898
898
|
"primaryDark": "rgb(255 242 221)",
|
|
899
|
-
"primaryForeground": "rgb(
|
|
899
|
+
"primaryForeground": "rgb(255 255 255)",
|
|
900
900
|
"primaryLight": "rgb(255 242 221)",
|
|
901
901
|
"primarySubtle": "rgba(182, 138, 0, 0.13)",
|
|
902
902
|
"primarySubtleForeground": "rgb(90 67 0)",
|
|
@@ -909,7 +909,7 @@ exports[`theme.colors snapshot for all presets (catches unintended palette shift
|
|
|
909
909
|
"text": "rgb(33 27 13)",
|
|
910
910
|
"textSecondary": "rgb(79 70 49)",
|
|
911
911
|
"textTertiary": "rgb(130 118 95)",
|
|
912
|
-
"tint": "rgb(
|
|
912
|
+
"tint": "rgb(135 102 0)",
|
|
913
913
|
"warning": "rgb(150 94 0)",
|
|
914
914
|
},
|
|
915
915
|
}
|
|
@@ -117,11 +117,12 @@ describe('colour policy legibility', () => {
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
// The
|
|
121
|
-
// the
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
120
|
+
// The two modes want opposite things and the suite has to say which. Applying
|
|
121
|
+
// the budget in LIGHT let a light seed keep its own tone there, so faircoin
|
|
122
|
+
// rendered the same pale lime in both modes — no theme at all. Skipping it in
|
|
123
|
+
// DARK left every Follow button, avatar and chat bubble with a black label.
|
|
124
|
+
// Each half was individually legible, so nothing else could catch either.
|
|
125
|
+
it('the brand fill keeps light exemption-free and dark budgeted', () => {
|
|
125
126
|
const white = { light: 0, dark: 0 };
|
|
126
127
|
for (const preset of APP_COLOR_NAMES) {
|
|
127
128
|
for (const mode of ['light', 'dark'] as const) {
|
|
@@ -130,7 +131,9 @@ describe('colour policy legibility', () => {
|
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// Light admits no exemption, so every preset carries white there. Dark keeps
|
|
135
|
+
// the budget, so the seeds that are already light keep their colour instead.
|
|
136
|
+
expect(white.light).toBe(APP_COLOR_NAMES.length);
|
|
137
|
+
expect(white.dark).toBeGreaterThan(APP_COLOR_NAMES.length - 4);
|
|
135
138
|
});
|
|
136
139
|
});
|
|
@@ -214,18 +214,19 @@ function vividHueNear(hue: number): number {
|
|
|
214
214
|
* why 11 of the 13 presets carry white and exactly the two light ones do not.
|
|
215
215
|
*/
|
|
216
216
|
function whiteLabelTone(palette: TonalPalette, seedTone: number, isDark: boolean): number {
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
217
|
+
// LIGHT: every fill comes down, no exemption. On a near-white page a bright
|
|
218
|
+
// colour has nothing to read against — a lime at its own tone 90 is a pale
|
|
219
|
+
// smear there, indistinguishable from the same colour in dark, which is not a
|
|
220
|
+
// theme at all. Coming down is what makes faircoin a deep green in light and a
|
|
221
|
+
// bright lime in dark.
|
|
222
|
+
if (!isDark) return LIGHT_FILL_TONE;
|
|
223
|
+
// DARK: the budget applies. Most brands come down to where white fits, but a
|
|
224
|
+
// seed that is already light cannot without ceasing to be itself, so it keeps
|
|
225
|
+
// its own tone and takes a black label. That is the eleven-and-two pattern.
|
|
226
|
+
const candidate = Math.max(DARK_FILL_TONE, seedTone - TONE_BUDGET);
|
|
226
227
|
return contrastOf(palette.tone(candidate), true) >= AA
|
|
227
228
|
? candidate
|
|
228
|
-
: Math.max(seedTone,
|
|
229
|
+
: Math.max(seedTone, DARK_SEED_FLOOR);
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
/**
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
Object.defineProperty(exports, "ScrollRestorationProvider", {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
get: function () {
|
|
9
|
-
return _index.ScrollRestorationProvider;
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
var _index = require("../scroll/index.js");
|
|
13
|
-
//# sourceMappingURL=scroll-provider.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["_index","require"],"sourceRoot":"../../../src","sources":["provider/scroll-provider.ts"],"mappings":";;;;;;;;;;;AAYA,IAAAA,MAAA,GAAAC,OAAA","ignoreList":[]}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
Object.defineProperty(exports, "ScrollRestorationProvider", {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
get: function () {
|
|
9
|
-
return _indexWeb.ScrollRestorationProvider;
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
var _indexWeb = require("../scroll/index.web.js");
|
|
13
|
-
//# sourceMappingURL=scroll-provider.web.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["_indexWeb","require"],"sourceRoot":"../../../src","sources":["provider/scroll-provider.web.ts"],"mappings":";;;;;;;;;;;AACA,IAAAA,SAAA,GAAAC,OAAA","ignoreList":[]}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Native/default binding for the scroll-restoration provider used by
|
|
5
|
-
* {@link BloomProvider}.
|
|
6
|
-
*
|
|
7
|
-
* `@oxyhq/bloom/scroll` is web-forked, so its `browser` export condition hands
|
|
8
|
-
* web consumers the real implementation while native gets the no-op. A compiled
|
|
9
|
-
* `lib/module/provider/index.js` cannot benefit from that condition (it imports
|
|
10
|
-
* a relative path, not the package subpath), so the platform choice is made
|
|
11
|
-
* here by FILENAME instead: Metro picks `scroll-provider.web.ts` on web, and
|
|
12
|
-
* every web bundler that resolves `.web.js` picks the compiled sibling. Same
|
|
13
|
-
* mechanism the toast engine uses for `ToastHost.native.tsx`.
|
|
14
|
-
*/
|
|
15
|
-
export { ScrollRestorationProvider } from "../scroll/index.js";
|
|
16
|
-
//# sourceMappingURL=scroll-provider.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["ScrollRestorationProvider"],"sourceRoot":"../../../src","sources":["provider/scroll-provider.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,yBAAyB,QAAQ,oBAAW","ignoreList":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["ScrollRestorationProvider"],"sourceRoot":"../../../src","sources":["provider/scroll-provider.web.ts"],"mappings":";;AAAA;AACA,SAASA,yBAAyB,QAAQ,wBAAqB","ignoreList":[]}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Native/default binding for the scroll-restoration provider used by
|
|
3
|
-
* {@link BloomProvider}.
|
|
4
|
-
*
|
|
5
|
-
* `@oxyhq/bloom/scroll` is web-forked, so its `browser` export condition hands
|
|
6
|
-
* web consumers the real implementation while native gets the no-op. A compiled
|
|
7
|
-
* `lib/module/provider/index.js` cannot benefit from that condition (it imports
|
|
8
|
-
* a relative path, not the package subpath), so the platform choice is made
|
|
9
|
-
* here by FILENAME instead: Metro picks `scroll-provider.web.ts` on web, and
|
|
10
|
-
* every web bundler that resolves `.web.js` picks the compiled sibling. Same
|
|
11
|
-
* mechanism the toast engine uses for `ToastHost.native.tsx`.
|
|
12
|
-
*/
|
|
13
|
-
export { ScrollRestorationProvider } from '../scroll';
|
|
14
|
-
//# sourceMappingURL=scroll-provider.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-provider.d.ts","sourceRoot":"","sources":["../../../../src/provider/scroll-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-provider.web.d.ts","sourceRoot":"","sources":["../../../../src/provider/scroll-provider.web.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Native/default binding for the scroll-restoration provider used by
|
|
3
|
-
* {@link BloomProvider}.
|
|
4
|
-
*
|
|
5
|
-
* `@oxyhq/bloom/scroll` is web-forked, so its `browser` export condition hands
|
|
6
|
-
* web consumers the real implementation while native gets the no-op. A compiled
|
|
7
|
-
* `lib/module/provider/index.js` cannot benefit from that condition (it imports
|
|
8
|
-
* a relative path, not the package subpath), so the platform choice is made
|
|
9
|
-
* here by FILENAME instead: Metro picks `scroll-provider.web.ts` on web, and
|
|
10
|
-
* every web bundler that resolves `.web.js` picks the compiled sibling. Same
|
|
11
|
-
* mechanism the toast engine uses for `ToastHost.native.tsx`.
|
|
12
|
-
*/
|
|
13
|
-
export { ScrollRestorationProvider } from '../scroll';
|
|
14
|
-
//# sourceMappingURL=scroll-provider.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-provider.d.ts","sourceRoot":"","sources":["../../../../src/provider/scroll-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-provider.web.d.ts","sourceRoot":"","sources":["../../../../src/provider/scroll-provider.web.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ScrollRestorationProvider,
|
|
3
|
-
useScrollRestoration,
|
|
4
|
-
} from '../scroll/index';
|
|
5
|
-
|
|
6
|
-
// The native barrel is a deliberate no-op: native-stack already preserves
|
|
7
|
-
// scroll. These tests pin that contract so a future refactor can't silently
|
|
8
|
-
// turn the native path into something that touches the DOM or throws.
|
|
9
|
-
|
|
10
|
-
describe('native scroll-restoration barrel', () => {
|
|
11
|
-
it('ScrollRestorationProvider renders its children unchanged', () => {
|
|
12
|
-
const child = { sentinel: true } as unknown as React.ReactElement;
|
|
13
|
-
expect(ScrollRestorationProvider({ children: child })).toBe(child);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('useScrollRestoration is a no-op for any target/options', () => {
|
|
17
|
-
const ref = { current: null };
|
|
18
|
-
expect(() => useScrollRestoration(ref)).not.toThrow();
|
|
19
|
-
expect(() => useScrollRestoration('window')).not.toThrow();
|
|
20
|
-
expect(() =>
|
|
21
|
-
useScrollRestoration(ref, { key: 'feed', enabled: true }),
|
|
22
|
-
).not.toThrow();
|
|
23
|
-
expect(useScrollRestoration(ref)).toBeUndefined();
|
|
24
|
-
});
|
|
25
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Native/default binding for the scroll-restoration provider used by
|
|
3
|
-
* {@link BloomProvider}.
|
|
4
|
-
*
|
|
5
|
-
* `@oxyhq/bloom/scroll` is web-forked, so its `browser` export condition hands
|
|
6
|
-
* web consumers the real implementation while native gets the no-op. A compiled
|
|
7
|
-
* `lib/module/provider/index.js` cannot benefit from that condition (it imports
|
|
8
|
-
* a relative path, not the package subpath), so the platform choice is made
|
|
9
|
-
* here by FILENAME instead: Metro picks `scroll-provider.web.ts` on web, and
|
|
10
|
-
* every web bundler that resolves `.web.js` picks the compiled sibling. Same
|
|
11
|
-
* mechanism the toast engine uses for `ToastHost.native.tsx`.
|
|
12
|
-
*/
|
|
13
|
-
export { ScrollRestorationProvider } from '../scroll';
|