@krapwoo/ds-viewer 0.5.0 → 0.6.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 +31 -8
- package/dist/cli/doctor.js +42 -3
- package/dist/cli/explain.js +11 -20
- package/dist/cli/render.js +91 -0
- package/dist/cli/staticPage.js +17 -0
- package/dist/native/catalog/comparison.js +69 -30
- package/dist/native/catalog/tokens.js +12 -6
- package/native/catalog/CatalogButton.tsx +50 -11
- package/native/catalog/CatalogShell.tsx +29 -11
- package/native/catalog/CatalogSidebar.tsx +18 -6
- package/native/catalog/ComparisonGrid.tsx +9 -8
- package/native/catalog/ComparisonGroups.tsx +6 -5
- package/native/catalog/ComparisonList.tsx +12 -3
- package/native/catalog/SectionBlock.tsx +16 -11
- package/native/catalog/SpecimenSurface.tsx +10 -6
- package/native/catalog/TokenLayouts.tsx +14 -3
- package/native/catalog/UpdatePanel.tsx +29 -50
- package/native/catalog/comparison.ts +77 -33
- package/native/catalog/index.ts +1 -1
- package/native/catalog/pageApi.ts +3 -0
- package/native/catalog/specimenSurfaceStyle.ts +11 -0
- package/native/catalog/tokens.ts +12 -6
- package/native/catalog/types.ts +10 -3
- package/native/catalog/updatePanelState.ts +8 -0
- package/package.json +1 -1
|
@@ -52,7 +52,7 @@ function resetMainScroll(scrollView: ScrollView | null): void {
|
|
|
52
52
|
* with only `onPress` (as `CatalogSidebar.tsx`'s own finding already documents) never activates on
|
|
53
53
|
* Enter under react-native-web unless the element is a genuine anchor. `role="status"` (not
|
|
54
54
|
* `"alert"`, the mockup's own choice) — a non-urgent notice should not interrupt assistive tech. */
|
|
55
|
-
function MajorBanner({ update, onReviewUpdate, onDismiss }: { update: UpdateNotice; onReviewUpdate: () => void; onDismiss: () => void }) {
|
|
55
|
+
function MajorBanner({ update, onReviewUpdate, onDismiss, keepDocumentUrl = false }: { update: UpdateNotice; onReviewUpdate: () => void; onDismiss: () => void; keepDocumentUrl?: boolean }) {
|
|
56
56
|
const releaseUrl = `https://github.com/krapwoo/design-system-viewer/releases/tag/v${update.latest}`;
|
|
57
57
|
return (
|
|
58
58
|
<View style={bannerStyles.banner} role="status">
|
|
@@ -68,7 +68,16 @@ function MajorBanner({ update, onReviewUpdate, onDismiss }: { update: UpdateNoti
|
|
|
68
68
|
Migration guide
|
|
69
69
|
</Text>
|
|
70
70
|
{' · '}
|
|
71
|
-
<Text
|
|
71
|
+
<Text
|
|
72
|
+
role="link"
|
|
73
|
+
style={bannerStyles.link}
|
|
74
|
+
onPress={(event?: { preventDefault?: () => void }) => {
|
|
75
|
+
// An embedded shell opens its own update page; the host page's address stays put.
|
|
76
|
+
if (keepDocumentUrl) event?.preventDefault?.();
|
|
77
|
+
onReviewUpdate();
|
|
78
|
+
}}
|
|
79
|
+
{...({ href: hashForId(UPDATE_PAGE_ID) } as Record<string, unknown>)}
|
|
80
|
+
>
|
|
72
81
|
Review update
|
|
73
82
|
</Text>
|
|
74
83
|
</Text>
|
|
@@ -96,7 +105,7 @@ const bannerStyles = StyleSheet.create({
|
|
|
96
105
|
bold: { fontWeight: '700' },
|
|
97
106
|
link: { color: CATALOG_COLOR.warning, fontWeight: '700', textDecorationLine: 'underline' },
|
|
98
107
|
close: { width: 32, height: 32, borderRadius: CATALOG_RADIUS.sm, alignItems: 'center', justifyContent: 'center' },
|
|
99
|
-
closeGlyph: { fontSize:
|
|
108
|
+
closeGlyph: { fontSize: CATALOG_TYPE.xl, color: CATALOG_COLOR.warning },
|
|
100
109
|
});
|
|
101
110
|
|
|
102
111
|
/**
|
|
@@ -115,9 +124,16 @@ export function CatalogShell<TId extends string>({
|
|
|
115
124
|
update,
|
|
116
125
|
versionStatus,
|
|
117
126
|
updateEndpoint,
|
|
127
|
+
embedded = false,
|
|
128
|
+
initialPage,
|
|
118
129
|
}: {
|
|
119
130
|
/** Short product/app name — the sidebar logo and the breadcrumb root. */
|
|
120
131
|
appName: string;
|
|
132
|
+
/** A shell shown inside another page (a catalog demo): it keeps its selected page in its own
|
|
133
|
+
* state, never reads or writes the URL, and doesn't treat the page's address as its own. */
|
|
134
|
+
embedded?: boolean;
|
|
135
|
+
/** The page an embedded shell opens on (defaults to the first page). */
|
|
136
|
+
initialPage?: TId;
|
|
121
137
|
/** Optional logo image, validated and bundled by `cli/workspace.ts` from the project's `logo`
|
|
122
138
|
* config field. See `CatalogSidebar`'s own doc comment for the approved layout rule. */
|
|
123
139
|
logoImageSource?: ImageSourcePropType;
|
|
@@ -150,14 +166,16 @@ export function CatalogShell<TId extends string>({
|
|
|
150
166
|
// recognizing itself as the selected address) would otherwise render alongside it for nothing.
|
|
151
167
|
// Read once per mount, like `active`'s own initial read below: this mode is only ever entered by
|
|
152
168
|
// a fresh document load, never a same-document transition.
|
|
153
|
-
|
|
169
|
+
// An embedded shell never owns the URL (hash routing, the overlay child-document address).
|
|
170
|
+
const routesByUrl = isWeb() && !embedded;
|
|
171
|
+
const overlayAddress = useMemo(() => (routesByUrl ? overlayAddressFromSearch(window.location.search) : undefined), [routesByUrl]);
|
|
154
172
|
const overlayNode = useMemo(() => (overlayAddress ? findOverlayNode(sections, overlayAddress) : undefined), [overlayAddress, sections]);
|
|
155
173
|
|
|
156
174
|
const sectionsById = useMemo(() => new Map(sections.map((def) => [def.id, def])), [sections]);
|
|
157
175
|
const labels = useMemo(() => new Map(sections.filter((def) => def.title).map((def) => [def.id as string, def.title as string])), [sections]);
|
|
158
176
|
const order = useMemo(() => orderedIds(groups, new Set(sectionsById.keys())), [groups, sectionsById]);
|
|
159
177
|
const [active, setActive] = useState<ShellPageId<TId> | undefined>(() =>
|
|
160
|
-
isWeb() ? resolveActiveFromHash(window.location.hash, order) : order[0],
|
|
178
|
+
embedded ? initialPage ?? order[0] : isWeb() ? resolveActiveFromHash(window.location.hash, order) : order[0],
|
|
161
179
|
);
|
|
162
180
|
|
|
163
181
|
const scrollRef = useRef<ScrollView>(null);
|
|
@@ -179,7 +197,7 @@ export function CatalogShell<TId extends string>({
|
|
|
179
197
|
|
|
180
198
|
// Back/forward and manual fragment edits.
|
|
181
199
|
useEffect(() => {
|
|
182
|
-
if (!
|
|
200
|
+
if (!routesByUrl) return;
|
|
183
201
|
const onHashChange = () => {
|
|
184
202
|
const id = resolveActiveFromHash(window.location.hash, order);
|
|
185
203
|
if (id === undefined) return;
|
|
@@ -193,7 +211,7 @@ export function CatalogShell<TId extends string>({
|
|
|
193
211
|
};
|
|
194
212
|
window.addEventListener('hashchange', onHashChange);
|
|
195
213
|
return () => window.removeEventListener('hashchange', onHashChange);
|
|
196
|
-
}, [order]);
|
|
214
|
+
}, [order, routesByUrl]);
|
|
197
215
|
|
|
198
216
|
// After every page change: sync the fragment, return to the top, and move focus when the user
|
|
199
217
|
// navigated (never on first load).
|
|
@@ -203,7 +221,7 @@ export function CatalogShell<TId extends string>({
|
|
|
203
221
|
const focus = focusHeadingNext.current;
|
|
204
222
|
pushHistoryNext.current = false;
|
|
205
223
|
focusHeadingNext.current = false;
|
|
206
|
-
if (
|
|
224
|
+
if (routesByUrl) {
|
|
207
225
|
const expected = hashForId(active);
|
|
208
226
|
if (window.location.hash !== expected) {
|
|
209
227
|
if (push) window.location.hash = expected;
|
|
@@ -212,7 +230,7 @@ export function CatalogShell<TId extends string>({
|
|
|
212
230
|
}
|
|
213
231
|
resetMainScroll(scrollRef.current);
|
|
214
232
|
if (focus) focusElement(headingRef.current);
|
|
215
|
-
}, [active]);
|
|
233
|
+
}, [active, routesByUrl]);
|
|
216
234
|
|
|
217
235
|
const activeDef = active !== undefined && active !== UPDATE_PAGE_ID ? sectionsById.get(active) : undefined;
|
|
218
236
|
|
|
@@ -266,7 +284,7 @@ export function CatalogShell<TId extends string>({
|
|
|
266
284
|
|
|
267
285
|
return (
|
|
268
286
|
<View style={styles.root}>
|
|
269
|
-
<CatalogSidebar logo={appName} logoImageSource={logoImageSource} caption={title} groups={groups} labels={labels} active={active} onPress={select} footer={footer} />
|
|
287
|
+
<CatalogSidebar logo={appName} logoImageSource={logoImageSource} caption={title} groups={groups} labels={labels} active={active} onPress={select} footer={footer} keepDocumentUrl={embedded} />
|
|
270
288
|
<ScrollView ref={scrollRef} style={styles.main} contentContainerStyle={styles.mainContent}>
|
|
271
289
|
{active === UPDATE_PAGE_ID ? (
|
|
272
290
|
<UpdatePanel
|
|
@@ -286,7 +304,7 @@ export function CatalogShell<TId extends string>({
|
|
|
286
304
|
// The approved mockup's own `PAGE_BG` order: crumb, then banner, then title — the
|
|
287
305
|
// banner used to render above the breadcrumb instead (Important finding, Fable
|
|
288
306
|
// correction pass).
|
|
289
|
-
banner={showBanner && effectiveUpdate ? <MajorBanner update={effectiveUpdate} onReviewUpdate={() => select(UPDATE_PAGE_ID)} onDismiss={dismissMajorBanner} /> : undefined}
|
|
307
|
+
banner={showBanner && effectiveUpdate ? <MajorBanner update={effectiveUpdate} onReviewUpdate={() => select(UPDATE_PAGE_ID)} onDismiss={dismissMajorBanner} keepDocumentUrl={embedded} /> : undefined}
|
|
290
308
|
pager={{
|
|
291
309
|
previousId: neighbors(order, activeDef.id).previous,
|
|
292
310
|
nextId: neighbors(order, activeDef.id).next,
|
|
@@ -13,12 +13,21 @@ function webLinkProps(id: string, active: boolean) {
|
|
|
13
13
|
|
|
14
14
|
/** One nav link. Hover via onHoverIn/onHoverOut (react-native-web fires them; native never does);
|
|
15
15
|
* keyboard focus draws the catalog focus ring because react-native-web removes the browser's. */
|
|
16
|
-
|
|
16
|
+
/** In an embedded shell, a link's own navigation would change the host page's address; the shell
|
|
17
|
+
* handles the press itself, so the browser's default is cancelled. */
|
|
18
|
+
function pressHandler(onPress: () => void, keepDocumentUrl: boolean) {
|
|
19
|
+
return (event?: { preventDefault?: () => void }) => {
|
|
20
|
+
if (keepDocumentUrl) event?.preventDefault?.();
|
|
21
|
+
onPress();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function NavItem<TId extends string>({ id, label, active, onPress, keepDocumentUrl = false }: { id: TId; label: string; active: boolean; onPress: () => void; keepDocumentUrl?: boolean }) {
|
|
17
26
|
const [hovered, setHovered] = useState(false);
|
|
18
27
|
const [focused, setFocused] = useState(false);
|
|
19
28
|
return (
|
|
20
29
|
<Pressable
|
|
21
|
-
onPress={onPress}
|
|
30
|
+
onPress={pressHandler(onPress, keepDocumentUrl)}
|
|
22
31
|
onHoverIn={() => setHovered(true)}
|
|
23
32
|
onHoverOut={() => setHovered(false)}
|
|
24
33
|
onFocus={() => setFocused(true)}
|
|
@@ -43,12 +52,12 @@ function NavItem<TId extends string>({ id, label, active, onPress }: { id: TId;
|
|
|
43
52
|
);
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
function FooterLink({ label, kind, active, onPress }: { label: string; kind: 'update' | 'version'; active: boolean; onPress: () => void }) {
|
|
55
|
+
function FooterLink({ label, kind, active, onPress, keepDocumentUrl = false }: { label: string; kind: 'update' | 'version'; active: boolean; onPress: () => void; keepDocumentUrl?: boolean }) {
|
|
47
56
|
const [hovered, setHovered] = useState(false);
|
|
48
57
|
const [focused, setFocused] = useState(false);
|
|
49
58
|
return (
|
|
50
59
|
<Pressable
|
|
51
|
-
onPress={onPress}
|
|
60
|
+
onPress={pressHandler(onPress, keepDocumentUrl)}
|
|
52
61
|
onHoverIn={() => setHovered(true)}
|
|
53
62
|
onHoverOut={() => setHovered(false)}
|
|
54
63
|
onFocus={() => setFocused(true)}
|
|
@@ -84,8 +93,11 @@ export function CatalogSidebar<TId extends string>({
|
|
|
84
93
|
active,
|
|
85
94
|
onPress,
|
|
86
95
|
footer,
|
|
96
|
+
keepDocumentUrl = false,
|
|
87
97
|
}: {
|
|
88
98
|
logo: string;
|
|
99
|
+
/** For a shell embedded in another page: links never change the document's address. */
|
|
100
|
+
keepDocumentUrl?: boolean;
|
|
89
101
|
/** Optional logo image, validated and bundled by `cli/workspace.ts` from the project's `logo`
|
|
90
102
|
* config field. Visual spec: docs/design/2026-10-08-ds-viewer-sidebar-logo-approved.html
|
|
91
103
|
* ("C · Adaptive", approved 2026-10-08) — a mark (aspect ratio ≤ 2) sits beside the name; a
|
|
@@ -195,14 +207,14 @@ export function CatalogSidebar<TId extends string>({
|
|
|
195
207
|
<View key={group.label}>
|
|
196
208
|
<Text style={styles.groupLabel}>{group.label}</Text>
|
|
197
209
|
{group.ids.map((id) => (
|
|
198
|
-
<NavItem key={id} id={id} label={labels?.get(id) ?? id} active={active === id} onPress={() => onPress(id)} />
|
|
210
|
+
<NavItem key={id} id={id} label={labels?.get(id) ?? id} active={active === id} onPress={() => onPress(id)} keepDocumentUrl={keepDocumentUrl} />
|
|
199
211
|
))}
|
|
200
212
|
</View>
|
|
201
213
|
))}
|
|
202
214
|
</ScrollView>
|
|
203
215
|
{footer && (
|
|
204
216
|
<View style={styles.footerContainer}>
|
|
205
|
-
<FooterLink label={footer.label} kind={footer.kind ?? 'update'} active={footer.active} onPress={footer.onPress} />
|
|
217
|
+
<FooterLink label={footer.label} kind={footer.kind ?? 'update'} active={footer.active} onPress={footer.onPress} keepDocumentUrl={keepDocumentUrl} />
|
|
206
218
|
</View>
|
|
207
219
|
)}
|
|
208
220
|
</View>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import { COLUMN_MIN_WIDTH, MATRIX_LAYOUT, axisItems, cellKey,
|
|
4
|
+
import { COLUMN_MIN_WIDTH, MATRIX_LAYOUT, axisItems, cellKey, gridMinWidth, indexCells, validateComparison } from './comparison';
|
|
5
5
|
import { SpecimenContent, SpecimenSurface } from './SpecimenSurface';
|
|
6
6
|
import { SpecimenAddressProvider } from './SpecimenAddress';
|
|
7
7
|
import { comparisonCellItemKey } from './overlayViewport';
|
|
@@ -10,9 +10,11 @@ import type { ComparisonDef, SpecimenSize, SpecimenSurfaceKind } from './types';
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* A grid of two props that combine freely, rendered as an accessible table inside one card.
|
|
13
|
-
*
|
|
14
|
-
* a grid
|
|
15
|
-
*
|
|
13
|
+
* The card fills its container and its columns share that width equally, never shrinking below the
|
|
14
|
+
* specimen size's minimum; a grid narrower than that scrolls horizontally inside the card, never
|
|
15
|
+
* the page. Phone width (402px) caps the specimen inside a cell, not the column, so a two-column
|
|
16
|
+
* grid is as wide as a five-column one. `surface` is the page's `specimenSurface` behind each
|
|
17
|
+
* cell's example.
|
|
16
18
|
*/
|
|
17
19
|
export function ComparisonGrid({
|
|
18
20
|
def,
|
|
@@ -31,13 +33,12 @@ export function ComparisonGrid({
|
|
|
31
33
|
const cells = indexCells(def);
|
|
32
34
|
const rows = axisItems(def.rows);
|
|
33
35
|
const columns = axisItems(def.columns);
|
|
34
|
-
const
|
|
36
|
+
const minWidth = gridMinWidth(columns.length, size);
|
|
35
37
|
const column = {
|
|
36
38
|
flexGrow: 1,
|
|
37
39
|
flexShrink: 1,
|
|
38
40
|
flexBasis: COLUMN_MIN_WIDTH[size],
|
|
39
41
|
minWidth: COLUMN_MIN_WIDTH[size],
|
|
40
|
-
maxWidth: MATRIX_LAYOUT.columnMaxWidth,
|
|
41
42
|
};
|
|
42
43
|
const lastColumn = columns.length - 1;
|
|
43
44
|
|
|
@@ -46,7 +47,7 @@ export function ComparisonGrid({
|
|
|
46
47
|
<View
|
|
47
48
|
role="table"
|
|
48
49
|
aria-label={`${sectionId}: ${def.rowLabel} by ${def.columnLabel}`}
|
|
49
|
-
style={[styles.card, { minWidth
|
|
50
|
+
style={[styles.card, { minWidth }]}
|
|
50
51
|
>
|
|
51
52
|
<View role="row" style={styles.row}>
|
|
52
53
|
<View role="columnheader" style={[styles.cell, styles.rowHeader, styles.headerCell]}>
|
|
@@ -69,7 +70,7 @@ export function ComparisonGrid({
|
|
|
69
70
|
cell?.node !== undefined ? (
|
|
70
71
|
<SpecimenSurface key={c.key} role="cell" surface={cell.surface ?? surface} style={[styles.cell, column, styles.bodyRow, styles.specimenCell, ci === lastColumn && styles.lastColumn]}>
|
|
71
72
|
<SpecimenAddressProvider address={{ pageId: sectionId, slot: 'comparison', itemKey: comparisonCellItemKey(row.key, c.key) }}>
|
|
72
|
-
<SpecimenContent fill={cell.fill} align={cell.align}>{cell.node}</SpecimenContent>
|
|
73
|
+
<SpecimenContent fill={cell.fill} align={cell.align} capped={size !== 'page'}>{cell.node}</SpecimenContent>
|
|
73
74
|
</SpecimenAddressProvider>
|
|
74
75
|
</SpecimenSurface>
|
|
75
76
|
) : (
|
|
@@ -10,9 +10,10 @@ import type { SpecimenSize, SpecimenSurfaceKind } from './types';
|
|
|
10
10
|
/**
|
|
11
11
|
* Grouped rows: one row per variant, headed by the variant's name, holding that variant's own
|
|
12
12
|
* configurations. Each cell keeps its own caption because the rows do not share column meanings
|
|
13
|
-
* (a circle's size is not a bar's thickness). Each row is its own labeled list.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* (a circle's size is not a bar's thickness). Each row is its own labeled list. Cells share the
|
|
14
|
+
* card's width equally, the way a grid's columns do, and shorter rows end in blank cells; a card
|
|
15
|
+
* narrower than its minimum scrolls horizontally inside itself. `surface` is the page's
|
|
16
|
+
* `specimenSurface` behind each item.
|
|
16
17
|
*/
|
|
17
18
|
export function ComparisonGroups({
|
|
18
19
|
groups,
|
|
@@ -53,7 +54,7 @@ export function ComparisonGroups({
|
|
|
53
54
|
</View>
|
|
54
55
|
<SpecimenSurface surface={item.surface ?? surface} style={styles.specimen}>
|
|
55
56
|
<SpecimenAddressProvider address={pageId && item.slot ? { pageId, slot: item.slot, itemKey: item.key } : undefined}>
|
|
56
|
-
<SpecimenContent fill={item.fill} align={item.align}>{item.node as React.ReactNode}</SpecimenContent>
|
|
57
|
+
<SpecimenContent fill={item.fill} align={item.align} capped={size !== 'page'}>{item.node as React.ReactNode}</SpecimenContent>
|
|
57
58
|
</SpecimenAddressProvider>
|
|
58
59
|
</SpecimenSurface>
|
|
59
60
|
</View>
|
|
@@ -91,7 +92,7 @@ const styles = StyleSheet.create({
|
|
|
91
92
|
},
|
|
92
93
|
rowHeaderText: { fontSize: CATALOG_TYPE.md, fontWeight: '700', color: CATALOG_COLOR.text },
|
|
93
94
|
cells: { flex: 1, flexDirection: 'row' },
|
|
94
|
-
cell: { flex: 1,
|
|
95
|
+
cell: { flex: 1, borderRightWidth: 1, borderRightColor: CATALOG_COLOR.border },
|
|
95
96
|
lastColumn: { borderRightWidth: 0 },
|
|
96
97
|
caption: {
|
|
97
98
|
padding: MATRIX_LAYOUT.cellPadding,
|
|
@@ -10,7 +10,7 @@ import type { SpecimenSize, SpecimenSurfaceKind } from './types';
|
|
|
10
10
|
/**
|
|
11
11
|
* One-axis examples inside ONE shared card. Cells wrap into balanced rows (listGeometry); each cell
|
|
12
12
|
* has its own caption strip so labels stay attached when rows wrap. Blank cells complete an uneven
|
|
13
|
-
* last row. Wide specimens
|
|
13
|
+
* last row. Wide specimens keep 402px as a minimum and share the container above it. `maxColumns` caps
|
|
14
14
|
* how many columns a row ever wraps into (from `VariantSlot.maxColumns`); `surface` is the page's
|
|
15
15
|
* `specimenSurface` behind each item.
|
|
16
16
|
*/
|
|
@@ -42,7 +42,7 @@ export function ComparisonList({
|
|
|
42
42
|
for (let r = 0; r < geometry.rows; r += 1) rows.push(slots.slice(r * geometry.columns, (r + 1) * geometry.columns));
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
|
-
<View onLayout={onLayout}>
|
|
45
|
+
<View onLayout={onLayout} style={styles.measure}>
|
|
46
46
|
<View role="list" aria-label={label} style={[styles.card, { width: geometry.containerWidth }]}>
|
|
47
47
|
{rows.map((row, ri) => (
|
|
48
48
|
<View key={ri} role="none" style={[styles.row, ri === rows.length - 1 && styles.lastRow]}>
|
|
@@ -67,7 +67,7 @@ export function ComparisonList({
|
|
|
67
67
|
</View>
|
|
68
68
|
<SpecimenSurface surface={item.surface ?? surface} style={styles.specimen}>
|
|
69
69
|
<SpecimenAddressProvider address={pageId && item.slot ? { pageId, slot: item.slot, itemKey: item.key } : undefined}>
|
|
70
|
-
<SpecimenContent fill={item.fill} align={item.align}>{item.node as React.ReactNode}</SpecimenContent>
|
|
70
|
+
<SpecimenContent fill={item.fill} align={item.align} capped={size !== 'page'}>{item.node as React.ReactNode}</SpecimenContent>
|
|
71
71
|
</SpecimenAddressProvider>
|
|
72
72
|
</SpecimenSurface>
|
|
73
73
|
</View>
|
|
@@ -81,7 +81,16 @@ export function ComparisonList({
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const styles = StyleSheet.create({
|
|
84
|
+
// The wrapper that measures the space the list has. It must stretch: left to hug its own card it
|
|
85
|
+
// would keep reporting the card's width, so a list inside a narrower container (a specimen cell)
|
|
86
|
+
// would never learn to shrink and would overflow. `alignItems` keeps the card centred, the
|
|
87
|
+
// position it had when the wrapper hugged it.
|
|
88
|
+
measure: { alignSelf: 'stretch', alignItems: 'center' },
|
|
84
89
|
card: {
|
|
90
|
+
// The width below is computed from the last measured space; until the first measurement lands
|
|
91
|
+
// it is a laptop-sized guess, so the card is also clamped to whatever space it really has —
|
|
92
|
+
// a list has no scroller of its own, and a too-wide guess would be cut off for a frame.
|
|
93
|
+
maxWidth: '100%',
|
|
85
94
|
backgroundColor: CATALOG_COLOR.surface,
|
|
86
95
|
borderWidth: 1,
|
|
87
96
|
borderColor: CATALOG_COLOR.borderStrong,
|
|
@@ -9,7 +9,7 @@ import { SpecimenSurface } from './SpecimenSurface';
|
|
|
9
9
|
import { SpecimenAddressProvider } from './SpecimenAddress';
|
|
10
10
|
import { DEFAULT_SPECIMEN_SURFACE } from './specimenSurfaceStyle';
|
|
11
11
|
import { TokenSections } from './TokenLayouts';
|
|
12
|
-
import { MATRIX_LAYOUT,
|
|
12
|
+
import { MATRIX_LAYOUT, PREVIEW_CARD_CHROME, blockMinWidth, choosePlacement, presentationBlocks, type PlacementBlock, type PresentationBlock } from './comparison';
|
|
13
13
|
import type { PreviewWidths, SectionDef, SpecimenSurfaceKind } from './types';
|
|
14
14
|
|
|
15
15
|
// Matches a quoted-string-literal union type, e.g. "'primary' | 'secondary' | 'tertiary'" — anything
|
|
@@ -184,7 +184,7 @@ function BlockContent<TId extends string>({ block, def, surface }: { block: Pres
|
|
|
184
184
|
</SpecimenAddressProvider>
|
|
185
185
|
) : null;
|
|
186
186
|
case 'tokenSections':
|
|
187
|
-
return <TokenSections sections={block.sections} columns={block.columns} />;
|
|
187
|
+
return <TokenSections sections={block.sections} columns={block.columns} label={def.id} />;
|
|
188
188
|
default:
|
|
189
189
|
return (
|
|
190
190
|
<View style={styles.previewCard}>
|
|
@@ -195,8 +195,13 @@ function BlockContent<TId extends string>({ block, def, surface }: { block: Pres
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/** Width a block's card occupies: lists hug their columns; everything else fills the row. */
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
/** What `choosePlacement` needs about a block: how narrow it can get, and (for a list) the geometry
|
|
201
|
+
* that predicts its height in half a row. */
|
|
202
|
+
function placementBlock(block: PresentationBlock, height: number): PlacementBlock {
|
|
203
|
+
const base = { kind: block.kind, height, minWidth: blockMinWidth(block) };
|
|
204
|
+
return block.kind === 'list' ? { ...base, itemCount: block.items.length, size: block.size, maxColumns: block.maxColumns } : base;
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
/**
|
|
@@ -236,10 +241,8 @@ function Blocks<TId extends string>({ blocks, def, surface }: { blocks: Presenta
|
|
|
236
241
|
const decision = choosePlacement({
|
|
237
242
|
available,
|
|
238
243
|
gap: CATALOG_LAYOUT.blockGap,
|
|
239
|
-
first:
|
|
240
|
-
second: second
|
|
241
|
-
? { kind: 'list', height: heights[1], itemCount: second.items.length, size: second.size, maxColumns: second.maxColumns }
|
|
242
|
-
: { kind: second.kind, height: heights[1] },
|
|
244
|
+
first: placementBlock(first, heights[0]),
|
|
245
|
+
second: placementBlock(second, heights[1]),
|
|
243
246
|
});
|
|
244
247
|
if (decision === 'side') setPlacement('side');
|
|
245
248
|
}, [available, blocks, heights, placement]);
|
|
@@ -251,7 +254,8 @@ function Blocks<TId extends string>({ blocks, def, surface }: { blocks: Presenta
|
|
|
251
254
|
<View
|
|
252
255
|
key={block.title || block.kind}
|
|
253
256
|
onLayout={onBlockLayout(i)}
|
|
254
|
-
|
|
257
|
+
// Side by side, each block fills its own half: a card always fills the column it is given.
|
|
258
|
+
style={side ? styles.blockFill : undefined}
|
|
255
259
|
>
|
|
256
260
|
{block.title ? <Text style={styles.blockLabel}>{block.title}</Text> : null}
|
|
257
261
|
<BlockContent block={block} def={def} surface={surface} />
|
|
@@ -394,10 +398,11 @@ const styles = StyleSheet.create({
|
|
|
394
398
|
},
|
|
395
399
|
previewCard: {
|
|
396
400
|
backgroundColor: CATALOG_COLOR.surface,
|
|
397
|
-
|
|
401
|
+
// From comparison.ts, which adds the same numbers to a preview's minimum width.
|
|
402
|
+
borderWidth: PREVIEW_CARD_CHROME.border,
|
|
398
403
|
borderColor: CATALOG_COLOR.borderStrong,
|
|
399
404
|
borderRadius: CATALOG_RADIUS.card,
|
|
400
|
-
padding:
|
|
405
|
+
padding: PREVIEW_CARD_CHROME.padding,
|
|
401
406
|
gap: CATALOG_SPACE.md,
|
|
402
407
|
},
|
|
403
408
|
// `ComparisonList`'s own card/caption tokens, reused so a 'table' preview reads as the same shared
|
|
@@ -3,7 +3,7 @@ import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
|
3
3
|
import { CATALOG_COLOR } from './tokens';
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_SPECIMEN_SURFACE, SPECIMEN_SURFACE_WIDTH_STYLE, overflowsCell, paintedExtent, shouldCenterFilledContent, specimenContentAlignItems,
|
|
6
|
-
specimenContentBoundedStyle, visibleBoxes, type LayoutBox,
|
|
6
|
+
specimenContentBoundedStyle, visibleBoxes, filledContentStyle, type LayoutBox,
|
|
7
7
|
} from './specimenSurfaceStyle';
|
|
8
8
|
import { useSpecimenAddress } from './SpecimenAddress';
|
|
9
9
|
import { blendsIntoCell, measurePaintedBoxes } from './specimenFill';
|
|
@@ -115,7 +115,7 @@ function readTree(root: MeasurableNode, browser: Browser): LayoutBox {
|
|
|
115
115
|
return read(root);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
export function SpecimenContent({ fill, align, children }: { fill?: boolean; align?: SpecimenAlign; children: React.ReactNode }) {
|
|
118
|
+
export function SpecimenContent({ fill, align, capped = true, children }: { fill?: boolean; align?: SpecimenAlign; /** Phone-width cap for a filled example; off for page-width cells. */ capped?: boolean; children: React.ReactNode }) {
|
|
119
119
|
// After layout (browser only): a `fill` example whose visible content is narrower than the cell
|
|
120
120
|
// (a fixed-width skeleton, a tooltip around a small trigger) is centred instead of sitting
|
|
121
121
|
// against the leading edge; an explicit `align` always wins. And in development, an example
|
|
@@ -130,17 +130,21 @@ export function SpecimenContent({ fill, align, children }: { fill?: boolean; ali
|
|
|
130
130
|
if (!browser.Element || !browser.getComputedStyle || !node?.getBoundingClientRect) return;
|
|
131
131
|
const wrapperWidth = node.getBoundingClientRect().width;
|
|
132
132
|
const visible = paintedExtent(visibleBoxes(readTree(node, browser)));
|
|
133
|
-
|
|
133
|
+
// Only for a phone-width cell. Centring exists for a small control adrift in a wide cell (a
|
|
134
|
+
// tooltip, a fixed-width skeleton); a 'page' cell holds a layout, whose own content is
|
|
135
|
+
// supposed to sit at the left of its own measure, and centring it collapses it to the width of
|
|
136
|
+
// whatever it happens to have painted.
|
|
137
|
+
if (capped && fill && align === undefined && !centerNarrow && shouldCenterFilledContent(visible, wrapperWidth)) setCenterNarrow(true);
|
|
134
138
|
if ((globalThis as { __DEV__?: boolean }).__DEV__ && !warned.current && overflowsCell(visible, wrapperWidth)) {
|
|
135
139
|
warned.current = true;
|
|
136
140
|
console.warn(
|
|
137
|
-
`[Catalog] ${address ? `${address.pageId} (${address.itemKey})` : 'An example'} is ${Math.round(visible)}px wide in a ${Math.round(wrapperWidth)}px cell, so it's cut off. Give the page a wider specimenSize ('regular' or '
|
|
141
|
+
`[Catalog] ${address ? `${address.pageId} (${address.itemKey})` : 'An example'} is ${Math.round(visible)}px wide in a ${Math.round(wrapperWidth)}px cell, so it's cut off. Give the page a wider specimenSize ('regular', 'wide', or 'page' for components wider than a phone).`,
|
|
138
142
|
);
|
|
139
143
|
}
|
|
140
|
-
}, [fill, align, centerNarrow, address]);
|
|
144
|
+
}, [capped, fill, align, centerNarrow, address]);
|
|
141
145
|
if (fill) {
|
|
142
146
|
const alignItems = centerNarrow ? 'center' : specimenContentAlignItems(fill, align);
|
|
143
|
-
return <View ref={ref} onLayout={measure} style={[SPECIMEN_SURFACE_WIDTH_STYLE, { alignItems }]}>{children}</View>;
|
|
147
|
+
return <View ref={ref} onLayout={measure} style={[SPECIMEN_SURFACE_WIDTH_STYLE, filledContentStyle(capped), { alignItems }]}>{children}</View>;
|
|
144
148
|
}
|
|
145
149
|
return <View ref={ref} onLayout={measure} style={specimenContentBoundedStyle(align)}>{children}</View>;
|
|
146
150
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
2
|
import { View, Text, StyleSheet, type LayoutChangeEvent } from 'react-native';
|
|
3
3
|
import { CATALOG_COLOR, CATALOG_LAYOUT, CATALOG_RADIUS, CATALOG_SPACE, CATALOG_TYPE } from './tokens';
|
|
4
|
-
import { sectionColumns, tileGeometry, TOKEN_LAYOUT_GAP } from './tokenLayout';
|
|
4
|
+
import { sectionColumns, tileGeometry, TOKEN_LAYOUT_GAP, TOKEN_SECTION_MIN_WIDTH } from './tokenLayout';
|
|
5
5
|
import type { TokenSection } from './types';
|
|
6
6
|
|
|
7
7
|
/** Measures its own width; renders nothing inside until the width is known. */
|
|
@@ -18,9 +18,20 @@ function useWidth(): [number, (event: LayoutChangeEvent) => void] {
|
|
|
18
18
|
* in up to `columns` columns. A `wide` section takes a full row; the page drops to fewer columns
|
|
19
19
|
* rather than squeeze a section under its minimum width.
|
|
20
20
|
*/
|
|
21
|
-
export function TokenSections({ sections, columns }: { sections: TokenSection[]; columns: 1 | 2 | 3 }) {
|
|
21
|
+
export function TokenSections({ sections, columns, label }: { sections: TokenSection[]; columns: 1 | 2 | 3; label?: string }) {
|
|
22
22
|
const [width, onLayout] = useWidth();
|
|
23
23
|
const perRow = sectionColumns(width, columns);
|
|
24
|
+
// Degrading quietly is how a page ends up claiming a column count it never renders: the author
|
|
25
|
+
// reads `columns={3}`, the reader sees one column, and nothing says the space was the reason.
|
|
26
|
+
// Once per transition, not once per render: a resize re-renders continuously.
|
|
27
|
+
const warned = useRef('');
|
|
28
|
+
if (width > 0 && perRow < columns && (globalThis as { __DEV__?: boolean }).__DEV__) {
|
|
29
|
+
const key = `${columns}:${perRow}`;
|
|
30
|
+
if (warned.current !== key) {
|
|
31
|
+
warned.current = key;
|
|
32
|
+
console.warn(`[Catalog] ${label ?? 'TokenSections'}: asked for ${columns} columns but only ${perRow} fit in ${width}px; sections never go below ${TOKEN_SECTION_MIN_WIDTH}px.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
const columnWidth = perRow === 1 ? width : (width - TOKEN_LAYOUT_GAP * (perRow - 1)) / perRow;
|
|
25
36
|
return (
|
|
26
37
|
<View onLayout={onLayout} style={styles.sections}>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Platform, Pressable, ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
|
|
3
3
|
import { CATALOG_COLOR, CATALOG_LAYOUT, CATALOG_RADIUS, CATALOG_SPACE, CATALOG_TYPE } from './tokens';
|
|
4
|
+
import { CatalogButton } from './CatalogButton';
|
|
4
5
|
import { DS_VIEWER_SECRET_HEADER } from './catalogNavigation';
|
|
5
6
|
import {
|
|
6
|
-
autoCheckHelp, bumpLabel, checkedAgoLabel, resultNote, daysAgoLabel, isAlreadyUpToDateError, resolvePanelPhase, versionOverview, type UpdatePlanView, type UpdateStatusView,
|
|
7
|
+
autoCheckHelp, bumpLabel, checkedAgoLabel, resultNote, daysAgoLabel, isAlreadyUpToDateError, isRemoteOrigin, resolvePanelPhase, versionOverview, type UpdatePlanView, type UpdateStatusView,
|
|
7
8
|
} from './updatePanelState';
|
|
8
9
|
import type { UpdateNotice, VersionStatus } from './types';
|
|
9
10
|
|
|
@@ -11,6 +12,13 @@ function isWeb(): boolean {
|
|
|
11
12
|
return Platform.OS === 'web' && typeof window !== 'undefined';
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/** The page is open from somewhere other than the machine running the viewer, so every action will
|
|
16
|
+
* be refused however healthy the server is — see `isRemoteOrigin`. */
|
|
17
|
+
function remoteOrigin(endpoint: Endpoint | undefined): boolean {
|
|
18
|
+
if (!endpoint || !isWeb()) return false;
|
|
19
|
+
return isRemoteOrigin(window.location.protocol, window.location.hostname);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
type Endpoint = { baseUrl: string; secret: string };
|
|
15
23
|
|
|
16
24
|
async function authedFetch(endpoint: Endpoint, path: string, init: RequestInit = {}): Promise<Response> {
|
|
@@ -225,7 +233,12 @@ function VersionDetails({
|
|
|
225
233
|
setJustChecked(next.lastOutcome === 'ok' && !next.update);
|
|
226
234
|
onStatus(next);
|
|
227
235
|
} catch {
|
|
228
|
-
setLocalError(
|
|
236
|
+
setLocalError(remoteOrigin(endpoint)
|
|
237
|
+
// The endpoint only answers its own origin, and it can install packages and restart the
|
|
238
|
+
// server — so saying "restart the viewer" here would send the reader after a fault that
|
|
239
|
+
// isn't there. The page itself is fine over a tunnel; only its actions are local.
|
|
240
|
+
? 'This page is open from another address, and the viewer only takes actions from the machine it runs on. Open the catalog on that machine, or run npx ds-viewer update there.'
|
|
241
|
+
: 'Couldn’t reach the viewer’s local server. If this keeps happening, restart npx ds-viewer dev.');
|
|
229
242
|
} finally {
|
|
230
243
|
setChecking(false);
|
|
231
244
|
}
|
|
@@ -304,18 +317,12 @@ function VersionDetails({
|
|
|
304
317
|
{localError && <NoteBox variant="err">{localError}</NoteBox>}
|
|
305
318
|
{live && (
|
|
306
319
|
<View style={styles.actions}>
|
|
307
|
-
<
|
|
320
|
+
<CatalogButton
|
|
321
|
+
variant="secondary"
|
|
322
|
+
busy={checking}
|
|
308
323
|
onPress={checkNow}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
accessibilityState={{ disabled: checking, busy: checking }}
|
|
312
|
-
style={[styles.secondaryButton, styles.buttonRow, checking && styles.busyButton]}
|
|
313
|
-
>
|
|
314
|
-
{checking && <ActivityIndicator size="small" color={CATALOG_COLOR.textMuted} />}
|
|
315
|
-
<Text style={[styles.secondaryButtonText, checking && styles.busyButtonText]}>
|
|
316
|
-
{checking ? 'Checking…' : overview.action === 'retry' ? 'Try again' : 'Check now'}
|
|
317
|
-
</Text>
|
|
318
|
-
</Pressable>
|
|
324
|
+
label={checking ? 'Checking…' : overview.action === 'retry' ? 'Try again' : 'Check now'}
|
|
325
|
+
/>
|
|
319
326
|
{unreachable && !checking && <Text style={styles.muted}>or run <Code>npm view @krapwoo/ds-viewer version</Code></Text>}
|
|
320
327
|
</View>
|
|
321
328
|
)}
|
|
@@ -504,15 +511,7 @@ export function UpdatePanel({
|
|
|
504
511
|
<Text style={styles.muted}>Not a git repository, so uncommitted changes couldn’t be checked.</Text>
|
|
505
512
|
)}
|
|
506
513
|
<View style={styles.actions}>
|
|
507
|
-
<
|
|
508
|
-
onPress={startUpdate}
|
|
509
|
-
disabled={starting}
|
|
510
|
-
accessibilityRole="button"
|
|
511
|
-
accessibilityState={{ disabled: starting }}
|
|
512
|
-
style={[styles.primaryButton, starting && styles.disabledButton]}
|
|
513
|
-
>
|
|
514
|
-
<Text style={[styles.primaryButtonText, starting && styles.disabledButtonText]}>Update now</Text>
|
|
515
|
-
</Pressable>
|
|
514
|
+
<CatalogButton label="Update now" onPress={startUpdate} disabled={starting} />
|
|
516
515
|
{starting ? <Text style={styles.muted}>Starting…</Text> : <Text style={styles.muted}>or run <Code>npx ds-viewer update</Code></Text>}
|
|
517
516
|
</View>
|
|
518
517
|
</>
|
|
@@ -542,17 +541,8 @@ export function UpdatePanel({
|
|
|
542
541
|
</NoteBox>
|
|
543
542
|
<FilesList files={plan.files} />
|
|
544
543
|
<View style={styles.actions}>
|
|
545
|
-
<
|
|
546
|
-
|
|
547
|
-
accessibilityRole="button"
|
|
548
|
-
accessibilityState={{ disabled: true }}
|
|
549
|
-
style={[styles.primaryButton, styles.disabledButton]}
|
|
550
|
-
>
|
|
551
|
-
<Text style={[styles.primaryButtonText, styles.disabledButtonText]}>Update now</Text>
|
|
552
|
-
</Pressable>
|
|
553
|
-
<Pressable onPress={fetchPlan} accessibilityRole="button" style={styles.secondaryButton}>
|
|
554
|
-
<Text style={styles.secondaryButtonText}>Check again</Text>
|
|
555
|
-
</Pressable>
|
|
544
|
+
<CatalogButton label="Update now" disabled />
|
|
545
|
+
<CatalogButton label="Check again" variant="secondary" onPress={fetchPlan} />
|
|
556
546
|
</View>
|
|
557
547
|
</>
|
|
558
548
|
);
|
|
@@ -570,9 +560,7 @@ export function UpdatePanel({
|
|
|
570
560
|
{' You may be offline, or npm didn’t respond. Nothing was changed.'}
|
|
571
561
|
</NoteBox>
|
|
572
562
|
<View style={styles.actions}>
|
|
573
|
-
<
|
|
574
|
-
<Text style={styles.secondaryButtonText}>Try again</Text>
|
|
575
|
-
</Pressable>
|
|
563
|
+
<CatalogButton label="Try again" variant="secondary" onPress={fetchPlan} />
|
|
576
564
|
<Text style={styles.muted}>or run <Code>npx ds-viewer update</Code></Text>
|
|
577
565
|
</View>
|
|
578
566
|
</>
|
|
@@ -663,8 +651,8 @@ const styles = StyleSheet.create({
|
|
|
663
651
|
paddingVertical: 2,
|
|
664
652
|
},
|
|
665
653
|
versionsRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
|
666
|
-
versionsText: { fontSize:
|
|
667
|
-
arrow: { fontSize:
|
|
654
|
+
versionsText: { fontSize: CATALOG_TYPE['2xl'], fontWeight: '800', color: CATALOG_COLOR.text },
|
|
655
|
+
arrow: { fontSize: CATALOG_TYPE['2xl'], color: CATALOG_COLOR.textMuted },
|
|
668
656
|
pill: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10, backgroundColor: CATALOG_COLOR.accentSubtle },
|
|
669
657
|
pillMajor: { backgroundColor: CATALOG_COLOR.warningSubtle },
|
|
670
658
|
pillSuccess: { backgroundColor: CATALOG_COLOR.successSubtle },
|
|
@@ -681,9 +669,6 @@ const styles = StyleSheet.create({
|
|
|
681
669
|
switchLabel: { fontSize: CATALOG_TYPE.md, fontWeight: '700', color: CATALOG_COLOR.text },
|
|
682
670
|
switch: { width: 44, height: 26 },
|
|
683
671
|
switchHelp: { fontSize: CATALOG_TYPE.sm, color: CATALOG_COLOR.textMuted },
|
|
684
|
-
buttonRow: { flexDirection: 'row', gap: CATALOG_SPACE.sm },
|
|
685
|
-
busyButton: { borderColor: CATALOG_COLOR.border },
|
|
686
|
-
busyButtonText: { color: CATALOG_COLOR.textMuted },
|
|
687
672
|
muted: { fontSize: CATALOG_TYPE.sm, color: CATALOG_COLOR.textMuted, marginTop: 4 },
|
|
688
673
|
block: { marginTop: 18 },
|
|
689
674
|
h3: { fontSize: CATALOG_TYPE.panelHeading, textTransform: 'uppercase', letterSpacing: 0.5, color: CATALOG_COLOR.text, marginBottom: 8 },
|
|
@@ -695,12 +680,6 @@ const styles = StyleSheet.create({
|
|
|
695
680
|
fileReason: { fontSize: CATALOG_TYPE.sm, color: CATALOG_COLOR.textMuted },
|
|
696
681
|
fileReasonDirty: { color: CATALOG_COLOR.warning, fontWeight: '700' },
|
|
697
682
|
actions: { flexDirection: 'row', alignItems: 'center', gap: CATALOG_SPACE.md, marginTop: 20 },
|
|
698
|
-
primaryButton: { height: CATALOG_LAYOUT.controlSize, paddingHorizontal: 18, borderRadius: 22, backgroundColor: CATALOG_COLOR.text, alignItems: 'center', justifyContent: 'center' },
|
|
699
|
-
primaryButtonText: { color: '#fff', fontWeight: '700', fontSize: CATALOG_TYPE.md },
|
|
700
|
-
secondaryButton: { height: CATALOG_LAYOUT.controlSize, paddingHorizontal: 18, borderRadius: 22, borderWidth: 1, borderColor: CATALOG_COLOR.borderStrong, alignItems: 'center', justifyContent: 'center' },
|
|
701
|
-
secondaryButtonText: { fontWeight: '700', fontSize: CATALOG_TYPE.md, color: CATALOG_COLOR.text },
|
|
702
|
-
disabledButton: { backgroundColor: '#e6e6e6', borderColor: '#e6e6e6' },
|
|
703
|
-
disabledButtonText: { color: '#8a8a8a' },
|
|
704
683
|
noteBox: { borderRadius: CATALOG_RADIUS.md, padding: 12, marginTop: 18 },
|
|
705
684
|
noteBoxText: { fontSize: CATALOG_TYPE.md },
|
|
706
685
|
noteBoxBold: { fontWeight: '700' },
|
|
@@ -714,10 +693,10 @@ const styles = StyleSheet.create({
|
|
|
714
693
|
stepIconNow: { borderWidth: 2, borderColor: CATALOG_COLOR.accent },
|
|
715
694
|
stepIconTodo: { borderWidth: 2, borderColor: CATALOG_COLOR.borderStrong },
|
|
716
695
|
stepIconFail: { backgroundColor: CATALOG_COLOR.dangerSubtle },
|
|
717
|
-
stepIconGlyph: { fontSize:
|
|
696
|
+
stepIconGlyph: { fontSize: CATALOG_TYPE.sm, fontWeight: '800', color: CATALOG_COLOR.text },
|
|
718
697
|
stepLabel: { fontSize: CATALOG_TYPE.md, color: CATALOG_COLOR.textMuted },
|
|
719
698
|
stepLabelNow: { fontSize: CATALOG_TYPE.md, color: CATALOG_COLOR.text, fontWeight: '700' },
|
|
720
699
|
stepLabelFail: { fontSize: CATALOG_TYPE.md, color: CATALOG_COLOR.text, fontWeight: '700' },
|
|
721
700
|
logBox: { backgroundColor: '#1d1d1f', borderRadius: CATALOG_RADIUS.md, padding: 12, marginTop: 8, maxHeight: 240 },
|
|
722
|
-
logText: { color: '#e8e8e8', fontSize:
|
|
701
|
+
logText: { color: '#e8e8e8', fontSize: CATALOG_TYPE.sm, fontFamily: 'Menlo, monospace' },
|
|
723
702
|
});
|