@ctrliq/quantic-components
v1.96.2
Published
Readme
Quantic components
Component manifest
The package publishes every documented component as static data at
@ctrliq/quantic-components/meta.json, generated at build time from the static meta
each component declares. It is plain JSON, with no runtime imports and no loader:
const manifest = require('@ctrliq/quantic-components/meta.json');{
"schemaVersion": 1,
"generated": "2026-08-25T00:00:00.000Z",
"components": [
{
"tag": "quantic-dialog",
"description": "Modal overlay dialog with focus trap ...",
"category": "feedback",
"props": [
{
"name": "open",
"description": "Whether the dialog is visible.",
"type": "boolean",
"default": "false"
}
],
"events": [{ "name": "after-close", "description": "Fired after the dialog closes ..." }],
"slots": [{ "name": "trigger", "description": "Element that opens the dialog when clicked." }],
"examples": ["<quantic-dialog> ... </quantic-dialog>"],
"relatedTo": ["quantic-button"],
"avoidWhen": [
{
"when": "the dialog only confirms a destructive action",
"useInstead": "quantic-confirm-dialog"
}
],
"accessibility": {
"providedByComponent": ["focus trap, restore on close, Escape to dismiss"],
"requiredFromConsumer": ["a title, via quantic-dialog-title or aria-label"]
},
"subComponents": ["quantic-dialog-title", "quantic-dialog-actions"]
}
]
}Props, slots and events are arrays carrying their name, and empty fields are omitted.
Nothing is filtered out: forwarded native events are included and flagged, so a consumer
decides whether to care. Treat a missing meta.json as "this install predates the
manifest" rather than an error.
The docs tier
meta.json is the whole manifest and too large to put in context, so the package also
publishes a progressive-disclosure tier generated from it by scripts/gen-llms.ts:
| Export | What it is |
| --- | --- |
| @ctrliq/quantic-components/llms.txt | The index: how to install, and one line per component |
| @ctrliq/quantic-components/llms/{category}-components.md | One file per category |
| @ctrliq/quantic-components/llms/components/{tag}.md | one file per component, props, slots, events and examples |
Read the index, then the category, then the component. Every file resolves from
node_modules at the version the project installed, so no network call is involved and no
server can serve a version that disagrees with the installed package.
The index carries the install and wiring steps, extracted from the marked region of
docs/getting-started.md rather than restated, because naming the components correctly is
useless without knowing what to install and which stylesheets to load.
Storybook copies the same bytes into its public/ to serve them at /llms.txt, /llms/**
and /components.json. The package is the producer; the app is a consumer.
Fields
Everything except tag, description and category is optional and omitted when unset,
so a reader has to treat absence as "not stated" rather than as a default.
| Field | Type | What it carries |
| --- | --- | --- |
| tag | string | The custom element name. The React wrapper name is its pascal case. |
| description | string | One or more sentences. The first sentence alone is used in the generated indexes. |
| category | enum | action, form, display, layout, feedback, navigation or data. |
| props | array | Each with name, type, and optionally description, options, default, required, copyRule. |
| slots | array | Each with name and description, and optionally required, copyRule. The default slot has name: "". |
| events | array | Each with name, and optionally description, detail, forwarded. |
| examples | string[] | Markup, extracted from the Storybook stories at build time rather than hand-written. |
| relatedTo | string[] | Neighbouring tags. Says they are related, not which to choose. |
| avoidWhen | array | When to reach for something else. Each entry has when, and useInstead when there is an alternative to name. useInstead holds a tag when the alternative is a component and prose when it is not, since the answer is sometimes a stylesheet. |
| accessibility | object | providedByComponent is behaviour the component implements, so a consumer must not reimplement it. requiredFromConsumer is what it cannot supply for itself, such as an accessible name. |
| ascii | string | Diagram of the component's anatomy. |
| experimental | boolean | The API can change or be removed in a minor. Absence means supported. See rules/promotion.md. |
| deprecated | object | use names the replacement, reason says why. |
| subComponentOf | string | The parent this element is anatomy of, set only when the child is inert alone. |
| subComponents | string[] | Derived from the children's subComponentOf, so the two can never disagree. |
Copy rules
A prop or slot that holds user-facing text carries copyRule, a reference into the
lexicon the manifest ships as its top-level copyRules:
{
"copyRules": {
"action-label": {
"rule": "Verb phrase naming what happens, specific enough to act on without the surrounding text.",
"good": ["Delete project", "Restart node"],
"avoid": ["OK", "Yes", "Submit", "Continue"]
}
},
"components": [
{
"tag": "quantic-confirm-dialog",
"props": [{ "name": "confirmText", "type": "string", "copyRule": "action-label" }]
}
]
}The reference is what keeps the rules consistent. The wording rule for a placeholder is
the same on six inputs, so it is stated once and pointed at six times rather than copied
and left to drift. Rules are named after the job the text does rather than the component
that carries it, which is why one placeholder rule serves all of them.
Ids are typed as CopyRuleId in src/shared/copy-rules.ts, so a component referencing a
rule that does not exist is a compile error rather than a dangling pointer in the
published data. A reader resolving an id it cannot find should say nothing rather than
print the id.
copyRule is set only where getting the wording wrong is a real failure, which is labels,
headings, messages and placeholders. Most props do not carry one. An ariaLabel prop
never does: an accessible name is an accessibility requirement, so it belongs in
accessibility.requiredFromConsumer.
Versioning
schemaVersion is bumped when an existing reader could break: a field removed or renamed,
a type changed, or a meaning changed. Adding an optional field does not bump it, because a
reader that ignores the field is unaffected, so tooling must tolerate keys it does not
recognise rather than refusing the document. Refuse a schemaVersion you do not know.
Icons
Per-icon elements are registered with the rest of the library, so
<quantic-icon-lucide-rocket> needs nothing beyond the package entry point:
import '@ctrliq/quantic-components/register';@ctrliq/quantic-components/icons is the separate, opt-in entry for the name-based
lookup, <quantic-icon name="Rocket">. It inlines every Lucide glyph so a runtime name
can resolve, which is around 1 MB, and it is kept out of the package entry point for that
reason. Prefer the per-icon element unless the name is genuinely only known at runtime.
Recommended tools
(lit-plugin)[https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin] (VS code)
