@estiva-app/ui 0.21.1 → 0.22.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/dist/Avatar.d.ts +3 -0
- package/dist/Avatar.d.ts.map +1 -1
- package/dist/ChipInput.d.ts +3 -0
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/CommandPalette.d.ts +6 -0
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts +3 -0
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Menu.d.ts +7 -0
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Skeleton.d.ts +2 -0
- package/dist/Skeleton.d.ts.map +1 -1
- package/dist/Toast.d.ts +5 -0
- package/dist/Toast.d.ts.map +1 -1
- package/dist/Toolbar.d.ts +4 -0
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +4 -0
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/registry/build-GOVLABI6.js +13 -0
- package/dist/registry/build-GOVLABI6.js.map +7 -0
- package/dist/registry/build.d.ts +29 -0
- package/dist/registry/build.d.ts.map +1 -0
- package/dist/registry/chunk-IJNCYVH4.js +163 -0
- package/dist/registry/chunk-IJNCYVH4.js.map +7 -0
- package/dist/registry/chunk-MRSBS5OP.js +99 -0
- package/dist/registry/chunk-MRSBS5OP.js.map +7 -0
- package/dist/registry/chunk-QDYGB3QN.js +352 -0
- package/dist/registry/chunk-QDYGB3QN.js.map +7 -0
- package/dist/registry/cli.d.ts +2 -0
- package/dist/registry/cli.d.ts.map +1 -0
- package/dist/registry/cli.js +121 -0
- package/dist/registry/cli.js.map +7 -0
- package/dist/registry/find.d.ts +43 -0
- package/dist/registry/find.d.ts.map +1 -0
- package/dist/registry/index.d.ts +14 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +26 -0
- package/dist/registry/index.js.map +7 -0
- package/dist/registry/schema.d.ts +147 -0
- package/dist/registry/schema.d.ts.map +1 -0
- package/package.json +15 -2
- package/registry.json +4470 -0
- package/src/Avatar.tsx +3 -0
- package/src/ChipInput.tsx +3 -0
- package/src/CommandPalette.tsx +6 -0
- package/src/IdentityMenu.tsx +3 -0
- package/src/Menu.tsx +7 -0
- package/src/Skeleton.tsx +2 -0
- package/src/Toast.tsx +5 -0
- package/src/Toolbar.tsx +4 -0
- package/src/Tooltip.tsx +4 -0
- package/src/registry/build.ts +590 -0
- package/src/registry/cli.ts +141 -0
- package/src/registry/find.ts +195 -0
- package/src/registry/index.ts +24 -0
- package/src/registry/registry.test.ts +448 -0
- package/src/registry/schema.ts +268 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalogue's shape (UIG-12, seam S2 in docs/GATES.md §16).
|
|
3
|
+
*
|
|
4
|
+
* A **data source read by machines first** and rendered second — not a
|
|
5
|
+
* documentation generator. The skill (UIG-20), the copy detector (UIG-25) and
|
|
6
|
+
* the merged search (UIG-19) all read this and nothing else, so the field
|
|
7
|
+
* names are the contract and `schemaVersion` is what makes a change to them
|
|
8
|
+
* detectable.
|
|
9
|
+
*
|
|
10
|
+
* Names are plain and ours. Open-sourcing is far off; when it comes, a
|
|
11
|
+
* renaming pass is one version bump, and guessing at a public vocabulary now
|
|
12
|
+
* would cost every reader between here and there.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Raise this whenever a field is added, removed or given a new meaning. A
|
|
17
|
+
* reader that knows version N and is handed N+1 must be able to say so rather
|
|
18
|
+
* than silently read a field that moved.
|
|
19
|
+
*/
|
|
20
|
+
export const SCHEMA_VERSION = 1
|
|
21
|
+
|
|
22
|
+
/** What a name is, which decides how a reader offers it. */
|
|
23
|
+
export type EntryKind =
|
|
24
|
+
/** Draws something. Used in JSX. */
|
|
25
|
+
| 'component'
|
|
26
|
+
/** A React hook: `use…`, callable only inside a component. */
|
|
27
|
+
| 'hook'
|
|
28
|
+
/** A function or a constant. Neither of the above. */
|
|
29
|
+
| 'helper'
|
|
30
|
+
|
|
31
|
+
/** Where an entry's one-line purpose was read from. */
|
|
32
|
+
export type PurposeSource =
|
|
33
|
+
/** The `.mdx` page's opening paragraph — the name has a page of its own. */
|
|
34
|
+
| 'page'
|
|
35
|
+
/** The doc comment above the export — the name is documented on a sibling's page. */
|
|
36
|
+
| 'comment'
|
|
37
|
+
|
|
38
|
+
export type EntryStatus = 'stable' | 'migrating' | 'deprecated'
|
|
39
|
+
|
|
40
|
+
/** A prop that takes one of a known set of words, and the set. */
|
|
41
|
+
export interface EntryVariant {
|
|
42
|
+
prop: string
|
|
43
|
+
values: string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One prop a component declares itself.
|
|
48
|
+
*
|
|
49
|
+
* **Itself**, not what it inherits: a component that extends
|
|
50
|
+
* `ComponentPropsWithRef<'div'>` would otherwise drag in every DOM attribute,
|
|
51
|
+
* which is noise in a catalogue whose whole point is a short answer.
|
|
52
|
+
*/
|
|
53
|
+
export interface EntryProp {
|
|
54
|
+
name: string
|
|
55
|
+
/**
|
|
56
|
+
* What it takes, in a word a person reads: `true/false`, `number`, `text`,
|
|
57
|
+
* `anything`, `a handler`, the words themselves for a set (`left | right`),
|
|
58
|
+
* or the type as written where none of those fit.
|
|
59
|
+
*/
|
|
60
|
+
takes: string
|
|
61
|
+
required: boolean
|
|
62
|
+
/** The first sentence of the prop's own comment, where it has one. */
|
|
63
|
+
note: string | null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A behaviour this component owns, from UIG-8's enumeration. */
|
|
67
|
+
export interface EntryBehaviour {
|
|
68
|
+
/** `OwnedBehaviour.id` — `portal`, `walking`, `scroll`… */
|
|
69
|
+
id: string
|
|
70
|
+
/** What a person notices it doing, in words. */
|
|
71
|
+
behaviour: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface RegistryEntry {
|
|
75
|
+
/** The exported name, exactly as `index.ts` exports it. */
|
|
76
|
+
name: string
|
|
77
|
+
/** Which library it came from. One repo per registry until UIG-13 merges them. */
|
|
78
|
+
repo: string
|
|
79
|
+
kind: EntryKind
|
|
80
|
+
/** What a caller actually types. */
|
|
81
|
+
importPath: string
|
|
82
|
+
/** The file the name is declared in, relative to the repo. Several names share one file. */
|
|
83
|
+
sourceFile: string
|
|
84
|
+
/** One line: what it is for. Never empty — the build fails instead. */
|
|
85
|
+
purpose: string
|
|
86
|
+
purposeFrom: PurposeSource
|
|
87
|
+
/**
|
|
88
|
+
* Every prop it declares itself — what it can do. This is the field a reader
|
|
89
|
+
* asks "does it already do X?" of, before writing X by hand.
|
|
90
|
+
*/
|
|
91
|
+
props: EntryProp[]
|
|
92
|
+
/**
|
|
93
|
+
* The subset of `props` that takes one of a known set of words, structured.
|
|
94
|
+
* Kept beside `props` because it is what a search ranks well on, and what a
|
|
95
|
+
* caller most often needs named back at them.
|
|
96
|
+
*/
|
|
97
|
+
variants: EntryVariant[]
|
|
98
|
+
/** What it owns, so a caller never rebuilds it (UIG-8). Empty for most. */
|
|
99
|
+
ownsBehaviours: EntryBehaviour[]
|
|
100
|
+
status: EntryStatus
|
|
101
|
+
/**
|
|
102
|
+
* Which migration stage plans to change it.
|
|
103
|
+
*
|
|
104
|
+
* Always `null` here. The migration's plan lives in `K:/Estiva/migration
|
|
105
|
+
* docs`, which is not in this repo and is not in CI — and a copy kept here by
|
|
106
|
+
* hand is the drifting list this whole project exists to remove. The field
|
|
107
|
+
* stays in the schema because a later ticket may have a source a build can
|
|
108
|
+
* read; until then a reader gets an honest `null` rather than a stale number.
|
|
109
|
+
*/
|
|
110
|
+
migrationStage: number | null
|
|
111
|
+
/** The `.mdx` page's Storybook id, or `null` where the name has no page of its own. */
|
|
112
|
+
docsId: string | null
|
|
113
|
+
/** A story that shows it, by Storybook id. `null` where nothing renders it alone. */
|
|
114
|
+
storyId: string | null
|
|
115
|
+
/** The `.mdx` path, relative to the repo, or `null`. */
|
|
116
|
+
docPage: string | null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A name that is exported and deliberately not an entry. Empty today; the count still has to reconcile. */
|
|
120
|
+
export interface RegistryExclusion {
|
|
121
|
+
name: string
|
|
122
|
+
reason: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface Registry {
|
|
126
|
+
schemaVersion: number
|
|
127
|
+
builtFrom: {
|
|
128
|
+
repo: string
|
|
129
|
+
/** The npm package the entries are imported from. */
|
|
130
|
+
package: string
|
|
131
|
+
packageVersion: string
|
|
132
|
+
/** Value exports counted in `index.ts`. `entries + excluded` must equal it. */
|
|
133
|
+
exports: number
|
|
134
|
+
/** Type-only exports, which are not entries. Recorded so the count reconciles. */
|
|
135
|
+
typeExports: number
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* How to turn an id into a link. There is no Storybook on the internet yet
|
|
139
|
+
* (UIG-19), so an absolute URL committed here would resolve nowhere: the
|
|
140
|
+
* reader joins its own base to these.
|
|
141
|
+
*/
|
|
142
|
+
storybook: {
|
|
143
|
+
docsPath: string
|
|
144
|
+
storyPath: string
|
|
145
|
+
devUrl: string
|
|
146
|
+
}
|
|
147
|
+
entries: RegistryEntry[]
|
|
148
|
+
excluded: RegistryExclusion[]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const KINDS: EntryKind[] = ['component', 'hook', 'helper']
|
|
152
|
+
const STATUSES: EntryStatus[] = ['stable', 'migrating', 'deprecated']
|
|
153
|
+
const SOURCES: PurposeSource[] = ['page', 'comment']
|
|
154
|
+
|
|
155
|
+
const isString = (v: unknown): v is string => typeof v === 'string'
|
|
156
|
+
const isFilledString = (v: unknown): v is string => isString(v) && v.trim().length > 0
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check a registry against this schema. Returns every problem, not the first:
|
|
160
|
+
* a build that reports one missing purpose per run costs one run per entry.
|
|
161
|
+
*
|
|
162
|
+
* An empty list is the only pass. CI runs this on the committed file.
|
|
163
|
+
*/
|
|
164
|
+
export function validateRegistry(value: unknown): string[] {
|
|
165
|
+
const problems: string[] = []
|
|
166
|
+
const fail = (message: string) => problems.push(message)
|
|
167
|
+
|
|
168
|
+
if (typeof value !== 'object' || value === null) return ['the registry is not an object']
|
|
169
|
+
const registry = value as Partial<Registry>
|
|
170
|
+
|
|
171
|
+
if (registry.schemaVersion !== SCHEMA_VERSION) fail(`schemaVersion is ${String(registry.schemaVersion)}, expected ${SCHEMA_VERSION}`)
|
|
172
|
+
|
|
173
|
+
const built = registry.builtFrom
|
|
174
|
+
if (typeof built !== 'object' || built === null) fail('builtFrom is missing')
|
|
175
|
+
else {
|
|
176
|
+
if (!isFilledString(built.repo)) fail('builtFrom.repo is missing')
|
|
177
|
+
if (!isFilledString(built.package)) fail('builtFrom.package is missing')
|
|
178
|
+
if (!isFilledString(built.packageVersion)) fail('builtFrom.packageVersion is missing')
|
|
179
|
+
if (typeof built.exports !== 'number') fail('builtFrom.exports is not a number')
|
|
180
|
+
if (typeof built.typeExports !== 'number') fail('builtFrom.typeExports is not a number')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const book = registry.storybook
|
|
184
|
+
if (typeof book !== 'object' || book === null) fail('storybook is missing')
|
|
185
|
+
else {
|
|
186
|
+
for (const key of ['docsPath', 'storyPath', 'devUrl'] as const) {
|
|
187
|
+
if (!isFilledString(book[key])) fail(`storybook.${key} is missing`)
|
|
188
|
+
}
|
|
189
|
+
if (isString(book.docsPath) && !book.docsPath.includes('{docsId}')) fail('storybook.docsPath does not carry {docsId}')
|
|
190
|
+
if (isString(book.storyPath) && !book.storyPath.includes('{storyId}')) fail('storybook.storyPath does not carry {storyId}')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const entries = registry.entries
|
|
194
|
+
const excluded = registry.excluded
|
|
195
|
+
if (!Array.isArray(entries)) return [...problems, 'entries is not an array']
|
|
196
|
+
if (!Array.isArray(excluded)) return [...problems, 'excluded is not an array']
|
|
197
|
+
if (entries.length === 0) fail('entries is empty')
|
|
198
|
+
|
|
199
|
+
const seen = new Set<string>()
|
|
200
|
+
for (const [i, raw] of entries.entries()) {
|
|
201
|
+
const at = (field: string) => `entries[${i}] (${isString((raw as RegistryEntry)?.name) ? (raw as RegistryEntry).name : '?'}).${field}`
|
|
202
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
203
|
+
fail(`entries[${i}] is not an object`)
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
const entry = raw as Partial<RegistryEntry>
|
|
207
|
+
if (!isFilledString(entry.name)) fail(at('name') + ' is missing')
|
|
208
|
+
else if (seen.has(entry.name)) fail(`${entry.name} appears twice`)
|
|
209
|
+
else seen.add(entry.name)
|
|
210
|
+
|
|
211
|
+
if (!isFilledString(entry.repo)) fail(at('repo') + ' is missing')
|
|
212
|
+
if (!isFilledString(entry.importPath)) fail(at('importPath') + ' is missing')
|
|
213
|
+
if (!isFilledString(entry.sourceFile)) fail(at('sourceFile') + ' is missing')
|
|
214
|
+
// The reason this schema exists: an entry with no purpose is an entry the
|
|
215
|
+
// search cannot find and the skill cannot quote.
|
|
216
|
+
if (!isFilledString(entry.purpose)) fail(at('purpose') + ' is empty')
|
|
217
|
+
if (!KINDS.includes(entry.kind as EntryKind)) fail(at('kind') + ` is ${String(entry.kind)}`)
|
|
218
|
+
if (!SOURCES.includes(entry.purposeFrom as PurposeSource)) fail(at('purposeFrom') + ` is ${String(entry.purposeFrom)}`)
|
|
219
|
+
if (!STATUSES.includes(entry.status as EntryStatus)) fail(at('status') + ` is ${String(entry.status)}`)
|
|
220
|
+
if (!(entry.migrationStage === null || typeof entry.migrationStage === 'number')) fail(at('migrationStage') + ' is neither a number nor null')
|
|
221
|
+
if (!Array.isArray(entry.props)) fail(at('props') + ' is not an array')
|
|
222
|
+
else
|
|
223
|
+
for (const prop of entry.props) {
|
|
224
|
+
if (!isFilledString(prop?.name) || !isFilledString(prop?.takes) || typeof prop?.required !== 'boolean') fail(at('props') + ' has a malformed entry')
|
|
225
|
+
else if (!(prop.note === null || isFilledString(prop.note))) fail(at('props') + `.${prop.name}.note is neither a string nor null`)
|
|
226
|
+
}
|
|
227
|
+
if (!Array.isArray(entry.variants)) fail(at('variants') + ' is not an array')
|
|
228
|
+
else
|
|
229
|
+
for (const variant of entry.variants) {
|
|
230
|
+
if (!isFilledString(variant?.prop) || !Array.isArray(variant?.values) || variant.values.length === 0) fail(at('variants') + ' has a malformed entry')
|
|
231
|
+
// variants is a view of props, never a second source that can disagree.
|
|
232
|
+
else if (Array.isArray(entry.props) && !entry.props.some((prop) => prop?.name === variant.prop)) fail(at('variants') + `.${variant.prop} is not one of its props`)
|
|
233
|
+
}
|
|
234
|
+
if (!Array.isArray(entry.ownsBehaviours)) fail(at('ownsBehaviours') + ' is not an array')
|
|
235
|
+
else for (const behaviour of entry.ownsBehaviours) {
|
|
236
|
+
if (!isFilledString(behaviour?.id) || !isFilledString(behaviour?.behaviour)) fail(at('ownsBehaviours') + ' has a malformed entry')
|
|
237
|
+
}
|
|
238
|
+
for (const key of ['docsId', 'storyId', 'docPage'] as const) {
|
|
239
|
+
const held = entry[key]
|
|
240
|
+
if (!(held === null || isFilledString(held))) fail(at(key) + ' is neither a string nor null')
|
|
241
|
+
}
|
|
242
|
+
// "Every storyUrl resolves, or is recorded as absent with a reason" — the
|
|
243
|
+
// reason is enforced rather than written down: only something that draws
|
|
244
|
+
// nothing may have nowhere to be looked at. A component with no story is
|
|
245
|
+
// already an error of UIG-5's component-has-a-story; this stops the
|
|
246
|
+
// catalogue quietly carrying one.
|
|
247
|
+
if (entry.kind === 'component' && (entry.docsId === null || entry.storyId === null)) {
|
|
248
|
+
fail(at('docsId') + ' is null, and only a helper or a hook may have no page or story')
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const [i, raw] of excluded.entries()) {
|
|
253
|
+
const gap = raw as Partial<RegistryExclusion>
|
|
254
|
+
if (!isFilledString(gap?.name)) fail(`excluded[${i}].name is missing`)
|
|
255
|
+
if (!isFilledString(gap?.reason)) fail(`excluded[${i}].reason is missing`)
|
|
256
|
+
if (isString(gap?.name) && seen.has(gap.name)) fail(`${gap.name} is both an entry and excluded`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Completeness (the ticket's acceptance): every value export is either an
|
|
260
|
+
// entry or an exclusion with a reason. Averaging the difference away is the
|
|
261
|
+
// failure this check exists to make impossible.
|
|
262
|
+
const counted = entries.length + excluded.length
|
|
263
|
+
if (typeof built === 'object' && built !== null && typeof built.exports === 'number' && counted !== built.exports) {
|
|
264
|
+
fail(`${entries.length} entries + ${excluded.length} excluded = ${counted}, but index.ts exports ${built.exports} values`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return problems
|
|
268
|
+
}
|