@fusion.dev/helix-docs 0.0.0-dev

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/content/accessibility.mdx +117 -0
  3. package/content/angular.mdx +60 -0
  4. package/content/browser-support.mdx +83 -0
  5. package/content/building-on-the-headless-tier.mdx +143 -0
  6. package/content/charting.mdx +93 -0
  7. package/content/coding-agents.mdx +139 -0
  8. package/content/color-modes.mdx +87 -0
  9. package/content/color.mdx +92 -0
  10. package/content/composition.mdx +171 -0
  11. package/content/elevation-and-material.mdx +93 -0
  12. package/content/events.mdx +125 -0
  13. package/content/forms.mdx +170 -0
  14. package/content/iconography.mdx +80 -0
  15. package/content/installation.mdx +125 -0
  16. package/content/intent-reference.mdx +107 -0
  17. package/content/introduction.mdx +69 -0
  18. package/content/localization.mdx +95 -0
  19. package/content/metrics.mdx +399 -0
  20. package/content/motion.mdx +91 -0
  21. package/content/other-frameworks.mdx +70 -0
  22. package/content/overlays.mdx +102 -0
  23. package/content/overriding-a-component.mdx +160 -0
  24. package/content/react.mdx +144 -0
  25. package/content/recipes.mdx +131 -0
  26. package/content/routing.mdx +208 -0
  27. package/content/sandbox.mdx +76 -0
  28. package/content/server-rendering.mdx +103 -0
  29. package/content/shape-and-borders.mdx +80 -0
  30. package/content/size-and-density.mdx +86 -0
  31. package/content/space-and-size.mdx +89 -0
  32. package/content/state-and-control.mdx +132 -0
  33. package/content/surfaces-and-separation.mdx +99 -0
  34. package/content/svelte.mdx +45 -0
  35. package/content/theme-builder.mdx +66 -0
  36. package/content/theming.mdx +111 -0
  37. package/content/tiers.mdx +159 -0
  38. package/content/tokens.mdx +88 -0
  39. package/content/typography.mdx +117 -0
  40. package/content/using-helix-with-an-agent.mdx +231 -0
  41. package/content/using-the-elements.mdx +125 -0
  42. package/content/versioning.mdx +72 -0
  43. package/content/visual-axes.mdx +75 -0
  44. package/content/vue.mdx +80 -0
  45. package/content/without-a-bundler.mdx +119 -0
  46. package/dist/catalog.d.ts +186 -0
  47. package/dist/catalog.js +296 -0
  48. package/dist/index.d.ts +7 -0
  49. package/dist/index.js +6 -0
  50. package/dist/naming.d.ts +14 -0
  51. package/dist/naming.js +28 -0
  52. package/dist/pages.generated.d.ts +7 -0
  53. package/dist/pages.generated.js +284 -0
  54. package/dist/reference.d.ts +105 -0
  55. package/dist/reference.js +181 -0
  56. package/dist/registry.d.ts +89 -0
  57. package/dist/registry.js +70 -0
  58. package/dist/sections.d.ts +44 -0
  59. package/dist/sections.js +36 -0
  60. package/dist/sources.generated.d.ts +6 -0
  61. package/dist/sources.generated.js +5158 -0
  62. package/package.json +46 -0
@@ -0,0 +1,119 @@
1
+ ---
2
+ description: A page that uses Helix with no build step, and what that costs.
3
+ parent: installation
4
+ section: Getting started
5
+ title: Without a bundler
6
+ trailing: true
7
+ ---
8
+
9
+ # Without a bundler
10
+
11
+ Helix ships standard ES modules and plain CSS files, so a page can use it with
12
+ no build step at all. Two ways, and they suit different things.
13
+
14
+ ## Styles alone
15
+
16
+ ```html
17
+ <link
18
+ href="/vendor/helix/all.css"
19
+ rel="stylesheet"
20
+ />
21
+
22
+ <button
23
+ data-button="solid"
24
+ data-button-tone="primary"
25
+ >Save</button>
26
+ <div data-card>
27
+ <h2>A card</h2>
28
+ <p data-prose>Written in your own markup.</p>
29
+ </div>
30
+ ```
31
+
32
+ `@fusion.dev/helix-styles` has no dependencies and no JavaScript. It carries the token
33
+ layer, the element reset, the type scale on bare headings, and recipes for
34
+ markup this library does not render, so a page can look like Helix without a
35
+ single custom element on it.
36
+
37
+ This is a real way to adopt Helix rather than a fallback, and it is the whole of
38
+ what some pages need. See [Recipes](./recipes) for what the base layer provides.
39
+
40
+ ## The elements, through an import map
41
+
42
+ ```html
43
+ <script type="importmap">
44
+ {
45
+ "imports": {
46
+ "@fusion.dev/helix": "/vendor/helix/styled/dist/index.js",
47
+ "@fusion.dev/helix-core": "/vendor/helix/core/dist/index.js",
48
+ "@fusion.dev/helix-icons": "/vendor/helix/icons/dist/index.js",
49
+ "lit": "/vendor/lit/index.js"
50
+ }
51
+ }
52
+ </script>
53
+ <script type="module">
54
+ import '@fusion.dev/helix'
55
+ </script>
56
+ ```
57
+
58
+ The components are built on Lit, so `lit` has to resolve too. That is the part
59
+ that makes an import map necessary rather than optional: the package's own
60
+ imports are bare specifiers, and a browser needs to be told where each one is.
61
+
62
+ **Copy the files, do not point at a CDN.** An import map pointing at someone
63
+ else's server makes your page stop working when theirs does, which is the
64
+ dependency the Sovereignty Constraint exists to refuse. `bun add` and then copy
65
+ `node_modules/@fusion.dev/helix-*/dist` into whatever you serve.
66
+
67
+ ## What this costs
68
+
69
+ **Every element ships, whether the page uses one or not.** A bundler is what
70
+ drops the components you never wrote, and without one the browser fetches the
71
+ whole library. Fine for an internal tool or a prototype; not what you want on a
72
+ landing page.
73
+
74
+ You can narrow it by importing subpaths instead:
75
+
76
+ ```js
77
+ import '@fusion.dev/helix/button'
78
+ import '@fusion.dev/helix/dialog'
79
+ ```
80
+
81
+ Each still pulls its own dependencies, so this is a reduction rather than a
82
+ minimum, but for a page using five components it is a large one.
83
+
84
+ **No TypeScript, so no typings.** A misspelled property is silent here where it
85
+ is a compile error in React, Vue and Svelte, which is worth a note in review.
86
+ Nothing else about writing Helix changes: a declared property is a seed and the
87
+ element applies its own changes. See [State and
88
+ control](./state-and-control).
89
+
90
+ ## The stylesheets are the same
91
+
92
+ ```html
93
+ <link
94
+ href="/vendor/helix/all.css"
95
+ rel="stylesheet"
96
+ />
97
+ <link
98
+ href="/vendor/helix/styled/elements.css"
99
+ rel="stylesheet"
100
+ />
101
+ ```
102
+
103
+ `all.css` carries `primitives.css`, `tokens.css` and `base.css` in the order they
104
+ have to be in, and its `@import`s are relative, so a browser resolves them
105
+ against the stylesheet's own URL with no bundler involved. Copy the whole
106
+ `dist` folder and link the one file. Verified in Chromium: one `<link>` produces
107
+ four requests and both layers resolve.
108
+
109
+ *This page listed `tokens.css` and `base.css` and no `primitives.css` until 24
110
+ Aug 2026, which is the same omission `installation.mdx`, `introduction.mdx` and
111
+ the agent page carried. `tokens.css` refers to a primitive 235 times and imports
112
+ nothing, so a reader who followed this got all 178 semantic tokens declared and
113
+ every one resolving to `rgba(0, 0, 0, 0)`. Linking `all.css` is the fix that
114
+ cannot be got wrong again.*
115
+
116
+ `elements.css` stays its own link, being `@fusion.dev/helix`'s rather than this
117
+ package's, and it matters more here than anywhere: with no bundler the module
118
+ graph arrives over the network, so the interval where the markup exists and the
119
+ definitions do not is longer than it is in a built app.
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The component catalog: manifests and example markup, as one shape a reader
3
+ * can render.
4
+ *
5
+ * **Pure, and it lives here for the same reason `reference()` does.** This is
6
+ * Helix knowledge, and it used to sit in a marketing app: once both an HTML
7
+ * page and a `.md` route need it, leaving it there means one of them imports
8
+ * across a domain boundary to get at it. Reading the manifests and the
9
+ * `.examples/*.html` files off disk is the caller's job, because a content
10
+ * package that touched the filesystem would trace itself into every consumer's
11
+ * server bundle.
12
+ *
13
+ * Two sources and no third: the manifests describe the API, and the example
14
+ * files hold the markup. Nothing about a component is written down here, which
15
+ * is what keeps a site from drifting away from the library it documents.
16
+ */
17
+ import type { HelixManifest } from '@fusion.dev/helix-cem';
18
+ export interface ApiMember {
19
+ description: string;
20
+ name: string;
21
+ /**
22
+ * Where the platform feature this is built on is specified, if it is one.
23
+ *
24
+ * Optional here and required on `PropertyDoc`, because a property always has
25
+ * the field even when it is empty and a slot or a part is built on nothing.
26
+ * It is on the member rather than only on the property so the Read table can
27
+ * carry a mark: `form` and `labels` are read-only and are the two members in
28
+ * the library that most need one.
29
+ */
30
+ references?: {
31
+ label: string;
32
+ url: string;
33
+ }[];
34
+ type: string;
35
+ }
36
+ /**
37
+ * A property, which carries a default where an attribute does not.
38
+ *
39
+ * Separate from `ApiMember` rather than an optional field on it, because a slot
40
+ * and a CSS part have no default and a column of empty cells is a table saying
41
+ * the data is missing rather than that the concept does not apply.
42
+ */
43
+ export interface PropertyDoc {
44
+ default: string;
45
+ description: string;
46
+ name: string;
47
+ /** Whether it is a getter, so a reader is not invited to set it. */
48
+ readonly: boolean;
49
+ /** Where the platform feature this is built on is specified. */
50
+ references: {
51
+ label: string;
52
+ url: string;
53
+ }[];
54
+ type: string;
55
+ }
56
+ export interface EventDoc {
57
+ description: string;
58
+ name: string;
59
+ reactProp: string;
60
+ type: string;
61
+ }
62
+ export interface Element {
63
+ attributes: ApiMember[];
64
+ cssParts: ApiMember[];
65
+ cssProperties: ApiMember[];
66
+ description: string;
67
+ events: EventDoc[];
68
+ label: string;
69
+ methods: ApiMember[];
70
+ /** Role within the family. `Root` for its primary element. */
71
+ part: string;
72
+ properties: PropertyDoc[];
73
+ slots: ApiMember[];
74
+ summary: string;
75
+ tagName: string;
76
+ tier: 'headless' | 'styled';
77
+ }
78
+ export interface Example {
79
+ /** The example's own stylesheet, or empty where it needs none. */
80
+ css: string;
81
+ /** One line from the example's `@summary` comment, or empty where unwritten. */
82
+ description: string;
83
+ html: string;
84
+ label: string;
85
+ /**
86
+ * The frame layout this example asks for, where it asks for one.
87
+ *
88
+ * Absent on almost every example, which is the design rather than an
89
+ * oversight: the reference picks `centered` and only the exceptions say
90
+ * otherwise, so what is written down is the handful of facts that differ
91
+ * rather than a table of defaults kept by hand. A consumer that has no
92
+ * layouts can ignore it.
93
+ */
94
+ layout?: string;
95
+ /**
96
+ * The least height the preview pane should give this example, in pixels.
97
+ *
98
+ * Present on the six overlay families and nowhere else. Height is derived
99
+ * from what the frame drew, so a declared one is a fact that can go stale;
100
+ * these are the examples where there is nothing to derive, because what they
101
+ * open is `position: fixed` and excluded from measurement by design.
102
+ */
103
+ min?: number;
104
+ slug: string;
105
+ /**
106
+ * The same example authored in TSX, which is a file and never a conversion.
107
+ *
108
+ * **Both syntaxes are authored, neither derived.** The sandbox used to run
109
+ * the HTML through `htmlToJsx` when a reader asked for TSX, which is the
110
+ * mistake `build-headless-examples.mjs` made one axis over and this package
111
+ * already rejected once: a derived example is a function of its twin rather
112
+ * than a thing anybody wrote. It also did not work. Every example carrying a
113
+ * `<script>` converted to a JSX fragment with a `<script>` in it, which is a
114
+ * syntax error rather than a bad example, and three separate faults hit the
115
+ * script-free ones as well.
116
+ *
117
+ * `check:examples-paired` holds the two files to each other and
118
+ * `check:visual` holds the two pictures to each other.
119
+ */
120
+ tsx: string;
121
+ /**
122
+ * Which tier the example shows, read from what it renders.
123
+ *
124
+ * Off the markup rather than off the filename, the same test `check:tokens`
125
+ * uses to know a headless example's stylesheet may write raw values: an
126
+ * example is a headless one when it renders a `helix-headless-` element.
127
+ * When this was written the file was called `headless.html` and the name
128
+ * could have answered, which is why the comment argued against relying on
129
+ * it. Both tiers call theirs `preview.html` now, the package directory
130
+ * carrying the tier, so the name cannot answer and this is the only fact
131
+ * available. The word `base` was the tier's old name and is gone with it.
132
+ */
133
+ tier: 'headless' | 'styled';
134
+ }
135
+ export interface Family {
136
+ aliases: string[];
137
+ /** What the component is for, from `@category`. The gallery groups on it. */
138
+ category: string;
139
+ documentation: string;
140
+ elements: Element[];
141
+ examples: Example[];
142
+ label: string;
143
+ since: string;
144
+ slug: string;
145
+ /** The family's source directory, relative to the repository root. */
146
+ source: string;
147
+ status: string;
148
+ summary: string;
149
+ }
150
+ /**
151
+ * One example as it sits on disk, before this decides what to call it.
152
+ *
153
+ * An example is up to two files, `<slug>.html` and an optional `<slug>.css`.
154
+ * The HTML is the list: a stylesheet on its own is not an example, and
155
+ * `check:examples` fails on one whose markup is missing rather than showing a
156
+ * sandbox with a stylesheet and nothing to style.
157
+ *
158
+ * The pair is what the sandbox already shows, `index.html` beside `styles.css`,
159
+ * so the second file is the one the reader is looking at rather than a build
160
+ * step. It is also what replaced the `style` attributes these examples used to
161
+ * carry: an inline style is unreachable from a stylesheet, cannot express a
162
+ * hover or a media query, and teaches a reader to write the one thing the
163
+ * library exists to avoid.
164
+ */
165
+ export interface ExampleSource {
166
+ css: string;
167
+ /** Family folder the example was found in. */
168
+ family: string;
169
+ html: string;
170
+ /** File name without its extension: `default`, `base`, `cards.base`. */
171
+ slug: string;
172
+ /** The authored TSX beside the HTML, never a conversion of it. */
173
+ tsx: string;
174
+ }
175
+ /**
176
+ * Every family across the given manifests, alphabetical, each with its elements
177
+ * and examples.
178
+ *
179
+ * The family a declaration belongs to comes from where its file is rather than
180
+ * from its tag name, which is the same reasoning `place()` carries for the
181
+ * reference: `helix-button-group` is one family and `helix-tab` is a part
182
+ * of another, and nothing in the name says which.
183
+ */
184
+ export declare const catalog: (manifests: HelixManifest[], examples?: ExampleSource[]) => Family[];
185
+ /** The family of a given slug, from an already-built catalog. */
186
+ export declare const findInCatalog: (families: Family[], slug: string) => Family | undefined;
@@ -0,0 +1,296 @@
1
+ import { displayName, startCase, tierOf } from './naming.js';
2
+ import { place } from './reference.js';
3
+ /* The headless twin's slug, and the suffix every other one carries. Named
4
+ because the sort and the label both ask, and a tier that has been renamed
5
+ once can be renamed again. */
6
+ /**
7
+ * The canonical example's slug, named once because three places knew it.
8
+ *
9
+ * The tier pairing, the first-position pin and the label all had it written
10
+ * out, and renaming `default` to `preview` broke each of them separately: the
11
+ * pin because the old name sorted early by accident, the pairing because a twin
12
+ * mirrors this one, the label because it reads from the name. A fact three
13
+ * places hold is a fact that drifts, which the comment on the sort below
14
+ * already records happening once when the tier was called `base`.
15
+ */
16
+ const CANONICAL = 'preview';
17
+ const HEADLESS = 'headless';
18
+ const HEADLESS_SUFFIX = /\.headless$/;
19
+ /**
20
+ * Drops `undefined` from a union.
21
+ *
22
+ * Every reflected property is optional, because the default state of a Helix
23
+ * component is the absent attribute: a reflected default would put an attribute
24
+ * on the host that a React consumer's server markup never had. That makes the
25
+ * declared type `'primary' | 'neutral' | undefined`, and the `undefined` is an
26
+ * implementation detail of how the default is expressed rather than a value
27
+ * anyone sets. The table reads better without it, and the attribute being
28
+ * omissible is already what "attribute" means.
29
+ */
30
+ const withoutUndefined = (text) => text
31
+ .split('|')
32
+ .map((part) => part.trim())
33
+ .filter((part) => part !== 'undefined')
34
+ .join(' | ');
35
+ /**
36
+ * A method the platform calls, rather than one a consumer does.
37
+ *
38
+ * `formResetCallback` and its siblings are public because the platform has to
39
+ * reach them, and they are 55 of the 152 public methods in the library:
40
+ * documenting them puts a reader's own `focus()` and `show()` in a minority on
41
+ * their own component's page. Matching the suffix rather than listing the names
42
+ * covers the custom-element lifecycle and the form-associated one together, and
43
+ * keeps covering them when the platform adds a third.
44
+ */
45
+ const PLATFORM_CALLBACK = /Callback$/;
46
+ /** Public only. Private and protected are the component's business. */
47
+ const isPublic = (row) => row.privacy === undefined || row.privacy === 'public';
48
+ /**
49
+ * Properties, with the default each one falls back to.
50
+ *
51
+ * Fields rather than attributes, which is the change this page needed: an
52
+ * attribute carries no default in the manifest, so the old table could say a
53
+ * component took `appearance` and never what it did when you left it alone.
54
+ */
55
+ const properties = (rows) => (rows ?? [])
56
+ /* Instance fields only. A static is the class's, not an element's, and the
57
+ two that reach here are the platform's rather than ours:
58
+ `shadowRootOptions` is Lit's and `formAssociated` is the flag the browser
59
+ reads to decide a control participates in a form. Thirty-five rows across
60
+ the library, none of them anything a consumer sets on a tag. */
61
+ /* `@internal` needs nothing here: the analyzer drops a member carrying it
62
+ before the manifest is written, which was worth finding out before
63
+ building a plugin and a filter to do the same thing twice. */
64
+ .filter((row) => row.kind === 'field' && isPublic(row) && row.static !== true)
65
+ .map((row) => ({
66
+ /* `undefined` is not a default, it is the absence of one, and the
67
+ analyzer records the initialiser verbatim. Printed, the column reads
68
+ "undefined" for every optional property, which is a table telling a
69
+ reader a word rather than a value. Blank says the same thing and says
70
+ it quietly. */
71
+ default: row.default === 'undefined' ? '' : (row.default ?? ''),
72
+ description: row.description ?? '',
73
+ name: row.name ?? '',
74
+ /* A getter reads as a field in the manifest and assigning to one throws,
75
+ which is the same trap the React generator already skips these for. In
76
+ a table they are indistinguishable from a property a reader may set:
77
+ `AvatarGroup.avatars` sat beside its slot looking like a second way to
78
+ pass the same thing, and there is no second way. A hundred and
79
+ fifty-five of them. */
80
+ readonly: row.readonly === true,
81
+ /* From `@reference` on the property. A row for `command` can say it is
82
+ the Invoker Commands API and point at where that is specified, which a
83
+ description sentence can gesture at and cannot carry. */
84
+ references: row.helix?.references ?? [],
85
+ type: withoutUndefined(row.type?.text ?? ''),
86
+ }));
87
+ /**
88
+ * Methods, typed by the signature a caller writes rather than by a return type
89
+ * alone. `(next: string) => boolean` says what to pass; `boolean` does not.
90
+ */
91
+ const methods = (rows) => (rows ?? [])
92
+ .filter((row) => row.kind === 'method' && isPublic(row) && !PLATFORM_CALLBACK.test(row.name ?? ''))
93
+ .map((row) => {
94
+ const args = (row.parameters ?? [])
95
+ .map((arg) => `${arg.name ?? ''}: ${arg.type?.text ?? 'unknown'}`)
96
+ .join(', ');
97
+ return {
98
+ description: row.description ?? '',
99
+ name: row.name ?? '',
100
+ type: `(${args}) => ${row.return?.type?.text ?? 'void'}`,
101
+ };
102
+ });
103
+ const members = (rows) => (rows ?? []).map((row) => ({
104
+ description: row.description ?? '',
105
+ name: row.name ?? '',
106
+ type: withoutUndefined(row.type?.text ?? ''),
107
+ }));
108
+ /**
109
+ * An annotation comment: a leading `@` inside an HTML comment, and its line.
110
+ *
111
+ * **Only comments that begin with `@`.** An example is allowed one comment
112
+ * about its own scaffolding, and those are written for the reader:
113
+ * `<!-- Preview frame: the submit is swallowed. -->` exists so nobody copies
114
+ * `onsubmit="event.preventDefault()"` into production. Stripping every comment
115
+ * would take that note with it. The sigil is what separates a line addressed to
116
+ * this documentation system from a line addressed to a person.
117
+ */
118
+ const ANNOTATION = /^[^\S\n]*<!--\s*@[\s\S]*?-->[^\S\n]*\n?/gm;
119
+ /**
120
+ * The example's markup as a reader should receive it.
121
+ *
122
+ * `@summary` is metadata about the example rather than part of it: the cem
123
+ * plugin lifts it into the manifest, the reference prints it as the
124
+ * description, and it was *also* left sitting on line one of every example, so
125
+ * the copy button handed back a comment describing the thing you had just
126
+ * copied. Ninety-odd examples, each one telling a reader that a Helix component
127
+ * needs a docs annotation above it.
128
+ *
129
+ * Stripped here rather than at the reader, because every consumer of the
130
+ * catalog wants the same thing and a site that trimmed it in its own renderer
131
+ * would leave the sandbox and any other consumer showing it.
132
+ */
133
+ const withoutAnnotations = (html) => html.replace(ANNOTATION, '').trimStart();
134
+ /**
135
+ * The examples of one family, named and ordered.
136
+ *
137
+ * The prose comes from the manifest and the markup from the caller. The
138
+ * composition plugin reads the same folder and records what each example is;
139
+ * the payload stays on disk because it is large and the manifest is parsed by
140
+ * everything. Keyed by slug so the two halves cannot drift into each other's
141
+ * order.
142
+ */
143
+ const examplesOf = (slug, declared, sources) => {
144
+ const described = new Map((declared?.examples ?? []).map((entry) => [entry.slug ?? '', entry.description ?? '']));
145
+ /* Keyed on slug the same way the descriptions are, and for the same reason:
146
+ the manifest half and the on-disk half are matched rather than zipped, so
147
+ the two cannot drift into each other's order. */
148
+ const layouts = new Map((declared?.examples ?? []).map((entry) => [entry.slug ?? '', entry.layout ?? '']));
149
+ const minimums = new Map((declared?.examples ?? []).map((entry) => [entry.slug ?? '', entry.min ?? 0]));
150
+ return (sources
151
+ .filter((source) => source.family === slug)
152
+ /* `default` first, then alphabetical, with each headless twin directly
153
+ behind the example it mirrors.
154
+
155
+ By slug, not by filename. This used to compare `compact-counts.html`
156
+ against `compact.html`, where `-` precedes `.` and so the longer name
157
+ sorted first, which is a list ordered by a file extension no reader
158
+ ever sees. The extension is not part of an example's identity:
159
+ `compact` and `compact-counts` are what the tabs say, and `compact`
160
+ comes first.
161
+
162
+ **Pairing on the mirrored name, because sorting on the raw slug only
163
+ ever worked by luck.** The twin used to be called `base`, and `base`
164
+ sorts before every family's other examples, so it landed under
165
+ `default` and looked deliberate. Renaming the tier to `headless` moved
166
+ it to the end of the list and broke two specs that had been reading
167
+ that accident as the rule. What the pairing is for is that the tabs
168
+ show the same list on both tiers, which is what `mirrored` below is
169
+ computing anyway: sort on it and the position is a consequence of the
170
+ relationship rather than of where the tier's name falls in the
171
+ alphabet. */
172
+ .sort((a, b) => {
173
+ const pairOf = (slug) => slug === HEADLESS ? CANONICAL : slug.replace(HEADLESS_SUFFIX, '');
174
+ const twin = (slug) => (slug === HEADLESS || HEADLESS_SUFFIX.test(slug) ? 1 : 0);
175
+ const left = pairOf(a.slug);
176
+ const right = pairOf(b.slug);
177
+ if (left !== right) {
178
+ return left === CANONICAL ? -1 : right === CANONICAL ? 1 : left.localeCompare(right);
179
+ }
180
+ return twin(a.slug) - twin(b.slug);
181
+ })
182
+ .map((source) => {
183
+ /* Which styled example this one is the headless twin of. `headless` is
184
+ the twin of `preview`, and `<name>.headless` of `<name>`. Both names
185
+ have moved: the tier was `base` and the canonical example was
186
+ `default`, and this line is the one place that has to know the pairing
187
+ because a headless example is not named for itself. */
188
+ /* Read once and narrowed here rather than spread from the map
189
+ directly: `get` answers `string | undefined`, and under
190
+ `exactOptionalPropertyTypes` an optional property will not take the
191
+ `undefined`. */
192
+ const layout = layouts.get(source.slug);
193
+ const min = minimums.get(source.slug);
194
+ const mirrored = source.slug === HEADLESS ? CANONICAL : source.slug.replace(HEADLESS_SUFFIX, '');
195
+ return {
196
+ css: source.css,
197
+ description: described.get(source.slug) ?? '',
198
+ html: withoutAnnotations(source.html),
199
+ /* **The label is the file's name, with no special case left.** The
200
+ canonical example was called `default` and read as "Preview", a
201
+ translation that existed only because the name did not say what the
202
+ file was: it is the component's headline, not a named variant sitting
203
+ beside the others. Renamed, `startCase` gives "Preview" on its own.
204
+
205
+ **A headless example takes the name of the one it mirrors**, so the two
206
+ tiers show the same list rather than a list that changes its labels
207
+ when a reader changes a preference. Only one of a pair is ever on
208
+ screen, the tier deciding which, so a differing name would be the only
209
+ thing suggesting they are different examples. */
210
+ label: startCase(mirrored),
211
+ ...(layout ? { layout } : {}),
212
+ ...(min ? { min } : {}),
213
+ slug: source.slug,
214
+ tier: (source.html.includes('<helix-headless-') ? 'headless' : 'styled'),
215
+ tsx: source.tsx,
216
+ };
217
+ }));
218
+ };
219
+ /**
220
+ * Every family across the given manifests, alphabetical, each with its elements
221
+ * and examples.
222
+ *
223
+ * The family a declaration belongs to comes from where its file is rather than
224
+ * from its tag name, which is the same reasoning `place()` carries for the
225
+ * reference: `helix-button-group` is one family and `helix-tab` is a part
226
+ * of another, and nothing in the name says which.
227
+ */
228
+ export const catalog = (manifests, examples = []) => {
229
+ const byFamily = new Map();
230
+ for (const manifest of manifests) {
231
+ for (const module of manifest.modules ?? []) {
232
+ for (const declaration of module.declarations ?? []) {
233
+ if (!declaration.tagName) {
234
+ continue;
235
+ }
236
+ const { family } = place(module.path);
237
+ if (!family) {
238
+ continue;
239
+ }
240
+ byFamily.set(family, [...(byFamily.get(family) ?? []), declaration]);
241
+ }
242
+ }
243
+ }
244
+ return [...byFamily.entries()]
245
+ .map(([slug, entries]) => {
246
+ /* The family's own description comes from its styled root: the element
247
+ whose tag is the family name, rather than a part of it. */
248
+ const root = entries.find((entry) => entry.tagName === `helix-${slug}`) ??
249
+ entries[0];
250
+ return {
251
+ aliases: root.helix?.aliases ?? [],
252
+ category: root.helix?.category ?? '',
253
+ documentation: root.helix?.documentation ?? '',
254
+ elements: entries
255
+ .map((entry) => ({
256
+ attributes: members(entry.attributes),
257
+ cssParts: members(entry.cssParts),
258
+ cssProperties: members(entry.cssProperties),
259
+ description: entry.description ?? '',
260
+ events: (entry.events ?? []).map((event) => ({
261
+ description: event.description ?? '',
262
+ name: event.name ?? '',
263
+ reactProp: event.reactProp ?? `on${event.name ?? ''}`,
264
+ type: event.type?.text ?? '',
265
+ })),
266
+ label: displayName(entry.tagName),
267
+ methods: methods(entry.members),
268
+ /* From the manifest, not by stripping the family name off the
269
+ label. The composition plugin already derives this from the
270
+ folder, and "Avatar Group" minus "Avatar" is a string operation
271
+ that happens to work until a part's name contains the family's. */
272
+ part: entry.helix?.part ?? '',
273
+ properties: properties(entry.members),
274
+ slots: members(entry.slots),
275
+ summary: entry.summary ?? '',
276
+ tagName: entry.tagName,
277
+ tier: tierOf(entry.tagName),
278
+ }))
279
+ /* Styled first, then headless. Alphabetical order would put the
280
+ headless tier on top, which is the one most readers never reach
281
+ for. */
282
+ .sort((a, b) => Number(a.tier === 'headless') - Number(b.tier === 'headless') ||
283
+ a.tagName.localeCompare(b.tagName)),
284
+ examples: examplesOf(slug, root.helix, examples),
285
+ label: startCase(slug),
286
+ since: root.helix?.since ?? '',
287
+ slug,
288
+ source: root.helix?.source ?? '',
289
+ status: root.helix?.status ?? '',
290
+ summary: root.summary ?? '',
291
+ };
292
+ })
293
+ .sort((a, b) => a.label.localeCompare(b.label));
294
+ };
295
+ /** The family of a given slug, from an already-built catalog. */
296
+ export const findInCatalog = (families, slug) => families.find((family) => family.slug === slug);
@@ -0,0 +1,7 @@
1
+ export type { ApiMember, Element, EventDoc, Example, ExampleSource, Family, PropertyDoc, } from './catalog.js';
2
+ export { catalog, findInCatalog } from './catalog.js';
3
+ export { displayName, startCase, tierOf } from './naming.js';
4
+ export { place, type ReferenceFamily, type ReferenceOptions, reference, referenceFamilies, } from './reference.js';
5
+ export { contentPath, type DocNavPage, type DocNavSection, type DocPage, type DocSection, findPage, navigation, pages, sections, } from './registry.js';
6
+ export { findSection } from './sections.js';
7
+ export { docSources } from './sources.generated.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { catalog, findInCatalog } from './catalog.js';
2
+ export { displayName, startCase, tierOf } from './naming.js';
3
+ export { place, reference, referenceFamilies, } from './reference.js';
4
+ export { contentPath, findPage, navigation, pages, sections, } from './registry.js';
5
+ export { findSection } from './sections.js';
6
+ export { docSources } from './sources.generated.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Display names for components.
3
+ *
4
+ * The product is Helix, so every component being called "HelixCard" inside the
5
+ * Helix site is noise: it repeats the one thing the reader already knows on
6
+ * every row of the sidebar. The tag keeps its prefix, because that is a real
7
+ * identifier a consumer types.
8
+ */
9
+ /** `app-header` becomes `App Header`, and `qr-code` becomes `QR Code`. */
10
+ export declare const startCase: (slug: string) => string;
11
+ /** `helix-app-header` becomes `App Header`, `helix-headless-card` becomes `Card`. */
12
+ export declare const displayName: (tagName: string) => string;
13
+ /** Which tier a tag belongs to, read from the prefix rather than the manifest. */
14
+ export declare const tierOf: (tagName: string) => 'headless' | 'styled';
package/dist/naming.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Display names for components.
3
+ *
4
+ * The product is Helix, so every component being called "HelixCard" inside the
5
+ * Helix site is noise: it repeats the one thing the reader already knows on
6
+ * every row of the sidebar. The tag keeps its prefix, because that is a real
7
+ * identifier a consumer types.
8
+ */
9
+ /**
10
+ * Words a slug spells in lower case that are read as letters.
11
+ *
12
+ * Capitalising the first letter of `qr` gives `Qr`, which is a word nobody says.
13
+ * A slug is lower case because a URL is, and the initialism it came from is
14
+ * lost by the time this sees it, so it has to be put back by name. Kept small
15
+ * and explicit rather than guessed at from length: `ui`, `id` and `api` are
16
+ * initialisms and `is`, `on` and `to` are not.
17
+ */
18
+ const INITIALISMS = new Set(['api', 'css', 'html', 'id', 'qr', 'ui', 'url']);
19
+ /** `app-header` becomes `App Header`, and `qr-code` becomes `QR Code`. */
20
+ export const startCase = (slug) => slug
21
+ .split('-')
22
+ .filter(Boolean)
23
+ .map((word) => INITIALISMS.has(word) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1))
24
+ .join(' ');
25
+ /** `helix-app-header` becomes `App Header`, `helix-headless-card` becomes `Card`. */
26
+ export const displayName = (tagName) => startCase(tagName.replace(/^helix-(?:headless-)?/, ''));
27
+ /** Which tier a tag belongs to, read from the prefix rather than the manifest. */
28
+ export const tierOf = (tagName) => tagName.startsWith('helix-headless-') ? 'headless' : 'styled';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Every page this package documents, derived from the frontmatter of each MDX
3
+ * file by `bun run docs`. Do not edit: `bun run check:docs` fails when this
4
+ * file and the content disagree, and the content is the source.
5
+ */
6
+ import type { DocPage } from './registry.js';
7
+ export declare const docPages: DocPage[];