@krapwoo/ds-viewer 0.4.4 → 0.4.5
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 +9 -7
- package/dist/native/catalog/tokens.js +2 -0
- package/native/catalog/ComparisonGrid.tsx +11 -11
- package/native/catalog/ComparisonGroups.tsx +6 -8
- package/native/catalog/ComparisonList.tsx +6 -8
- package/native/catalog/SectionBlock.tsx +9 -6
- package/native/catalog/SpecimenSurface.tsx +64 -26
- package/native/catalog/specimenFill.ts +102 -0
- package/native/catalog/specimenSurfaceStyle.ts +9 -3
- package/native/catalog/tokens.ts +2 -0
- package/native/catalog/types.ts +7 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,13 +190,15 @@ actually apply:
|
|
|
190
190
|
within what fits a 1280px laptop: 5 compact, 3 regular, or 2 wide columns. `states` items whose
|
|
191
191
|
key matches a row or column key are not repeated below the grid.
|
|
192
192
|
- `specimenSize: 'compact' | 'regular' | 'wide'` — the page's specimen width class.
|
|
193
|
-
- `specimenSurface: 'neutral' | 'white' | 'dark' | 'transparent'` — the
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
white
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
- `specimenSurface: 'auto' | 'neutral' | 'white' | 'dark' | 'transparent'` — the fill of each
|
|
194
|
+
example's cell (list, grouped and grid cells, and a non-token `render()` preview). Defaults to
|
|
195
|
+
`'auto'`: after the page draws, a cell turns light gray only when its own example would vanish
|
|
196
|
+
against white (its largest coloured box is near-white with no visible border, like a white card,
|
|
197
|
+
sheet or neutral banner; a shadow alone doesn't count). Neighbouring examples keep a white cell,
|
|
198
|
+
and most pages need nothing. Override it when the guess is wrong: `'neutral'` makes every example
|
|
199
|
+
cell gray, `'white'` or `'transparent'` keeps them all white, and `'dark'` checks a light
|
|
200
|
+
component on a dark fill. Detection runs in the browser viewer; elsewhere cells stay white.
|
|
201
|
+
Token galleries and sections are never filled.
|
|
200
202
|
- `variants`/`states`' own `maxColumns: 1 | 2 | 3 | 4 | 5` — caps how many columns that slot's list
|
|
201
203
|
ever wraps into (e.g. a 3-item size scale that should always read as one row of exactly 3, never
|
|
202
204
|
more just because a laptop is wide). A narrow window still drops below the cap when it has to —
|
|
@@ -111,6 +111,8 @@ export const CATALOG_COLOR = {
|
|
|
111
111
|
surfaceMuted: '#fafafa',
|
|
112
112
|
/** Hover and pressed feedback on nav rows and buttons. */
|
|
113
113
|
surfacePressed: '#f1f3f8',
|
|
114
|
+
/** An example's cell when the example itself is white (`specimenSurface`), darker than surfaceMuted so a white card's edges show. */
|
|
115
|
+
specimenStage: '#f2f2f2',
|
|
114
116
|
pageBackground: '#f6f6f4',
|
|
115
117
|
chip: '#eeeeee',
|
|
116
118
|
/** Active nav label, prop types, scale bars. */
|
|
@@ -2,7 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import { View, Text, ScrollView, StyleSheet } from 'react-native';
|
|
3
3
|
import { CATALOG_COLOR, CATALOG_RADIUS, CATALOG_TYPE } from './tokens';
|
|
4
4
|
import { COLUMN_MIN_WIDTH, MATRIX_LAYOUT, axisItems, cellKey, gridWidthBounds, indexCells, validateComparison } from './comparison';
|
|
5
|
-
import { SpecimenSurface } from './SpecimenSurface';
|
|
5
|
+
import { SpecimenContent, SpecimenSurface } from './SpecimenSurface';
|
|
6
|
+
import { DEFAULT_SPECIMEN_SURFACE } from './specimenSurfaceStyle';
|
|
6
7
|
import type { ComparisonDef, SpecimenSize, SpecimenSurfaceKind } from './types';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -15,7 +16,7 @@ export function ComparisonGrid({
|
|
|
15
16
|
def,
|
|
16
17
|
size,
|
|
17
18
|
sectionId,
|
|
18
|
-
surface =
|
|
19
|
+
surface = DEFAULT_SPECIMEN_SURFACE,
|
|
19
20
|
}: {
|
|
20
21
|
def: ComparisonDef;
|
|
21
22
|
size: SpecimenSize;
|
|
@@ -63,17 +64,17 @@ export function ComparisonGrid({
|
|
|
63
64
|
{columns.map((c, ci) => {
|
|
64
65
|
const cell = cells.get(cellKey(row.key, c.key));
|
|
65
66
|
return (
|
|
66
|
-
|
|
67
|
-
{cell
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
cell?.node !== undefined ? (
|
|
68
|
+
<SpecimenSurface key={c.key} role="cell" surface={surface} style={[styles.cell, column, styles.bodyRow, styles.specimenCell, ci === lastColumn && styles.lastColumn]}>
|
|
69
|
+
<SpecimenContent fill={cell.fill}>{cell.node}</SpecimenContent>
|
|
70
|
+
</SpecimenSurface>
|
|
71
|
+
) : (
|
|
72
|
+
<View key={c.key} role="cell" style={[styles.cell, column, styles.bodyRow, styles.specimenCell, ci === lastColumn && styles.lastColumn]}>
|
|
72
73
|
<Text style={styles.unavailable}>
|
|
73
74
|
{cell?.unavailableReason ? `Not supported — ${cell.unavailableReason}` : 'Missing example'}
|
|
74
75
|
</Text>
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
</View>
|
|
77
|
+
)
|
|
77
78
|
);
|
|
78
79
|
})}
|
|
79
80
|
</View>
|
|
@@ -102,7 +103,6 @@ const styles = StyleSheet.create({
|
|
|
102
103
|
headerCell: { backgroundColor: CATALOG_COLOR.surfaceMuted, justifyContent: 'center' },
|
|
103
104
|
bodyRow: { minHeight: MATRIX_LAYOUT.rowMinHeight },
|
|
104
105
|
specimenCell: { alignItems: 'center', justifyContent: 'center' },
|
|
105
|
-
fill: { alignSelf: 'stretch' },
|
|
106
106
|
headerText: { fontSize: CATALOG_TYPE.tableHeader, fontWeight: '800', letterSpacing: 0.44, color: CATALOG_COLOR.text },
|
|
107
107
|
rowHeaderText: { fontSize: CATALOG_TYPE.md, fontWeight: '700', color: CATALOG_COLOR.text },
|
|
108
108
|
unavailable: { fontSize: CATALOG_TYPE.sm, fontStyle: 'italic', textAlign: 'center', color: CATALOG_COLOR.textMuted },
|
|
@@ -2,7 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import { View, Text, ScrollView, StyleSheet } from 'react-native';
|
|
3
3
|
import { CATALOG_COLOR, CATALOG_RADIUS, CATALOG_TYPE } from './tokens';
|
|
4
4
|
import { COLUMN_MIN_WIDTH, MATRIX_LAYOUT, type ListGroup } from './comparison';
|
|
5
|
-
import { SpecimenSurface } from './SpecimenSurface';
|
|
5
|
+
import { SpecimenContent, SpecimenSurface } from './SpecimenSurface';
|
|
6
|
+
import { DEFAULT_SPECIMEN_SURFACE } from './specimenSurfaceStyle';
|
|
6
7
|
import type { SpecimenSize, SpecimenSurfaceKind } from './types';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -16,7 +17,7 @@ export function ComparisonGroups({
|
|
|
16
17
|
groups,
|
|
17
18
|
size,
|
|
18
19
|
label,
|
|
19
|
-
surface =
|
|
20
|
+
surface = DEFAULT_SPECIMEN_SURFACE,
|
|
20
21
|
}: {
|
|
21
22
|
groups: ListGroup[];
|
|
22
23
|
size: SpecimenSize;
|
|
@@ -46,11 +47,9 @@ export function ComparisonGroups({
|
|
|
46
47
|
<View style={styles.caption}>
|
|
47
48
|
<Text style={styles.captionText}>{item.label}</Text>
|
|
48
49
|
</View>
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
</SpecimenSurface>
|
|
53
|
-
</View>
|
|
50
|
+
<SpecimenSurface surface={surface} style={styles.specimen}>
|
|
51
|
+
<SpecimenContent fill={item.fill}>{item.node as React.ReactNode}</SpecimenContent>
|
|
52
|
+
</SpecimenSurface>
|
|
54
53
|
</View>
|
|
55
54
|
);
|
|
56
55
|
})}
|
|
@@ -102,5 +101,4 @@ const styles = StyleSheet.create({
|
|
|
102
101
|
alignItems: 'center',
|
|
103
102
|
justifyContent: 'center',
|
|
104
103
|
},
|
|
105
|
-
fill: { alignSelf: 'stretch' },
|
|
106
104
|
});
|
|
@@ -2,7 +2,8 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import { View, Text, StyleSheet, type LayoutChangeEvent } from 'react-native';
|
|
3
3
|
import { CATALOG_COLOR, CATALOG_RADIUS, CATALOG_TYPE } from './tokens';
|
|
4
4
|
import { MATRIX_LAYOUT, listGeometry, type ListItem } from './comparison';
|
|
5
|
-
import { SpecimenSurface } from './SpecimenSurface';
|
|
5
|
+
import { SpecimenContent, SpecimenSurface } from './SpecimenSurface';
|
|
6
|
+
import { DEFAULT_SPECIMEN_SURFACE } from './specimenSurfaceStyle';
|
|
6
7
|
import type { SpecimenSize, SpecimenSurfaceKind } from './types';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -17,7 +18,7 @@ export function ComparisonList({
|
|
|
17
18
|
size,
|
|
18
19
|
label,
|
|
19
20
|
maxColumns,
|
|
20
|
-
surface =
|
|
21
|
+
surface = DEFAULT_SPECIMEN_SURFACE,
|
|
21
22
|
}: {
|
|
22
23
|
items: ListItem[];
|
|
23
24
|
size: SpecimenSize;
|
|
@@ -60,11 +61,9 @@ export function ComparisonList({
|
|
|
60
61
|
<View style={styles.caption}>
|
|
61
62
|
<Text style={styles.captionText}>{item.label}</Text>
|
|
62
63
|
</View>
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
</SpecimenSurface>
|
|
67
|
-
</View>
|
|
64
|
+
<SpecimenSurface surface={surface} style={styles.specimen}>
|
|
65
|
+
<SpecimenContent fill={item.fill}>{item.node as React.ReactNode}</SpecimenContent>
|
|
66
|
+
</SpecimenSurface>
|
|
68
67
|
</View>
|
|
69
68
|
);
|
|
70
69
|
})}
|
|
@@ -101,5 +100,4 @@ const styles = StyleSheet.create({
|
|
|
101
100
|
alignItems: 'center',
|
|
102
101
|
justifyContent: 'center',
|
|
103
102
|
},
|
|
104
|
-
fill: { alignSelf: 'stretch' },
|
|
105
103
|
});
|
|
@@ -6,6 +6,7 @@ import { ComparisonGroups } from './ComparisonGroups';
|
|
|
6
6
|
import { ComparisonList } from './ComparisonList';
|
|
7
7
|
import { ReferenceDetails } from './ReferenceDetails';
|
|
8
8
|
import { SpecimenSurface } from './SpecimenSurface';
|
|
9
|
+
import { DEFAULT_SPECIMEN_SURFACE } from './specimenSurfaceStyle';
|
|
9
10
|
import { TokenSections } from './TokenLayouts';
|
|
10
11
|
import { choosePlacement, listGeometry, presentationBlocks, type PresentationBlock } from './comparison';
|
|
11
12
|
import type { PreviewWidths, SectionDef, SpecimenSurfaceKind } from './types';
|
|
@@ -104,21 +105,23 @@ function frameLabel(width: number): string {
|
|
|
104
105
|
* chrome and token galleries. Every frame is its own live instance with its own state. `surface`
|
|
105
106
|
* is omitted for token galleries, which keep their current, unstaged presentation. */
|
|
106
107
|
function Preview({ render, widths, surface }: { render: () => React.ReactNode; widths: PreviewWidths; surface?: SpecimenSurfaceKind }) {
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// Token galleries pass no surface: their card stays white.
|
|
109
|
+
const kind = surface ?? 'transparent';
|
|
110
|
+
if (widths === 'full') return <SpecimenSurface surface={kind} style={styles.previewCard}>{render()}</SpecimenSurface>;
|
|
109
111
|
return (
|
|
110
112
|
<View style={styles.previewCard}>
|
|
111
113
|
<View style={styles.frames}>
|
|
112
114
|
{widths.map((width, i) => (
|
|
113
|
-
<
|
|
115
|
+
<SpecimenSurface
|
|
114
116
|
key={`${width}-${i}`}
|
|
117
|
+
surface={kind}
|
|
115
118
|
// Two live instances of the same component: name each so assistive tech can tell them apart.
|
|
116
119
|
{...(widths.length > 1 ? ({ role: 'group', 'aria-label': frameLabel(width) } as Record<string, unknown>) : null)}
|
|
117
120
|
style={[styles.frame, { width }]}
|
|
118
121
|
>
|
|
119
122
|
{widths.length > 1 && <Text style={styles.frameLabel}>{frameLabel(width)}</Text>}
|
|
120
|
-
{
|
|
121
|
-
</
|
|
123
|
+
{render()}
|
|
124
|
+
</SpecimenSurface>
|
|
122
125
|
))}
|
|
123
126
|
</View>
|
|
124
127
|
</View>
|
|
@@ -246,7 +249,7 @@ export function SectionBlock<TId extends string>({
|
|
|
246
249
|
checkCompleteness(def);
|
|
247
250
|
// Memoized so Blocks' placement effect runs on real changes, not on every render.
|
|
248
251
|
const blocks = useMemo(() => presentationBlocks(def, { defaultPreviewWidths }), [def, defaultPreviewWidths]);
|
|
249
|
-
const surface = def.specimenSurface ??
|
|
252
|
+
const surface = def.specimenSurface ?? DEFAULT_SPECIMEN_SURFACE;
|
|
250
253
|
// react-native-web reads `aria-level`; React Native's prop types do not declare it.
|
|
251
254
|
const headingLevelProps = { 'aria-level': headingLevel } as Record<string, unknown>;
|
|
252
255
|
|
|
@@ -1,46 +1,84 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
-
import { CATALOG_COLOR
|
|
4
|
-
import { SPECIMEN_SURFACE_WIDTH_STYLE } from './specimenSurfaceStyle';
|
|
3
|
+
import { CATALOG_COLOR } from './tokens';
|
|
4
|
+
import { DEFAULT_SPECIMEN_SURFACE, SPECIMEN_SURFACE_WIDTH_STYLE } from './specimenSurfaceStyle';
|
|
5
|
+
import { blendsIntoCell, measurePaintedBoxes } from './specimenFill';
|
|
5
6
|
import type { SpecimenSurfaceKind } from './types';
|
|
6
7
|
|
|
8
|
+
type Browser = {
|
|
9
|
+
getComputedStyle?: (element: unknown) => never;
|
|
10
|
+
Element?: new () => unknown;
|
|
11
|
+
requestAnimationFrame?: (callback: () => void) => number;
|
|
12
|
+
};
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the real comparison-cell width instead of collapsing to 0px.
|
|
15
|
+
* An example's whole cell area — shared by `ComparisonList`, `ComparisonGroups`,
|
|
16
|
+
* `ComparisonGrid`, and `SectionBlock`'s preview frames, so a page's `specimenSurface` fills every
|
|
17
|
+
* example cell the same way. With `'auto'` (the default) the cell measures its example after it
|
|
18
|
+
* draws and turns gray only when the example would vanish against white (a white card, sheet or
|
|
19
|
+
* neutral banner; see `specimenFill.ts`). Explicit kinds fill the cell without measuring. Adds no
|
|
20
|
+
* padding or border of its own: pass the cell's own layout in `style`. Measuring needs the
|
|
21
|
+
* browser's DOM; elsewhere `'auto'` leaves the cell white. Extra props (e.g. `role`) go to the View.
|
|
16
22
|
*/
|
|
17
23
|
export function SpecimenSurface({
|
|
18
|
-
surface =
|
|
24
|
+
surface = DEFAULT_SPECIMEN_SURFACE,
|
|
19
25
|
style,
|
|
20
26
|
children,
|
|
27
|
+
...viewProps
|
|
21
28
|
}: {
|
|
22
29
|
surface?: SpecimenSurfaceKind;
|
|
23
30
|
style?: StyleProp<ViewStyle>;
|
|
24
31
|
children: React.ReactNode;
|
|
32
|
+
[prop: string]: unknown;
|
|
25
33
|
}) {
|
|
26
|
-
|
|
34
|
+
const ref = useRef<View>(null);
|
|
35
|
+
const [blends, setBlends] = useState(false);
|
|
36
|
+
|
|
37
|
+
const measure = useCallback(() => {
|
|
38
|
+
if (surface !== 'auto') return;
|
|
39
|
+
const browser = globalThis as unknown as Browser;
|
|
40
|
+
const node = ref.current as unknown;
|
|
41
|
+
if (!browser.getComputedStyle || !browser.Element || !(node instanceof browser.Element)) return;
|
|
42
|
+
const { boxes, cellArea } = measurePaintedBoxes(node as Parameters<typeof measurePaintedBoxes>[0], (element) => browser.getComputedStyle!(element));
|
|
43
|
+
setBlends(blendsIntoCell(boxes, cellArea));
|
|
44
|
+
}, [surface]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const raf = (globalThis as unknown as Browser).requestAnimationFrame;
|
|
48
|
+
if (surface !== 'auto' || !raf) return;
|
|
49
|
+
// Two frames, so the example's own styles and fonts have applied before measuring.
|
|
50
|
+
let cancelled = false;
|
|
51
|
+
raf(() => raf(() => {
|
|
52
|
+
if (!cancelled) measure();
|
|
53
|
+
}));
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true;
|
|
56
|
+
};
|
|
57
|
+
}, [surface, measure]);
|
|
58
|
+
|
|
59
|
+
const fill = surface === 'auto' ? (blends ? FILL.neutral : undefined) : FILL[surface];
|
|
60
|
+
return (
|
|
61
|
+
<View ref={ref} onLayout={surface === 'auto' ? measure : undefined} style={[style, fill]} {...viewProps}>
|
|
62
|
+
{children}
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
27
65
|
}
|
|
28
66
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
67
|
+
/** Wraps one example inside its cell: full cell width (so a percentage-width example resolves
|
|
68
|
+
* against the cell), centered unless the example fills the cell. */
|
|
69
|
+
export function SpecimenContent({ fill, children }: { fill?: boolean; children: React.ReactNode }) {
|
|
70
|
+
return <View style={fill ? styles.fill : styles.center}>{children}</View>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 'dark' reuses CATALOG_COLOR.text (the catalog's own darkest token); there is no dark-surface token.
|
|
74
|
+
const FILL = StyleSheet.create({
|
|
75
|
+
neutral: { backgroundColor: CATALOG_COLOR.specimenStage },
|
|
33
76
|
white: { backgroundColor: CATALOG_COLOR.surface },
|
|
34
77
|
dark: { backgroundColor: CATALOG_COLOR.text },
|
|
35
|
-
transparent: {
|
|
36
|
-
};
|
|
78
|
+
transparent: {},
|
|
79
|
+
});
|
|
37
80
|
|
|
38
81
|
const styles = StyleSheet.create({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
alignItems: 'center',
|
|
42
|
-
justifyContent: 'center',
|
|
43
|
-
borderRadius: CATALOG_RADIUS.sm,
|
|
44
|
-
padding: CATALOG_SPACE.md,
|
|
45
|
-
},
|
|
82
|
+
center: { ...SPECIMEN_SURFACE_WIDTH_STYLE, alignItems: 'center' },
|
|
83
|
+
fill: { ...SPECIMEN_SURFACE_WIDTH_STYLE },
|
|
46
84
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic cell fill for `specimenSurface: 'auto'` (the default): decides whether an example
|
|
3
|
+
* blends into its white comparison cell, so that one cell can turn gray. Kept free of any runtime
|
|
4
|
+
* import so `node --test` can assert on it directly (see `__tests__/specimenFill.test.ts`).
|
|
5
|
+
*
|
|
6
|
+
* The rule looks only at the example's largest painted box (the element with a background that
|
|
7
|
+
* covers the most area): if that box is near-white (every channel close to 255, so pale tints
|
|
8
|
+
* don't count) and has no visible border, the example's edges
|
|
9
|
+
* vanish against the white cell. A shadow alone doesn't count as an edge. Small boxes (under a
|
|
10
|
+
* tenth of the cell) and text-only examples never trigger it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface Rgba { r: number; g: number; b: number; a: number }
|
|
14
|
+
|
|
15
|
+
/** One element inside an example, as measured in the browser. */
|
|
16
|
+
export interface PaintedBox {
|
|
17
|
+
area: number;
|
|
18
|
+
background: Rgba | null;
|
|
19
|
+
/** The top border's colour when its width is above 0, else null. */
|
|
20
|
+
border: Rgba | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A fill is near-white when every channel (after compositing over the white cell) is at least
|
|
24
|
+
* this. #fafafa and #f8f8f8 qualify; #f4f4f4 already reads as gray, and pale tints such as a
|
|
25
|
+
* positive banner's rgb(240, 255, 244) read as colour, even when they're as bright as white. */
|
|
26
|
+
const NEAR_WHITE_CHANNEL = 247;
|
|
27
|
+
/** At or above this, a border draws the edge itself (#e4e4e4 is 1.27:1). */
|
|
28
|
+
const VISIBLE_BORDER_CONTRAST = 1.2;
|
|
29
|
+
/** A box smaller than this share of the cell is a detail, not the example's body. */
|
|
30
|
+
const MIN_AREA_SHARE = 0.1;
|
|
31
|
+
|
|
32
|
+
/** Parses a computed CSS colour (`rgb(…)`/`rgba(…)`). Fully transparent or unknown values are null. */
|
|
33
|
+
export function parseCssColor(value: string): Rgba | null {
|
|
34
|
+
const match = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(value.trim());
|
|
35
|
+
if (!match) return null;
|
|
36
|
+
const alphaText = match[4];
|
|
37
|
+
const a = alphaText === undefined ? 1 : alphaText.endsWith('%') ? Number(alphaText.slice(0, -1)) / 100 : Number(alphaText);
|
|
38
|
+
if (!(a > 0)) return null;
|
|
39
|
+
return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]), a };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function channel(value: number): number {
|
|
43
|
+
const s = value / 255;
|
|
44
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when every channel of `color`, composited over white, is at least `NEAR_WHITE_CHANNEL`. */
|
|
48
|
+
export function isNearWhite(color: Rgba): boolean {
|
|
49
|
+
const over = (c: number) => color.a * c + (1 - color.a) * 255;
|
|
50
|
+
return Math.min(over(color.r), over(color.g), over(color.b)) >= NEAR_WHITE_CHANNEL;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** WCAG contrast ratio against white, after compositing a translucent colour over the white cell. */
|
|
54
|
+
export function contrastWithWhite(color: Rgba): number {
|
|
55
|
+
const over = (c: number) => color.a * c + (1 - color.a) * 255;
|
|
56
|
+
const luminance = 0.2126 * channel(over(color.r)) + 0.7152 * channel(over(color.g)) + 0.0722 * channel(over(color.b));
|
|
57
|
+
return 1.05 / (luminance + 0.05);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True when the example's body is near-white with no visible border, so it vanishes on a white cell. */
|
|
61
|
+
export function blendsIntoCell(boxes: readonly PaintedBox[], cellArea: number): boolean {
|
|
62
|
+
let largest: PaintedBox | undefined;
|
|
63
|
+
for (const box of boxes) {
|
|
64
|
+
if (!box.background || box.area < cellArea * MIN_AREA_SHARE) continue;
|
|
65
|
+
if (!largest || box.area > largest.area) largest = box;
|
|
66
|
+
}
|
|
67
|
+
if (!largest?.background) return false;
|
|
68
|
+
if (!isNearWhite(largest.background)) return false;
|
|
69
|
+
return !(largest.border && contrastWithWhite(largest.border) >= VISIBLE_BORDER_CONTRAST);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Minimal DOM shapes, so this file needs no DOM lib types. */
|
|
73
|
+
interface MeasurableElement {
|
|
74
|
+
getBoundingClientRect(): { width: number; height: number };
|
|
75
|
+
querySelectorAll(selector: string): ArrayLike<MeasurableElement>;
|
|
76
|
+
}
|
|
77
|
+
type ComputedStyleReader = (element: MeasurableElement) => {
|
|
78
|
+
backgroundColor: string;
|
|
79
|
+
borderTopColor: string;
|
|
80
|
+
borderTopWidth: string;
|
|
81
|
+
visibility: string;
|
|
82
|
+
opacity: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Measures every element inside `cell` (up to `limit`) in the browser. */
|
|
86
|
+
export function measurePaintedBoxes(cell: MeasurableElement, readStyle: ComputedStyleReader, limit = 300): { boxes: PaintedBox[]; cellArea: number } {
|
|
87
|
+
const cellRect = cell.getBoundingClientRect();
|
|
88
|
+
const elements = cell.querySelectorAll('*');
|
|
89
|
+
const boxes: PaintedBox[] = [];
|
|
90
|
+
for (let i = 0; i < Math.min(elements.length, limit); i++) {
|
|
91
|
+
const element = elements[i];
|
|
92
|
+
const style = readStyle(element);
|
|
93
|
+
if (style.visibility === 'hidden' || Number(style.opacity) === 0) continue;
|
|
94
|
+
const rect = element.getBoundingClientRect();
|
|
95
|
+
boxes.push({
|
|
96
|
+
area: rect.width * rect.height,
|
|
97
|
+
background: parseCssColor(style.backgroundColor),
|
|
98
|
+
border: parseFloat(style.borderTopWidth) > 0 ? parseCssColor(style.borderTopColor) : null,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { boxes, cellArea: cellRect.width * cellRect.height };
|
|
102
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { SpecimenSurfaceKind } from './types.ts';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
* assert on it directly (see `
|
|
4
|
+
* The example wrapper's width policy inside a cell, kept free of any `react-native` import so
|
|
5
|
+
* `node --test` can assert on it directly (see `__tests__/specimenSurfaceStyle.test.ts`).
|
|
4
6
|
*
|
|
5
|
-
* The
|
|
7
|
+
* The wrapper always stretches to the full cross-axis width of whatever wraps it (a comparison
|
|
6
8
|
* cell, typically centered) rather than shrink-wrapping to its own content — `alignSelf: 'stretch'`
|
|
7
9
|
* overrides a centered parent's `alignItems` for this one child. A compact specimen (e.g. Button)
|
|
8
10
|
* still reads as centered, via `SpecimenSurface`'s own `alignItems`/`justifyContent: 'center'` on
|
|
@@ -13,3 +15,7 @@
|
|
|
13
15
|
export const SPECIMEN_SURFACE_WIDTH_STYLE = {
|
|
14
16
|
alignSelf: 'stretch',
|
|
15
17
|
} as const;
|
|
18
|
+
|
|
19
|
+
/** Each example's cell turns gray only when its example would vanish against white (see
|
|
20
|
+
* `specimenFill.ts`). A page can force a fill with `specimenSurface`. */
|
|
21
|
+
export const DEFAULT_SPECIMEN_SURFACE: SpecimenSurfaceKind = 'auto';
|
package/native/catalog/tokens.ts
CHANGED
|
@@ -119,6 +119,8 @@ export const CATALOG_COLOR = {
|
|
|
119
119
|
surfaceMuted: '#fafafa',
|
|
120
120
|
/** Hover and pressed feedback on nav rows and buttons. */
|
|
121
121
|
surfacePressed: '#f1f3f8',
|
|
122
|
+
/** An example's cell when the example itself is white (`specimenSurface`), darker than surfaceMuted so a white card's edges show. */
|
|
123
|
+
specimenStage: '#f2f2f2',
|
|
122
124
|
pageBackground: '#f6f6f4',
|
|
123
125
|
chip: '#eeeeee',
|
|
124
126
|
/** Active nav label, prop types, scale bars. */
|
package/native/catalog/types.ts
CHANGED
|
@@ -65,14 +65,12 @@ export type SpecimenSize = 'compact' | 'regular' | 'wide';
|
|
|
65
65
|
* content that is not a phone component (catalog chrome, token galleries). */
|
|
66
66
|
export type PreviewWidths = readonly number[] | 'full';
|
|
67
67
|
|
|
68
|
-
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
* their current full-width, unstaged presentation regardless of this setting. */
|
|
75
|
-
export type SpecimenSurfaceKind = 'neutral' | 'white' | 'dark' | 'transparent';
|
|
68
|
+
/** Fill of each example's cell. `'auto'` (default): a cell turns gray (#f2f2f2) only when its own
|
|
69
|
+
* example would vanish against white — a near-white body with no visible border, like a white
|
|
70
|
+
* card, sheet or neutral banner — so neighbouring examples keep a white cell. `'neutral'` makes
|
|
71
|
+
* every example cell gray, `'white'` and `'transparent'` keep them white, `'dark'` checks light
|
|
72
|
+
* components on a dark fill. Never applied to token galleries or sections. */
|
|
73
|
+
export type SpecimenSurfaceKind = 'auto' | 'neutral' | 'white' | 'dark' | 'transparent';
|
|
76
74
|
|
|
77
75
|
/** One real component this page's component is built from or composed with — rendered as a
|
|
78
76
|
* "Composition" subsection in Quick reference. `relationship` is `'built-in'` for a component
|
|
@@ -218,7 +216,7 @@ export interface SectionDef<TId extends string = string> {
|
|
|
218
216
|
* and everything else is 'regular'. */
|
|
219
217
|
specimenSize?: SpecimenSize;
|
|
220
218
|
/** Background stage behind every live component specimen on this page (list/grouped/grid cells,
|
|
221
|
-
* and a `render()` preview that isn't a token gallery). Defaults to `'
|
|
219
|
+
* and a `render()` preview that isn't a token gallery). Defaults to `'auto'`. See
|
|
222
220
|
* `SpecimenSurfaceKind`. */
|
|
223
221
|
specimenSurface?: SpecimenSurfaceKind;
|
|
224
222
|
/** Widths for this page's `render()` preview, e.g. `[402, 320]` to add a small-phone example.
|