@oxy.so/bloom 4.26.0 → 4.27.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/NOTICE +2 -0
- package/README.md +1 -1
- package/docs/icons.mdx +21 -2
- package/lib/commonjs/icons/index.js +12 -0
- package/lib/commonjs/icons/index.js.map +1 -1
- package/lib/commonjs/icons/remix/RiInstagramFill.js +12 -0
- package/lib/commonjs/icons/remix/RiInstagramFill.js.map +1 -0
- package/lib/commonjs/icons/remix/RiMastodonFill.js +12 -0
- package/lib/commonjs/icons/remix/RiMastodonFill.js.map +1 -0
- package/lib/commonjs/icons/remix/RiMediumFill.js +12 -0
- package/lib/commonjs/icons/remix/RiMediumFill.js.map +1 -0
- package/lib/commonjs/icons/remix/RiThreadsFill.js +12 -0
- package/lib/commonjs/icons/remix/RiThreadsFill.js.map +1 -0
- package/lib/commonjs/icons/remix/index.js +28 -0
- package/lib/commonjs/icons/remix/index.js.map +1 -1
- package/lib/commonjs/icons/simple-icons/SiSubstack.js +22 -0
- package/lib/commonjs/icons/simple-icons/SiSubstack.js.map +1 -0
- package/lib/commonjs/icons/simple-icons/index.js +13 -0
- package/lib/commonjs/icons/simple-icons/index.js.map +1 -0
- package/lib/module/icons/index.js +5 -3
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/icons/remix/RiInstagramFill.js +9 -0
- package/lib/module/icons/remix/RiInstagramFill.js.map +1 -0
- package/lib/module/icons/remix/RiMastodonFill.js +9 -0
- package/lib/module/icons/remix/RiMastodonFill.js.map +1 -0
- package/lib/module/icons/remix/RiMediumFill.js +9 -0
- package/lib/module/icons/remix/RiMediumFill.js.map +1 -0
- package/lib/module/icons/remix/RiThreadsFill.js +9 -0
- package/lib/module/icons/remix/RiThreadsFill.js.map +1 -0
- package/lib/module/icons/remix/index.js +4 -0
- package/lib/module/icons/remix/index.js.map +1 -1
- package/lib/module/icons/simple-icons/SiSubstack.js +19 -0
- package/lib/module/icons/simple-icons/SiSubstack.js.map +1 -0
- package/lib/module/icons/simple-icons/index.js +7 -0
- package/lib/module/icons/simple-icons/index.js.map +1 -0
- package/lib/typescript/commonjs/icons/index.d.ts +1 -0
- package/lib/typescript/commonjs/icons/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/icons/remix/RiInstagramFill.d.ts +8 -0
- package/lib/typescript/commonjs/icons/remix/RiInstagramFill.d.ts.map +1 -0
- package/lib/typescript/commonjs/icons/remix/RiMastodonFill.d.ts +8 -0
- package/lib/typescript/commonjs/icons/remix/RiMastodonFill.d.ts.map +1 -0
- package/lib/typescript/commonjs/icons/remix/RiMediumFill.d.ts +8 -0
- package/lib/typescript/commonjs/icons/remix/RiMediumFill.d.ts.map +1 -0
- package/lib/typescript/commonjs/icons/remix/RiThreadsFill.d.ts +8 -0
- package/lib/typescript/commonjs/icons/remix/RiThreadsFill.d.ts.map +1 -0
- package/lib/typescript/commonjs/icons/remix/index.d.ts +4 -0
- package/lib/typescript/commonjs/icons/remix/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/icons/simple-icons/SiSubstack.d.ts +17 -0
- package/lib/typescript/commonjs/icons/simple-icons/SiSubstack.d.ts.map +1 -0
- package/lib/typescript/commonjs/icons/simple-icons/index.d.ts +2 -0
- package/lib/typescript/commonjs/icons/simple-icons/index.d.ts.map +1 -0
- package/lib/typescript/module/icons/index.d.ts +1 -0
- package/lib/typescript/module/icons/index.d.ts.map +1 -1
- package/lib/typescript/module/icons/remix/RiInstagramFill.d.ts +8 -0
- package/lib/typescript/module/icons/remix/RiInstagramFill.d.ts.map +1 -0
- package/lib/typescript/module/icons/remix/RiMastodonFill.d.ts +8 -0
- package/lib/typescript/module/icons/remix/RiMastodonFill.d.ts.map +1 -0
- package/lib/typescript/module/icons/remix/RiMediumFill.d.ts +8 -0
- package/lib/typescript/module/icons/remix/RiMediumFill.d.ts.map +1 -0
- package/lib/typescript/module/icons/remix/RiThreadsFill.d.ts +8 -0
- package/lib/typescript/module/icons/remix/RiThreadsFill.d.ts.map +1 -0
- package/lib/typescript/module/icons/remix/index.d.ts +4 -0
- package/lib/typescript/module/icons/remix/index.d.ts.map +1 -1
- package/lib/typescript/module/icons/simple-icons/SiSubstack.d.ts +17 -0
- package/lib/typescript/module/icons/simple-icons/SiSubstack.d.ts.map +1 -0
- package/lib/typescript/module/icons/simple-icons/index.d.ts +2 -0
- package/lib/typescript/module/icons/simple-icons/index.d.ts.map +1 -0
- package/package.json +18 -1
- package/src/__tests__/support/adoption-matrix.ts +703 -0
- package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
- package/src/__tests__/support/commerce-harness.tsx +97 -0
- package/src/__tests__/support/composite.ts +68 -0
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/__tests__/support/press-host.ts +30 -0
- package/src/__tests__/support/rendered-style.ts +99 -0
- package/src/__tests__/support/unread-hook-fixture.ts +33 -0
- package/src/__tests__/support/worklet-capture-fixture.tsx +28 -0
- package/src/icons/index.ts +5 -3
- package/src/icons/remix/RiInstagramFill.tsx +6 -0
- package/src/icons/remix/RiMastodonFill.tsx +6 -0
- package/src/icons/remix/RiMediumFill.tsx +6 -0
- package/src/icons/remix/RiThreadsFill.tsx +6 -0
- package/src/icons/remix/index.ts +4 -0
- package/src/icons/simple-icons/SiSubstack.tsx +16 -0
- package/src/icons/simple-icons/index.ts +4 -0
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +9346 -0
- package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +69 -0
- package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A barrel that DELIBERATELY offers one name from two declarations, so that
|
|
3
|
+
* `barrel-name-collisions.test.ts` can prove its detector fires. It reproduces
|
|
4
|
+
* the shipped `Item` shape: a star export carrying `Item` from `item/`, and an
|
|
5
|
+
* explicit re-export binding that same name to a different component.
|
|
6
|
+
*
|
|
7
|
+
* Nothing imports this at runtime. bob's `exclude` keeps it out of `lib/`, and
|
|
8
|
+
* `files: ["src", …]` does ship it inside the tarball's `src/` like the other
|
|
9
|
+
* test files there — but no entry point reaches it, so no bundler links it, and
|
|
10
|
+
* `package.json#exports` never names it.
|
|
11
|
+
*
|
|
12
|
+
* The `Card` lines below are the other half of the
|
|
13
|
+
* control: one name offered twice from ONE declaration, which the detector must
|
|
14
|
+
* stay quiet about — otherwise its cheapest fix would be deleting a legitimate
|
|
15
|
+
* redundant re-export.
|
|
16
|
+
*/
|
|
17
|
+
export * from '../../item';
|
|
18
|
+
export { Card as Item } from '../../card';
|
|
19
|
+
export { Card } from '../../card';
|
|
20
|
+
export * from '../../card';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mount harness the four commerce suites share.
|
|
3
|
+
*
|
|
4
|
+
* Every one of them asserts EMITTED DOM — an `aria-*` attribute, a computed
|
|
5
|
+
* colour, whether a node exists at all — rather than the props it just passed,
|
|
6
|
+
* so each renders through the real react-native-web. The mock has to be
|
|
7
|
+
* installed by the SUITE (a `jest.mock` call is hoisted to the top of the file
|
|
8
|
+
* it is written in and does not travel through an import), which is why this
|
|
9
|
+
* module exports the harness and not the mock.
|
|
10
|
+
*/
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { act } from 'react';
|
|
13
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
14
|
+
|
|
15
|
+
import { BloomThemeProvider } from '../../theme/BloomThemeProvider';
|
|
16
|
+
import type { Theme } from '../../theme/types';
|
|
17
|
+
import { useTheme } from '../../theme/use-theme';
|
|
18
|
+
|
|
19
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
20
|
+
|
|
21
|
+
let container: HTMLDivElement;
|
|
22
|
+
let root: Root;
|
|
23
|
+
let lastTheme: Theme | null = null;
|
|
24
|
+
|
|
25
|
+
function ThemeProbe() {
|
|
26
|
+
lastTheme = useTheme();
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setupHarness(): void {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
container = document.createElement('div');
|
|
33
|
+
document.body.appendChild(container);
|
|
34
|
+
root = createRoot(container);
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
act(() => root.unmount());
|
|
38
|
+
container.remove();
|
|
39
|
+
lastTheme = null;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function mount(ui: React.ReactElement, mode: 'light' | 'dark' = 'light'): HTMLElement {
|
|
44
|
+
act(() => {
|
|
45
|
+
root.render(
|
|
46
|
+
<BloomThemeProvider mode={mode} colorPreset="teal">
|
|
47
|
+
<ThemeProbe />
|
|
48
|
+
{ui}
|
|
49
|
+
</BloomThemeProvider>,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
return container;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function theme(): Theme {
|
|
56
|
+
if (!lastTheme) throw new Error('theme not captured');
|
|
57
|
+
return lastTheme;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function root$(): HTMLElement {
|
|
61
|
+
return container;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function byTestId(id: string): HTMLElement {
|
|
65
|
+
const el = document.querySelector(`[data-testid="${id}"]`);
|
|
66
|
+
if (!(el instanceof HTMLElement)) throw new Error(`No element for testID "${id}"`);
|
|
67
|
+
return el;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function queryTestId(id: string): HTMLElement | null {
|
|
71
|
+
return document.querySelector(`[data-testid="${id}"]`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function byLabel(label: string): HTMLElement {
|
|
75
|
+
const el = document.querySelector(`[aria-label="${label}"]`);
|
|
76
|
+
if (!(el instanceof HTMLElement)) throw new Error(`No element labelled "${label}"`);
|
|
77
|
+
return el;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function allByRole(role: string): HTMLElement[] {
|
|
81
|
+
return Array.from(document.querySelectorAll(`[role="${role}"]`)).filter(
|
|
82
|
+
(el): el is HTMLElement => el instanceof HTMLElement,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function click(el: HTMLElement): void {
|
|
87
|
+
act(() => {
|
|
88
|
+
el.click();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A colour as the DOM serialises it, for comparing against a computed style. */
|
|
93
|
+
export function css(color: string): string {
|
|
94
|
+
const probe = document.createElement('div');
|
|
95
|
+
probe.style.color = color;
|
|
96
|
+
return probe.style.color;
|
|
97
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compositing and WCAG maths for suites that measure a TRANSLUCENT surface.
|
|
3
|
+
*
|
|
4
|
+
* A pane you can see through has no colour of its own to read off a style prop:
|
|
5
|
+
* what a label lands on is the fill flattened onto whatever is behind it. Every
|
|
6
|
+
* gate on a glass material therefore needs the same four functions, and they
|
|
7
|
+
* are here so a new one does not start by copying them — which is how two
|
|
8
|
+
* copies of a luminance formula end up disagreeing about the sRGB knee.
|
|
9
|
+
*
|
|
10
|
+
* `theme/__tests__/glass-colors.test.ts` keeps its OWN copies, deliberately and
|
|
11
|
+
* with its own reasons written down; this is for everything since.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface Rgba {
|
|
15
|
+
r: number;
|
|
16
|
+
g: number;
|
|
17
|
+
b: number;
|
|
18
|
+
a: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Parse `#rgb`, `#rrggbb`, `rgb(...)` or `rgba(...)`. Throws on anything else. */
|
|
22
|
+
export function parseColor(value: string): Rgba {
|
|
23
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.trim());
|
|
24
|
+
if (hex?.[1] !== undefined) {
|
|
25
|
+
const h = hex[1].length === 3 ? hex[1].replace(/./g, (c) => c + c) : hex[1];
|
|
26
|
+
return {
|
|
27
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
28
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
29
|
+
b: parseInt(h.slice(4, 6), 16),
|
|
30
|
+
a: 1,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const fn = /^rgba?\(([^)]*)\)$/.exec(value.trim());
|
|
34
|
+
if (!fn?.[1]) throw new Error(`unparseable colour: ${JSON.stringify(value)}`);
|
|
35
|
+
const parts = fn[1].split(/[\s,/]+/).filter(Boolean).map(Number);
|
|
36
|
+
if (parts.length < 3 || parts.some((n) => Number.isNaN(n))) {
|
|
37
|
+
throw new Error(`unparseable colour: ${JSON.stringify(value)}`);
|
|
38
|
+
}
|
|
39
|
+
return { r: parts[0] ?? 0, g: parts[1] ?? 0, b: parts[2] ?? 0, a: parts[3] ?? 1 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Flatten a translucent colour onto an opaque one — source-over. */
|
|
43
|
+
export function over(top: Rgba, bottom: Rgba): Rgba {
|
|
44
|
+
return {
|
|
45
|
+
r: top.r * top.a + bottom.r * (1 - top.a),
|
|
46
|
+
g: top.g * top.a + bottom.g * (1 - top.a),
|
|
47
|
+
b: top.b * top.a + bottom.b * (1 - top.a),
|
|
48
|
+
a: 1,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** WCAG relative luminance. */
|
|
53
|
+
export function luminance({ r, g, b }: Rgba): number {
|
|
54
|
+
const channel = (v: number): number => {
|
|
55
|
+
const c = v / 255;
|
|
56
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
57
|
+
};
|
|
58
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** WCAG contrast ratio, order-independent. */
|
|
62
|
+
export function contrastRatio(a: Rgba, b: Rgba): number {
|
|
63
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x) as [number, number];
|
|
64
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const WHITE: Rgba = { r: 255, g: 255, b: 255, a: 1 };
|
|
68
|
+
export const BLACK: Rgba = { r: 0, g: 0, b: 0, a: 1 };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A spec-shaped stand-in for constructed stylesheets, for the jsdom suites.
|
|
3
|
+
*
|
|
4
|
+
* jsdom 26 ships the `CSSStyleSheet` CONSTRUCTOR but neither `replaceSync` nor
|
|
5
|
+
* `document.adoptedStyleSheets`, so every jsdom test takes Bloom's `<style>`
|
|
6
|
+
* fallback. That makes the whole CSP-safe path — the reason
|
|
7
|
+
* `styles/adopt-style-sheet.ts` exists — invisible to jest unless a test
|
|
8
|
+
* installs the API itself. Without this, a mutation that deleted the adoption
|
|
9
|
+
* branch entirely would leave the suite green.
|
|
10
|
+
*
|
|
11
|
+
* Not collected as a suite: jest's `testMatch` wants `*.test.ts` / `*.spec.ts`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface FakeStyleSheet {
|
|
15
|
+
/** The CSS most recently handed to `replaceSync`. */
|
|
16
|
+
cssText: string;
|
|
17
|
+
/** How many times the sheet has been re-parsed, so a test can prove it wasn't. */
|
|
18
|
+
replaceSyncCalls: number;
|
|
19
|
+
replaceSync(css: string): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ConstructedStyleSheetsHarness {
|
|
23
|
+
/** The document's adopted sheets, read live (the code reassigns the array). */
|
|
24
|
+
adopted(): readonly FakeStyleSheet[];
|
|
25
|
+
/** Drop the API again, restoring jsdom's own `CSSStyleSheet`. */
|
|
26
|
+
uninstall(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function installConstructedStyleSheets(): ConstructedStyleSheetsHarness {
|
|
30
|
+
class FakeCSSStyleSheet implements FakeStyleSheet {
|
|
31
|
+
cssText = '';
|
|
32
|
+
replaceSyncCalls = 0;
|
|
33
|
+
|
|
34
|
+
replaceSync(css: string): void {
|
|
35
|
+
this.cssText = css;
|
|
36
|
+
this.replaceSyncCalls += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const previousConstructor = Object.getOwnPropertyDescriptor(
|
|
41
|
+
globalThis,
|
|
42
|
+
'CSSStyleSheet',
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
Object.defineProperty(globalThis, 'CSSStyleSheet', {
|
|
46
|
+
value: FakeCSSStyleSheet,
|
|
47
|
+
writable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(document, 'adoptedStyleSheets', {
|
|
51
|
+
value: [],
|
|
52
|
+
writable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
adopted: () =>
|
|
58
|
+
document.adoptedStyleSheets as unknown as readonly FakeStyleSheet[],
|
|
59
|
+
uninstall: () => {
|
|
60
|
+
Reflect.deleteProperty(document, 'adoptedStyleSheets');
|
|
61
|
+
if (previousConstructor) {
|
|
62
|
+
Object.defineProperty(globalThis, 'CSSStyleSheet', previousConstructor);
|
|
63
|
+
} else {
|
|
64
|
+
Reflect.deleteProperty(globalThis, 'CSSStyleSheet');
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pressing a component's OWN host node, and proving the component is what
|
|
3
|
+
* installed the handler being measured.
|
|
4
|
+
*
|
|
5
|
+
* `fireEvent.press` walks UP from the element it is handed to the nearest
|
|
6
|
+
* ancestor carrying an `onPress` prop, and it does not stop at host nodes — a
|
|
7
|
+
* COMPOSITE counts, because `getEventHandler` only reads `element.props`. So
|
|
8
|
+
* `<Thing onPress={fn}>` written in a test's own JSX catches the press itself.
|
|
9
|
+
* Measured on a (since removed) card component: with the handler deleted from the
|
|
10
|
+
* component the card rendered as an inert `View`, and the test still reported exactly one
|
|
11
|
+
* call. Green, and measuring nothing. Nine suites had the same shape.
|
|
12
|
+
*
|
|
13
|
+
* Asserting the host node's own `onPress` FIRST closes both halves:
|
|
14
|
+
*
|
|
15
|
+
* - a component that installs no handler at all fails here, and
|
|
16
|
+
* - a component that installs one which drops the caller's fails on the call
|
|
17
|
+
* count, because `fireEvent` now finds a handler AT the node and never walks
|
|
18
|
+
* past it to the test's own JSX.
|
|
19
|
+
*
|
|
20
|
+
* `host` must therefore be the node the component itself made pressable — the
|
|
21
|
+
* one carrying its `testID` / `accessibilityLabel` — not a `Text` deep inside
|
|
22
|
+
* it, or the walk-up is back and so is the hole.
|
|
23
|
+
*/
|
|
24
|
+
import { fireEvent } from '@testing-library/react-native';
|
|
25
|
+
import type { ReactTestInstance } from 'react-test-renderer';
|
|
26
|
+
|
|
27
|
+
export function pressHost(host: ReactTestInstance): void {
|
|
28
|
+
expect(typeof host.props.onPress).toBe('function');
|
|
29
|
+
fireEvent.press(host);
|
|
30
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading what actually landed on a rendered node — shared by every suite that
|
|
3
|
+
* asserts the "className lands on the node the parent lays out" rule.
|
|
4
|
+
*
|
|
5
|
+
* Three components had the same two-node defect (`Button`, then `Fab` and
|
|
6
|
+
* `FrostedIconButton`), and asserting it needs the same two things each time:
|
|
7
|
+
* the HOST tree (composites do not appear in `toJSON()`, so a wrapper is only
|
|
8
|
+
* visible there) and a deep flatten of the `style` prop, because react-native-css
|
|
9
|
+
* merges a caller's class in as a `{ $$css: true, className }` descriptor
|
|
10
|
+
* ALONGSIDE the style objects rather than in a prop of its own.
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactTestRendererJSON } from 'react-test-renderer';
|
|
13
|
+
|
|
14
|
+
export type StyleEntry = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
export interface HostNode {
|
|
17
|
+
type: string;
|
|
18
|
+
props: Record<string, unknown>;
|
|
19
|
+
children: Array<HostNode | string> | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Deep-flatten an RN `style` prop (nested arrays, `false`/`null` holes) into the
|
|
24
|
+
* list of style objects that actually reached the node.
|
|
25
|
+
*/
|
|
26
|
+
export function styleEntries(style: unknown, out: StyleEntry[] = []): StyleEntry[] {
|
|
27
|
+
if (!style || typeof style !== 'object') return out;
|
|
28
|
+
if (Array.isArray(style)) {
|
|
29
|
+
for (const entry of style) styleEntries(entry, out);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
out.push(style as StyleEntry);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Every style key/value that landed on a node, later entries winning. */
|
|
37
|
+
export function resolvedStyle(style: unknown): StyleEntry {
|
|
38
|
+
return Object.assign({}, ...styleEntries(style)) as StyleEntry;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The class tokens react-native-css accepted for a node, in arrival order. */
|
|
42
|
+
export function classNamesOn(style: unknown): string[] {
|
|
43
|
+
return styleEntries(style)
|
|
44
|
+
.filter((entry) => entry.$$css === true && typeof entry.className === 'string')
|
|
45
|
+
.map((entry) => String(entry.className));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isHostNode(value: unknown): value is HostNode {
|
|
49
|
+
return (
|
|
50
|
+
typeof value === 'object' && value !== null && typeof (value as HostNode).type === 'string'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Walk the HOST tree (composites are absent from `toJSON()`) for a testID. */
|
|
55
|
+
export function findHost(
|
|
56
|
+
node: ReactTestRendererJSON | ReactTestRendererJSON[] | unknown,
|
|
57
|
+
testID: string,
|
|
58
|
+
): HostNode | null {
|
|
59
|
+
if (Array.isArray(node)) {
|
|
60
|
+
for (const child of node) {
|
|
61
|
+
const hit = findHost(child, testID);
|
|
62
|
+
if (hit) return hit;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (!isHostNode(node)) return null;
|
|
67
|
+
if (node.props.testID === testID) return node;
|
|
68
|
+
return findHost(node.children, testID);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every host node in a rendered tree, in document order.
|
|
73
|
+
*
|
|
74
|
+
* The reason to walk `toJSON()` rather than `UNSAFE_root.findAll` is that the
|
|
75
|
+
* latter yields COMPOSITE instances too, so a `memo(fn)` component contributes
|
|
76
|
+
* two matches for one rendered element and every count is silently doubled —
|
|
77
|
+
* which reads as "the component rendered the thing twice", not as an artefact
|
|
78
|
+
* of the query.
|
|
79
|
+
*/
|
|
80
|
+
export function hostNodes(tree: unknown, out: HostNode[] = []): HostNode[] {
|
|
81
|
+
if (Array.isArray(tree)) {
|
|
82
|
+
for (const child of tree) hostNodes(child, out);
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
if (!isHostNode(tree)) return out;
|
|
86
|
+
out.push(tree);
|
|
87
|
+
return hostNodes(tree.children, out);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The host nodes a parent laid out. Callers assert the LENGTH themselves —
|
|
92
|
+
* "exactly one" is the property, and a helper that returned only the first would
|
|
93
|
+
* hide the second.
|
|
94
|
+
*/
|
|
95
|
+
export function renderedChildren(tree: unknown, hostTestID: string): HostNode[] {
|
|
96
|
+
const host = findHost(tree, hostTestID);
|
|
97
|
+
if (host === null) throw new Error(`no host rendered for testID "${hostTestID}"`);
|
|
98
|
+
return (host.children ?? []).filter(isHostNode);
|
|
99
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A module that DELIBERATELY throws away one of the values a hook hands back, so
|
|
3
|
+
* `hook-subscriptions-are-read.test.ts` can prove its detector fires. It carries
|
|
4
|
+
* both halves of the control: `unreadFlag` is bound and never read, which the
|
|
5
|
+
* detector must report, and `readFlag` is bound and read, which it must stay
|
|
6
|
+
* quiet about — otherwise the cheapest way to green the gate would be deleting a
|
|
7
|
+
* binding that is doing its job.
|
|
8
|
+
*
|
|
9
|
+
* Nothing imports this at runtime. bob's `exclude` keeps it out of `lib/`, and
|
|
10
|
+
* `files: ["src", …]` does ship it inside the tarball's `src/` like the other
|
|
11
|
+
* 137 test files there — but no entry point reaches it, so no bundler links it.
|
|
12
|
+
* It is a plain `.ts` with no React import because the
|
|
13
|
+
* detector reads source, never types: a call named `use…` bound to a name is all
|
|
14
|
+
* the shape there is.
|
|
15
|
+
*
|
|
16
|
+
* The destructuring RENAMES, which is not decoration — it reproduces the shipped
|
|
17
|
+
* shape (`const { state: pressed, onIn: onPressIn } = useInteractionState()`) and
|
|
18
|
+
* it is what keeps the control honest. A hook declared in the same file as its
|
|
19
|
+
* caller mentions its own property names in its return type and its return
|
|
20
|
+
* object, and the detector counts every occurrence of a spelling as a possible
|
|
21
|
+
* read, so an unrenamed fixture would be silently unreportable for a reason no
|
|
22
|
+
* real call site has.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Stands in for a real subscription — a hook by name, with two return values. */
|
|
26
|
+
function useFixtureInteractionState(): { first: boolean; second: boolean } {
|
|
27
|
+
return { first: false, second: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function fixtureComponentBody(): boolean {
|
|
31
|
+
const { first: readFlag, second: unreadFlag } = useFixtureInteractionState();
|
|
32
|
+
return readFlag;
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Positive control for `worklet-captures.test.ts`. Never rendered: the gate
|
|
3
|
+
* builds a program over this file and reads its worklets.
|
|
4
|
+
*
|
|
5
|
+
* `history` carries a `Date` and `slot` a React element — the two shapes the
|
|
6
|
+
* native worklet runtime cannot copy — and each is captured whole by a mapper.
|
|
7
|
+
* `hasHistory` is the fix: the primitive derived OUTSIDE the worklet, which the
|
|
8
|
+
* gate must leave alone.
|
|
9
|
+
*/
|
|
10
|
+
import type { ReactNode } from 'react';
|
|
11
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
12
|
+
import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
|
|
13
|
+
|
|
14
|
+
interface Conversation {
|
|
15
|
+
title: string;
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useWorkletCaptureFixture(history: Conversation[] | undefined, slot: ReactNode) {
|
|
20
|
+
const progress = useSharedValue(0);
|
|
21
|
+
const hasHistory = Boolean(history);
|
|
22
|
+
const whole = useAnimatedStyle(() => ({ opacity: history ? progress.value : 1 }), [progress, history]);
|
|
23
|
+
const derived = useAnimatedStyle(() => ({ opacity: hasHistory ? progress.value : 1 }), [progress, hasHistory]);
|
|
24
|
+
const pan = Gesture.Pan().onUpdate(() => {
|
|
25
|
+
progress.value = slot ? 1 : 0;
|
|
26
|
+
});
|
|
27
|
+
return { whole, derived, pan };
|
|
28
|
+
}
|
package/src/icons/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Bloom's icon set is Remix Icon (Remix Icon License 1.0, `remix/LICENSE.txt`),
|
|
2
|
-
// one file per icon under `remix/`.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// one file per icon under `remix/`. The few brand marks Remix lacks come from
|
|
3
|
+
// Simple Icons (CC0 1.0) under `simple-icons/`. Old Bloom icon names map to
|
|
4
|
+
// their Remix equivalents in `remix-mapping.json`;
|
|
5
|
+
// `scripts/migrate-icons-to-remix.mjs` rewrites an app's imports.
|
|
5
6
|
export type { IconStyle, Props } from './shared';
|
|
6
7
|
export type { BloomIconComponent } from './icon-component';
|
|
7
8
|
export type { BloomIconPaint, BloomIconRenderer } from './render-icon';
|
|
8
9
|
export { resolveIconSlot } from './render-icon';
|
|
9
10
|
export { sizes, useCommonSVGProps } from './shared';
|
|
10
11
|
export * from './remix';
|
|
12
|
+
export * from './simple-icons';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createSinglePathSVG } from '../TEMPLATE';
|
|
2
|
+
|
|
3
|
+
/** Remix Icon `instagram-fill` (Logos). */
|
|
4
|
+
export const RiInstagramFill = createSinglePathSVG({
|
|
5
|
+
path: 'M13.0281 2.00073C14.1535 2.00259 14.7238 2.00855 15.2166 2.02322L15.4107 2.02956C15.6349 2.03753 15.8561 2.04753 16.1228 2.06003C17.1869 2.1092 17.9128 2.27753 18.5503 2.52503C19.2094 2.7792 19.7661 3.12253 20.3219 3.67837C20.8769 4.2342 21.2203 4.79253 21.4753 5.45003C21.7219 6.0867 21.8903 6.81337 21.9403 7.87753C21.9522 8.1442 21.9618 8.3654 21.9697 8.58964L21.976 8.78373C21.9906 9.27647 21.9973 9.84686 21.9994 10.9723L22.0002 11.7179C22.0003 11.809 22.0003 11.903 22.0003 12L22.0002 12.2821L21.9996 13.0278C21.9977 14.1532 21.9918 14.7236 21.9771 15.2163L21.9707 15.4104C21.9628 15.6347 21.9528 15.8559 21.9403 16.1225C21.8911 17.1867 21.7219 17.9125 21.4753 18.55C21.2211 19.2092 20.8769 19.7659 20.3219 20.3217C19.7661 20.8767 19.2069 21.22 18.5503 21.475C17.9128 21.7217 17.1869 21.89 16.1228 21.94C15.8561 21.9519 15.6349 21.9616 15.4107 21.9694L15.2166 21.9757C14.7238 21.9904 14.1535 21.997 13.0281 21.9992L12.2824 22C12.1913 22 12.0973 22 12.0003 22L11.7182 22L10.9725 21.9993C9.8471 21.9975 9.27672 21.9915 8.78397 21.9768L8.58989 21.9705C8.36564 21.9625 8.14444 21.9525 7.87778 21.94C6.81361 21.8909 6.08861 21.7217 5.45028 21.475C4.79194 21.2209 4.23444 20.8767 3.67861 20.3217C3.12278 19.7659 2.78028 19.2067 2.52528 18.55C2.27778 17.9125 2.11028 17.1867 2.06028 16.1225C2.0484 15.8559 2.03871 15.6347 2.03086 15.4104L2.02457 15.2163C2.00994 14.7236 2.00327 14.1532 2.00111 13.0278L2.00098 10.9723C2.00284 9.84686 2.00879 9.27647 2.02346 8.78373L2.02981 8.58964C2.03778 8.3654 2.04778 8.1442 2.06028 7.87753C2.10944 6.81253 2.27778 6.08753 2.52528 5.45003C2.77944 4.7917 3.12278 4.2342 3.67861 3.67837C4.23444 3.12253 4.79278 2.78003 5.45028 2.52503C6.08778 2.27753 6.81278 2.11003 7.87778 2.06003C8.14444 2.04816 8.36564 2.03847 8.58989 2.03062L8.78397 2.02433C9.27672 2.00969 9.8471 2.00302 10.9725 2.00086L13.0281 2.00073ZM12.0003 7.00003C9.23738 7.00003 7.00028 9.23956 7.00028 12C7.00028 14.7629 9.23981 17 12.0003 17C14.7632 17 17.0003 14.7605 17.0003 12C17.0003 9.23713 14.7607 7.00003 12.0003 7.00003ZM12.0003 9.00003C13.6572 9.00003 15.0003 10.3427 15.0003 12C15.0003 13.6569 13.6576 15 12.0003 15C10.3434 15 9.00028 13.6574 9.00028 12C9.00028 10.3431 10.3429 9.00003 12.0003 9.00003ZM17.2503 5.50003C16.561 5.50003 16.0003 6.05994 16.0003 6.74918C16.0003 7.43843 16.5602 7.9992 17.2503 7.9992C17.9395 7.9992 18.5003 7.4393 18.5003 6.74918C18.5003 6.05994 17.9386 5.49917 17.2503 5.50003Z',
|
|
6
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createSinglePathSVG } from '../TEMPLATE';
|
|
2
|
+
|
|
3
|
+
/** Remix Icon `mastodon-fill` (Logos). */
|
|
4
|
+
export const RiMastodonFill = createSinglePathSVG({
|
|
5
|
+
path: 'M21.2595 13.9898C20.9852 15.4006 18.8033 16.9446 16.2974 17.2439C14.9907 17.3998 13.7041 17.5431 12.3321 17.4802C10.0885 17.3774 8.31809 16.9446 8.31809 16.9446C8.31809 17.163 8.33156 17.371 8.3585 17.5655C8.65019 19.7797 10.5541 19.9124 12.3576 19.9742C14.1779 20.0365 15.7987 19.5254 15.7987 19.5254L15.8735 21.1711C15.8735 21.1711 14.6003 21.8548 12.3321 21.9805C11.0814 22.0493 9.52849 21.9491 7.71973 21.4703C3.79684 20.432 3.12219 16.2504 3.01896 12.0074C2.98749 10.7477 3.00689 9.55981 3.00689 8.56632C3.00689 4.22771 5.84955 2.95599 5.84955 2.95599C7.2829 2.29772 9.74238 2.0209 12.2993 2H12.3621C14.919 2.0209 17.3801 2.29772 18.8133 2.95599C18.8133 2.95599 21.6559 4.22771 21.6559 8.56632C21.6559 8.56632 21.6916 11.7674 21.2595 13.9898ZM18.3029 8.9029C18.3029 7.82924 18.0295 6.97604 17.4805 6.34482C16.9142 5.71359 16.1726 5.39001 15.2522 5.39001C14.187 5.39001 13.3805 5.79937 12.8473 6.61819L12.3288 7.48723L11.8104 6.61819C11.2771 5.79937 10.4706 5.39001 9.40554 5.39001C8.485 5.39001 7.74344 5.71359 7.17719 6.34482C6.62807 6.97604 6.3547 7.82924 6.3547 8.9029V14.1562H8.43597V9.05731C8.43597 7.98246 8.88822 7.4369 9.79281 7.4369C10.793 7.4369 11.2944 8.08408 11.2944 9.36376V12.1547H13.3634V9.36376C13.3634 8.08408 13.8646 7.4369 14.8648 7.4369C15.7694 7.4369 16.2216 7.98246 16.2216 9.05731V14.1562H18.3029V8.9029Z',
|
|
6
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createSinglePathSVG } from '../TEMPLATE';
|
|
2
|
+
|
|
3
|
+
/** Remix Icon `medium-fill` (Logos). */
|
|
4
|
+
export const RiMediumFill = createSinglePathSVG({
|
|
5
|
+
path: 'M6.15789 3H17.8421C19.5862 3 21 4.41384 21 6.15789V9.52368C20.9185 9.51825 20.8369 9.51611 20.7552 9.51611C20.3527 9.51611 19.8589 9.61374 19.5076 9.79063C19.1075 9.97583 18.7548 10.2528 18.4613 10.6162C17.9888 11.2018 17.7025 11.9931 17.6329 12.8824C17.618 13.059 17.6131 13.2366 17.6163 13.4138C17.6632 15.4279 18.7512 17.0376 20.6837 17.0376C20.7919 17.0376 20.8974 17.0325 21 17.0226V17.8421C21 19.5862 19.5862 21 17.8421 21H6.15789C4.41384 21 3 19.5862 3 17.8421V6.15789C3 4.41384 4.41384 3 6.15789 3ZM21 15.3878V12.6621H19.3473C19.2728 13.9784 19.9927 15.0913 21 15.3878ZM21 12.2813V9.97343C20.92 9.95153 20.8345 9.93978 20.7435 9.93827C19.9294 9.95506 19.4442 10.9302 19.3838 12.2813H21ZM17.6973 7.30004L17.7115 7.29693V7.18701H14.8127L12.1211 13.5132L9.42952 7.18701H6.30572V7.29693L6.31975 7.30004C6.84885 7.41946 7.11748 7.59757 7.11748 8.23987V15.7595C7.11748 16.4018 6.84782 16.5799 6.31871 16.6993L6.30469 16.7024V16.8127H8.42408V16.7028L8.41007 16.6996C7.88095 16.5802 7.61232 16.4021 7.61232 15.7598V8.67605L11.07 16.8127H11.2661L14.8245 8.44914V15.9455C14.7792 16.4528 14.513 16.6095 14.0339 16.7176L14.0197 16.7209V16.83H17.7115V16.7209L17.6973 16.7176C17.2177 16.6095 16.9451 16.4528 16.8998 15.9455L16.8974 8.23987H16.8998C16.8998 7.59757 17.1684 7.41946 17.6973 7.30004Z',
|
|
6
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createSinglePathSVG } from '../TEMPLATE';
|
|
2
|
+
|
|
3
|
+
/** Remix Icon `threads-fill` (Logos). */
|
|
4
|
+
export const RiThreadsFill = createSinglePathSVG({
|
|
5
|
+
path: 'M16.7051 11.1081C16.543 8.12137 14.911 6.41148 12.1708 6.39398C10.5193 6.3838 9.13771 7.08389 8.29233 8.36664L9.79941 9.40046C10.4334 8.43852 11.4342 8.24015 12.1593 8.24685C13.0616 8.2526 13.7425 8.51494 14.1832 9.02653C14.5038 9.39899 14.7183 9.91367 14.8245 10.5632C14.0246 10.4273 13.1594 10.3855 12.2345 10.4385C9.62919 10.5886 7.95426 12.1081 8.06675 14.2194C8.12384 15.2904 8.65739 16.2118 9.56906 16.8137C10.3399 17.3225 11.3326 17.5713 12.3644 17.515C13.727 17.4403 14.7959 16.9205 15.5416 15.9699C16.1079 15.248 16.4661 14.3125 16.6243 13.1338C17.2737 13.5257 17.7549 14.0414 18.0207 14.6613C18.4726 15.7151 18.499 17.4469 17.086 18.8587C15.848 20.0955 14.3598 20.6306 12.1108 20.6471C9.61601 20.6286 7.72924 19.8285 6.50253 18.269C5.35381 16.8088 4.76014 14.6996 4.73799 12C4.76014 9.30038 5.35381 7.19117 6.50253 5.73092C7.72924 4.17147 9.61597 3.37141 12.1107 3.35287C14.6236 3.37155 16.5433 4.17547 17.8169 5.74244C18.4415 6.51086 18.9123 7.47721 19.2227 8.60394L20.9888 8.13274C20.6125 6.74587 20.0205 5.55078 19.2148 4.55966C17.582 2.55073 15.1816 1.52134 12.1046 1.5C9.03385 1.52127 6.6725 2.55457 5.08614 4.57117C3.67451 6.3657 2.94634 8.87742 2.92188 12.0074C2.94634 15.1373 3.67451 17.6343 5.08614 19.4289C6.6725 21.4454 9.04616 22.4788 12.1169 22.5C14.847 22.4811 16.7713 21.7663 18.3566 20.1825C20.4307 18.1103 20.3682 15.513 19.6846 13.9185C19.1595 12.6943 18.1141 11.7129 16.7051 11.1081ZM12.2669 15.6648C11.125 15.7291 9.93869 15.2166 9.88019 14.1188C9.83684 13.3048 10.4595 12.3966 12.3369 12.2884C13.2594 12.2352 14.1138 12.2976 14.8701 12.463C14.6538 15.1648 13.3848 15.6035 12.2669 15.6648Z',
|
|
6
|
+
});
|
package/src/icons/remix/index.ts
CHANGED
|
@@ -245,6 +245,7 @@ export { RiIndeterminateCircleFill } from './RiIndeterminateCircleFill';
|
|
|
245
245
|
export { RiInfinityLine } from './RiInfinityLine';
|
|
246
246
|
export { RiInformationFill } from './RiInformationFill';
|
|
247
247
|
export { RiInformationLine } from './RiInformationLine';
|
|
248
|
+
export { RiInstagramFill } from './RiInstagramFill';
|
|
248
249
|
export { RiItalic } from './RiItalic';
|
|
249
250
|
export { RiKanbanView2 } from './RiKanbanView2';
|
|
250
251
|
export { RiKey2Line } from './RiKey2Line';
|
|
@@ -280,7 +281,9 @@ export { RiMapPin2Fill } from './RiMapPin2Fill';
|
|
|
280
281
|
export { RiMapPin2Line } from './RiMapPin2Line';
|
|
281
282
|
export { RiMapPinFill } from './RiMapPinFill';
|
|
282
283
|
export { RiMapPinLine } from './RiMapPinLine';
|
|
284
|
+
export { RiMastodonFill } from './RiMastodonFill';
|
|
283
285
|
export { RiMedalLine } from './RiMedalLine';
|
|
286
|
+
export { RiMediumFill } from './RiMediumFill';
|
|
284
287
|
export { RiMegaphoneLine } from './RiMegaphoneLine';
|
|
285
288
|
export { RiMenLine } from './RiMenLine';
|
|
286
289
|
export { RiMenuLine } from './RiMenuLine';
|
|
@@ -428,6 +431,7 @@ export { RiTentLine } from './RiTentLine';
|
|
|
428
431
|
export { RiTerminalBoxLine } from './RiTerminalBoxLine';
|
|
429
432
|
export { RiTerminalFill } from './RiTerminalFill';
|
|
430
433
|
export { RiTestTubeFill } from './RiTestTubeFill';
|
|
434
|
+
export { RiThreadsFill } from './RiThreadsFill';
|
|
431
435
|
export { RiThumbDownLine } from './RiThumbDownLine';
|
|
432
436
|
export { RiThumbUpFill } from './RiThumbUpFill';
|
|
433
437
|
export { RiThumbUpLine } from './RiThumbUpLine';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createSinglePathSVG } from '../TEMPLATE';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple Icons `substack` (simple-icons@16.32.0, CC0 1.0). Remix Icon has no
|
|
5
|
+
* Substack glyph, so this one mark comes from the set `social-button` already
|
|
6
|
+
* draws brand marks from. The path is verbatim.
|
|
7
|
+
*
|
|
8
|
+
* Simple Icons draws edge to edge on its 24 grid (this path spans y 0–24);
|
|
9
|
+
* Remix Logos keep a 2-unit margin (Bluesky, X and Mastodon sit inside 2–22).
|
|
10
|
+
* The `-2 -2 28 28` viewBox scales the same path onto that 20-unit live area,
|
|
11
|
+
* so the mark sits at the same visual size as the Remix brand glyphs beside it.
|
|
12
|
+
*/
|
|
13
|
+
export const SiSubstack = createSinglePathSVG({
|
|
14
|
+
path: 'M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z',
|
|
15
|
+
viewBox: '-2 -2 28 28',
|
|
16
|
+
});
|