@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.
Files changed (58) hide show
  1. package/dist/Avatar.d.ts +3 -0
  2. package/dist/Avatar.d.ts.map +1 -1
  3. package/dist/ChipInput.d.ts +3 -0
  4. package/dist/ChipInput.d.ts.map +1 -1
  5. package/dist/CommandPalette.d.ts +6 -0
  6. package/dist/CommandPalette.d.ts.map +1 -1
  7. package/dist/IdentityMenu.d.ts +3 -0
  8. package/dist/IdentityMenu.d.ts.map +1 -1
  9. package/dist/Menu.d.ts +7 -0
  10. package/dist/Menu.d.ts.map +1 -1
  11. package/dist/Skeleton.d.ts +2 -0
  12. package/dist/Skeleton.d.ts.map +1 -1
  13. package/dist/Toast.d.ts +5 -0
  14. package/dist/Toast.d.ts.map +1 -1
  15. package/dist/Toolbar.d.ts +4 -0
  16. package/dist/Toolbar.d.ts.map +1 -1
  17. package/dist/Tooltip.d.ts +4 -0
  18. package/dist/Tooltip.d.ts.map +1 -1
  19. package/dist/index.js.map +2 -2
  20. package/dist/registry/build-GOVLABI6.js +13 -0
  21. package/dist/registry/build-GOVLABI6.js.map +7 -0
  22. package/dist/registry/build.d.ts +29 -0
  23. package/dist/registry/build.d.ts.map +1 -0
  24. package/dist/registry/chunk-IJNCYVH4.js +163 -0
  25. package/dist/registry/chunk-IJNCYVH4.js.map +7 -0
  26. package/dist/registry/chunk-MRSBS5OP.js +99 -0
  27. package/dist/registry/chunk-MRSBS5OP.js.map +7 -0
  28. package/dist/registry/chunk-QDYGB3QN.js +352 -0
  29. package/dist/registry/chunk-QDYGB3QN.js.map +7 -0
  30. package/dist/registry/cli.d.ts +2 -0
  31. package/dist/registry/cli.d.ts.map +1 -0
  32. package/dist/registry/cli.js +121 -0
  33. package/dist/registry/cli.js.map +7 -0
  34. package/dist/registry/find.d.ts +43 -0
  35. package/dist/registry/find.d.ts.map +1 -0
  36. package/dist/registry/index.d.ts +14 -0
  37. package/dist/registry/index.d.ts.map +1 -0
  38. package/dist/registry/index.js +26 -0
  39. package/dist/registry/index.js.map +7 -0
  40. package/dist/registry/schema.d.ts +147 -0
  41. package/dist/registry/schema.d.ts.map +1 -0
  42. package/package.json +15 -2
  43. package/registry.json +4470 -0
  44. package/src/Avatar.tsx +3 -0
  45. package/src/ChipInput.tsx +3 -0
  46. package/src/CommandPalette.tsx +6 -0
  47. package/src/IdentityMenu.tsx +3 -0
  48. package/src/Menu.tsx +7 -0
  49. package/src/Skeleton.tsx +2 -0
  50. package/src/Toast.tsx +5 -0
  51. package/src/Toolbar.tsx +4 -0
  52. package/src/Tooltip.tsx +4 -0
  53. package/src/registry/build.ts +590 -0
  54. package/src/registry/cli.ts +141 -0
  55. package/src/registry/find.ts +195 -0
  56. package/src/registry/index.ts +24 -0
  57. package/src/registry/registry.test.ts +448 -0
  58. 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
+ }