|
|
|
|
# Demo authoring guide
|
|
|
|
|
|
|
|
|
|
This document is the canonical reference for authoring component demo
|
|
|
|
|
pages under `web/routes/uix/components/{name}/+page.svelte`. The
|
|
|
|
|
template is **locked** — every demo must follow it. Per-component
|
|
|
|
|
creativity is forbidden; the only thing that varies is the data the
|
|
|
|
|
component declares.
|
|
|
|
|
|
|
|
|
|
The canary template is the **drawer** demo —
|
|
|
|
|
`web/routes/uix/components/drawer/+page.svelte`. When in doubt, copy
|
|
|
|
|
from drawer and adapt.
|
|
|
|
|
|
|
|
|
|
## 1. File location + frame
|
|
|
|
|
|
|
|
|
|
Every demo lives at `web/routes/uix/components/{kebab}/+page.svelte`.
|
|
|
|
|
The file is wrapped by `web/routes/uix/+layout@.svelte` (the `@`
|
|
|
|
|
resets intermediate layouts) so the chrome — top bar, sidebar rail,
|
|
|
|
|
canvas frame — is provided. The demo only renders the canvas inner.
|
|
|
|
|
|
|
|
|
|
The outermost element of every demo is:
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<div data-uix-canvas-inner>…</div>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 2. Required imports
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import {
|
|
|
|
|
X,
|
|
|
|
|
type XSize,
|
|
|
|
|
type XVariant
|
|
|
|
|
// …other public types
|
|
|
|
|
} from '$uix/eidos/components/{name}';
|
|
|
|
|
import { compileMorfo } from '$uix/morfo';
|
|
|
|
|
import { xMorfo } from '@/uix/morfo/components/{name}';
|
|
|
|
|
import { getActiveUix } from '$active-uix';
|
|
|
|
|
|
|
|
|
|
const uix = getActiveUix();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Multi-instance components (toast) also import `createToaster` (or the
|
|
|
|
|
analogous instance constructor). Components with 0 semantic events still
|
|
|
|
|
render the Sema tab; the tab states that Morfo declares no events. A
|
|
|
|
|
0-event demo is only valid after the component README explicitly justifies
|
|
|
|
|
why the component is passive or why its actions do not belong to Sema. Those
|
|
|
|
|
pages do not need `getActiveUix` unless they offer interactive playback.
|
|
|
|
|
|
|
|
|
|
## 3. Tabs
|
|
|
|
|
|
|
|
|
|
Six tabs in this order:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Live · API · [morfo badge] Np·Me · [sema badge] N · Recipe · A11y
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The Sema tab is never hidden. When the morfo declares 0 events, render
|
|
|
|
|
an explicit empty state instead of making the absence invisible. Do not
|
|
|
|
|
assume 0 events is correct because the current morfo has no `events` array:
|
|
|
|
|
the component must first pass the Morfo/Sema audit in
|
|
|
|
|
`src/uix/eidos/components/{name}/README.md`. The badge counters use
|
|
|
|
|
`partsList.length` and `events.length`.
|
|
|
|
|
|
|
|
|
|
Tab union:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
|
|
|
|
|
let tab = $state<Tab>('live');
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
There is **no** `Translates` tab and **no** `Anatomy` tab — both were
|
|
|
|
|
inventions that have been removed.
|
|
|
|
|
|
|
|
|
|
## 4. Header
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<header>
|
|
|
|
|
<div data-uix-eyebrow>{Category} · {Name}</div>
|
|
|
|
|
<h1 data-uix-page-title>{Name}</h1>
|
|
|
|
|
<p data-uix-page-lede>
|
|
|
|
|
{one-paragraph summary — what the component does, how many parts
|
|
|
|
|
and events, anything notable about its semantic shape}
|
|
|
|
|
</p>
|
|
|
|
|
<div data-uix-page-meta>
|
|
|
|
|
<span data-uix-meta-pill>
|
|
|
|
|
<span data-uix-meta-key>parts</span>{compiled.parts.order.length}
|
|
|
|
|
</span>
|
|
|
|
|
<span data-uix-meta-pill>
|
|
|
|
|
<span data-uix-meta-key>events</span>{events.length}
|
|
|
|
|
</span>
|
|
|
|
|
<!-- additional pills: variants, sizes, intents, apg link, scope -->
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 5. Live preview is always rendered
|
|
|
|
|
|
|
|
|
|
The stage sits **between** the header and the tablist, not inside the
|
|
|
|
|
Live tab. This is critical so that:
|
|
|
|
|
|
|
|
|
|
- The Sema tab's `▶ play` buttons can fire on a real instance.
|
|
|
|
|
- The trace strip stays visible across tab switches.
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<div data-uix-stage>
|
|
|
|
|
<div data-uix-stage-area bind:this={stageRef}>
|
|
|
|
|
<X bind:open …>
|
|
|
|
|
…
|
|
|
|
|
</X>
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-stage-trace>
|
|
|
|
|
<span data-uix-stage-trace-key>trace</span>
|
|
|
|
|
{#if trace.length === 0}
|
|
|
|
|
<span>{instructional copy — "open the X to see events"}</span>
|
|
|
|
|
{:else}
|
|
|
|
|
{#each trace.slice(0, 3) as entry}
|
|
|
|
|
<span><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent ? ' · ' + entry.intent : ''}</span>
|
|
|
|
|
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
|
|
|
|
|
{/each}
|
|
|
|
|
{/if}
|
|
|
|
|
<span style="margin-inline-start: auto;">
|
|
|
|
|
<span data-uix-stage-trace-key>{state-key}</span> {String(stateValue)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 6. MutationObserver trace
|
|
|
|
|
|
|
|
|
|
Watch `data-event` on the stage subtree. The runtime stamps it via
|
|
|
|
|
the sema engine before each event fires:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type TraceEntry = { event: string; family: string; intent?: string; at: number };
|
|
|
|
|
let trace = $state<TraceEntry[]>([]);
|
|
|
|
|
|
|
|
|
|
let stageRef = $state<HTMLElement | null>(null);
|
|
|
|
|
$effect(() => {
|
|
|
|
|
const el = stageRef;
|
|
|
|
|
if (!el) return;
|
|
|
|
|
const obs = new MutationObserver((mutations) => {
|
|
|
|
|
for (const m of mutations) {
|
|
|
|
|
if (m.attributeName !== 'data-event') continue;
|
|
|
|
|
const target = m.target as Element;
|
|
|
|
|
const ev = target.getAttribute('data-event');
|
|
|
|
|
if (!ev) continue;
|
|
|
|
|
trace = [
|
|
|
|
|
{
|
|
|
|
|
event: ev,
|
|
|
|
|
family: target.getAttribute('data-event-family') ?? '—',
|
|
|
|
|
intent: target.getAttribute('data-event-intent') ?? undefined,
|
|
|
|
|
at: Date.now()
|
|
|
|
|
},
|
|
|
|
|
...trace
|
|
|
|
|
].slice(0, 6);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
|
|
|
|
|
return () => obs.disconnect();
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For multi-instance components (toast Item) the observer still works:
|
|
|
|
|
each Item gets its own stamps because the runtime is per-instance.
|
|
|
|
|
|
|
|
|
|
## 7. Live tab — per-architectural-layer subsections
|
|
|
|
|
|
|
|
|
|
The Live tab groups controls by the layer that owns them. **There is
|
|
|
|
|
no `[sema]` subsection in Live** — sema events live in their dedicated
|
|
|
|
|
Sema tab.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Controls
|
|
|
|
|
── intro paragraph: layer-grouped, links to Sema tab for events
|
|
|
|
|
── [soma] props · headless behavior (subsection)
|
|
|
|
|
── [eidos] props · visual treatment (subsection)
|
|
|
|
|
── (additional groups: Group / Item / Demo content / Header etc.)
|
|
|
|
|
── [soma] somaSnippet pre/code (reactive $derived)
|
|
|
|
|
── [eidos] eidosSnippet pre/code (reactive $derived)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Subsection headings carry layer badges:
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<div data-uix-subsection-head>
|
|
|
|
|
<span data-uix-layer-badge="soma">soma</span> props · headless behavior
|
|
|
|
|
</div>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Both code snippets are at the bottom of the Live tab, in this order:
|
|
|
|
|
soma first, eidos second. Each in its own `data-uix-code` block with
|
|
|
|
|
`data-uix-code-head` showing the layer badge + a one-line caption +
|
|
|
|
|
language tag.
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<div data-uix-code>
|
|
|
|
|
<div data-uix-code-head>
|
|
|
|
|
<span data-uix-layer-badge="soma">soma</span>
|
|
|
|
|
<span>headless · ARIA + behavior only</span>
|
|
|
|
|
<span data-uix-code-lang>svelte</span>
|
|
|
|
|
</div>
|
|
|
|
|
<pre><code>{somaSnippet}</code></pre>
|
|
|
|
|
</div>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 8. Snippet derivation pattern
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
const somaSnippet = $derived(
|
|
|
|
|
[
|
|
|
|
|
"<script lang='ts'>",
|
|
|
|
|
" import * as X from '$soma/components/{name}';",
|
|
|
|
|
' let value = $state(...);',
|
|
|
|
|
'</' + 'script>',
|
|
|
|
|
'',
|
|
|
|
|
'<X.Provider …>',
|
|
|
|
|
' …',
|
|
|
|
|
'</X.Provider>'
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join('\n')
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Two non-obvious rules:
|
|
|
|
|
|
|
|
|
|
- **`'</' + 'script>'` is split** to prevent Svelte's parser from
|
|
|
|
|
closing the demo's own `<script>` block.
|
|
|
|
|
- **`.filter(Boolean)`** lets you write `someProp && ' someProp'`
|
|
|
|
|
inline — `false` entries drop out cleanly so the rendered snippet
|
|
|
|
|
only shows the props the consumer actually passed.
|
|
|
|
|
|
|
|
|
|
### 8.1 Snippet parity is mandatory
|
|
|
|
|
|
|
|
|
|
The snippets are part of the demo contract. They must match the live preview's
|
|
|
|
|
real API surface and data contract:
|
|
|
|
|
|
|
|
|
|
- If the live preview renders a field/part, the snippet must render it too.
|
|
|
|
|
- If the live preview uses a schema, the snippet must declare the same visible
|
|
|
|
|
fields, imports, validators and defaults. Do not show a reduced schema while
|
|
|
|
|
the preview validates extra fields.
|
|
|
|
|
- If a control changes a prop (`size`, `variant`, `validationBehaviour`,
|
|
|
|
|
`disabled`, etc.), the snippet must reflect the active value.
|
|
|
|
|
- If a snippet is intentionally minimal, the live preview must also be minimal
|
|
|
|
|
or the snippet caption must say it is a separate minimal example. Component
|
|
|
|
|
demos should prefer parity over abbreviated examples.
|
|
|
|
|
|
|
|
|
|
For form demos specifically:
|
|
|
|
|
|
|
|
|
|
- Real-time validation is `validationBehaviour: 'onChange'`.
|
|
|
|
|
- A demo that starts in `onChange` must use initially valid defaults unless it
|
|
|
|
|
explicitly documents that it is demonstrating an initially invalid form.
|
|
|
|
|
- `Form.AutoFields` examples must keep SIUM metadata, fields and defaults in
|
|
|
|
|
sync with the manual `Form + Field` example.
|
|
|
|
|
|
|
|
|
|
## 9. Sema tab — events + interactive playback
|
|
|
|
|
|
|
|
|
|
Header with `<span data-uix-layer-badge="sema">sema</span>`. One table:
|
|
|
|
|
`name / family / verb / sequence / intent / play`. The play button
|
|
|
|
|
emits via the active uix's `EngineSemantic` onto the real DOM target:
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<button
|
|
|
|
|
data-uix-play
|
|
|
|
|
onclick={() => {
|
|
|
|
|
const target = (
|
|
|
|
|
stageRef?.querySelector('[data-{component}-{eventTargetPart}]')
|
|
|
|
|
?? stageRef?.querySelector('[data-{component}-{fallbackPart}]')
|
|
|
|
|
?? stageRef
|
|
|
|
|
) as HTMLElement | null;
|
|
|
|
|
if (!target) return;
|
|
|
|
|
void uix.events?.emit({
|
|
|
|
|
name: action.name,
|
|
|
|
|
family: action.semantic.family,
|
|
|
|
|
target,
|
|
|
|
|
...(effectiveIntent ? { intent: effectiveIntent } : {})
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>▶ play</button>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`effectiveIntent` resolves the morfo's intent declaration:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
|
|
|
|
|
{@const effectiveIntent = typeof intentDecl === 'string'
|
|
|
|
|
? intentDecl
|
|
|
|
|
: (intentDecl ? intentBoundProp : undefined)}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
(Static literal → use it; fromProp binding → use the demo's local
|
|
|
|
|
prop value; absent → don't pass intent.)
|
|
|
|
|
|
|
|
|
|
## 10. Morfo tab — declarative contract
|
|
|
|
|
|
|
|
|
|
Sections in this order:
|
|
|
|
|
|
|
|
|
|
1. **Header table**: name / kebab / scope / apg / parts.length / events.length
|
|
|
|
|
2. **Parts overview table**: kebab / marker / element / role / archetype / states / optional
|
|
|
|
|
3. **Per-part subsection** (only when the part declares any data / aria / keyboard)
|
|
|
|
|
- data-attrs table: attr / values / source kind
|
|
|
|
|
- aria-attrs table: attr / source kind / condition / severity
|
|
|
|
|
- keyboard table: key / action — key wrapped in `<span data-uix-kbd>`
|
|
|
|
|
4. **Events declaration table**: name / family / verb / sequence / intent / target / prewrite / commit
|
|
|
|
|
|
|
|
|
|
Iterate raw morfo (`xMorfo.parts`, `xMorfo.events`) for declaration
|
|
|
|
|
data — NOT `compiled` everywhere. The compiler exposes
|
|
|
|
|
`compiled.contracts.dataAttrsByPart.get(kebab)` for data-attrs only.
|
|
|
|
|
ARIA + keyboard come from raw morfo. Cast for type narrowing inside
|
|
|
|
|
the each block:
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
{@const partAny = rawPart as unknown as {
|
|
|
|
|
kebab: string;
|
|
|
|
|
aria?: ReadonlyArray<{
|
|
|
|
|
attr: string;
|
|
|
|
|
value: { kind: string };
|
|
|
|
|
condition?: { when: string; prop?: string; part?: string };
|
|
|
|
|
severity?: string;
|
|
|
|
|
}>;
|
|
|
|
|
keyboard?: ReadonlyArray<{ key: string; action: string }>;
|
|
|
|
|
}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
When the morfo has no events (icon, tooltip), hardcode `<td>0</td>`
|
|
|
|
|
for `events.length` — `xMorfo.events?.length` TypeScript-errors
|
|
|
|
|
because the property doesn't exist on the const-narrowed type when
|
|
|
|
|
omitted.
|
|
|
|
|
|
|
|
|
|
## 11. Recipe + A11y tabs
|
|
|
|
|
|
|
|
|
|
Brief tables.
|
|
|
|
|
|
|
|
|
|
- **Recipe** = list of selectors with morfo / eidos source tag —
|
|
|
|
|
every selector is classified as `morfo`-backed or `eidos`-only.
|
|
|
|
|
- **A11y** = keyboard table + ARIA contract table. For overlay
|
|
|
|
|
components also include intent → role / aria-live mapping when the
|
|
|
|
|
morfo derives them.
|
|
|
|
|
|
|
|
|
|
## 12. Critical pitfalls
|
|
|
|
|
|
|
|
|
|
### 12.1 `<Foo>` element name in `<p>` parses as HTML
|
|
|
|
|
|
|
|
|
|
Writing ``Toggle is single-part — `<Toggle>` IS the button.`` inside
|
|
|
|
|
a `<p>` produces a Svelte parse error — it tries to close the `<p>`
|
|
|
|
|
because `<Toggle>` looks like an element. Use `<Toggle>` instead.
|
|
|
|
|
|
|
|
|
|
### 12.2 Curly braces inside literal text
|
|
|
|
|
|
|
|
|
|
Svelte interprets `{ … }` in template text as a JavaScript
|
|
|
|
|
expression. Wrap in template literal expression:
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<!-- bad -->
|
|
|
|
|
<td class="type">Snippet<[{ checked: boolean }]></td>
|
|
|
|
|
|
|
|
|
|
<!-- good -->
|
|
|
|
|
<td class="type">{`Snippet<[{ checked: boolean }]>`}</td>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 12.3 Object.assign on Svelte component constructors
|
|
|
|
|
|
|
|
|
|
Bulk `Object.assign(Root, { Trigger, Content, … })` causes a Svelte 5
|
|
|
|
|
hydration glitch where children re-mount on hydration (button appears
|
|
|
|
|
then disappears). The disciplined fix in `eidos/components/*/index.ts`
|
|
|
|
|
is **explicit per-property assignment** — see
|
|
|
|
|
`src/uix/eidos/components/README.md` rule #7 and the drawer
|
|
|
|
|
`index.ts` for the canonical pattern. Until every eidos package
|
|
|
|
|
adopts it, watch for this when adding new compound parts.
|
|
|
|
|
|
|
|
|
|
### 12.4 Children snippet self-shadow
|
|
|
|
|
|
|
|
|
|
When a wrapper renders `<Provider>{@render children?.()}</Provider>`,
|
|
|
|
|
the `children` prop must NOT be re-declared as `{#snippet children}`
|
|
|
|
|
in the same scope — it shadows the prop and recurses. The drawer
|
|
|
|
|
wrapper uses `let { children, ...rest } = $props()` and renders
|
|
|
|
|
directly.
|
|
|
|
|
|
|
|
|
|
### 12.5 Live state vs at-construction reads
|
|
|
|
|
|
|
|
|
|
Constructors that capture local state at mount time (e.g.
|
|
|
|
|
`createToaster({ duration, max, … })`) snapshot the **initial** value
|
|
|
|
|
of those `$state()` props. Subsequent slider changes do not re-create
|
|
|
|
|
the toaster. Either:
|
|
|
|
|
|
|
|
|
|
- accept the snapshot semantics and document it, or
|
|
|
|
|
- recreate the instance via a `$derived` (forces a rebuild on every
|
|
|
|
|
change — usually not desirable for stateful instances), or
|
|
|
|
|
- expose runtime config setters on the instance.
|
|
|
|
|
|
|
|
|
|
Toast demo accepts the snapshot semantics (the warning is benign).
|
|
|
|
|
|
uix: date-picker + date-range-picker components + component audit infra
Two new Eidos components shipped end-to-end (wrapper + recipe + demo +
Soma provider hardening) plus a checklist-driven audit pipeline that
scores all 67 morfo components against doctrinal completion criteria.
New components:
- date-picker: full popover-anchored picker over date-field + calendar,
with calendar/content/trigger parts and demo route.
- date-range-picker: standalone wrapper with own calendar/grid/segment
surface, demo route, and recipe CSS.
- Both wrappers follow Option C disciplined (root + parts attached via
explicit assignment, no Object.assign).
Supporting Soma changes:
- range-calendar provider tightened (211 LOC of behavior, 167 LOC of
tests), README brought up to component doctrine.
- date-field, date-picker, date-range-field, date-range-picker Soma
providers + READMEs updated for new wrappers.
- popover provider/close gain props needed by the picker wrappers.
Morfo updates:
- date-picker / date-range-picker / range-calendar morfos refined for
the new APIs (parts, events, ARIA).
Audit infrastructure (new):
- src/uix/COMPONENT_COMPLETION_CHECKLIST.md — 81 doctrinal rules across
morfo / eidos wrapper / recipe CSS / demo / README / cross-layer
scripts. Each rule keyed to active_architecture.md, GUIA_IMPLEMENTACION
and DEMO_AUTHORING_GUIDE.
- scripts/component-audit.ts + `npm run component:audit` — regex parser
over all 67 components, emits tmp/component-audit.md with summary
scoreboard + per-component findings. Validates against canonical
SEMA_FAMILIES / SEMA_VERBS / ARCHETYPE_VOCABULARY / INTENTS.
- Initial baseline: 1 PASS, 64 NEEDS-WORK, 2 BROKEN (tooltip,
date-range-picker). Top systemic gaps: translations.label (49),
README Gaps/Comparativa/Baseline sections (87 combined), keyboard
/event ratio under-declaration (15), apg URL absent (19).
Misc:
- src/uix/kimi-audit-eidos.md — supplementary audit notes.
- .gitignore: ignore .codex-* agent scratch artifacts at repo root.
- continue.md + READMEs updated through the migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
### 12.6 Visible controls and Morfo-owned attrs
|
|
|
|
|
|
|
|
|
|
Do not expose a control that has no visible effect in the live preview. If a
|
|
|
|
|
demo has `segments`, `paged navigation`, `modal`, `clear`, `min`, `max`,
|
|
|
|
|
`minDays` or `maxDays`, the stage must make the effect observable immediately.
|
|
|
|
|
Otherwise remove the control or mark it as a separate non-preview API note.
|
|
|
|
|
|
|
|
|
|
Demos must not hand-stamp required `data-*` attrs to simulate missing component
|
|
|
|
|
parts. If a selector is needed for Calendar, RangeCalendar, DatePicker or
|
|
|
|
|
DateRangePicker styling, declare the part/data attr in Morfo/Soma first and let
|
|
|
|
|
the component emit it. Manual attrs in the demo are only acceptable for the UIX
|
|
|
|
|
docs shell itself (`data-uix-*`).
|
|
|
|
|
|
eidos: theming fixes + size/variant parity batch + docs
- theme: add surface.muted + content.muted to color contract (neutral-3
+ neutral-10). Plugs 17 broken --color-content-muted and 3 broken
--color-surface-muted references in recipes/components.
- form.css: fix --color-neutral-element-hover typo → --color-neutral-hover.
- archetypes.css + events.css: replace raw hsl/rgba indigo with
color-mix(var(--color-primary-solid) …) — no raw colors left in eidos.
- combobox dark scrollbar: unscope ::-webkit-scrollbar rules in uix.css
and duplicate --uix-line on :root + :root[data-mode='dark'] so portaled
overlays (Combobox listbox, Popover, Dialog, Drawer) inherit the theme.
- sizes: 20 components expand from sm/md/lg to xs..xl (form controls,
text inputs, progress/meter, field/form) or xs..lg (nav controls:
breadcrumb, pagination, tag-group, toolbar). Composite panels keep
sm/md/lg deliberately.
- variants: field + toolbar drop arbitrary ControlVariant narrowings;
both expose all 3 (surface | outline | ghost) with new outline CSS.
- pagination demo: disambiguate siblingCount/boundaryCount as "per side"
in label + API table (Radix/MUI convention).
- docs: CHECKLIST §C-2.6 (contract token presence) + §D-7.4 (chip parity)
+ §D-7.5 (size category) added. DEMO_AUTHORING §12.7 (chip parity) +
§12.8 (size category cheatsheet) added. eidos/README +
active_architecture.md sync texts/migration nomenclature.
- PENDIENTES.md: normas N-1..N-5 implantadas en esta sesión.
Verification: 88/88 eidos tests, 0 type errors, 67/67 component:audit PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
### 12.7 Chip parity — every theme value is selectable
|
|
|
|
|
|
|
|
|
|
Norm implanted 2026-05-21 (CHECKLIST §D-7.4).
|
|
|
|
|
|
|
|
|
|
Every chip-group control (`size`, `variant`, `color`, `intent`, …) must
|
|
|
|
|
enumerate the **full** union declared in the component's `types.ts`. If
|
|
|
|
|
`ControlVariant` is `'surface' | 'outline' | 'ghost'` and the component's
|
|
|
|
|
`Variant` type does not narrow it, the demo exposes 3 chips. Truncated
|
|
|
|
|
arrays — showing 2 of 3 — are a contract bug; the demo lies about the
|
|
|
|
|
component's surface.
|
|
|
|
|
|
|
|
|
|
If the component narrows the union deliberately (`Extract<…>`), the demo
|
|
|
|
|
matches the narrowed set exactly. Narrowings must be justified in the
|
|
|
|
|
component README; arbitrary narrowings just to "show fewer chips" are
|
|
|
|
|
removed (see `field`, `toolbar` — both widened to full `ControlVariant`
|
|
|
|
|
on 2026-05-21).
|
|
|
|
|
|
|
|
|
|
### 12.8 Size category cheatsheet
|
|
|
|
|
|
|
|
|
|
The shared `Size` scale is `xxs · xs · sm · md · lg · xl · xxl · full`.
|
|
|
|
|
Each component's recipe declares which size primitives it maps, the
|
|
|
|
|
`*Size` type narrows the union, and the demo chips reflect that narrowing
|
|
|
|
|
1:1.
|
|
|
|
|
|
|
|
|
|
Canonical category mapping (2026-05-21):
|
|
|
|
|
|
|
|
|
|
| Category | Sizes exposed | Components |
|
|
|
|
|
| --------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
|
|
|
| Form controls (toggle-like, single tactile target) | `xs · sm · md · lg · xl` | checkbox, switch, toggle, radio-group, rating-group, slider |
|
|
|
|
|
| Text inputs (control with input field) | `xs · sm · md · lg · xl` | search-field, number-field, date-field, editable, tags-input, combobox, select |
|
|
|
|
|
| Progress / meter (scalable bar) | `xs · sm · md · lg · xl` | progress, meter |
|
|
|
|
|
| Layout containers (field + form) | `xs · sm · md · lg · xl` | field, form |
|
|
|
|
|
| Nav controls (inline navigation strips) | `xs · sm · md · lg` | breadcrumb, pagination, tag-group, toolbar |
|
|
|
|
|
| Composite panels (no smaller-than-sm usable) | `sm · md · lg` | calendar, date-picker, date-range-picker, file-upload, stepper, tooltip |
|
|
|
|
|
| Pre-existing wide scales (kept) | `xs..xl` or `xxs..xxl` | avatar, dialog, drawer, popover, tabs, icon |
|
|
|
|
|
|
|
|
|
|
When adding a new size to a component:
|
|
|
|
|
|
|
|
|
|
1. Add `'size-{X}-{prop}': '…'` entries to the component's recipe in
|
|
|
|
|
`src/uix/eidos/lib/recipes/base.ts`.
|
|
|
|
|
2. Add `[data-{kebab}][data-size='{X}']` selector(s) to the component's
|
|
|
|
|
`*.css`.
|
|
|
|
|
3. Widen `Size = Extract<Size, …>` in the component's `types.ts`.
|
|
|
|
|
4. Regenerate: `npm run generate:eidos-css`.
|
|
|
|
|
5. Update the demo's chip array.
|
|
|
|
|
|
|
|
|
|
If the recipe has **no per-size entries** (component reads `--control-height-X`,
|
|
|
|
|
`--space-X`, `--font-size-X` directly — e.g. combobox), step 1 is just adding
|
|
|
|
|
the `[data-size='X']` selector with the right primitive `var()` references.
|
|
|
|
|
|
|
|
|
|
### 12.9 Composition over visibility props
|
|
|
|
|
|
|
|
|
|
Norm implanted 2026-05-21 (`PENDIENTES.md` N-7).
|
|
|
|
|
|
|
|
|
|
Optional parts (Footer, Clear, Cancel, Close, Header sub-items, etc.)
|
|
|
|
|
**must not** be controlled by `*Button` boolean props on the
|
|
|
|
|
component root. Visibility is owned by **composition**: include the
|
|
|
|
|
part to render it, omit it to hide it.
|
|
|
|
|
|
|
|
|
|
Wrong (the previous shape we removed from `<DatePicker>` /
|
|
|
|
|
`<DateRangePicker>`):
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<!-- DON'T: visibility decided by root prop, parts conditionally render -->
|
|
|
|
|
<DatePicker clearButton cancelButton closeButton>
|
|
|
|
|
<DatePicker.Footer>
|
|
|
|
|
<DatePicker.Clear /> <!-- decides internally if it renders -->
|
|
|
|
|
<DatePicker.Cancel />
|
|
|
|
|
<DatePicker.Close />
|
|
|
|
|
</DatePicker.Footer>
|
|
|
|
|
</DatePicker>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Right:
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<!-- DO: parts render unconditionally; consumer composes what they want -->
|
|
|
|
|
<DatePicker>
|
|
|
|
|
<DatePicker.Footer>
|
|
|
|
|
<DatePicker.Clear /> <!-- always renders when mounted -->
|
|
|
|
|
<DatePicker.Cancel />
|
|
|
|
|
<DatePicker.Close />
|
|
|
|
|
</DatePicker.Footer>
|
|
|
|
|
</DatePicker>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For demos: wrap the parts in `{#if showX}` with local state so the
|
|
|
|
|
user can toggle via switches, but the parts themselves don't read
|
|
|
|
|
that state. The demo's switches decide whether the part is
|
|
|
|
|
INCLUDED in the markup.
|
|
|
|
|
|
|
|
|
|
Modal mode (`mode='modal'`) does NOT force the Close button to show;
|
|
|
|
|
the consumer must include `<X.Close/>` if they need an exit
|
|
|
|
|
affordance. Documented in the Close part's source.
|
|
|
|
|
|
|
|
|
|
### 12.10 Chakra-style `kind` for picker variants
|
|
|
|
|
|
|
|
|
|
Norm implanted 2026-05-21 (`PENDIENTES.md` N-6).
|
|
|
|
|
|
|
|
|
|
For `DatePicker` and `DateRangePicker`, the `kind: 'date' | 'month' | 'year'`
|
|
|
|
|
prop is the single source for granularity. Drives:
|
|
|
|
|
|
|
|
|
|
1. **Input segments**: filtered at the `DateFieldProvider` (soma).
|
|
|
|
|
Consumers iterate `segments` from the snippet without filtering.
|
|
|
|
|
2. **Popover view**: the consumer branches structurally on `kind` to
|
|
|
|
|
render `<Picker.YearView>` / `<Picker.MonthView>` / `<Picker.Calendar>`.
|
|
|
|
|
|
|
|
|
|
Demo skeleton:
|
|
|
|
|
|
|
|
|
|
```svelte
|
|
|
|
|
<!-- Input: no filter — soma emits the right segments per kind -->
|
|
|
|
|
<DatePicker.Input>
|
|
|
|
|
{#snippet children({ segments })}
|
|
|
|
|
{#each segments as { part, value }}
|
|
|
|
|
<DatePicker.Segment {part}>{value}</DatePicker.Segment>
|
|
|
|
|
{/each}
|
|
|
|
|
{/snippet}
|
|
|
|
|
</DatePicker.Input>
|
|
|
|
|
|
|
|
|
|
<!-- Popover: branch on kind to pick the right view -->
|
|
|
|
|
<DatePicker.Content>
|
|
|
|
|
{#if kind === 'year'}<DatePicker.YearView />
|
|
|
|
|
{:else if kind === 'month'}<DatePicker.MonthView />
|
|
|
|
|
{:else}<DatePicker.Calendar>…</DatePicker.Calendar>{/if}
|
|
|
|
|
<DatePicker.Footer>…</DatePicker.Footer>
|
|
|
|
|
</DatePicker.Content>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
No separate components — `MonthPicker`, `YearPicker`, `MonthRangePicker`,
|
|
|
|
|
`YearRangePicker` are achieved via `<DatePicker kind='X'>` /
|
|
|
|
|
`<DateRangePicker kind='X'>`.
|
|
|
|
|
|
|
|
|
|
## 13. Verification checklist
|
|
|
|
|
|
|
|
|
|
Before declaring a demo done:
|
|
|
|
|
|
|
|
|
|
1. `npm run check` reports 0 errors for that file.
|
|
|
|
|
2. Walk every tab in dev mode — every tab renders.
|
|
|
|
|
3. Every prop control changes something visible on the live preview.
|
|
|
|
|
4. Every Sema `▶ play` button fires (look for `data-event` in the
|
|
|
|
|
trace strip).
|
|
|
|
|
5. Snippets reflect the current control values without stale
|
|
|
|
|
placeholders.
|
uix: date-picker + date-range-picker components + component audit infra
Two new Eidos components shipped end-to-end (wrapper + recipe + demo +
Soma provider hardening) plus a checklist-driven audit pipeline that
scores all 67 morfo components against doctrinal completion criteria.
New components:
- date-picker: full popover-anchored picker over date-field + calendar,
with calendar/content/trigger parts and demo route.
- date-range-picker: standalone wrapper with own calendar/grid/segment
surface, demo route, and recipe CSS.
- Both wrappers follow Option C disciplined (root + parts attached via
explicit assignment, no Object.assign).
Supporting Soma changes:
- range-calendar provider tightened (211 LOC of behavior, 167 LOC of
tests), README brought up to component doctrine.
- date-field, date-picker, date-range-field, date-range-picker Soma
providers + READMEs updated for new wrappers.
- popover provider/close gain props needed by the picker wrappers.
Morfo updates:
- date-picker / date-range-picker / range-calendar morfos refined for
the new APIs (parts, events, ARIA).
Audit infrastructure (new):
- src/uix/COMPONENT_COMPLETION_CHECKLIST.md — 81 doctrinal rules across
morfo / eidos wrapper / recipe CSS / demo / README / cross-layer
scripts. Each rule keyed to active_architecture.md, GUIA_IMPLEMENTACION
and DEMO_AUTHORING_GUIDE.
- scripts/component-audit.ts + `npm run component:audit` — regex parser
over all 67 components, emits tmp/component-audit.md with summary
scoreboard + per-component findings. Validates against canonical
SEMA_FAMILIES / SEMA_VERBS / ARCHETYPE_VOCABULARY / INTENTS.
- Initial baseline: 1 PASS, 64 NEEDS-WORK, 2 BROKEN (tooltip,
date-range-picker). Top systemic gaps: translations.label (49),
README Gaps/Comparativa/Baseline sections (87 combined), keyboard
/event ratio under-declaration (15), apg URL absent (19).
Misc:
- src/uix/kimi-audit-eidos.md — supplementary audit notes.
- .gitignore: ignore .codex-* agent scratch artifacts at repo root.
- continue.md + READMEs updated through the migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
6. Composite components list the inherited event surface; avoid claiming "0
|
|
|
|
|
events" when Popover, Field, Calendar or RangeCalendar actions are visible.
|
|
|
|
|
7. Any `data-*` selector used by the preview is backed by Morfo/Soma, not by a
|
|
|
|
|
demo-only attr that hides a missing contract.
|
|
|
|
|
8. Date/calendar demos are visually checked in one-month, two-month,
|
|
|
|
|
modal/no-modal, min/max and clear/deselect states before being called done.
|
|
|
|
|
|
|
|
|
|
## 14. Status of demos under `web/routes/uix/components/`
|
|
|
|
|
|
|
|
|
|
All public Eidos components currently exposed in the UIX docs have
|
|
|
|
|
new-layout demos. The legacy `web/routes/{name}/` set is removed; this
|
|
|
|
|
is the only canonical demo home. `svg` is an internal rendering helper
|
|
|
|
|
and does not get a standalone component demo unless it becomes a public
|
|
|
|
|
component.
|
|
|
|
|
|
|
|
|
|
| Component | Demo |
|
|
|
|
|
| ------------ | --------------------------- |
|
|
|
|
|
| accordion | ✓ |
|
|
|
|
|
| avatar | ✓ (0 events, pending audit) |
|
|
|
|
|
| breadcrumb | ✓ (0 events, structural) |
|
|
|
|
|
| calendar | ✓ (3 events) |
|
|
|
|
|
| checkbox | ✓ |
|
|
|
|
|
| collapsible | ✓ |
|
|
|
|
|
| dialog | ✓ |
|
|
|
|
|
| drawer | ✓ (canary template) |
|
|
|
|
|
| field | ✓ (0 events, pending audit) |
|
|
|
|
|
| form | ✓ (3 events, SIUM demo) |
|
|
|
|
|
| icon | ✓ (0 events, structural) |
|
|
|
|
|
| meter | ✓ (0 events, passive) |
|
|
|
|
|
| number-field | ✓ (2 events) |
|
|
|
|
|
| pagination | ✓ (1 event) |
|
|
|
|
|
| popover | ✓ |
|
|
|
|
|
| progress | ✓ (0 events, passive) |
|
|
|
|
|
| radio-group | ✓ |
|
|
|
|
|
| rating-group | ✓ (1 event) |
|
|
|
|
|
| search-field | ✓ (2 events) |
|
|
|
|
|
| slider | ✓ (2 events) |
|
|
|
|
|
| switch | ✓ |
|
|
|
|
|
| tabs | ✓ |
|
|
|
|
|
| toast | ✓ |
|
|
|
|
|
| toggle | ✓ |
|
|
|
|
|
| toolbar | ✓ (1 event) |
|
|
|
|
|
| tooltip | ✓ (0 events, pending audit) |
|