@krapwoo/ds-viewer 0.4.6 → 0.4.8
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 +121 -21
- package/dist/cli/doctor.js +141 -2
- package/dist/cli/explain.js +44 -6
- package/dist/cli/init.js +5 -8
- package/dist/cli/props.js +80 -1
- package/dist/cli/staticPage.js +228 -6
- package/dist/native/catalog/comparison.js +43 -9
- package/native/catalog/BoundedOverlayViewport.tsx +112 -0
- package/native/catalog/CatalogShell.tsx +26 -0
- package/native/catalog/ComparisonGrid.tsx +6 -2
- package/native/catalog/ComparisonGroups.tsx +8 -2
- package/native/catalog/ComparisonList.tsx +8 -2
- package/native/catalog/PhoneFrame.tsx +109 -19
- package/native/catalog/ReferenceDetails.tsx +83 -20
- package/native/catalog/SectionBlock.tsx +75 -7
- package/native/catalog/SpecimenAddress.tsx +15 -0
- package/native/catalog/SpecimenSurface.tsx +80 -10
- package/native/catalog/comparison.ts +57 -10
- package/native/catalog/deviceFrame.ts +14 -0
- package/native/catalog/index.ts +4 -0
- package/native/catalog/overlayViewport.ts +158 -0
- package/native/catalog/pageApi.ts +6 -0
- package/native/catalog/specimenSurfaceStyle.ts +124 -1
- package/native/catalog/types.ts +42 -12
- package/package.json +5 -4
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +1 -1
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +1 -1
- package/starter-kit/components/Badge/Badge.catalog.tsx +1 -1
- package/starter-kit/components/Banner/Banner.catalog.tsx +5 -1
- package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +5 -1
- package/starter-kit/components/Button/Button.catalog.tsx +4 -1
- package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +8 -1
- package/starter-kit/components/Card/Card.catalog.tsx +2 -1
- package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +2 -2
- package/starter-kit/components/Dialog/Dialog.catalog.tsx +1 -1
- package/starter-kit/components/Divider/Divider.catalog.tsx +2 -1
- package/starter-kit/components/Dock/Dock.catalog.tsx +4 -1
- package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +7 -1
- package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +7 -1
- package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +5 -1
- package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +1 -1
- package/starter-kit/components/InputField/InputField.catalog.tsx +6 -1
- package/starter-kit/components/List/List.catalog.tsx +5 -1
- package/starter-kit/components/ListItem/ListItem.catalog.tsx +7 -1
- package/starter-kit/components/Loading/Loading.catalog.tsx +2 -1
- package/starter-kit/components/Pill/Pill.catalog.tsx +4 -1
- package/starter-kit/components/PillRow/PillRow.catalog.tsx +4 -1
- package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +1 -1
- package/starter-kit/components/Radio/Radio.catalog.tsx +2 -2
- package/starter-kit/components/SearchField/SearchField.catalog.tsx +6 -1
- package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +5 -1
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +1 -3
- package/starter-kit/components/Shimmer/Breathing.catalog.tsx +2 -1
- package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +2 -1
- package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +5 -1
- package/starter-kit/components/Surface/Surface.catalog.tsx +2 -1
- package/starter-kit/components/Switch/Switch.catalog.tsx +2 -2
- package/starter-kit/components/TextArea/TextArea.catalog.tsx +5 -1
- package/starter-kit/components/Toast/Toast.catalog.tsx +9 -1
- package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +2 -1
- package/starter-kit/components/TopNav/TopNav.catalog.tsx +4 -1
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +1 -3
- package/starter-kit/pages/ReportIssue.catalog.tsx +18 -1
- package/starter-kit/pages/SavedTrips.catalog.tsx +27 -6
- package/starter-kit/pages/TripPlannerForm.catalog.tsx +7 -1
|
@@ -8,7 +8,9 @@
|
|
|
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 {
|
|
11
|
+
import type {
|
|
12
|
+
ComparisonAxisItem, ComparisonDef, GridAxis, PreviewWidths, SectionDef, SpecimenAlign, SpecimenSize, SpecimenSurfaceKind, TokenSection, VariantSlot,
|
|
13
|
+
} from './types.ts';
|
|
12
14
|
import type React from 'react';
|
|
13
15
|
|
|
14
16
|
/** Approved geometry. `cellPadding` equals CATALOG_SPACE.lg (asserted in tests). The laptop
|
|
@@ -33,6 +35,14 @@ export const PROPS_MIN_COLUMN_WIDTH = 360;
|
|
|
33
35
|
/** Gap between the two Props columns (CATALOG_SPACE['2xl']). */
|
|
34
36
|
export const PROPS_COLUMN_GAP = 32;
|
|
35
37
|
|
|
38
|
+
/** A reference column (Guidance, Quick reference, or Composition) is never narrower than this;
|
|
39
|
+
* below it, `referenceColumns` drops to fewer columns. */
|
|
40
|
+
export const REFERENCE_MIN_COLUMN_WIDTH = 280;
|
|
41
|
+
|
|
42
|
+
/** Gap between reference columns (CATALOG_SPACE['2xl'], the same token the card's columns already
|
|
43
|
+
* use). */
|
|
44
|
+
export const REFERENCE_COLUMN_GAP = 32;
|
|
45
|
+
|
|
36
46
|
/** Smallest column or cell width per specimen size. Wide specimens are fixed at phone width. */
|
|
37
47
|
export const COLUMN_MIN_WIDTH: Record<SpecimenSize, number> = { compact: 160, regular: 240, wide: 402 };
|
|
38
48
|
|
|
@@ -219,9 +229,18 @@ export function remainingStates(states: VariantSlot | undefined, comparison: Com
|
|
|
219
229
|
|
|
220
230
|
export interface ListItem {
|
|
221
231
|
key: string;
|
|
232
|
+
/** Which authored slot this item came from, so a device frame inside it can address it
|
|
233
|
+
* (`overlayViewport.ts`). */
|
|
234
|
+
slot?: 'variants' | 'states';
|
|
222
235
|
label: string;
|
|
223
236
|
node: unknown;
|
|
224
237
|
fill?: boolean;
|
|
238
|
+
/** This item's own `specimenSurface` override — see `SpecimenPresentation`. Omit to inherit the
|
|
239
|
+
* surface already in effect (the slot's/page's `specimenSurface`, else `'auto'`). */
|
|
240
|
+
surface?: SpecimenSurfaceKind;
|
|
241
|
+
/** This item's own cross-axis alignment override — see `SpecimenPresentation.align`. Omit to
|
|
242
|
+
* inherit the established default (centered, or stretched when `fill` is set). */
|
|
243
|
+
align?: SpecimenAlign;
|
|
225
244
|
}
|
|
226
245
|
|
|
227
246
|
export interface ListGroup {
|
|
@@ -249,8 +268,20 @@ export function slotSize<TId extends string>(def: SectionDef<TId>, slot: Variant
|
|
|
249
268
|
return def.specimenSize ?? (slot.itemsFill ? 'wide' : 'regular');
|
|
250
269
|
}
|
|
251
270
|
|
|
252
|
-
|
|
253
|
-
|
|
271
|
+
/** Full-width (filled) examples are block components (cards, banners, rows): narrower than about a
|
|
272
|
+
* third of a laptop's content width they stop reading as themselves, so they wrap at most 3
|
|
273
|
+
* across unless the page sets its own `maxColumns`. Small centred examples wrap freely. */
|
|
274
|
+
export const FILLED_SLOT_MAX_COLUMNS = 3;
|
|
275
|
+
|
|
276
|
+
function slotColumnCap(slot: VariantSlot): { maxColumns?: 1 | 2 | 3 | 4 | 5 } {
|
|
277
|
+
if (slot.maxColumns !== undefined) return { maxColumns: slot.maxColumns };
|
|
278
|
+
return slot.itemsFill ? { maxColumns: FILLED_SLOT_MAX_COLUMNS } : {};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function slotItems(slot: VariantSlot, from: 'variants' | 'states'): ListItem[] {
|
|
282
|
+
// Nullish, not `||`: an item's own explicit `fill: false` must override an inherited
|
|
283
|
+
// `itemsFill: true`, which `false || slot.itemsFill` would silently discard.
|
|
284
|
+
return slot.items.map((item) => ({ key: item.key, slot: from, label: item.name, node: item.node, fill: item.fill ?? slot.itemsFill, surface: item.surface, align: item.align }));
|
|
254
285
|
}
|
|
255
286
|
|
|
256
287
|
/** Preview widths, each capped at phone width. 'full' is kept for catalog chrome and token pages. */
|
|
@@ -265,12 +296,14 @@ function previewWidths(requested: PreviewWidths | undefined, fallback: PreviewWi
|
|
|
265
296
|
function groupStates(variants: VariantSlot, states: VariantSlot): { groups: ListGroup[]; leftovers: VariantSlot | undefined } | undefined {
|
|
266
297
|
const variantKeys = new Set(variants.items.map((v) => v.key));
|
|
267
298
|
if (!states.items.some((s) => s.group !== undefined && variantKeys.has(s.group))) return undefined;
|
|
268
|
-
|
|
299
|
+
// Nullish, not `||` — same reasoning as `slotItems`: an explicit `fill: false` must win over an
|
|
300
|
+
// inherited `itemsFill: true`.
|
|
301
|
+
const fill = (item: VariantSlot['items'][number], slot: VariantSlot) => item.fill ?? slot.itemsFill;
|
|
269
302
|
const groups = variants.items.map((variant) => {
|
|
270
303
|
const own = states.items.filter((s) => s.group === variant.key);
|
|
271
304
|
const items = own.length > 0
|
|
272
|
-
? own.map((s) => ({ key: s.key, label: s.name, node: s.node, fill: fill(s, states) }))
|
|
273
|
-
: [{ key: variant.key, label: variant.name, node: variant.node, fill: fill(variant, variants) }];
|
|
305
|
+
? own.map((s) => ({ key: s.key, slot: 'states' as const, label: s.name, node: s.node, fill: fill(s, states), surface: s.surface, align: s.align }))
|
|
306
|
+
: [{ key: variant.key, slot: 'variants' as const, label: variant.name, node: variant.node, fill: fill(variant, variants), surface: variant.surface, align: variant.align }];
|
|
274
307
|
return { key: variant.key, label: variant.name, items };
|
|
275
308
|
});
|
|
276
309
|
const rest = states.items.filter((s) => s.group === undefined || !variantKeys.has(s.group));
|
|
@@ -317,8 +350,8 @@ export function presentationBlocks<TId extends string>(def: SectionDef<TId>, opt
|
|
|
317
350
|
kind: 'list',
|
|
318
351
|
title: 'Variants',
|
|
319
352
|
size: slotSize(def, def.variants),
|
|
320
|
-
items: slotItems(def.variants),
|
|
321
|
-
...(def.variants
|
|
353
|
+
items: slotItems(def.variants, 'variants'),
|
|
354
|
+
...slotColumnCap(def.variants),
|
|
322
355
|
});
|
|
323
356
|
} else if (def.render) {
|
|
324
357
|
blocks.push({ kind: 'preview', title: 'Preview', widths: previewWidths(def.previewWidths, options.defaultPreviewWidths) });
|
|
@@ -331,8 +364,8 @@ export function presentationBlocks<TId extends string>(def: SectionDef<TId>, opt
|
|
|
331
364
|
kind: 'list',
|
|
332
365
|
title: secondary ? 'Other configurations' : 'States / configurations',
|
|
333
366
|
size: slotSize(def, states),
|
|
334
|
-
items: slotItems(states),
|
|
335
|
-
...(states
|
|
367
|
+
items: slotItems(states, 'states'),
|
|
368
|
+
...slotColumnCap(states),
|
|
336
369
|
});
|
|
337
370
|
}
|
|
338
371
|
|
|
@@ -377,3 +410,17 @@ export function choosePlacement({ available, gap, first, second }: { available:
|
|
|
377
410
|
export function propsColumns(count: number, width: number): 1 | 2 {
|
|
378
411
|
return count >= 4 && width >= 2 * PROPS_MIN_COLUMN_WIDTH + PROPS_COLUMN_GAP ? 2 : 1;
|
|
379
412
|
}
|
|
413
|
+
|
|
414
|
+
/** How many columns the reference card's Guidance / Quick reference / Composition panel uses at a
|
|
415
|
+
* given measured inner card width: 3 only when there is confirmed composition to show as its own
|
|
416
|
+
* column AND the card is wide enough for three REFERENCE_MIN_COLUMN_WIDTH columns; 2 when wide
|
|
417
|
+
* enough for two (any composition then stays nested under Quick reference, as it always has been);
|
|
418
|
+
* 1 (stacked, full content in document order — nothing dropped) when even two columns wouldn't
|
|
419
|
+
* each keep their minimum. An unmeasured width (`<= 0`) stacks, so the first frame before layout
|
|
420
|
+
* settles never overflows. */
|
|
421
|
+
export function referenceColumns(width: number, hasComposition: boolean): 1 | 2 | 3 {
|
|
422
|
+
const fits = (n: number) => width >= n * REFERENCE_MIN_COLUMN_WIDTH + (n - 1) * REFERENCE_COLUMN_GAP;
|
|
423
|
+
if (hasComposition && fits(3)) return 3;
|
|
424
|
+
if (fits(2)) return 2;
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The device every `PhoneFrame` shows: a real phone's viewport, in points, at the small end of
|
|
3
|
+
* current phones (iPhone SE, 3rd generation). Examples lay out at exactly these dimensions; a cell
|
|
4
|
+
* narrower than the device scales the whole viewport down instead of squeezing its layout. Kept
|
|
5
|
+
* free of runtime imports so `node --test` can check it.
|
|
6
|
+
*/
|
|
7
|
+
export const DEVICE_FRAME = { name: 'iPhone SE', width: 375, height: 667 } as const;
|
|
8
|
+
|
|
9
|
+
/** How much to scale the device so it fits `availableWidth` (never enlarged). An unmeasured cell
|
|
10
|
+
* (0) renders at full size rather than collapsing. */
|
|
11
|
+
export function deviceFrameScale(availableWidth: number): number {
|
|
12
|
+
if (!(availableWidth > 0)) return 1;
|
|
13
|
+
return Math.min(1, availableWidth / DEVICE_FRAME.width);
|
|
14
|
+
}
|
package/native/catalog/index.ts
CHANGED
|
@@ -31,6 +31,10 @@ export { TokenSections, TokenGrid, TokenTile } from './TokenLayouts';
|
|
|
31
31
|
export { DividedStack } from './DividedStack';
|
|
32
32
|
export { Swatch } from './Swatch';
|
|
33
33
|
export { PhoneFrame } from './PhoneFrame';
|
|
34
|
+
export { BoundedOverlayViewport } from './BoundedOverlayViewport';
|
|
35
|
+
export type { BoundedOverlayViewportProps } from './BoundedOverlayViewport';
|
|
36
|
+
export { comparisonCellItemKey } from './overlayViewport';
|
|
37
|
+
export type { OverlayAddress, OverlaySlot } from './overlayViewport';
|
|
34
38
|
export { SpacingScaleGallery } from './SpacingScaleGallery';
|
|
35
39
|
export { TypeScaleGallery } from './TypeScaleGallery';
|
|
36
40
|
export { buildComponentManifest } from './manifest';
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic behind `BoundedOverlayViewport.tsx` — no React/React Native imports, so it runs
|
|
3
|
+
* under Node's test runner the same way `catalogNavigation.ts` does for `CatalogShell`. Addresses
|
|
4
|
+
* one specific authored specimen, decides whether an instance should frame itself in a bounded
|
|
5
|
+
* child document or render inline, and (for `CatalogShell`'s own chromeless selected-document
|
|
6
|
+
* render) resolves an address back to the `node` it names.
|
|
7
|
+
*/
|
|
8
|
+
import type React from 'react';
|
|
9
|
+
import type { SectionDef } from './types.ts';
|
|
10
|
+
|
|
11
|
+
/** Which block on a page an addressed specimen's `node` lives in — see `findOverlayNode`. */
|
|
12
|
+
export type OverlaySlot = 'variants' | 'states' | 'comparison' | 'preview';
|
|
13
|
+
|
|
14
|
+
/** Identifies exactly one authored specimen: a page id (`SectionDef.id`), which block it lives
|
|
15
|
+
* in, and that item's own key — `VariantExample.key` for `'variants'`/`'states'`, or this
|
|
16
|
+
* module's own `comparisonCellItemKey(rowKey, columnKey)` for a `'comparison'` cell (NOT an ad
|
|
17
|
+
* hoc `"<rowKey>:<columnKey>"` join — a plain colon join is ambiguous whenever a row or column
|
|
18
|
+
* key contains a colon itself). Always an existing author-assigned identifier, never inferred
|
|
19
|
+
* from pixels, source strings, or a DOM selector. */
|
|
20
|
+
export interface OverlayAddress {
|
|
21
|
+
pageId: string;
|
|
22
|
+
slot: OverlaySlot;
|
|
23
|
+
itemKey: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const OVERLAY_SLOTS: readonly OverlaySlot[] = ['variants', 'states', 'comparison', 'preview'];
|
|
27
|
+
|
|
28
|
+
/** `encodeURIComponent` leaves `.` untouched (it's an unreserved character), which would let a
|
|
29
|
+
* literal `.` inside a segment collide with the `.`-joined encoding below; escape it too so
|
|
30
|
+
* splitting on `.` is always safe. `decodeURIComponent` already reverses `%2E` with no special
|
|
31
|
+
* casing needed. */
|
|
32
|
+
function encodeSegment(segment: string): string {
|
|
33
|
+
return encodeURIComponent(segment).replace(/\./g, '%2E');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The query param a `BoundedOverlayViewport`'s child document is opened with — distinct from
|
|
37
|
+
* `CatalogShell`'s own page-routing hash (`#PageId`), which the child document's URL also keeps
|
|
38
|
+
* (see `overlayViewportHrefSuffix`) so the SAME app entry still resolves the right page/providers
|
|
39
|
+
* before the viewport component itself ever runs. */
|
|
40
|
+
export const OVERLAY_ADDRESS_PARAM = 'ds-viewer-overlay';
|
|
41
|
+
|
|
42
|
+
/** `pageId.slot.itemKey`, each segment percent-encoded — one URL-safe token for a query value. */
|
|
43
|
+
export function encodeOverlayAddress(address: OverlayAddress): string {
|
|
44
|
+
return [address.pageId, address.slot, address.itemKey].map(encodeSegment).join('.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The inverse of `encodeOverlayAddress` — `undefined` for anything that isn't exactly three
|
|
48
|
+
* non-empty segments with a recognized slot: the same "malformed means absent" recovery
|
|
49
|
+
* `catalogNavigation.ts`'s `idFromHash` uses for an unknown page fragment, rather than guessing
|
|
50
|
+
* at a partial or corrupted address. */
|
|
51
|
+
export function decodeOverlayAddress(value: string): OverlayAddress | undefined {
|
|
52
|
+
const parts = value.split('.');
|
|
53
|
+
if (parts.length !== 3) return undefined;
|
|
54
|
+
let decoded: string[];
|
|
55
|
+
try {
|
|
56
|
+
decoded = parts.map((part) => decodeURIComponent(part));
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const [pageId, slot, itemKey] = decoded;
|
|
61
|
+
if (!pageId || !itemKey) return undefined;
|
|
62
|
+
if (!OVERLAY_SLOTS.includes(slot as OverlaySlot)) return undefined;
|
|
63
|
+
return { pageId, slot: slot as OverlaySlot, itemKey };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Reads `OVERLAY_ADDRESS_PARAM` out of a URL search string (e.g. `window.location.search`);
|
|
67
|
+
* `undefined` when the param is absent or its value doesn't decode. A trailing `#fragment` is
|
|
68
|
+
* dropped first — `URLSearchParams` has no notion of a URL fragment, so without this a caller
|
|
69
|
+
* that (like `overlayViewportHrefSuffix`'s own combined query+hash string) passes one along
|
|
70
|
+
* would see it swallowed into the last param's value instead of ignored. */
|
|
71
|
+
export function overlayAddressFromSearch(search: string): OverlayAddress | undefined {
|
|
72
|
+
const raw = new URLSearchParams(search.split('#')[0]).get(OVERLAY_ADDRESS_PARAM);
|
|
73
|
+
return raw ? decodeOverlayAddress(raw) : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function overlayAddressesEqual(a: OverlayAddress, b: OverlayAddress): boolean {
|
|
77
|
+
return a.pageId === b.pageId && a.slot === b.slot && a.itemKey === b.itemKey;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The query + hash suffix that boots the SAME app entry directly into this address's
|
|
81
|
+
* selected-document mode: the overlay param (so the `BoundedOverlayViewport` instance that
|
|
82
|
+
* matches this address renders its children inline instead of framing itself again) plus the
|
|
83
|
+
* page's own routing hash (so `CatalogShell`'s existing hash-based routing still resolves the
|
|
84
|
+
* right `SectionDef`/providers before `findOverlayNode` narrows further). */
|
|
85
|
+
export function overlayViewportHrefSuffix(address: OverlayAddress): string {
|
|
86
|
+
return `?${OVERLAY_ADDRESS_PARAM}=${encodeURIComponent(encodeOverlayAddress(address))}#${encodeURIComponent(address.pageId)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `'inline'`: render the wrapped demo directly, no frame — either this isn't a web runtime (no
|
|
90
|
+
* same-origin child-document mechanism to offer; see `BoundedOverlayViewport`'s own doc comment)
|
|
91
|
+
* or `currentSearch` already names this exact address, meaning this instance IS the selected
|
|
92
|
+
* child document's own addressed specimen and must not frame itself again (recursion guard).
|
|
93
|
+
* `'frame'`: open `href` — the current pathname plus this address's query+hash — in a bounded
|
|
94
|
+
* child document. */
|
|
95
|
+
export type OverlayViewportDecision = { mode: 'inline' } | { mode: 'frame'; href: string };
|
|
96
|
+
|
|
97
|
+
export function resolveOverlayViewport(args: {
|
|
98
|
+
address: OverlayAddress;
|
|
99
|
+
isWeb: boolean;
|
|
100
|
+
/** The hosting document's current `location.search` (e.g. `window.location.search`). */
|
|
101
|
+
currentSearch: string;
|
|
102
|
+
/** The hosting document's current `location.pathname` — same pathname the child document loads,
|
|
103
|
+
* since this viewer is a single-page app whose page lives in the hash, not the path. */
|
|
104
|
+
pathname: string;
|
|
105
|
+
}): OverlayViewportDecision {
|
|
106
|
+
if (!args.isWeb) return { mode: 'inline' };
|
|
107
|
+
const current = overlayAddressFromSearch(args.currentSearch);
|
|
108
|
+
if (current && overlayAddressesEqual(current, args.address)) return { mode: 'inline' };
|
|
109
|
+
return { mode: 'frame', href: `${args.pathname}${overlayViewportHrefSuffix(args.address)}` };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The authored `node` an `OverlayAddress` points at, or `undefined` for a stale/unknown address
|
|
113
|
+
* (a removed example, a renamed key, a typo) — `CatalogShell`'s chromeless selected-document
|
|
114
|
+
* render shows an honest "unavailable" recovery rather than rendering nothing silently. */
|
|
115
|
+
export function findOverlayNode(sections: readonly SectionDef<string>[], address: OverlayAddress): React.ReactNode | undefined {
|
|
116
|
+
const def = sections.find((section) => section.id === address.pageId);
|
|
117
|
+
if (!def) return undefined;
|
|
118
|
+
if (address.slot === 'variants') return def.variants?.items.find((item) => item.key === address.itemKey)?.node;
|
|
119
|
+
if (address.slot === 'states') return def.states?.items.find((item) => item.key === address.itemKey)?.node;
|
|
120
|
+
// A `render()` page's preview (one per page; `itemKey` is always 'preview').
|
|
121
|
+
if (address.slot === 'preview') return def.render?.();
|
|
122
|
+
// Matched by recomputing each cell's own canonical key, never by splitting `address.itemKey` —
|
|
123
|
+
// a row/column key may itself contain any delimiter this module could pick.
|
|
124
|
+
return def.comparison?.cells.find((cell) => comparisonCellItemKey(cell.rowKey, cell.columnKey) === address.itemKey)?.node;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The canonical, unambiguous `itemKey` for one comparison cell's row+column — this module's own
|
|
128
|
+
* counterpart to `comparison.ts`'s `cellKey` (not imported from there: a VALUE import between two
|
|
129
|
+
* pure `.ts` logic modules here can't carry the `.ts` extension that both Node's direct test run
|
|
130
|
+
* and `kit-host`'s bundler-mode typechecker require/reject at once — `import type` is exempt
|
|
131
|
+
* since it is fully erased, which is why `./types.ts` above is fine). Exported so a `.tsx` call
|
|
132
|
+
* site building a `'comparison'` `OverlayAddress` constructs the exact same key this module looks
|
|
133
|
+
* it up by — a plain `"${rowKey}:${columnKey}"` join is ambiguous whenever a row or column key
|
|
134
|
+
* contains a colon itself. */
|
|
135
|
+
export function comparisonCellItemKey(rowKey: string, columnKey: string): string {
|
|
136
|
+
return `${rowKey}${columnKey}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Usable CSS pixel dimensions for a bounded overlay viewport: finite and strictly positive.
|
|
140
|
+
* `NaN`/`Infinity`/zero/negative (a measurement that hasn't settled yet, a miscomputed layout
|
|
141
|
+
* value, an author typo) must never reach `BoundedOverlayViewport`'s iframe — an iframe sized to
|
|
142
|
+
* a meaningless bound is worse than no iframe, since it still renders (and can still mount the
|
|
143
|
+
* wrapped demo's overlay) at some browser-decided fallback size instead of honestly reporting
|
|
144
|
+
* unavailable. */
|
|
145
|
+
export function isFiniteOverlaySize(width: number, height: number): boolean {
|
|
146
|
+
return Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The framed stage's own box dimensions. `height` is exact — the production overlay's full
|
|
150
|
+
* usable height is never renegotiated by its owner. `width` is only a MAXIMUM: `'100%'` lets the
|
|
151
|
+
* stage fill and shrink to whatever its real owning specimen cell actually gives it, capped at
|
|
152
|
+
* `width` when the owner has at least that much room. A fixed pixel width here previously let the
|
|
153
|
+
* stage overflow a narrower owner (e.g. a 240px comparison cell at a narrow browser width) while
|
|
154
|
+
* that owner's own `overflow: hidden` silently clipped the excess — a passing-looking but false
|
|
155
|
+
* containment, not the honest unavailable/shrink this design requires. */
|
|
156
|
+
export function overlayStageDimensions(width: number, height: number): { width: '100%'; maxWidth: number; height: number } {
|
|
157
|
+
return { width: '100%', maxWidth: width, height };
|
|
158
|
+
}
|
|
@@ -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,129 @@ 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';
|
|
91
|
+
|
|
92
|
+
/** A `fill` example stretches to its cell, but one with its own fixed width (a 160-point skeleton
|
|
93
|
+
* bar) stays that width; it should then sit in the middle of the cell, not against its leading
|
|
94
|
+
* edge. True when the measured example is narrower than its wrapper (by more than rounding). */
|
|
95
|
+
export function shouldCenterFilledContent(contentWidth: number, wrapperWidth: number): boolean {
|
|
96
|
+
if (!(contentWidth > 0) || !(wrapperWidth > 0)) return false;
|
|
97
|
+
return contentWidth < wrapperWidth - 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The width of what's actually visible in an example: from the leftmost to the rightmost painted
|
|
101
|
+
* box (text, an icon, anything with a fill), ignoring transparent layout wrappers that may span
|
|
102
|
+
* the whole cell. */
|
|
103
|
+
export function paintedExtent(boxes: readonly { left: number; right: number; painted: boolean }[]): number {
|
|
104
|
+
let left = Infinity;
|
|
105
|
+
let right = -Infinity;
|
|
106
|
+
for (const box of boxes) {
|
|
107
|
+
if (!box.painted || box.right <= box.left) continue;
|
|
108
|
+
left = Math.min(left, box.left);
|
|
109
|
+
right = Math.max(right, box.right);
|
|
110
|
+
}
|
|
111
|
+
return Number.isFinite(left) ? right - left : 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** True when an example is wider than its cell (beyond rounding): its edges are cut off or touch
|
|
115
|
+
* the neighbouring cell, so the page should use a wider `specimenSize`. */
|
|
116
|
+
export function overflowsCell(contentWidth: number, cellWidth: number): boolean {
|
|
117
|
+
return contentWidth > 0 && cellWidth > 0 && contentWidth > cellWidth + 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One element of an example as measured in the browser. `clips` is true for a container that
|
|
121
|
+
* hides or scrolls its overflow (a scrolling row, a device frame). */
|
|
122
|
+
export interface LayoutBox {
|
|
123
|
+
left: number;
|
|
124
|
+
right: number;
|
|
125
|
+
painted: boolean;
|
|
126
|
+
clips: boolean;
|
|
127
|
+
children: LayoutBox[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The boxes a reader can actually see: a clipping container counts as its own box, and what it
|
|
131
|
+
* hides or scrolls inside it is skipped. */
|
|
132
|
+
export function visibleBoxes(root: LayoutBox): { left: number; right: number; painted: boolean }[] {
|
|
133
|
+
const out: { left: number; right: number; painted: boolean }[] = [];
|
|
134
|
+
const walk = (box: LayoutBox, isRoot: boolean) => {
|
|
135
|
+
if (!isRoot && box.clips) {
|
|
136
|
+
out.push({ left: box.left, right: box.right, painted: true });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!isRoot) out.push({ left: box.left, right: box.right, painted: box.painted });
|
|
140
|
+
for (const child of box.children) walk(child, false);
|
|
141
|
+
};
|
|
142
|
+
walk(root, true);
|
|
143
|
+
return out;
|
|
144
|
+
}
|
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"). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krapwoo/ds-viewer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A browsable, always-current catalog for an Expo / React Native design system.",
|
|
@@ -65,9 +65,10 @@
|
|
|
65
65
|
"test": "node --experimental-strip-types --test 'cli/**/*.test.ts' 'config/**/*.test.ts' 'native/catalog/__tests__/*.test.ts'",
|
|
66
66
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
67
67
|
"kit:dev": "npm run build && cd kit-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js dev",
|
|
68
|
-
"
|
|
69
|
-
"check:
|
|
70
|
-
"check:
|
|
68
|
+
"viewer:dev": "npm run build && cd viewer-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js dev",
|
|
69
|
+
"check:catalog": "npm run build && node scripts/checkCatalogConsole.mjs kit-host && node scripts/checkCatalogConsole.mjs viewer-host",
|
|
70
|
+
"check:doctor": "npm run build && cd kit-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js doctor --ci && cd ../viewer-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js doctor --ci",
|
|
71
|
+
"check:types": "tsc --noEmit -p kit-host && tsc --noEmit -p viewer-host",
|
|
71
72
|
"release": "node scripts/release.mjs"
|
|
72
73
|
}
|
|
73
74
|
}
|
|
@@ -5,7 +5,7 @@ import { DS_SEMANTIC } from '../../tokens';
|
|
|
5
5
|
|
|
6
6
|
export default defineCatalogPage({
|
|
7
7
|
component: 'AnimatedChevron',
|
|
8
|
-
group: '
|
|
8
|
+
group: 'Components',
|
|
9
9
|
description: 'A chevron that morphs between down (collapsed) and up (expanded) — flipping vertically in place instead of rotating through a sideways-pointing angle, and instead of swapping icons. The morph Banner\'s own collapsible header uses for its disclosure indicator. Not commonly used on its own; exported for building a custom disclosure/accordion toggle.',
|
|
10
10
|
a11y: 'Purely decorative — no accessibility role of its own. Wrap it in an accessible parent (the way Banner\'s own header Pressable does) if the toggle needs to be announced.',
|
|
11
11
|
variants: {
|
|
@@ -31,7 +31,7 @@ const AVATAR_COMPARISON = grid(
|
|
|
31
31
|
|
|
32
32
|
export default defineCatalogPage({
|
|
33
33
|
component: 'Avatar',
|
|
34
|
-
group: '
|
|
34
|
+
group: 'Components',
|
|
35
35
|
specimenSize: 'compact',
|
|
36
36
|
comparison: AVATAR_COMPARISON,
|
|
37
37
|
description: 'A circular image, or an initials fallback on a solid fill when there\'s no image (or it fails to load).',
|
|
@@ -51,7 +51,7 @@ const BADGE_COMPARISON = grid(
|
|
|
51
51
|
|
|
52
52
|
export default defineCatalogPage({
|
|
53
53
|
component: 'Badge',
|
|
54
|
-
group: '
|
|
54
|
+
group: 'Components',
|
|
55
55
|
specimenSize: 'compact',
|
|
56
56
|
comparison: BADGE_COMPARISON,
|
|
57
57
|
description: 'A small status chip — five semantic variants, with optional leading/trailing icons or icon-only.',
|
|
@@ -4,7 +4,11 @@ import { Banner } from './Banner';
|
|
|
4
4
|
|
|
5
5
|
export default defineCatalogPage({
|
|
6
6
|
component: 'Banner',
|
|
7
|
-
group: '
|
|
7
|
+
group: 'Components',
|
|
8
|
+
specimenSize: 'regular',
|
|
9
|
+
composedOf: [
|
|
10
|
+
{ component: 'AnimatedChevron', role: "The expand/collapse chevron in the header of a collapsible banner.", relationship: 'built-in' },
|
|
11
|
+
],
|
|
8
12
|
description: 'An inline callout for status/announcements — five semantic variants, optional collapsible body, inline link, and action button.',
|
|
9
13
|
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
14
|
a11y: 'When onPress/action is set the header/button are Pressables; the collapsible header toggles the description with a chevron affordance.',
|