@krapwoo/ds-viewer 0.14.2 → 0.16.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 +249 -3
- package/config/index.ts +3 -0
- package/dist/cli/config.js +9 -1
- package/dist/cli/doctor.js +452 -19
- package/dist/cli/explain.js +26 -0
- package/dist/cli/staticPage.js +99 -10
- package/dist/cli/tokenDefinition.js +125 -0
- package/dist/native/catalog/catalogNavigation.js +27 -11
- package/dist/native/catalog/pageApi.js +557 -26
- package/dist/native/catalog/tokenDefinition.js +205 -0
- package/native/catalog/CatalogSearchInput.tsx +18 -3
- package/native/catalog/CatalogShell.tsx +318 -46
- package/native/catalog/CatalogSidebar.tsx +37 -1
- package/native/catalog/FamilyPage.tsx +567 -137
- package/native/catalog/GroupPage.tsx +230 -0
- package/native/catalog/JumpRow.tsx +81 -0
- package/native/catalog/ReferenceDetails.tsx +144 -16
- package/native/catalog/SectionBlock.tsx +49 -5
- package/native/catalog/SourceReferencePicker.tsx +180 -0
- package/native/catalog/StructuredTokenPage.tsx +592 -0
- package/native/catalog/Swatch.tsx +29 -2
- package/native/catalog/TokenLayouts.tsx +30 -9
- package/native/catalog/catalogNavigation.ts +30 -10
- package/native/catalog/catalogReferences.ts +212 -0
- package/native/catalog/familyModel.ts +10 -4
- package/native/catalog/index.ts +22 -0
- package/native/catalog/pageApi.ts +598 -23
- package/native/catalog/tokenDefinition.ts +430 -0
- package/native/catalog/types.ts +77 -5
- package/package.json +1 -1
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +1 -0
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +1 -0
- package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +1 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +1 -0
- package/starter-kit/components/Divider/Divider.catalog.tsx +1 -0
- package/starter-kit/components/Pill/Pill.catalog.tsx +1 -0
- package/starter-kit/components/TopNav/TopNav.catalog.tsx +1 -0
|
@@ -18,7 +18,18 @@ function useWidth(): [number, (event: LayoutChangeEvent) => void] {
|
|
|
18
18
|
* in up to `columns` columns. A `wide` section takes a full row; the page drops to fewer columns
|
|
19
19
|
* rather than squeeze a section under its minimum width.
|
|
20
20
|
*/
|
|
21
|
-
export function TokenSections({
|
|
21
|
+
export function TokenSections({
|
|
22
|
+
sections, columns, label, onSectionNode,
|
|
23
|
+
}: {
|
|
24
|
+
sections: TokenSection[];
|
|
25
|
+
columns: 1 | 2 | 3;
|
|
26
|
+
label?: string;
|
|
27
|
+
/** #64 B2/B5: called with each section's own outer node (web: the real DOM node) as it mounts,
|
|
28
|
+
* and `undefined` as it unmounts — keyed by the same id/title React already keys the section
|
|
29
|
+
* by. Lets a caller (the jump row, or a section-alias arrival) scroll a given section into view.
|
|
30
|
+
* Omitted (every existing caller) leaves every section's own rendering untouched. */
|
|
31
|
+
onSectionNode?: (key: string, node: unknown) => void;
|
|
32
|
+
}) {
|
|
22
33
|
const [width, onLayout] = useWidth();
|
|
23
34
|
const perRow = sectionColumns(width, columns);
|
|
24
35
|
// Degrading quietly is how a page ends up claiming a column count it never renders: the author
|
|
@@ -35,13 +46,23 @@ export function TokenSections({ sections, columns, label }: { sections: TokenSec
|
|
|
35
46
|
const columnWidth = perRow === 1 ? width : (width - TOKEN_LAYOUT_GAP * (perRow - 1)) / perRow;
|
|
36
47
|
return (
|
|
37
48
|
<View onLayout={onLayout} style={styles.sections}>
|
|
38
|
-
{width > 0 && sections.map((section) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
{width > 0 && sections.map((section) => {
|
|
50
|
+
// #61: `section.id` when the author set one (two sections may legitimately share a human
|
|
51
|
+
// `title`, e.g. two scoped "Palette" cards — `TokenSection.id`'s own doc comment) — omitted
|
|
52
|
+
// keeps the prior `title`-keyed behavior exactly as it was.
|
|
53
|
+
const key = section.id ?? section.title;
|
|
54
|
+
return (
|
|
55
|
+
<View
|
|
56
|
+
key={key}
|
|
57
|
+
ref={onSectionNode ? (node: unknown) => onSectionNode(key, node) : undefined}
|
|
58
|
+
style={{ width: section.wide || perRow === 1 ? width : columnWidth }}
|
|
59
|
+
>
|
|
60
|
+
<Text style={styles.sectionLabel}>{section.title}</Text>
|
|
61
|
+
{section.desc ? <Text style={styles.sectionDesc}>{section.desc}</Text> : null}
|
|
62
|
+
<View style={styles.card}>{section.render()}</View>
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
45
66
|
</View>
|
|
46
67
|
);
|
|
47
68
|
}
|
|
@@ -64,7 +85,7 @@ export function TokenGrid({ children, minTileWidth = 140, maxColumns }: { childr
|
|
|
64
85
|
* One token in a `TokenGrid`: its preview, then its name and value, then its role. `note` says
|
|
65
86
|
* where or why the token is used (from its comments or real usages) — omit it when there is none.
|
|
66
87
|
*/
|
|
67
|
-
export function TokenTile({ name, value, note, children }: { name: string; value?: string; note?:
|
|
88
|
+
export function TokenTile({ name, value, note, children }: { name: string; value?: string; note?: React.ReactNode; children: React.ReactNode }) {
|
|
68
89
|
return (
|
|
69
90
|
<View style={styles.tile}>
|
|
70
91
|
<View style={styles.tilePreview}>{children}</View>
|
|
@@ -157,26 +157,42 @@ export function resolveActiveFromHash<TId extends string>(
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/** Where an alias or member id resolves: the family page `pageId`, and — unless it names the
|
|
160
|
-
* whole itself — the specific `memberId` within it. Built by `aliasTargets`.
|
|
160
|
+
* whole itself — the specific `memberId` within it. Built by `aliasTargets`. #64 B5: a
|
|
161
|
+
* tokenDefinition page's section alias instead sets `sectionId` (never together with
|
|
162
|
+
* `memberId`), so the page can scroll that section into view on arrival. */
|
|
161
163
|
export interface AliasTarget {
|
|
162
164
|
pageId: string;
|
|
163
165
|
memberId?: string;
|
|
166
|
+
sectionId?: string;
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
/** Every member id other than the whole, every member alias, and every family page's own
|
|
167
|
-
* (page-level) alias, mapped to where it resolves (spec §5.3 "Old addresses").
|
|
168
|
-
* `
|
|
170
|
+
* (page-level) alias, mapped to where it resolves (spec §5.3 "Old addresses"). #64 B5: also, a
|
|
171
|
+
* tokenDefinition page's own page-level aliases (`tokenDefinitionAliases`) and each of its
|
|
172
|
+
* sections' own `aliases` — the same routing a family page's aliases already get, extended to
|
|
173
|
+
* tokenDefinition pages. A page with neither contributes nothing. */
|
|
169
174
|
export function aliasTargets<TId extends string>(sections: readonly SectionDef<TId>[]): Map<string, AliasTarget> {
|
|
170
175
|
const targets = new Map<string, AliasTarget>();
|
|
171
176
|
for (const section of sections) {
|
|
172
177
|
const family = section.family;
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
if (family) {
|
|
179
|
+
for (const member of family.members) {
|
|
180
|
+
if (member.id === family.whole) {
|
|
181
|
+
// C3: the whole's own id already resolves via `order` (it equals the page's own id), but
|
|
182
|
+
// its own aliases were never registered anywhere — routing one to the page, with no member
|
|
183
|
+
// focus (unlike a non-whole member's alias, which focuses that member).
|
|
184
|
+
for (const alias of member.aliases ?? []) targets.set(alias, { pageId: section.id });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
targets.set(member.id, { pageId: section.id, memberId: member.id });
|
|
188
|
+
for (const alias of member.aliases ?? []) targets.set(alias, { pageId: section.id, memberId: member.id });
|
|
189
|
+
}
|
|
190
|
+
for (const alias of family.aliases ?? []) targets.set(alias, { pageId: section.id });
|
|
191
|
+
}
|
|
192
|
+
for (const alias of section.tokenDefinitionAliases ?? []) targets.set(alias, { pageId: section.id });
|
|
193
|
+
for (const tdSection of section.tokenDefinition?.sections ?? []) {
|
|
194
|
+
for (const alias of tdSection.aliases ?? []) targets.set(alias, { pageId: section.id, sectionId: tdSection.id });
|
|
178
195
|
}
|
|
179
|
-
for (const alias of family.aliases ?? []) targets.set(alias, { pageId: section.id });
|
|
180
196
|
}
|
|
181
197
|
return targets;
|
|
182
198
|
}
|
|
@@ -185,7 +201,10 @@ export function aliasTargets<TId extends string>(sections: readonly SectionDef<T
|
|
|
185
201
|
* (kept, unchanged, for existing callers), this never silently falls back to the first page: an
|
|
186
202
|
* unknown or malformed fragment is reported as `notFound` instead. */
|
|
187
203
|
export type Route<TId extends string> =
|
|
188
|
-
|
|
204
|
+
// #64 B5: `sectionId` is set only when a tokenDefinition page's own section alias resolved here
|
|
205
|
+
// — the page then scrolls that section into view on arrival. Absent for every other page route,
|
|
206
|
+
// exactly as before.
|
|
207
|
+
| { kind: 'page'; id: TId; sectionId?: string }
|
|
189
208
|
| { kind: 'member'; pageId: TId; memberId: string }
|
|
190
209
|
| { kind: 'update' }
|
|
191
210
|
| { kind: 'notFound'; raw: string }
|
|
@@ -209,6 +228,7 @@ export function resolveRoute<TId extends string>(
|
|
|
209
228
|
const target = aliases.get(decoded);
|
|
210
229
|
if (target) {
|
|
211
230
|
if (target.memberId !== undefined) return { kind: 'member', pageId: target.pageId as TId, memberId: target.memberId };
|
|
231
|
+
if (target.sectionId !== undefined) return { kind: 'page', id: target.pageId as TId, sectionId: target.sectionId };
|
|
212
232
|
return { kind: 'page', id: target.pageId as TId };
|
|
213
233
|
}
|
|
214
234
|
return { kind: 'notFound', raw: decoded };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #60 "Source reference" layer — pure data/navigation foundation (plan Task 1). No React or React
|
|
3
|
+
* Native imports, so it runs under Node's test runner, the same reasoning `catalogNavigation.ts`
|
|
4
|
+
* documents for itself. Everything here derives from the catalog's own registered `SectionDef[]`
|
|
5
|
+
* (and the ordering/alias primitives `catalogNavigation.ts` already owns) — never a second,
|
|
6
|
+
* hand-maintained registry.
|
|
7
|
+
*/
|
|
8
|
+
import type { SectionDef } from './types.ts';
|
|
9
|
+
import type { AliasTarget } from './catalogNavigation.ts';
|
|
10
|
+
|
|
11
|
+
/** True once any page in the catalog opts into the `'reference'` layer — the controller's trigger
|
|
12
|
+
* for showing the separate Source reference affordance at all. A catalog with no such page keeps
|
|
13
|
+
* its present sidebar/page set unchanged. */
|
|
14
|
+
export function hasReferenceNavigation<TId extends string>(sections: readonly SectionDef<TId>[]): boolean {
|
|
15
|
+
return sections.some((section) => section.navigation === 'reference');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Every page id whose own `navigation` is not `'reference'` (omitted counts as `'browse'`) — the
|
|
19
|
+
* browse-facing subset the sidebar, pager, and hash fallback show. Pass this as `orderedIds`'s own
|
|
20
|
+
* `availableIds` to drop reference-only pages from the browse sequence while leaving the complete
|
|
21
|
+
* routing universe (every id, alias, and member address) untouched — a reference page, and every
|
|
22
|
+
* member of a reference family, stays fully routable; only this browse-facing presentation filter
|
|
23
|
+
* ever excludes it. */
|
|
24
|
+
export function browsableIds<TId extends string>(sections: readonly SectionDef<TId>[]): Set<TId> {
|
|
25
|
+
const ids = new Set<TId>();
|
|
26
|
+
for (const section of sections) if (section.navigation !== 'reference') ids.add(section.id);
|
|
27
|
+
return ids;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One address the catalog-wide Source reference search can find: a whole page, or (for a family
|
|
31
|
+
* page) one of its non-whole members — the whole is never listed twice (see `aliasTargets`'s own
|
|
32
|
+
* precedent of skipping the whole among member addresses). `title` is the page's own displayed
|
|
33
|
+
* title for a page-level entry, or the member's own id for a member entry (a family member has no
|
|
34
|
+
* separate display title of its own, the same convention `MemberSearchEntry`'s `label` already
|
|
35
|
+
* uses). `component` is set only from an actual detected binding (`SectionDef.component` /
|
|
36
|
+
* `CatalogMember.component`), never guessed. */
|
|
37
|
+
export interface ReferenceEntry<TId extends string = string> {
|
|
38
|
+
pageId: TId;
|
|
39
|
+
/** Present only for a family member other than its own whole. */
|
|
40
|
+
memberId?: string;
|
|
41
|
+
title: string;
|
|
42
|
+
component?: string;
|
|
43
|
+
aliases: readonly string[];
|
|
44
|
+
/** Authored declaration source, when distinct from the detected implementation path. */
|
|
45
|
+
source?: string;
|
|
46
|
+
path: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Builds the catalog-wide reference index from the registered `SectionDef[]` — one entry per
|
|
50
|
+
* page (its own id/title/component/path, plus its family-level aliases, when any), and one per
|
|
51
|
+
* non-whole family member (its own id/component/aliases/path). Never a second, hand-authored
|
|
52
|
+
* registry: every field here already exists on the runtime catalog record. */
|
|
53
|
+
export function buildReferenceIndex<TId extends string>(sections: readonly SectionDef<TId>[]): ReferenceEntry<TId>[] {
|
|
54
|
+
const entries: ReferenceEntry<TId>[] = [];
|
|
55
|
+
for (const section of sections) {
|
|
56
|
+
const family = section.family;
|
|
57
|
+
// Family members carry the implementation binding/path and authored declaration source;
|
|
58
|
+
// section.path may instead point to the catalog page. Keep the whole in the single page entry,
|
|
59
|
+
// retaining both locations when they differ. Documentation-only wholes keep no binding.
|
|
60
|
+
// Merge aliases without mutating either authored array.
|
|
61
|
+
const whole = family?.members.find((member) => member.id === family.whole);
|
|
62
|
+
entries.push({
|
|
63
|
+
pageId: section.id,
|
|
64
|
+
title: section.title ?? section.id,
|
|
65
|
+
component: whole?.component ?? section.component,
|
|
66
|
+
aliases: [...new Set([...(family?.aliases ?? []), ...(whole?.aliases ?? [])])],
|
|
67
|
+
path: whole?.path ?? section.path,
|
|
68
|
+
...(whole?.source && whole.source !== (whole.path ?? section.path) ? { source: whole.source } : {}),
|
|
69
|
+
});
|
|
70
|
+
if (!family) continue;
|
|
71
|
+
for (const member of family.members) {
|
|
72
|
+
if (member.id === family.whole) continue; // the whole already has its own page-level entry above
|
|
73
|
+
entries.push({
|
|
74
|
+
pageId: section.id,
|
|
75
|
+
memberId: member.id,
|
|
76
|
+
title: member.id,
|
|
77
|
+
component: member.component,
|
|
78
|
+
aliases: member.aliases ?? [],
|
|
79
|
+
path: member.path,
|
|
80
|
+
...(member.source !== member.path ? { source: member.source } : {}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Reference search hits (spec #60's "Search IDs, human titles, component names where known,
|
|
88
|
+
* aliases and source paths"): case-insensitive substring over every one of those fields. Mirrors
|
|
89
|
+
* `catalogNavigation.ts`'s own `searchResults`/`filterGroups` convention — trimmed, empty query
|
|
90
|
+
* returns no hits. */
|
|
91
|
+
export function searchReferenceIndex<TId extends string>(
|
|
92
|
+
entries: readonly ReferenceEntry<TId>[],
|
|
93
|
+
query: string,
|
|
94
|
+
): ReferenceEntry<TId>[] {
|
|
95
|
+
const q = query.trim().toLowerCase();
|
|
96
|
+
if (!q) return [];
|
|
97
|
+
return entries.filter((entry) =>
|
|
98
|
+
entry.pageId.toLowerCase().includes(q)
|
|
99
|
+
|| (entry.memberId?.toLowerCase().includes(q) ?? false)
|
|
100
|
+
|| entry.title.toLowerCase().includes(q)
|
|
101
|
+
|| (entry.component?.toLowerCase().includes(q) ?? false)
|
|
102
|
+
|| entry.aliases.some((alias) => alias.toLowerCase().includes(q))
|
|
103
|
+
|| (entry.source?.toLowerCase().includes(q) ?? false)
|
|
104
|
+
|| entry.path.toLowerCase().includes(q));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Where a name (not a URL fragment — see `catalogNavigation.ts`'s own `resolveRoute` for that)
|
|
108
|
+
* resolves, for #60's reference journey: a real address, found to be claimed by more than one
|
|
109
|
+
* entry's component binding (never guessed — the caller must say so rather than silently picking
|
|
110
|
+
* one), or not found at all. */
|
|
111
|
+
export type ReferenceResolution<TId extends string> =
|
|
112
|
+
| { kind: 'address'; pageId: TId; memberId?: string }
|
|
113
|
+
| { kind: 'ambiguous' }
|
|
114
|
+
| { kind: 'unresolved' };
|
|
115
|
+
|
|
116
|
+
/** Resolves `name` to its one catalog address: an exact registered page id or member/page alias
|
|
117
|
+
* always wins first (reusing the same `order`/`aliases` the viewer's own routing already derives
|
|
118
|
+
* — see `catalogNavigation.ts`'s `orderedIds`/`aliasTargets`, never duplicated here), falling back
|
|
119
|
+
* to the one unambiguous reference-index entry whose own `component` binding names it. Lets a
|
|
120
|
+
* normal page be found by its component export even when that name differs from the page's own
|
|
121
|
+
* id — but never invents a new global URL alias from it: a component name shared by more than one
|
|
122
|
+
* entry resolves to `'ambiguous'`, not a guess. */
|
|
123
|
+
export function resolveReference<TId extends string>(
|
|
124
|
+
name: string,
|
|
125
|
+
order: readonly TId[],
|
|
126
|
+
aliases: ReadonlyMap<string, AliasTarget>,
|
|
127
|
+
entries: readonly ReferenceEntry<TId>[],
|
|
128
|
+
): ReferenceResolution<TId> {
|
|
129
|
+
if ((order as readonly string[]).includes(name)) return { kind: 'address', pageId: name as TId };
|
|
130
|
+
const aliasTarget = aliases.get(name);
|
|
131
|
+
if (aliasTarget) return { kind: 'address', pageId: aliasTarget.pageId as TId, memberId: aliasTarget.memberId };
|
|
132
|
+
const matches = entries.filter((entry) => entry.component === name);
|
|
133
|
+
if (matches.length === 0) return { kind: 'unresolved' };
|
|
134
|
+
if (matches.length > 1) return { kind: 'ambiguous' };
|
|
135
|
+
const [match] = matches;
|
|
136
|
+
return { kind: 'address', pageId: match.pageId, memberId: match.memberId };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Task 2B2: the smallest shared contract `CatalogShell` hands its own ordinary/family renderers
|
|
140
|
+
* (`SectionBlock` → `ReferenceDetails`, `FamilyPage`) so an existing reference fact (a Composition
|
|
141
|
+
* name, an "also used by" user, a `related`/anatomy name) can become a real, canonical,
|
|
142
|
+
* activatable link — through the one accepted resolver above, never a second lookup. Explicit
|
|
143
|
+
* props, not a global context: a standalone demo renderer (this very framework's own catalog
|
|
144
|
+
* pages included) never receives one unless its own caller passes it, so it can never accidentally
|
|
145
|
+
* inherit an outer catalog's registry. Omitted everywhere it's optional — a caller with no
|
|
146
|
+
* `navigation` keeps its own prior resolution/activation default untouched (see each call site's
|
|
147
|
+
* own doc comment, e.g. `FamilyPage`'s `resolvesAddress`). */
|
|
148
|
+
export interface ReferenceNavigation<TId extends string = string> {
|
|
149
|
+
/** Resolves `name` to its one real catalog address through `resolveReference`'s own order —
|
|
150
|
+
* `undefined` for `'ambiguous'`/`'unresolved'` (never a guess). Method syntax (not a
|
|
151
|
+
* property-typed arrow function), deliberately, so a `ReferenceNavigation<TId>` built with the
|
|
152
|
+
* catalog's own real union type stays assignable to the plain `ReferenceNavigation` (its default
|
|
153
|
+
* `TId = string`) every non-generic internal renderer below this file declares — `FamilyPage.tsx`
|
|
154
|
+
* already keeps the same page/member ids as plain `string` once past its own top-level props. */
|
|
155
|
+
resolve(name: string): { pageId: TId; memberId?: string } | undefined;
|
|
156
|
+
/** Opens the resolved address through the shell's own real activation, recording the real
|
|
157
|
+
* page/member origin and focusing the destination exactly like every other navigation.
|
|
158
|
+
* `sourceMemberId`, when supplied, names the family member (or whole) that actually owns the
|
|
159
|
+
* activated reference (e.g. a Part's own Uses/related/"also used by" line) — the shell validates
|
|
160
|
+
* it against the current page's own family before trusting it, so the recorded origin is the
|
|
161
|
+
* part itself rather than just the page. Omitted (a page-level reference, e.g. `relatedPages` or
|
|
162
|
+
* an anatomy "other page" entry): the origin is the page alone. */
|
|
163
|
+
open(pageId: TId, memberId?: string, sourceMemberId?: string): void;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `name`'s resolved activation — its real target address plus the activation handler — or
|
|
167
|
+
* `undefined` when `navigation` is omitted (the caller's own prior default; never fabricate one)
|
|
168
|
+
* or `name` doesn't resolve (honest plain text). One path for every call site that today repeats
|
|
169
|
+
* its own "resolve, then build a link" — `ReferenceDetails.tsx`'s Composition facts, and
|
|
170
|
+
* `FamilyPage.tsx`'s below-specimens/whole/uses/related-page/anatomy links.
|
|
171
|
+
*
|
|
172
|
+
* Deliberately returns the target, not a ready-made `href` string: this file's own cross-module
|
|
173
|
+
* imports (`catalogNavigation.ts`) stay type-only (see the `.ts`-suffixed `import type` above),
|
|
174
|
+
* which is what lets it run both under Node's direct `--experimental-strip-types` test execution
|
|
175
|
+
* (which requires that suffix on a relative import) and under the published package's own
|
|
176
|
+
* consumer-facing bundler typecheck (which forbids a *value* import with that same suffix,
|
|
177
|
+
* `allowImportingTsExtensions` being off) — a plain value import of `hashForId` here could only
|
|
178
|
+
* ever satisfy one of the two. Every caller is already a `.tsx` file with its own ordinary,
|
|
179
|
+
* extension-less `hashForId` import, so building the real `href` (`hashForId(memberId ??
|
|
180
|
+
* pageId)`) stays one line at each call site rather than a second, duplicated implementation. */
|
|
181
|
+
/** A modified click (open in a new tab/window) or a non-primary button must reach the browser's
|
|
182
|
+
* own anchor handling untouched — the same check `SourceReferencePicker.tsx`'s own `ResultRow`
|
|
183
|
+
* already makes for its result links, duplicated here (rather than exported from there) because
|
|
184
|
+
* that file is a React component this pure module never imports. */
|
|
185
|
+
function isModifiedPress(event: unknown): boolean {
|
|
186
|
+
const e = event as { metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; button?: number } | null | undefined;
|
|
187
|
+
return Boolean(e?.metaKey || e?.ctrlKey || e?.shiftKey || (typeof e?.button === 'number' && e.button !== 0));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function referenceLinkProps<TId extends string>(
|
|
191
|
+
navigation: ReferenceNavigation<TId> | undefined,
|
|
192
|
+
name: string,
|
|
193
|
+
/** The family member (or whole) that owns this particular reference, when the call site is
|
|
194
|
+
* scoped to one — see `ReferenceNavigation.open`'s own doc comment. */
|
|
195
|
+
sourceMemberId?: string,
|
|
196
|
+
): { pageId: TId; memberId?: string; onPress: (event?: { preventDefault?: () => void; metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; button?: number }) => void } | undefined {
|
|
197
|
+
if (!navigation) return undefined;
|
|
198
|
+
const target = navigation.resolve(name);
|
|
199
|
+
if (!target) return undefined;
|
|
200
|
+
return {
|
|
201
|
+
pageId: target.pageId,
|
|
202
|
+
memberId: target.memberId,
|
|
203
|
+
onPress: (event) => {
|
|
204
|
+
if (isModifiedPress(event)) return; // let the browser open it in a new tab/window
|
|
205
|
+
// The shell owns normal activation and its own URL synchronization — the native anchor
|
|
206
|
+
// default must never race it, embedded or not (an embedded shell additionally never lets
|
|
207
|
+
// that synchronization touch the host document's own address).
|
|
208
|
+
event?.preventDefault?.();
|
|
209
|
+
navigation.open(target.pageId, target.memberId, sourceMemberId);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -53,9 +53,13 @@ export function anatomyPinFor(
|
|
|
53
53
|
outline: ReadonlyMap<string, { number: string }>,
|
|
54
54
|
): { number: string; dashed: boolean } | undefined {
|
|
55
55
|
const member = members.get(memberId);
|
|
56
|
-
if (!member) return undefined;
|
|
57
56
|
const entry = outline.get(memberId);
|
|
58
57
|
if (!entry) return undefined;
|
|
58
|
+
// #63: an external part is numbered in this family's outline but documented on a group page, so
|
|
59
|
+
// it isn't in `members`; it keeps its number. Callers that know the owner's fidelity pass it in
|
|
60
|
+
// `members` (`FamilyDef.externalParts`), so the pin is dashed exactly as an own part's would be;
|
|
61
|
+
// with none, the pin is the plain solid one.
|
|
62
|
+
if (!member) return { number: entry.number, dashed: false };
|
|
59
63
|
return { number: entry.number, dashed: member.fidelity === 'documentation-only' };
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -171,9 +175,11 @@ export function sectionFor(sections: readonly CatalogMemberSection[], memberId:
|
|
|
171
175
|
* bad data (a `whole` that isn't one of its own members, already reported by `validateFamily`),
|
|
172
176
|
* so `FamilyPage` can skip rendering the whole block instead of crashing on an absent member.
|
|
173
177
|
* Generic over the member's own shape so `FamilyPage` keeps its resolved `props`/`path` fields
|
|
174
|
-
* (`FamilyDef.members`'s own type) through the lookup.
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
* (`FamilyDef.members`'s own type) through the lookup. #63: `whole` is `undefined` for a group
|
|
179
|
+
* page's own `FamilyDef` — degrades the same way, `undefined`, so a group page never renders a
|
|
180
|
+
* whole block either (`FamilyPage` itself still never dispatches to a group page in this slice). */
|
|
181
|
+
export function resolveWhole<T>(members: ReadonlyMap<string, T>, whole: string | undefined): T | undefined {
|
|
182
|
+
return whole === undefined ? undefined : members.get(whole);
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
/** V2c item 1 (one-shot jump) + item 11 (repeat activation): in-flight state for a member-address
|
package/native/catalog/index.ts
CHANGED
|
@@ -18,6 +18,9 @@ export { CatalogSearchInput } from './CatalogSearchInput';
|
|
|
18
18
|
export { SectionBlock, PagerButton, SectionPagerRow } from './SectionBlock';
|
|
19
19
|
export type { SectionPager } from './SectionBlock';
|
|
20
20
|
export { FamilyPage, AnatomyPin } from './FamilyPage';
|
|
21
|
+
export { GroupPage } from './GroupPage';
|
|
22
|
+
export { JumpRow, JumpChip } from './JumpRow';
|
|
23
|
+
export type { JumpItem } from './JumpRow';
|
|
21
24
|
export { ComparisonGrid } from './ComparisonGrid';
|
|
22
25
|
export { grid, axisItems, axisProp } from './comparison';
|
|
23
26
|
export { ComparisonGroups } from './ComparisonGroups';
|
|
@@ -30,12 +33,31 @@ export { MotionSpecimen } from './MotionSpecimen';
|
|
|
30
33
|
export type { MotionSpecimenProps, MotionSpecimenSpringConfig } from './MotionSpecimen';
|
|
31
34
|
export { SpecimenSurface } from './SpecimenSurface';
|
|
32
35
|
export { TokenSections, TokenGrid, TokenTile } from './TokenLayouts';
|
|
36
|
+
export { StructuredTokenPage, tokenDefinitionSections } from './StructuredTokenPage';
|
|
37
|
+
export {
|
|
38
|
+
TOKEN_KINDS, isTokenKind, canonicalTokenTitle, findDuplicateIds, tokenEntryAddress,
|
|
39
|
+
validateTokenDefinition, validateTokenDefinitionUsage,
|
|
40
|
+
} from './tokenDefinition';
|
|
41
|
+
export type {
|
|
42
|
+
TokenKind, TokenProvenance, TokenResolution, TokenValueInfo, TokenEntryBase, TokenEntry,
|
|
43
|
+
ColorTokenValue, ColorTokenEntry, DimensionTokenValue, SpacingTokenEntry, RadiusTokenEntry,
|
|
44
|
+
DimensionsTokenEntry, OpacityTokenValue, OpacityTokenEntry, TypographyTokenValue, TypographyTokenEntry,
|
|
45
|
+
ShadowTokenValue, ShadowTokenEntry, MotionTimingValue, MotionSpringValue, MotionNativeExpressionValue,
|
|
46
|
+
MotionTokenValue, MotionTokenEntry, TokenDefinitionSection, TokenDefinition, TokenDefinitionPageUsage,
|
|
47
|
+
} from './tokenDefinition';
|
|
33
48
|
export { DividedStack } from './DividedStack';
|
|
34
49
|
export { Swatch } from './Swatch';
|
|
35
50
|
export { PhoneFrame } from './PhoneFrame';
|
|
36
51
|
export { PhoneScreen } from './PhoneScreen';
|
|
37
52
|
export { OverlayDemo } from './OverlayDemo';
|
|
38
53
|
export { CatalogButton } from './CatalogButton';
|
|
54
|
+
export { SourceReferencePicker } from './SourceReferencePicker';
|
|
55
|
+
export type { SourceReferencePickerProps } from './SourceReferencePicker';
|
|
56
|
+
// #60: only the shape a caller needs to pass real (or, for the self-catalog demo, sample)
|
|
57
|
+
// entries into the picker — `buildReferenceIndex`/`searchReferenceIndex`/`resolveReference` stay
|
|
58
|
+
// internal plumbing for `CatalogShell.tsx`'s own Task 2B wiring, the same precedent Task 1 already
|
|
59
|
+
// established for `catalogReferences.ts`'s other exports.
|
|
60
|
+
export type { ReferenceEntry } from './catalogReferences';
|
|
39
61
|
export { BoundedOverlayViewport } from './BoundedOverlayViewport';
|
|
40
62
|
export type { BoundedOverlayViewportProps } from './BoundedOverlayViewport';
|
|
41
63
|
export { comparisonCellItemKey } from './overlayViewport';
|