@krapwoo/ds-viewer 0.2.0 → 0.4.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/README.md +151 -0
- package/config/index.ts +4 -2
- package/dist/cli/agentsFile.js +31 -0
- package/dist/cli/dev.js +312 -15
- package/dist/cli/doctor.js +502 -0
- package/dist/cli/endpoint.js +142 -0
- package/dist/cli/explain.js +151 -0
- package/dist/cli/githubAction.js +65 -0
- package/dist/cli/init.js +9 -5
- package/dist/cli/kitDiff.js +132 -0
- package/dist/cli/kitRoot.js +20 -0
- package/dist/cli/main.js +123 -15
- package/dist/cli/migrate.js +30 -0
- package/dist/cli/migrateEntry.js +19 -0
- package/dist/cli/migrations/index.js +14 -0
- package/dist/cli/migrations/types.js +9 -0
- package/dist/cli/packageManager.js +29 -0
- package/dist/cli/packageVersion.js +24 -0
- package/dist/cli/pageIndex.js +27 -1
- package/dist/cli/props.js +4 -3
- package/dist/cli/semver.js +26 -0
- package/dist/cli/staticPage.js +356 -0
- package/dist/cli/sync.js +11 -4
- package/dist/cli/update.js +93 -0
- package/dist/cli/updateCheck.js +143 -0
- package/dist/cli/updatePlan.js +202 -0
- package/dist/cli/workspace.js +10 -5
- package/dist/native/catalog/catalogNavigation.js +129 -0
- package/dist/native/catalog/comparison.js +283 -0
- package/dist/native/catalog/tokens.js +134 -0
- package/dist/native/catalog/types.js +1 -0
- package/native/catalog/CatalogShell.tsx +105 -11
- package/native/catalog/CatalogSidebar.tsx +58 -2
- package/native/catalog/ComparisonGrid.tsx +9 -7
- package/native/catalog/SectionBlock.tsx +5 -0
- package/native/catalog/SpacingScaleGallery.tsx +1 -1
- package/native/catalog/UpdatePanel.tsx +556 -0
- package/native/catalog/catalogNavigation.ts +70 -1
- package/native/catalog/comparison.ts +63 -5
- package/native/catalog/index.ts +5 -1
- package/native/catalog/pageApi.ts +11 -2
- package/native/catalog/tokens.ts +15 -0
- package/native/catalog/types.ts +19 -5
- package/native/catalog/updatePanelState.ts +74 -0
- package/package.json +5 -1
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +4 -22
- package/starter-kit/components/Badge/Badge.catalog.tsx +15 -23
- package/starter-kit/components/Banner/Banner.tsx +1 -1
- package/starter-kit/components/BottomSheet/BottomSheet.tsx +2 -2
- package/starter-kit/components/Button/Button.catalog.tsx +15 -29
- package/starter-kit/components/Dialog/Dialog.tsx +2 -2
- package/starter-kit/components/Pill/Pill.catalog.tsx +12 -26
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* It also decides whether two blocks sit side by side (choosePlacement) and how many columns the
|
|
9
9
|
* Props box uses (propsColumns). ComparisonGrid, ComparisonGroups, and ComparisonList render it.
|
|
10
10
|
*/
|
|
11
|
-
import type { ComparisonDef, PreviewWidths, SectionDef, SpecimenSize, VariantSlot } from './types';
|
|
11
|
+
import type { ComparisonAxisItem, ComparisonDef, GridAxis, PreviewWidths, SectionDef, SpecimenSize, VariantSlot } from './types.ts';
|
|
12
|
+
import type React from 'react';
|
|
12
13
|
|
|
13
14
|
/** Approved geometry. `cellPadding` equals CATALOG_SPACE.lg (asserted in tests). The laptop
|
|
14
15
|
* content width is a 1280px viewport minus the 264px sidebar and 32px side padding. */
|
|
@@ -35,6 +36,61 @@ export const PROPS_COLUMN_GAP = 32;
|
|
|
35
36
|
/** Smallest column or cell width per specimen size. Wide specimens are fixed at phone width. */
|
|
36
37
|
export const COLUMN_MIN_WIDTH: Record<SpecimenSize, number> = { compact: 160, regular: 240, wide: 402 };
|
|
37
38
|
|
|
39
|
+
/** `Array.isArray`'s built-in type guard narrows to a mutable `any[]`, which a `readonly T[]`
|
|
40
|
+
* union member is never assignable to — so the `else` branch of `Array.isArray(axis) ? ... :
|
|
41
|
+
* ...` keeps `axis`'s full union type instead of narrowing away the array member. This explicit
|
|
42
|
+
* predicate sidesteps that (never caught before Task 4: this file had never been typechecked by
|
|
43
|
+
* `tsc` until `cli/doctor.ts`'s import pulled it into the root `tsconfig.json`'s program). */
|
|
44
|
+
function isBoundAxis(axis: GridAxis): axis is { prop: string; items: readonly ComparisonAxisItem[] } {
|
|
45
|
+
return !Array.isArray(axis);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A `GridAxis`'s own items, whether it's a plain array or bound to a prop — every reader of
|
|
49
|
+
* `ComparisonDef.rows`/`columns` goes through this (never `Array.isArray` directly), so a future
|
|
50
|
+
* third axis shape only needs to change this one function. */
|
|
51
|
+
export function axisItems(axis: GridAxis): ComparisonAxisItem[] {
|
|
52
|
+
return isBoundAxis(axis) ? [...axis.items] : [...axis];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The prop name a `GridAxis` is bound to, or undefined for a plain, unbound axis. */
|
|
56
|
+
export function axisProp(axis: GridAxis): string | undefined {
|
|
57
|
+
return isBoundAxis(axis) ? axis.prop : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Shared page-authoring helper (design §1 "Page shape", §4 "Binding examples to props"). Every
|
|
62
|
+
* grid comparison in the starter kit builds its `ComparisonDef` with this instead of its own local
|
|
63
|
+
* copy, so `cli/staticPage.ts`'s static reader has exactly one call shape to recognize anywhere a
|
|
64
|
+
* page's `comparison` field is produced by a function call rather than written as a plain object
|
|
65
|
+
* literal. `rows`/`columns` accept either a plain item array (unbound) or `{ prop, items }` (bound)
|
|
66
|
+
* — the cell grid itself is unaffected either way, since cells are always keyed by each axis's own
|
|
67
|
+
* items regardless of whether that axis names a prop. Lives here, beside `axisItems`/`axisProp`,
|
|
68
|
+
* rather than in its own `grid.ts` — every other file in this module has only *type-only* relative
|
|
69
|
+
* imports; a bare, no-extension value import would be the first of its kind in `native/catalog`.
|
|
70
|
+
*
|
|
71
|
+
* Each cell is a real instance with exactly the props its row and column name. Never build these by
|
|
72
|
+
* multiplying `variants` with `states` — those are pre-rendered nodes and cannot be combined.
|
|
73
|
+
*/
|
|
74
|
+
export function grid(
|
|
75
|
+
rowLabel: string,
|
|
76
|
+
columnLabel: string,
|
|
77
|
+
rows: GridAxis,
|
|
78
|
+
columns: GridAxis,
|
|
79
|
+
cell: (row: string, column: string) => React.ReactNode,
|
|
80
|
+
size?: SpecimenSize,
|
|
81
|
+
): ComparisonDef {
|
|
82
|
+
const rowItems = axisItems(rows);
|
|
83
|
+
const columnItems = axisItems(columns);
|
|
84
|
+
return {
|
|
85
|
+
rowLabel,
|
|
86
|
+
columnLabel,
|
|
87
|
+
rows,
|
|
88
|
+
columns,
|
|
89
|
+
cells: rowItems.flatMap((row) => columnItems.map((column) => ({ rowKey: row.key, columnKey: column.key, node: cell(row.key, column.key) }))),
|
|
90
|
+
size,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
38
94
|
/** How many grid columns fit beside the row header on a 1280px laptop without scrolling. */
|
|
39
95
|
export function gridColumnLimit(size: SpecimenSize): number {
|
|
40
96
|
return Math.floor((MATRIX_LAYOUT.laptopContentWidth - MATRIX_LAYOUT.rowHeaderWidth) / COLUMN_MIN_WIDTH[size]);
|
|
@@ -94,17 +150,19 @@ export function indexCells(def: ComparisonDef): Map<string, ComparisonDef['cells
|
|
|
94
150
|
/** Human-readable problems in a stable order: axis duplicates, column fit, each cell in
|
|
95
151
|
* declaration order, then missing coordinates in row-major order. Empty when valid. */
|
|
96
152
|
export function validateComparison(def: ComparisonDef): string[] {
|
|
97
|
-
|
|
153
|
+
const rowItems = axisItems(def.rows);
|
|
154
|
+
const columnItems = axisItems(def.columns);
|
|
155
|
+
if (rowItems.length === 0 || columnItems.length === 0) {
|
|
98
156
|
return ['Comparison needs at least one row and one column.'];
|
|
99
157
|
}
|
|
100
158
|
const issues: string[] = [];
|
|
101
159
|
const rowKeys = new Set<string>();
|
|
102
|
-
for (const row of
|
|
160
|
+
for (const row of rowItems) {
|
|
103
161
|
if (rowKeys.has(row.key)) issues.push(`Duplicate row key "${row.key}".`);
|
|
104
162
|
rowKeys.add(row.key);
|
|
105
163
|
}
|
|
106
164
|
const columnKeys = new Set<string>();
|
|
107
|
-
for (const column of
|
|
165
|
+
for (const column of columnItems) {
|
|
108
166
|
if (columnKeys.has(column.key)) issues.push(`Duplicate column key "${column.key}".`);
|
|
109
167
|
columnKeys.add(column.key);
|
|
110
168
|
}
|
|
@@ -149,7 +207,7 @@ export function validateComparison(def: ComparisonDef): string[] {
|
|
|
149
207
|
export function remainingStates(states: VariantSlot | undefined, comparison: ComparisonDef | undefined): VariantSlot | undefined {
|
|
150
208
|
if (!states) return undefined;
|
|
151
209
|
if (!comparison) return states;
|
|
152
|
-
const covered = new Set([...comparison.rows.map((row) => row.key), ...comparison.columns.map((column) => column.key)]);
|
|
210
|
+
const covered = new Set([...axisItems(comparison.rows).map((row) => row.key), ...axisItems(comparison.columns).map((column) => column.key)]);
|
|
153
211
|
const items = states.items.filter((item) => !covered.has(item.key));
|
|
154
212
|
return items.length > 0 ? { ...states, items } : undefined;
|
|
155
213
|
}
|
package/native/catalog/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { CatalogSidebar } from './CatalogSidebar';
|
|
|
17
17
|
export { CatalogSearchInput } from './CatalogSearchInput';
|
|
18
18
|
export { SectionBlock } from './SectionBlock';
|
|
19
19
|
export { ComparisonGrid } from './ComparisonGrid';
|
|
20
|
+
export { grid, axisItems, axisProp } from './comparison';
|
|
20
21
|
export { ComparisonGroups } from './ComparisonGroups';
|
|
21
22
|
export { ComparisonList } from './ComparisonList';
|
|
22
23
|
export { ReferenceDetails } from './ReferenceDetails';
|
|
@@ -32,7 +33,10 @@ export { buildComponentManifest } from './manifest';
|
|
|
32
33
|
export type { ComponentManifestEntry, ManifestExample } from './manifest';
|
|
33
34
|
export { defineCatalogPage, buildCatalogSections } from './pageApi';
|
|
34
35
|
export type { CatalogPage, CatalogPageInput, GeneratedComponent, GeneratedPropRecord } from './pageApi';
|
|
35
|
-
export type { PropDef, SectionDef, NavGroup, SpecimenSize, ComparisonDef, ComparisonCell, ComparisonAxisItem, PreviewWidths } from './types';
|
|
36
|
+
export type { PropDef, SectionDef, NavGroup, SpecimenSize, ComparisonDef, ComparisonCell, ComparisonAxisItem, GridAxis, PreviewWidths } from './types';
|
|
37
|
+
export { UpdatePanel } from './UpdatePanel';
|
|
38
|
+
export { UPDATE_PAGE_ID } from './catalogNavigation';
|
|
39
|
+
export type { UpdateNotice } from './types';
|
|
36
40
|
export type { ListGroup, ListItem } from './comparison';
|
|
37
41
|
export {
|
|
38
42
|
CATALOG_TYPE,
|
|
@@ -77,7 +77,7 @@ function mergePropNotes(component: GeneratedComponent, propNotes: Record<string,
|
|
|
77
77
|
return [...own, ...inherited];
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function orderGroups(groupIds: Map<string,
|
|
80
|
+
function orderGroups<TId extends string>(groupIds: Map<string, TId[]>, groupOrder: string[] | undefined): NavGroup<TId>[] {
|
|
81
81
|
const listed = groupOrder ?? [];
|
|
82
82
|
const labels = [...groupIds.keys()];
|
|
83
83
|
const ordered = [...listed.filter((label) => groupIds.has(label)), ...labels.filter((label) => !listed.includes(label)).sort()];
|
|
@@ -103,7 +103,16 @@ export function buildCatalogSections<TId extends string>(
|
|
|
103
103
|
const groupIds = new Map<string, TId[]>();
|
|
104
104
|
const addToGroup = (group: string, id: TId) => groupIds.set(group, [...(groupIds.get(group) ?? []), id]);
|
|
105
105
|
|
|
106
|
+
// Design §4 "duplicate-page-id" rule's viewer-side equivalent — this is the one place in the
|
|
107
|
+
// whole viewer that already resolves every page's id to build sections/groups, so it costs
|
|
108
|
+
// nothing extra to also notice two pages claiming the same one (`sectionsById` in
|
|
109
|
+
// `CatalogShell.tsx` would otherwise just let the last one silently win).
|
|
110
|
+
const seenIds = new Set<TId>();
|
|
106
111
|
const sections: SectionDef<TId>[] = pages.map((page) => {
|
|
112
|
+
if ((globalThis as { __DEV__?: boolean }).__DEV__) {
|
|
113
|
+
if (seenIds.has(page.id)) console.warn(`[Catalog] Page id "${page.id}" is used by more than one page; only the last one will show. Give one of them its own "id".`);
|
|
114
|
+
seenIds.add(page.id);
|
|
115
|
+
}
|
|
107
116
|
const componentName = page.component ?? page.id;
|
|
108
117
|
const component = componentByName.get(componentName);
|
|
109
118
|
addToGroup(page.group, page.id);
|
|
@@ -136,5 +145,5 @@ export function buildCatalogSections<TId extends string>(
|
|
|
136
145
|
addToGroup('Components', id);
|
|
137
146
|
}
|
|
138
147
|
|
|
139
|
-
return { sections, groups: orderGroups(groupIds, groupOrder)
|
|
148
|
+
return { sections, groups: orderGroups(groupIds, groupOrder) };
|
|
140
149
|
}
|
package/native/catalog/tokens.ts
CHANGED
|
@@ -63,6 +63,10 @@ export const CATALOG_LAYOUT = {
|
|
|
63
63
|
logoMark: 32,
|
|
64
64
|
logoWordmarkHeight: 28,
|
|
65
65
|
logoCaptionGap: 6,
|
|
66
|
+
/** Max width of the update page's content column (approved mockup: direction "C · Update
|
|
67
|
+
* page", content at most 512px) — independent of `CATALOG_MAX_CONTENT_WIDTH` (1200px), which
|
|
68
|
+
* is every other page's column width. */
|
|
69
|
+
updateContentMaxWidth: 512,
|
|
66
70
|
} as const;
|
|
67
71
|
|
|
68
72
|
/** Max width of the main content column. */
|
|
@@ -124,4 +128,15 @@ export const CATALOG_COLOR = {
|
|
|
124
128
|
/** 3px keyboard focus ring on every catalog control. */
|
|
125
129
|
focusRing: '#c9d7ff',
|
|
126
130
|
code: 'Menlo',
|
|
131
|
+
/** Major-update banner background/border/text (design §5 "Viewer notice"). */
|
|
132
|
+
warning: '#8a4b00',
|
|
133
|
+
warningSubtle: '#fff4e0',
|
|
134
|
+
warningBorder: '#f0cf97',
|
|
135
|
+
/** "Can't prepare"/failure note-boxes on the update page. */
|
|
136
|
+
danger: '#b3261e',
|
|
137
|
+
dangerSubtle: '#fdecea',
|
|
138
|
+
dangerBorder: '#f2b8b5',
|
|
139
|
+
/** The update page's post-update success note-box and "Updated" pill. */
|
|
140
|
+
success: '#1e6b3a',
|
|
141
|
+
successSubtle: '#e8f5ec',
|
|
127
142
|
} as const;
|
package/native/catalog/types.ts
CHANGED
|
@@ -65,6 +65,13 @@ export interface ComparisonAxisItem {
|
|
|
65
65
|
label: string;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** A grid axis's rows or columns: a plain list of items (today's shape, unchanged), or items bound
|
|
69
|
+
* to a real prop so `doctor` can check each item's key against that prop's actual options (design
|
|
70
|
+
* §4 "Binding examples to props"). Only a prop typed as a string-literal union can be bound —
|
|
71
|
+
* binding any other prop is a `doctor` error, never a runtime check here (the viewer renders a
|
|
72
|
+
* bound and an unbound axis identically; see `native/catalog/comparison.ts`'s `axisItems`). */
|
|
73
|
+
export type GridAxis = readonly ComparisonAxisItem[] | { prop: string; items: readonly ComparisonAxisItem[] };
|
|
74
|
+
|
|
68
75
|
/** One authored row × column specimen. Provide exactly one of `node` (a real instance of the
|
|
69
76
|
* documented component) or `unavailableReason` (the combination genuinely does not exist). */
|
|
70
77
|
export interface ComparisonCell {
|
|
@@ -83,8 +90,8 @@ export interface ComparisonDef {
|
|
|
83
90
|
rowLabel: string;
|
|
84
91
|
/** Names the column axis (e.g. "State"). */
|
|
85
92
|
columnLabel: string;
|
|
86
|
-
rows:
|
|
87
|
-
columns:
|
|
93
|
+
rows: GridAxis;
|
|
94
|
+
columns: GridAxis;
|
|
88
95
|
cells: ComparisonCell[];
|
|
89
96
|
/** Defaults to the section's `specimenSize`, then 'regular'. */
|
|
90
97
|
size?: SpecimenSize;
|
|
@@ -189,6 +196,13 @@ export interface NavGroup<TId extends string = string> {
|
|
|
189
196
|
ids: readonly TId[];
|
|
190
197
|
}
|
|
191
198
|
|
|
192
|
-
/**
|
|
193
|
-
*
|
|
194
|
-
|
|
199
|
+
/** Design §5 "Update check"/"Viewer notice" — the generated entry file (Task 17) imports this
|
|
200
|
+
* shape directly from `.ds-viewer/update.json`; `null` when no update is known (disabled,
|
|
201
|
+
* offline, or genuinely up to date — the viewer treats all three identically). */
|
|
202
|
+
export interface UpdateNotice {
|
|
203
|
+
current: string;
|
|
204
|
+
latest: string;
|
|
205
|
+
breaking: boolean;
|
|
206
|
+
summary: string[];
|
|
207
|
+
releasedAt?: string;
|
|
208
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** The same shape as `cli/updatePlan.ts`'s `UpdatePlan` — duplicated, not imported: `native/catalog/`
|
|
2
|
+
* is the published package root and never imports from `cli/` (the reverse is normal: `cli/`
|
|
3
|
+
* already imports `native/catalog/` throughout). The endpoint's `POST /plan` response is this
|
|
4
|
+
* exact shape (confirmed by `cli/endpoint.ts`, Task 12, which forwards `cli/updatePlan.ts`'s own
|
|
5
|
+
* `UpdatePlan` verbatim as JSON). */
|
|
6
|
+
export interface UpdatePlanView {
|
|
7
|
+
current: string;
|
|
8
|
+
latest: string;
|
|
9
|
+
breaking: boolean;
|
|
10
|
+
summary: string[];
|
|
11
|
+
releasedAt?: string;
|
|
12
|
+
files: { path: string; reason: string; dirty: boolean }[];
|
|
13
|
+
dirtyFiles: string[];
|
|
14
|
+
kitFilesDiffering: number;
|
|
15
|
+
outsideGitRepo: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The same shape as `cli/endpoint.ts`'s `UpdateStatus`, same reason as above. `updating`'s each
|
|
19
|
+
* step carries a three-way `state`, not a boolean `done` (Important finding, Fable correction
|
|
20
|
+
* pass: the approved mockup's own `updating` state shows one step mid-flight — `ic now`, a
|
|
21
|
+
* distinct icon from both `done` and the not-yet-reached `todo` — which a boolean cannot
|
|
22
|
+
* represent). `success` also carries the changed files' paths and the version just installed
|
|
23
|
+
* (Errata 1b: `update.json` becomes `null` once latest equals current, so `success` must carry
|
|
24
|
+
* its own `latest` rather than relying on the now-gone `update` prop) — Task 16's `ready` phase
|
|
25
|
+
* has `plan.files`/`plan.latest` to show the same values, but `plan` is gone by the time
|
|
26
|
+
* `success` renders (the page has reloaded onto the new version), so the result itself must carry
|
|
27
|
+
* them. */
|
|
28
|
+
export type UpdateStatusView =
|
|
29
|
+
| { phase: 'idle' }
|
|
30
|
+
| { phase: 'updating'; steps: { label: string; state: 'done' | 'now' | 'todo' }[] }
|
|
31
|
+
| { phase: 'restarting' }
|
|
32
|
+
| { phase: 'success'; doctorSummary: string; files: string[]; latest: string }
|
|
33
|
+
| { phase: 'failure'; log: string; failedStep: string };
|
|
34
|
+
|
|
35
|
+
export type UpdatePhase = 'checking' | 'ready' | 'dirty' | 'offline' | 'updating' | 'restarting' | 'success' | 'failure';
|
|
36
|
+
|
|
37
|
+
/** Minor finding, Fable correction pass: every `{ error }` previously mapped to the same
|
|
38
|
+
* `'offline'` phase regardless of *which* error — including `buildUpdatePlan`'s own "Already on
|
|
39
|
+
* the latest version" message, which is not an offline/failure condition at all. `UpdatePanel`
|
|
40
|
+
* (Task 16) still only needs one phase to key its `switch` on; it reads `result.error` itself,
|
|
41
|
+
* directly, to pick between the two copies the mockup's `offline` state and a plain "you're
|
|
42
|
+
* already up to date" note both need. */
|
|
43
|
+
export function phaseFromPlanResult(result: UpdatePlanView | { error: string }): 'ready' | 'dirty' | 'offline' {
|
|
44
|
+
if ('error' in result) return 'offline';
|
|
45
|
+
return result.dirtyFiles.length > 0 ? 'dirty' : 'ready';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `true` only for `buildUpdatePlan`'s own "Already on the latest version (…)." message — never a
|
|
49
|
+
* real failure, so `UpdatePanel`'s `offline` phase shows a plain up-to-date note instead of the
|
|
50
|
+
* "Couldn't prepare the update" error copy for this one case. */
|
|
51
|
+
export function isAlreadyUpToDateError(result: UpdatePlanView | { error: string }): boolean {
|
|
52
|
+
return 'error' in result && result.error.startsWith('Already on the latest version');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The one function `UpdatePanel.tsx` calls to decide which of the mockup's 10 states to render.
|
|
56
|
+
* `status` (from the endpoint's own `/update/status`, authoritative once an update has started)
|
|
57
|
+
* always wins over `planResult` — a plan fetched before an update began is stale the moment one
|
|
58
|
+
* starts, and the mockup itself never shows plan content and progress content at once. While
|
|
59
|
+
* `status.phase` is `'idle'` and no plan has resolved yet (`planResult === undefined`), the panel
|
|
60
|
+
* is still in the mockup's own `'checking'` state. */
|
|
61
|
+
export function resolvePanelPhase(status: UpdateStatusView, planResult: UpdatePlanView | { error: string } | undefined): UpdatePhase {
|
|
62
|
+
if (status.phase !== 'idle') return status.phase;
|
|
63
|
+
if (planResult === undefined) return 'checking';
|
|
64
|
+
return phaseFromPlanResult(planResult);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The panel's `"Released N days ago."` line — floors to whole days, never "23 hours ago" (the
|
|
68
|
+
* mockup's own example, "Released 2 days ago.", is always a whole number of days). */
|
|
69
|
+
export function daysAgoLabel(releasedAt: string, nowMs: number): string {
|
|
70
|
+
const days = Math.floor((nowMs - Date.parse(releasedAt)) / (24 * 60 * 60 * 1000));
|
|
71
|
+
if (days <= 0) return 'Released today.';
|
|
72
|
+
if (days === 1) return 'Released 1 day ago.';
|
|
73
|
+
return `Released ${days} days ago.`;
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krapwoo/ds-viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A browsable, always-current catalog for an Expo / React Native design system.",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"starter-kit",
|
|
30
30
|
"dist/cli",
|
|
31
31
|
"dist/config",
|
|
32
|
+
"dist/native",
|
|
32
33
|
"config/index.ts",
|
|
33
34
|
"README.md",
|
|
34
35
|
"LICENSE"
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@types/node": "^22.0.0",
|
|
55
|
+
"@types/react": "^19.0.0",
|
|
54
56
|
"puppeteer": "^25.12.0"
|
|
55
57
|
},
|
|
56
58
|
"scripts": {
|
|
@@ -59,6 +61,8 @@
|
|
|
59
61
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
60
62
|
"kit:dev": "npm run build && cd kit-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js dev",
|
|
61
63
|
"check:catalog": "npm run build && node scripts/checkCatalogConsole.mjs kit-host",
|
|
64
|
+
"check:doctor": "npm run build && cd kit-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js doctor --ci",
|
|
65
|
+
"check:types": "tsc --noEmit -p kit-host",
|
|
62
66
|
"release": "node scripts/release.mjs"
|
|
63
67
|
}
|
|
64
68
|
}
|
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { defineCatalogPage,
|
|
2
|
+
import { defineCatalogPage, grid } from '@krapwoo/ds-viewer';
|
|
3
3
|
import { Avatar } from './Avatar';
|
|
4
4
|
import { DS_SEMANTIC } from '../../tokens';
|
|
5
5
|
|
|
6
|
-
// Grid comparisons: each cell is a real instance with exactly the props its row and column name.
|
|
7
|
-
// Never build these by multiplying `variants` with `states` — those are pre-rendered nodes and
|
|
8
|
-
// cannot combine.
|
|
9
|
-
function grid(
|
|
10
|
-
rowLabel: string,
|
|
11
|
-
columnLabel: string,
|
|
12
|
-
rows: { key: string; label: string }[],
|
|
13
|
-
columns: { key: string; label: string }[],
|
|
14
|
-
cell: (row: string, column: string) => React.ReactNode,
|
|
15
|
-
size?: ComparisonDef['size'],
|
|
16
|
-
): ComparisonDef {
|
|
17
|
-
return {
|
|
18
|
-
rowLabel,
|
|
19
|
-
columnLabel,
|
|
20
|
-
rows,
|
|
21
|
-
columns,
|
|
22
|
-
cells: rows.flatMap((row) => columns.map((column) => ({ rowKey: row.key, columnKey: column.key, node: cell(row.key, column.key) }))),
|
|
23
|
-
size,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
6
|
// Avatar: content Kind × Size (Avatar's `size` is continuous; 24 / 40 default / 64 is the sweep).
|
|
7
|
+
// Neither axis names a real prop — "Kind" is which of 3 optional props is set, not one enum, and
|
|
8
|
+
// `size` has no fixed option set — so both stay unbound. An unbound axis is fully valid; `doctor`
|
|
9
|
+
// only ever reports how many examples are unbound as information, never as an error or a warning.
|
|
28
10
|
const AVATAR_COMPARISON = grid(
|
|
29
11
|
'Kind',
|
|
30
12
|
'Size',
|
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { defineCatalogPage,
|
|
2
|
+
import { defineCatalogPage, grid } from '@krapwoo/ds-viewer';
|
|
3
3
|
import { Badge } from './Badge';
|
|
4
4
|
import type { BadgeVariant } from './Badge.types';
|
|
5
5
|
|
|
6
|
-
// Grid comparisons: each cell is a real instance with exactly the props its row and column name.
|
|
7
|
-
// Never build these by multiplying `variants` with `states` — those are pre-rendered nodes and
|
|
8
|
-
// cannot combine.
|
|
9
|
-
function grid(
|
|
10
|
-
rowLabel: string,
|
|
11
|
-
columnLabel: string,
|
|
12
|
-
rows: { key: string; label: string }[],
|
|
13
|
-
columns: { key: string; label: string }[],
|
|
14
|
-
cell: (row: string, column: string) => React.ReactNode,
|
|
15
|
-
size?: ComparisonDef['size'],
|
|
16
|
-
): ComparisonDef {
|
|
17
|
-
return {
|
|
18
|
-
rowLabel,
|
|
19
|
-
columnLabel,
|
|
20
|
-
rows,
|
|
21
|
-
columns,
|
|
22
|
-
cells: rows.flatMap((row) => columns.map((column) => ({ rowKey: row.key, columnKey: column.key, node: cell(row.key, column.key) }))),
|
|
23
|
-
size,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
6
|
// Badge: Tone × Icon layout. One icon per tone, matching the Variants examples.
|
|
28
7
|
const BADGE_TONES: { key: BadgeVariant; label: string; text: string; icon: React.ComponentProps<typeof Badge>['leadingIcon'] }[] = [
|
|
29
8
|
{ key: 'neutral', label: 'Neutral', text: 'Local', icon: 'pin' },
|
|
@@ -32,10 +11,23 @@ const BADGE_TONES: { key: BadgeVariant; label: string; text: string; icon: React
|
|
|
32
11
|
{ key: 'warning', label: 'Warning', text: 'Delayed', icon: 'triangle-alert' },
|
|
33
12
|
{ key: 'negative', label: 'Negative', text: 'Suspended', icon: 'circle-slash' },
|
|
34
13
|
];
|
|
14
|
+
// `rows` is bound to Badge's own `variant` prop (design §4) — written as its own literal array
|
|
15
|
+
// (not `BADGE_TONES.map(...)`) so `doctor`'s static reader can check it without evaluating a
|
|
16
|
+
// function call; `BADGE_TONES` above still drives the real rendering in `cell` below.
|
|
17
|
+
const BADGE_ROWS = {
|
|
18
|
+
prop: 'variant',
|
|
19
|
+
items: [
|
|
20
|
+
{ key: 'neutral', label: 'Neutral' },
|
|
21
|
+
{ key: 'info', label: 'Info' },
|
|
22
|
+
{ key: 'positive', label: 'Positive' },
|
|
23
|
+
{ key: 'warning', label: 'Warning' },
|
|
24
|
+
{ key: 'negative', label: 'Negative' },
|
|
25
|
+
],
|
|
26
|
+
} as const;
|
|
35
27
|
const BADGE_COMPARISON = grid(
|
|
36
28
|
'Tone',
|
|
37
29
|
'Icon',
|
|
38
|
-
|
|
30
|
+
BADGE_ROWS,
|
|
39
31
|
[
|
|
40
32
|
{ key: 'label-only', label: 'Label only' },
|
|
41
33
|
{ key: 'leading-icon', label: 'Leading icon' },
|
|
@@ -235,7 +235,7 @@ const styles = StyleSheet.create({
|
|
|
235
235
|
},
|
|
236
236
|
// Translucent pressed overlay — sits above the banner bg so the tinted bg shows through.
|
|
237
237
|
pressOverlay: {
|
|
238
|
-
...StyleSheet.
|
|
238
|
+
...StyleSheet.absoluteFill,
|
|
239
239
|
backgroundColor: DS_SEMANTIC.interaction.pressed,
|
|
240
240
|
borderRadius: DS_RADIUS.medium,
|
|
241
241
|
},
|
|
@@ -126,8 +126,8 @@ export function BottomSheet({ visible, onDismiss, header, children, footer, styl
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
const styles = StyleSheet.create({
|
|
129
|
-
overlay: { ...StyleSheet.
|
|
130
|
-
backdrop: { ...StyleSheet.
|
|
129
|
+
overlay: { ...StyleSheet.absoluteFill, justifyContent: 'flex-end' },
|
|
130
|
+
backdrop: { ...StyleSheet.absoluteFill, backgroundColor: DS_SEMANTIC.element.overlayBackdrop },
|
|
131
131
|
sheet: {
|
|
132
132
|
maxHeight: MAX_HEIGHT_PERCENT,
|
|
133
133
|
backgroundColor: DS_SEMANTIC.surface.white,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { View, StyleSheet } from 'react-native';
|
|
3
|
-
import { defineCatalogPage,
|
|
3
|
+
import { defineCatalogPage, grid } from '@krapwoo/ds-viewer';
|
|
4
4
|
import { Button } from './Button';
|
|
5
5
|
import type { ButtonVariant } from './Button.types';
|
|
6
6
|
import { DS_SEMANTIC } from '../../tokens';
|
|
@@ -16,38 +16,24 @@ const styles = StyleSheet.create({
|
|
|
16
16
|
},
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
// Grid comparisons: each cell is a real instance with exactly the props its row and column name.
|
|
20
|
-
// Never build these by multiplying `variants` with `states` — those are pre-rendered nodes and
|
|
21
|
-
// cannot combine.
|
|
22
|
-
function grid(
|
|
23
|
-
rowLabel: string,
|
|
24
|
-
columnLabel: string,
|
|
25
|
-
rows: { key: string; label: string }[],
|
|
26
|
-
columns: { key: string; label: string }[],
|
|
27
|
-
cell: (row: string, column: string) => React.ReactNode,
|
|
28
|
-
size?: ComparisonDef['size'],
|
|
29
|
-
): ComparisonDef {
|
|
30
|
-
return {
|
|
31
|
-
rowLabel,
|
|
32
|
-
columnLabel,
|
|
33
|
-
rows,
|
|
34
|
-
columns,
|
|
35
|
-
cells: rows.flatMap((row) => columns.map((column) => ({ rowKey: row.key, columnKey: column.key, node: cell(row.key, column.key) }))),
|
|
36
|
-
size,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
19
|
// Button: Variant × State. `white` keeps the dark backdrop its variant requires.
|
|
20
|
+
// `rows` is bound to Button's own `variant` prop (design §4): each row key below is an actual
|
|
21
|
+
// `ButtonVariant` option, so `doctor` can tell when one is renamed or removed. `columns` names no
|
|
22
|
+
// single prop (disabled/loading are two separate booleans, not one "State" prop) and stays unbound
|
|
23
|
+
// — unbound axes are just as valid, and `doctor` never flags one.
|
|
41
24
|
const BUTTON_COMPARISON = grid(
|
|
42
25
|
'Variant',
|
|
43
26
|
'State',
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
27
|
+
{
|
|
28
|
+
prop: 'variant',
|
|
29
|
+
items: [
|
|
30
|
+
{ key: 'primary', label: 'Primary' },
|
|
31
|
+
{ key: 'secondary', label: 'Secondary' },
|
|
32
|
+
{ key: 'tertiary', label: 'Tertiary' },
|
|
33
|
+
{ key: 'white', label: 'White' },
|
|
34
|
+
{ key: 'ghost', label: 'Ghost' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
51
37
|
[
|
|
52
38
|
{ key: 'default', label: 'Default' },
|
|
53
39
|
{ key: 'disabled', label: 'Disabled' },
|
|
@@ -65,12 +65,12 @@ export function Dialog({ visible, onDismiss, children, style }: DialogProps) {
|
|
|
65
65
|
|
|
66
66
|
const styles = StyleSheet.create({
|
|
67
67
|
overlay: {
|
|
68
|
-
...StyleSheet.
|
|
68
|
+
...StyleSheet.absoluteFill,
|
|
69
69
|
alignItems: 'center',
|
|
70
70
|
justifyContent: 'center',
|
|
71
71
|
padding: DS_SPACING[800],
|
|
72
72
|
},
|
|
73
|
-
backdrop: { ...StyleSheet.
|
|
73
|
+
backdrop: { ...StyleSheet.absoluteFill, backgroundColor: DS_SEMANTIC.element.overlayBackdrop },
|
|
74
74
|
card: {
|
|
75
75
|
width: '100%',
|
|
76
76
|
maxWidth: 400,
|
|
@@ -1,36 +1,22 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { defineCatalogPage,
|
|
2
|
+
import { defineCatalogPage, grid } from '@krapwoo/ds-viewer';
|
|
3
3
|
import { Pill } from './Pill';
|
|
4
4
|
|
|
5
|
-
// Grid comparisons: each cell is a real instance with exactly the props its row and column name.
|
|
6
|
-
// Never build these by multiplying `variants` with `states` — those are pre-rendered nodes and
|
|
7
|
-
// cannot combine.
|
|
8
|
-
function grid(
|
|
9
|
-
rowLabel: string,
|
|
10
|
-
columnLabel: string,
|
|
11
|
-
rows: { key: string; label: string }[],
|
|
12
|
-
columns: { key: string; label: string }[],
|
|
13
|
-
cell: (row: string, column: string) => React.ReactNode,
|
|
14
|
-
size?: ComparisonDef['size'],
|
|
15
|
-
): ComparisonDef {
|
|
16
|
-
return {
|
|
17
|
-
rowLabel,
|
|
18
|
-
columnLabel,
|
|
19
|
-
rows,
|
|
20
|
-
columns,
|
|
21
|
-
cells: rows.flatMap((row) => columns.map((column) => ({ rowKey: row.key, columnKey: column.key, node: cell(row.key, column.key) }))),
|
|
22
|
-
size,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
5
|
// Pill: Selection × State.
|
|
6
|
+
// `rows` is bound to Pill's own `variant` prop (design §4); the second item's key is
|
|
7
|
+
// `'not_selected'` (underscore), matching `PillVariant` exactly — not `'not-selected'`, which is
|
|
8
|
+
// only this page's own label-ish spelling and would fail `doctor`'s "item key must be a real
|
|
9
|
+
// option" check.
|
|
27
10
|
const PILL_COMPARISON = grid(
|
|
28
11
|
'Selection',
|
|
29
12
|
'State',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
13
|
+
{
|
|
14
|
+
prop: 'variant',
|
|
15
|
+
items: [
|
|
16
|
+
{ key: 'selected', label: 'Selected' },
|
|
17
|
+
{ key: 'not_selected', label: 'Not selected' },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
34
20
|
[
|
|
35
21
|
{ key: 'icon-text', label: 'Icon + Text' },
|
|
36
22
|
{ key: 'icon-only', label: 'Icon-only' },
|