@krapwoo/ds-viewer 0.4.5 → 0.4.7
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 +111 -18
- package/dist/cli/dev.js +31 -6
- package/dist/cli/doctor.js +137 -0
- package/dist/cli/endpoint.js +76 -1
- package/dist/cli/explain.js +44 -6
- package/dist/cli/main.js +4 -2
- package/dist/cli/props.js +80 -1
- package/dist/cli/staticPage.js +228 -6
- package/dist/cli/updateCheck.js +174 -43
- package/dist/cli/workspace.js +2 -1
- package/dist/native/catalog/catalogNavigation.js +11 -0
- package/dist/native/catalog/comparison.js +29 -4
- package/native/catalog/BoundedOverlayViewport.tsx +112 -0
- package/native/catalog/CatalogShell.tsx +69 -12
- package/native/catalog/CatalogSidebar.tsx +8 -7
- package/native/catalog/ComparisonGrid.tsx +2 -2
- package/native/catalog/ComparisonGroups.tsx +2 -2
- package/native/catalog/ComparisonList.tsx +2 -2
- package/native/catalog/PhoneFrame.tsx +6 -0
- package/native/catalog/ReferenceDetails.tsx +75 -20
- package/native/catalog/SectionBlock.tsx +66 -4
- package/native/catalog/SpecimenSurface.tsx +18 -10
- package/native/catalog/UpdatePanel.tsx +161 -5
- package/native/catalog/catalogNavigation.ts +14 -1
- package/native/catalog/comparison.ts +39 -5
- package/native/catalog/index.ts +4 -0
- package/native/catalog/overlayViewport.ts +156 -0
- package/native/catalog/pageApi.ts +6 -0
- package/native/catalog/specimenSurfaceStyle.ts +70 -1
- package/native/catalog/types.ts +62 -12
- package/native/catalog/updatePanelState.ts +72 -0
- package/package.json +1 -1
- package/starter-kit/components/Banner/Banner.catalog.tsx +3 -0
- package/starter-kit/components/Button/Button.catalog.tsx +3 -0
- package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +6 -0
- package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +5 -0
- package/starter-kit/components/InputField/InputField.catalog.tsx +3 -0
- package/starter-kit/components/List/List.catalog.tsx +3 -0
- package/starter-kit/components/Pill/Pill.catalog.tsx +3 -0
- package/starter-kit/components/PillRow/PillRow.catalog.tsx +3 -0
- package/starter-kit/components/SearchField/SearchField.catalog.tsx +4 -0
- package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +3 -0
- package/starter-kit/components/TextArea/TextArea.catalog.tsx +3 -0
- package/starter-kit/components/Toast/Toast.catalog.tsx +7 -0
|
@@ -31,8 +31,12 @@ export interface CatalogPageInput<TId extends string = string> {
|
|
|
31
31
|
specimenSize?: SpecimenSize;
|
|
32
32
|
specimenSurface?: SpecimenSurfaceKind;
|
|
33
33
|
previewWidths?: PreviewWidths;
|
|
34
|
+
/** See `SectionDef.previewLayout`. */
|
|
35
|
+
previewLayout?: 'frames' | 'table';
|
|
34
36
|
hide?: SectionDef['hide'];
|
|
35
37
|
composedOf?: ComposedOfEntry[];
|
|
38
|
+
/** See `SectionDef.intentionalStaticPreview`. */
|
|
39
|
+
intentionalStaticPreview?: { reason: string };
|
|
36
40
|
/** Export name this page documents; defaults to `id` for component pages. */
|
|
37
41
|
component?: string;
|
|
38
42
|
/** Sidebar group label. */
|
|
@@ -168,8 +172,10 @@ export function buildCatalogSections<TId extends string>(
|
|
|
168
172
|
specimenSize: page.specimenSize,
|
|
169
173
|
specimenSurface: page.specimenSurface,
|
|
170
174
|
previewWidths: page.previewWidths,
|
|
175
|
+
previewLayout: page.previewLayout,
|
|
171
176
|
hide: component ? page.hide : { ...page.hide, props: true },
|
|
172
177
|
composedOf: page.composedOf,
|
|
178
|
+
intentionalStaticPreview: page.intentionalStaticPreview,
|
|
173
179
|
};
|
|
174
180
|
});
|
|
175
181
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SpecimenSurfaceKind } from './types.ts';
|
|
1
|
+
import type { SpecimenAlign, SpecimenSurfaceKind } from './types.ts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The example wrapper's width policy inside a cell, kept free of any `react-native` import so
|
|
@@ -16,6 +16,75 @@ export const SPECIMEN_SURFACE_WIDTH_STYLE = {
|
|
|
16
16
|
alignSelf: 'stretch',
|
|
17
17
|
} as const;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Centers a non-`fill` specimen horizontally inside its (now full-width) wrapper — unchanged,
|
|
21
|
+
* bounded (non-`fill`) specimens stay centered by default. See `SpecimenSurface.tsx`'s
|
|
22
|
+
* `SpecimenContent`, the one place every presentation shape (list/grouped/grid) wraps a specimen
|
|
23
|
+
* before handing it to its cell.
|
|
24
|
+
*/
|
|
25
|
+
export const SPECIMEN_CONTENT_CENTER_STYLE = {
|
|
26
|
+
...SPECIMEN_SURFACE_WIDTH_STYLE,
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A `fill` specimen's content stretches instead of centering: `alignItems: 'center'` on this
|
|
32
|
+
* wrapper previously applied to `fill` specimens too, which collapses an auto-sized, already
|
|
33
|
+
* full-width child (e.g. NextTrainInfo's row) down to its own intrinsic content width instead of
|
|
34
|
+
* leaving it at the wrapper's full width — `alignItems: 'center'` resolves a child with no definite
|
|
35
|
+
* cross-axis size to its content size, the opposite of what `fill` promises. A `fill` specimen
|
|
36
|
+
* whose own size is capped below the wrapper's width (e.g. a phone-frame preview's `maxWidth`) is
|
|
37
|
+
* expected to center itself (`alignSelf: 'center'`, see `PhoneFrame.tsx`) rather than lean on this
|
|
38
|
+
* wrapper's `alignItems` — the same way `alignItems: 'stretch'` already behaves like `flex-start`,
|
|
39
|
+
* not an actual stretch, for any child with a definite cross-axis size (a set width/maxWidth) per
|
|
40
|
+
* the flexbox spec. An explicit `align` (see `specimenContentAlignItems`) still overrides this.
|
|
41
|
+
*/
|
|
42
|
+
export const SPECIMEN_CONTENT_FILL_STYLE = {
|
|
43
|
+
...SPECIMEN_SURFACE_WIDTH_STYLE,
|
|
44
|
+
alignItems: 'stretch',
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
const ALIGN_ITEMS_BY_SPECIMEN_ALIGN: Record<SpecimenAlign, 'flex-start' | 'center' | 'flex-end'> = {
|
|
48
|
+
start: 'flex-start',
|
|
49
|
+
center: 'center',
|
|
50
|
+
end: 'flex-end',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** The `SpecimenContent` wrapper's `alignItems`: an explicit `align` (`SpecimenPresentation.align`)
|
|
54
|
+
* always wins over `fill`. Omitted, `fill` decides the established default — `'stretch'` (preserve
|
|
55
|
+
* a full-width auto-sized child) when true, `'center'` (unchanged) otherwise. */
|
|
56
|
+
export function specimenContentAlignItems(fill: boolean | undefined, align: SpecimenAlign | undefined): 'flex-start' | 'center' | 'flex-end' | 'stretch' {
|
|
57
|
+
if (align !== undefined) return ALIGN_ITEMS_BY_SPECIMEN_ALIGN[align];
|
|
58
|
+
return fill ? 'stretch' : 'center';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A non-fill specimen's horizontal position, as `justifyContent` on `SpecimenContent`'s own row
|
|
62
|
+
* main axis rather than `alignItems` on a column: `alignItems` only sets a child's *default*
|
|
63
|
+
* `alignSelf`, which the specimen's own root can — and does, e.g. Badge's `alignSelf:
|
|
64
|
+
* 'flex-start'` — override outright, leaning the whole grid left regardless of the wrapper's
|
|
65
|
+
* intent. `justifyContent` positions along the main axis directly and cannot be overridden by a
|
|
66
|
+
* child's `alignSelf`, which only ever targets the cross axis (vertical, on a row). Reuses
|
|
67
|
+
* `ALIGN_ITEMS_BY_SPECIMEN_ALIGN`'s mapping — `flex-start`/`center`/`flex-end` are valid values
|
|
68
|
+
* for both `alignItems` and `justifyContent`. */
|
|
69
|
+
export function specimenContentJustifyContent(align: SpecimenAlign | undefined): 'flex-start' | 'center' | 'flex-end' {
|
|
70
|
+
return align !== undefined ? ALIGN_ITEMS_BY_SPECIMEN_ALIGN[align] : 'center';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The non-fill `SpecimenContent` wrapper's own style: still full-width (`SPECIMEN_SURFACE_WIDTH_STYLE`,
|
|
74
|
+
* so a percentage-width child keeps resolving against it), but a row main axis so
|
|
75
|
+
* `justifyContent` — not `alignItems` — decides horizontal placement (see
|
|
76
|
+
* `specimenContentJustifyContent`). Vertical cross-axis stays centered, as it already did. Only
|
|
77
|
+
* for non-`fill` specimens; `fill` keeps the existing column + `alignItems` stretch behavior
|
|
78
|
+
* (`specimenContentAlignItems`), unchanged. */
|
|
79
|
+
export function specimenContentBoundedStyle(align: SpecimenAlign | undefined) {
|
|
80
|
+
return {
|
|
81
|
+
...SPECIMEN_SURFACE_WIDTH_STYLE,
|
|
82
|
+
flexDirection: 'row' as const,
|
|
83
|
+
justifyContent: specimenContentJustifyContent(align),
|
|
84
|
+
alignItems: 'center' as const,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
19
88
|
/** Each example's cell turns gray only when its example would vanish against white (see
|
|
20
89
|
* `specimenFill.ts`). A page can force a fill with `specimenSurface`. */
|
|
21
90
|
export const DEFAULT_SPECIMEN_SURFACE: SpecimenSurfaceKind = 'auto';
|
package/native/catalog/types.ts
CHANGED
|
@@ -12,21 +12,39 @@ export interface PropDef {
|
|
|
12
12
|
desc: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** How a specimen's content is cross-axis aligned inside its (already full-width) wrapper —
|
|
16
|
+
* `start`/`center`/`end` map to `flex-start`/`center`/`flex-end`. Omit to inherit the established
|
|
17
|
+
* default: `center` when `fill` is false/omitted (unchanged), or the wrapper's own full-width
|
|
18
|
+
* stretch when `fill` is true — a capped-width `fill` specimen (e.g. a phone-frame preview) that
|
|
19
|
+
* still wants centering sets `align: 'center'` explicitly rather than relying on `fill` alone; see
|
|
20
|
+
* `specimenContentAlignItems` in `specimenSurfaceStyle.ts`. */
|
|
21
|
+
export type SpecimenAlign = 'start' | 'center' | 'end';
|
|
22
|
+
|
|
23
|
+
/** Per-specimen presentation, layered under whatever a slot or page already decides: this one
|
|
24
|
+
* specimen's own background (`surface`, overriding the slot's/page's `specimenSurface` — omit to
|
|
25
|
+
* inherit it), whether its wrapper stretches to the row's full width instead of shrinking to its
|
|
26
|
+
* own content width (`fill`, for a single wide-format instance, e.g. a `fullWidth` Button, sitting
|
|
27
|
+
* among otherwise compact, centered siblings — independent of a slot's own `itemsFill`, which
|
|
28
|
+
* applies to every item uniformly), and how its content cross-axis aligns inside that wrapper
|
|
29
|
+
* (`align` — see `SpecimenAlign`). Shared by `VariantExample` (list/grouped items) and
|
|
30
|
+
* `ComparisonCell` (grid cells) so the same knobs mean the same thing in every presentation shape
|
|
31
|
+
* ds-viewer renders a specimen in; an explicit `surface` here always bypasses `'auto'`'s own fill
|
|
32
|
+
* heuristic (`specimenFill.ts`), the same way a slot's/page's own explicit surface already does. */
|
|
33
|
+
export interface SpecimenPresentation {
|
|
34
|
+
/** @default false */
|
|
35
|
+
fill?: boolean;
|
|
36
|
+
surface?: SpecimenSurfaceKind;
|
|
37
|
+
align?: SpecimenAlign;
|
|
38
|
+
}
|
|
39
|
+
|
|
15
40
|
/** One individual example inside a variant/state cluster — e.g. one Button instance. */
|
|
16
|
-
export interface VariantExample {
|
|
41
|
+
export interface VariantExample extends SpecimenPresentation {
|
|
17
42
|
/** React key; keep stable and unique within its group. */
|
|
18
43
|
key: string;
|
|
19
44
|
/** Shown as a small caption under this item — the actual variant/state value it demonstrates
|
|
20
45
|
* (e.g. "Primary", "Icon-only"), not a generic label like "Example 1". */
|
|
21
46
|
name: string;
|
|
22
47
|
node: React.ReactNode;
|
|
23
|
-
/** Stretch *this item's* wrapper to the row's full width, instead of shrinking to its own content
|
|
24
|
-
* width — for a single wide-format instance (e.g. a `fullWidth` Button) sitting among otherwise
|
|
25
|
-
* compact, centered siblings in the same slot. Without this, a `fullWidth`/stretch-based prop on
|
|
26
|
-
* the instance itself has nothing to stretch into — its wrapper still shrinks to content, so the
|
|
27
|
-
* instance renders at its natural size regardless of the prop. Independent of the slot's own
|
|
28
|
-
* `itemsFill` (which applies to every item uniformly); this is a per-item override. @default false */
|
|
29
|
-
fill?: boolean;
|
|
30
48
|
/** Which real prop value(s) this instance demonstrates, e.g. `{ variant: 'primary' }` or
|
|
31
49
|
* `{ size: 'large', disabled: true }` — mirrors the actual props passed to `node`. Optional and
|
|
32
50
|
* additive: SectionBlock only cross-checks a section's enum props against this metadata once at
|
|
@@ -99,14 +117,13 @@ export interface ComparisonAxisItem {
|
|
|
99
117
|
export type GridAxis = readonly ComparisonAxisItem[] | { prop: string; items: readonly ComparisonAxisItem[] };
|
|
100
118
|
|
|
101
119
|
/** One authored row × column specimen. Provide exactly one of `node` (a real instance of the
|
|
102
|
-
* documented component) or `unavailableReason` (the combination genuinely does not exist).
|
|
103
|
-
|
|
120
|
+
* documented component) or `unavailableReason` (the combination genuinely does not exist). See
|
|
121
|
+
* `SpecimenPresentation` for `fill`/`surface`. */
|
|
122
|
+
export interface ComparisonCell extends SpecimenPresentation {
|
|
104
123
|
rowKey: string;
|
|
105
124
|
columnKey: string;
|
|
106
125
|
node?: React.ReactNode;
|
|
107
126
|
unavailableReason?: string;
|
|
108
|
-
/** Stretch this specimen to the cell width. @default false */
|
|
109
|
-
fill?: boolean;
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
/** A grid of two props that combine freely. Every row × column pair is authored — never inferred
|
|
@@ -222,6 +239,13 @@ export interface SectionDef<TId extends string = string> {
|
|
|
222
239
|
/** Widths for this page's `render()` preview, e.g. `[402, 320]` to add a small-phone example.
|
|
223
240
|
* Defaults to the catalog's default (CatalogShell `defaultPreviewWidths`), else `[402]`. */
|
|
224
241
|
previewWidths?: PreviewWidths;
|
|
242
|
+
/** Presentation for this page's numeric-width component Preview (guided intelligence,
|
|
243
|
+
* "Collapsible Preview table"): `'frames'` (default, unchanged) — one borderless-card frame per
|
|
244
|
+
* width, side by side. `'table'` — the widths share one `ComparisonList`-style card, each with
|
|
245
|
+
* its own muted caption strip above its live body; still an independent, stateful instance per
|
|
246
|
+
* width. Has no effect on a `'full'` preview width (catalog chrome) or a token gallery — those
|
|
247
|
+
* always render as `'frames'` regardless of this field. */
|
|
248
|
+
previewLayout?: 'frames' | 'table';
|
|
225
249
|
/** Remove specific parts of the page — for sections with no meaningful states, props, or
|
|
226
250
|
* accessibility story (e.g. a framework's own building-block pages). `variants` removes the
|
|
227
251
|
* first specimen block (grid, Variants list, or Preview); `states` removes the States /
|
|
@@ -237,6 +261,12 @@ export interface SectionDef<TId extends string = string> {
|
|
|
237
261
|
* Quick reference. See `ComposedOfEntry`. Omit when the component has no meaningful composition
|
|
238
262
|
* to disclose (most leaf components). */
|
|
239
263
|
composedOf?: ComposedOfEntry[];
|
|
264
|
+
/** Explicit author declaration (guided intelligence design §3) that this page's examples are
|
|
265
|
+
* intentionally static despite the documented component having a controlled state/event pair —
|
|
266
|
+
* suppresses `cli/doctor.ts`'s `missing-working-preview` advisory. `reason` is a short, bounded
|
|
267
|
+
* sentence explaining why (not free-form prose) — e.g. "Selection is already demonstrated by X's
|
|
268
|
+
* working preview." Omit when no such declaration applies; the advisory then runs normally. */
|
|
269
|
+
intentionalStaticPreview?: { reason: string };
|
|
240
270
|
}
|
|
241
271
|
|
|
242
272
|
/** A labeled group of section ids in the sidebar (e.g. "Components" vs "Tokens"). */
|
|
@@ -256,6 +286,26 @@ export interface UpdateNotice {
|
|
|
256
286
|
releasedAt?: string;
|
|
257
287
|
}
|
|
258
288
|
|
|
289
|
+
/** Where the "check automatically" setting in effect comes from, strongest first: the
|
|
290
|
+
* `DS_VIEWER_NO_UPDATE_CHECK=1` environment variable (CI), your own switch on the update page
|
|
291
|
+
* (saved per project on this computer), the project's `updateCheck` config, or the default (on). */
|
|
292
|
+
export type AutoCheckSource = 'env' | 'personal' | 'project' | 'default';
|
|
293
|
+
|
|
294
|
+
/** What the update page and the sidebar's version line show. Written by `dev` at startup
|
|
295
|
+
* (`.ds-viewer/version.json`) and kept current by the local endpoint (`GET /version`,
|
|
296
|
+
* `POST /version/check`, `POST /version/auto-check`). */
|
|
297
|
+
export interface VersionStatus {
|
|
298
|
+
/** The installed ds-viewer version. */
|
|
299
|
+
current: string;
|
|
300
|
+
autoCheck: { enabled: boolean; source: AutoCheckSource };
|
|
301
|
+
/** The last successful check on this computer (any project), when known. */
|
|
302
|
+
lastCheckedAt?: string;
|
|
303
|
+
/** This run's most recent check: answered, npm unreachable, or not run (checks off). */
|
|
304
|
+
lastOutcome: 'ok' | 'unreachable' | 'not-run';
|
|
305
|
+
/** A newer version, when one is known. */
|
|
306
|
+
update: UpdateNotice | null;
|
|
307
|
+
}
|
|
308
|
+
|
|
259
309
|
/** One titled card on a token page (`SectionDef.tokenSections`). */
|
|
260
310
|
export interface TokenSection {
|
|
261
311
|
/** The kind of token this card holds, e.g. "Semantic colours", "Palette", "Body". */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AutoCheckSource, VersionStatus } from './types.ts';
|
|
2
|
+
|
|
1
3
|
/** The same shape as `cli/updatePlan.ts`'s `UpdatePlan` — duplicated, not imported: `native/catalog/`
|
|
2
4
|
* is the published package root and never imports from `cli/` (the reverse is normal: `cli/`
|
|
3
5
|
* already imports `native/catalog/` throughout). The endpoint's `POST /plan` response is this
|
|
@@ -72,3 +74,73 @@ export function daysAgoLabel(releasedAt: string, nowMs: number): string {
|
|
|
72
74
|
if (days === 1) return 'Released 1 day ago.';
|
|
73
75
|
return `Released ${days} days ago.`;
|
|
74
76
|
}
|
|
77
|
+
|
|
78
|
+
/** "Last checked" on the update page. */
|
|
79
|
+
export function checkedAgoLabel(at: string | undefined, nowMs: number): string {
|
|
80
|
+
if (!at) return 'Never';
|
|
81
|
+
const minutes = Math.floor((nowMs - Date.parse(at)) / 60_000);
|
|
82
|
+
if (minutes < 1) return 'Just now';
|
|
83
|
+
if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
|
|
84
|
+
const hours = Math.floor(minutes / 60);
|
|
85
|
+
if (hours < 24) return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
86
|
+
const days = Math.floor(hours / 24);
|
|
87
|
+
return `${days} day${days === 1 ? '' : 's'} ago`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface VersionOverview {
|
|
91
|
+
pill?: 'latest' | 'off';
|
|
92
|
+
line: string;
|
|
93
|
+
note?: { variant: 'ok' | 'err'; bold?: string; text: string };
|
|
94
|
+
/** **Check now**, or **Try again** after npm didn't answer. */
|
|
95
|
+
action: 'check' | 'retry';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The update page when no newer version is known: which of "latest", "checks off", "not checked
|
|
99
|
+
* yet" and "couldn't reach npm" applies. Only a check that actually answered may say "latest".
|
|
100
|
+
* `justChecked` is true right after **Check now** found nothing newer. */
|
|
101
|
+
export function versionOverview(status: VersionStatus, justChecked: boolean, nowMs: number): VersionOverview {
|
|
102
|
+
if (status.lastOutcome === 'unreachable') {
|
|
103
|
+
const last = status.lastCheckedAt ? ` The last successful check, ${checkedAgoLabel(status.lastCheckedAt, nowMs).toLowerCase()}, found no newer version.` : '';
|
|
104
|
+
return { line: `You’re on ${status.current}.`, action: 'retry', note: { variant: 'err', bold: 'Couldn’t reach npm.', text: ` You may be offline.${last}` } };
|
|
105
|
+
}
|
|
106
|
+
if (status.lastOutcome === 'not-run') {
|
|
107
|
+
if (!status.autoCheck.enabled) {
|
|
108
|
+
return { pill: 'off', line: `You’re on ${status.current}. Automatic checks are off, so this can’t tell whether a newer version exists.`, action: 'check' };
|
|
109
|
+
}
|
|
110
|
+
return { line: `You’re on ${status.current}. Not checked yet: automatic checks run when the viewer starts.`, action: 'check' };
|
|
111
|
+
}
|
|
112
|
+
// Turned off after a check answered (approved mockup state 7b); a fresh Check now still wins.
|
|
113
|
+
if (!status.autoCheck.enabled && !justChecked) {
|
|
114
|
+
return { pill: 'off', line: `You’re on ${status.current}. Automatic checks are off.`, action: 'check' };
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
pill: 'latest',
|
|
118
|
+
line: 'You’re on the latest version.',
|
|
119
|
+
action: 'check',
|
|
120
|
+
...(justChecked ? { note: { variant: 'ok' as const, text: 'Checked just now. No newer version.' } } : {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The switch's helper text: where the setting in effect comes from. Only CI's environment
|
|
125
|
+
* variable locks the switch. */
|
|
126
|
+
export function autoCheckHelp(autoCheck: { enabled: boolean; source: AutoCheckSource }): { text: string; locked: boolean } {
|
|
127
|
+
if (autoCheck.source === 'env') return { text: 'Off in this environment (DS_VIEWER_NO_UPDATE_CHECK=1).', locked: true };
|
|
128
|
+
if (autoCheck.source === 'personal') {
|
|
129
|
+
return { text: autoCheck.enabled ? 'Once a day, when the viewer starts. Saved for you on this computer.' : 'Turned off by you on this computer.', locked: false };
|
|
130
|
+
}
|
|
131
|
+
if (!autoCheck.enabled) {
|
|
132
|
+
return { text: 'Off for this project (updateCheck: false in ds-viewer.config.ts). Turning it on here applies to you only.', locked: false };
|
|
133
|
+
}
|
|
134
|
+
return { text: 'Once a day, when the viewer starts.', locked: false };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The update's size pill. Breaking updates are always "Major" (the CLI decides that); otherwise
|
|
138
|
+
* a change in the second number is "Minor" and anything smaller "Patch". */
|
|
139
|
+
export function bumpLabel(current: string, latest: string, breaking: boolean): 'Major' | 'Minor' | 'Patch' {
|
|
140
|
+
if (breaking) return 'Major';
|
|
141
|
+
const parts = (v: string) => /^(\d+)\.(\d+)\.(\d+)/.exec(v)?.slice(1, 4).map(Number);
|
|
142
|
+
const a = parts(current);
|
|
143
|
+
const b = parts(latest);
|
|
144
|
+
if (!a || !b) return 'Minor';
|
|
145
|
+
return a[0] === b[0] && a[1] === b[1] ? 'Patch' : 'Minor';
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -5,6 +5,9 @@ import { Banner } from './Banner';
|
|
|
5
5
|
export default defineCatalogPage({
|
|
6
6
|
component: 'Banner',
|
|
7
7
|
group: 'Surfaces',
|
|
8
|
+
composedOf: [
|
|
9
|
+
{ component: 'AnimatedChevron', role: "The expand/collapse chevron in the header of a collapsible banner.", relationship: 'built-in' },
|
|
10
|
+
],
|
|
8
11
|
description: 'An inline callout for status/announcements — five semantic variants, optional collapsible body, inline link, and action button.',
|
|
9
12
|
whenToUse: "Persistent and in-flow, describing a standing condition about the screen's content. For a transient, self-contained event notification, use Toast instead.",
|
|
10
13
|
a11y: 'When onPress/action is set the header/button are Pressables; the collapsible header toggles the description with a chevron affordance.',
|
|
@@ -56,6 +56,9 @@ const BUTTON_COMPARISON = grid(
|
|
|
56
56
|
export default defineCatalogPage({
|
|
57
57
|
component: 'Button',
|
|
58
58
|
group: 'Actions',
|
|
59
|
+
composedOf: [
|
|
60
|
+
{ component: 'Loading', role: "The spinner that replaces the label and icon while loading is true.", relationship: 'built-in' },
|
|
61
|
+
],
|
|
59
62
|
comparison: BUTTON_COMPARISON,
|
|
60
63
|
description:
|
|
61
64
|
'The primary tap target. Five visual weights, three sizes, optional leading/trailing icon, plus loading and icon-only modes.',
|
|
@@ -39,6 +39,12 @@ function DropdownDemo() {
|
|
|
39
39
|
export default defineCatalogPage({
|
|
40
40
|
component: 'Dropdown',
|
|
41
41
|
group: 'Inputs',
|
|
42
|
+
composedOf: [
|
|
43
|
+
{ component: 'FieldContainer', role: "The field frame around the label, value and chevron (the trigger).", relationship: 'built-in' },
|
|
44
|
+
{ component: 'BottomSheet', role: "The sheet listing the options while the dropdown is open.", relationship: 'built-in' },
|
|
45
|
+
{ component: 'TopNav', role: "The sheet's header, titled with the field's label.", relationship: 'built-in' },
|
|
46
|
+
{ component: 'Button', role: "The icon-only Close button in the sheet header.", relationship: 'built-in' },
|
|
47
|
+
],
|
|
42
48
|
description: 'A labelled field that opens a BottomSheet picker on tap — the "bottom sheet picker" interaction: tap the trigger, pick an option from the sheet, it closes.',
|
|
43
49
|
whenToUse: 'The answer is one of a small, known set of choices — never free text. For free-text filtering, use SearchField; for a named field with a fixed identity, use InputField.',
|
|
44
50
|
a11y: 'The trigger is a Pressable (via FieldContainer) with accessibilityRole="button". Options render as accessibilityRole="radio" inside a "radiogroup", with accessibilityState.checked reflecting the current selection.',
|
|
@@ -6,6 +6,11 @@ import { DS_SEMANTIC } from '../../tokens';
|
|
|
6
6
|
export default defineCatalogPage({
|
|
7
7
|
component: 'EmptyState',
|
|
8
8
|
group: 'Layout',
|
|
9
|
+
composedOf: [
|
|
10
|
+
{ component: 'Avatar', role: "The icon circle above the title (iconName, default 'users').", relationship: 'built-in' },
|
|
11
|
+
{ component: 'ButtonGroup', role: "Stacks the action buttons when action is set.", relationship: 'built-in' },
|
|
12
|
+
{ component: 'Button', role: "The action button, plus a tertiary secondaryAction button when set.", relationship: 'built-in' },
|
|
13
|
+
],
|
|
9
14
|
description: 'A centred placeholder for a screen or section with nothing to show yet — no results, no saved items, a first-run state.',
|
|
10
15
|
whenToUse: "Fills the entire content area because there's nothing else to show. For a note that sits alongside other real content, use Banner instead.",
|
|
11
16
|
a11y: 'A plain View with text; the action(s) render as real Buttons inside a ButtonGroup, which carry their own accessibility.',
|
|
@@ -18,6 +18,9 @@ function InputFieldDemo() {
|
|
|
18
18
|
export default defineCatalogPage({
|
|
19
19
|
component: 'InputField',
|
|
20
20
|
group: 'Inputs',
|
|
21
|
+
composedOf: [
|
|
22
|
+
{ component: 'InputClearButton', role: "The clear (\u00d7) button, shown while the field has a value and is focused (or active, when not editable).", relationship: 'built-in' },
|
|
23
|
+
],
|
|
21
24
|
description: 'A floating-label field. Resting: a centred, body-sized label with no border. Active (focused, or picker with active set) or filled: the label floats to a small top caption, row 2 shows the value/input, and a border fades in while active.',
|
|
22
25
|
whenToUse: 'A named field with a fixed identity ("To", "Arrive by") across the interaction. For free-text search with no floating label, use SearchField; for one of a small known set of choices, use Dropdown.',
|
|
23
26
|
a11y: 'The editable mode is a live TextInput; the picker mode is a Pressable row. Either mode shows a clear button while active (focused, or active for a picker) with a value set. Provide a meaningful label.',
|
|
@@ -9,6 +9,9 @@ import { DS_SEMANTIC } from '../../tokens';
|
|
|
9
9
|
export default defineCatalogPage({
|
|
10
10
|
component: 'List',
|
|
11
11
|
group: 'Layout',
|
|
12
|
+
composedOf: [
|
|
13
|
+
{ component: 'Divider', role: "The hairline between consecutive items (none after the last).", relationship: 'built-in' },
|
|
14
|
+
],
|
|
12
15
|
description: 'Stacks ListItem rows on a rounded white surface, with a Divider automatically inserted between each consecutive pair — never after the last.',
|
|
13
16
|
a11y: 'A plain View; each ListItem child carries its own accessibility.',
|
|
14
17
|
variants: {
|
|
@@ -41,6 +41,9 @@ const PILL_COMPARISON = grid(
|
|
|
41
41
|
export default defineCatalogPage({
|
|
42
42
|
component: 'Pill',
|
|
43
43
|
group: 'Actions',
|
|
44
|
+
composedOf: [
|
|
45
|
+
{ component: 'Loading', role: "The spinner shown instead of the label and icon while loading is true.", relationship: 'built-in' },
|
|
46
|
+
],
|
|
44
47
|
specimenSize: 'compact',
|
|
45
48
|
comparison: PILL_COMPARISON,
|
|
46
49
|
description: 'A compact selectable chip — selected/unselected states with an optional leading icon.',
|
|
@@ -5,6 +5,9 @@ import { PillRow } from './PillRow';
|
|
|
5
5
|
export default defineCatalogPage({
|
|
6
6
|
component: 'PillRow',
|
|
7
7
|
group: 'Actions',
|
|
8
|
+
composedOf: [
|
|
9
|
+
{ component: 'Pill', role: "Each item in pills, plus the trailing icon-only Edit pill when shown.", relationship: 'built-in' },
|
|
10
|
+
],
|
|
8
11
|
description: 'A horizontally-scrolling row of Pill chips, with an optional trailing icon-only add pill — keeps the selected pill scrolled into view automatically.',
|
|
9
12
|
whenToUse: 'A scrollable row of selection chips (e.g. saved-place shortcuts) — for a fixed row of action buttons instead, use ButtonGroup.',
|
|
10
13
|
a11y: 'The row itself carries no accessibility role; each Pill (including the add pill) keeps its own accessibilityLabel/role from the Pill component.',
|
|
@@ -10,6 +10,10 @@ function SearchFieldDemo() {
|
|
|
10
10
|
export default defineCatalogPage({
|
|
11
11
|
component: 'SearchField',
|
|
12
12
|
group: 'Inputs',
|
|
13
|
+
composedOf: [
|
|
14
|
+
{ component: 'FieldContainer', role: "The field frame around the search icon, input and clear button.", relationship: 'built-in' },
|
|
15
|
+
{ component: 'InputClearButton', role: "The clear (\u00d7) button, shown while focused with text.", relationship: 'built-in' },
|
|
16
|
+
],
|
|
13
17
|
description: 'Single-line search input on the shared field chrome — leading search icon, a border that darkens on focus, and a clear button while active with text.',
|
|
14
18
|
whenToUse: 'Free-text filtering/searching only — never a named field with a fixed value. For that, use InputField; for a small known set of choices, use Dropdown.',
|
|
15
19
|
a11y: 'Forwards a ref to the underlying TextInput; all TextInputProps pass through, so pass accessibilityLabel/placeholder as needed.',
|
|
@@ -5,6 +5,9 @@ import { SectionHeader } from './SectionHeader';
|
|
|
5
5
|
export default defineCatalogPage({
|
|
6
6
|
component: 'SectionHeader',
|
|
7
7
|
group: 'Layout',
|
|
8
|
+
composedOf: [
|
|
9
|
+
{ component: 'Button', role: "The trailing button, rendered when trailingButtonLabel is set.", relationship: 'built-in' },
|
|
10
|
+
],
|
|
8
11
|
description: 'An uppercase muted section label with an optional inline icon and a right-aligned ghost button.',
|
|
9
12
|
whenToUse: "A label above a group of related rows within a screen (e.g. above a List) — for the screen's own top bar, use TopNav.",
|
|
10
13
|
a11y: 'Title renders as plain Text — no heading role. A tappable labelIcon (onPress set) becomes accessibilityRole="button" with accessibilityLabel falling back to the section\'s own title; a non-interactive labelIcon has no accessibility node of its own. The trailing button is a real Button, so it carries Button\'s own accessibility for free.',
|
|
@@ -10,6 +10,9 @@ function TextAreaDemo() {
|
|
|
10
10
|
export default defineCatalogPage({
|
|
11
11
|
component: 'TextArea',
|
|
12
12
|
group: 'Inputs',
|
|
13
|
+
composedOf: [
|
|
14
|
+
{ component: 'FieldContainer', role: "The field frame around the multi-line input.", relationship: 'built-in' },
|
|
15
|
+
],
|
|
13
16
|
description: 'A multi-line input that grows with its content from a minimum height.',
|
|
14
17
|
a11y: 'A multiline TextInput; pass accessibilityLabel via inputProps when there is no visible label beside it.',
|
|
15
18
|
variants: {
|
|
@@ -32,6 +32,13 @@ export default defineCatalogPage({
|
|
|
32
32
|
description: 'A transient confirmation bar. Without a variant it renders the dark surface with inverse text; a variant shifts to a pastel status color.',
|
|
33
33
|
whenToUse: "Transient and self-contained, floating over the screen, expected to go away on its own or via its own action. For a persistent, in-flow message about a standing condition, use Banner.",
|
|
34
34
|
a11y: 'Rendered with accessibilityRole="alert" so screen readers announce it when it appears.',
|
|
35
|
+
// Confirmed from Toast.tsx's own JSX, not inferred from its imports: it renders a Button itself
|
|
36
|
+
// (not a caller-supplied child) whenever `action` is set. Toast.tsx also renders an Icon for its
|
|
37
|
+
// status glyph, but Icon has no catalog page/known component name of its own to confirm against
|
|
38
|
+
// yet, so that entry is left out rather than logging an unknown-component warning.
|
|
39
|
+
composedOf: [
|
|
40
|
+
{ component: 'Button', role: 'Renders the optional action button when action is set.', relationship: 'built-in' },
|
|
41
|
+
],
|
|
35
42
|
variants: {
|
|
36
43
|
itemsFill: true,
|
|
37
44
|
items: [
|