|
|
|
|
@ -0,0 +1,623 @@
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import {
|
|
|
|
|
EmptyState,
|
|
|
|
|
type EmptyStateSize,
|
|
|
|
|
type EmptyStateMediaKind,
|
|
|
|
|
type EmptyStateTitleLevel
|
|
|
|
|
} from '$uix/eidos/components/empty-state';
|
|
|
|
|
import { Button } from '$uix/eidos/components/button';
|
|
|
|
|
import { Card } from '$uix/eidos/components/card';
|
|
|
|
|
import { compileMorfo } from '$uix/morfo';
|
|
|
|
|
import { emptyStateMorfo } from '@/uix/morfo/components/empty-state';
|
|
|
|
|
import { getActiveUix } from '$active-uix';
|
|
|
|
|
|
|
|
|
|
import SystemAxes from '../../lib/SystemAxes.svelte';
|
|
|
|
|
import MotionPanel from '../../lib/MotionPanel.svelte';
|
|
|
|
|
import SemaPanel from '../../lib/SemaPanel.svelte';
|
|
|
|
|
import { DemoTrace } from '../../lib/harness.svelte';
|
|
|
|
|
|
|
|
|
|
const uix = getActiveUix();
|
|
|
|
|
|
|
|
|
|
type Tab = 'live' | 'system' | 'motion' | 'sema' | 'services' | 'api' | 'morfo' | 'recipe' | 'a11y';
|
|
|
|
|
let tab = $state<Tab>('live');
|
|
|
|
|
|
|
|
|
|
// ── Stage + System axes ──────────────────────────────────────────────
|
|
|
|
|
const trace = new DemoTrace();
|
|
|
|
|
let stageRef = $state<HTMLElement | null>(null);
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (stageRef) return trace.observe(stageRef);
|
|
|
|
|
});
|
|
|
|
|
let density = $state('comfortable');
|
|
|
|
|
let scaling = $state(1);
|
|
|
|
|
let mode = $state<'inherit' | 'light' | 'dark'>('inherit');
|
|
|
|
|
let dir = $state<'ltr' | 'rtl'>('ltr');
|
|
|
|
|
let borderWidth = $state(1);
|
|
|
|
|
|
|
|
|
|
// ── Live state ───────────────────────────────────────────────────────
|
|
|
|
|
const sizes: EmptyStateSize[] = ['sm', 'md', 'lg'];
|
|
|
|
|
const kinds: EmptyStateMediaKind[] = ['icon', 'media'];
|
|
|
|
|
const levels: EmptyStateTitleLevel[] = [2, 3, 4, 5, 6];
|
|
|
|
|
|
|
|
|
|
let size = $state<EmptyStateSize>('md');
|
|
|
|
|
let kind = $state<EmptyStateMediaKind>('icon');
|
|
|
|
|
let level = $state<EmptyStateTitleLevel>(3);
|
|
|
|
|
let withMedia = $state(true);
|
|
|
|
|
let withDescription = $state(true);
|
|
|
|
|
let withActions = $state(true);
|
|
|
|
|
let withSecondary = $state(false);
|
|
|
|
|
let inCollection = $state(false);
|
|
|
|
|
|
|
|
|
|
// ── Compiled morfo ───────────────────────────────────────────────────
|
|
|
|
|
const compiled = compileMorfo(emptyStateMorfo);
|
|
|
|
|
const partsList = [...compiled.parts.byKebab.values()];
|
|
|
|
|
const events = [...compiled.actions.byName.values()];
|
|
|
|
|
|
|
|
|
|
// ── Code snippet (parity with the live controls) ─────────────────────
|
|
|
|
|
const eidosSnippet = $derived(
|
|
|
|
|
[
|
|
|
|
|
"<script lang='ts'>",
|
|
|
|
|
" import { EmptyState } from '$uix/eidos/components/empty-state';",
|
|
|
|
|
" import { Button } from '$uix/eidos/components/button';",
|
|
|
|
|
'</' + 'script>',
|
|
|
|
|
'',
|
|
|
|
|
`<EmptyState size="${size}">`,
|
|
|
|
|
withMedia && ` <EmptyState.Media kind="${kind}">…</EmptyState.Media>`,
|
|
|
|
|
level === 3
|
|
|
|
|
? ' <EmptyState.Title>No projects yet</EmptyState.Title>'
|
|
|
|
|
: ` <EmptyState.Title level={${level}}>No projects yet</EmptyState.Title>`,
|
|
|
|
|
withDescription &&
|
|
|
|
|
' <EmptyState.Description>Create your first project to get started.</EmptyState.Description>',
|
|
|
|
|
withActions && ' <EmptyState.Actions label="Empty state actions">',
|
|
|
|
|
withActions && ' <Button>Create project</Button>',
|
|
|
|
|
withActions && withSecondary && ' <Button variant="ghost">Learn more</Button>',
|
|
|
|
|
withActions && ' </EmptyState.Actions>',
|
|
|
|
|
'</EmptyState>'
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join('\n')
|
|
|
|
|
);
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
{#snippet demoEmptyState()}
|
|
|
|
|
<EmptyState {size}>
|
|
|
|
|
{#if withMedia}
|
|
|
|
|
<EmptyState.Media {kind}>
|
|
|
|
|
{#if kind === 'icon'}
|
|
|
|
|
<svg viewBox="0 0 24 24" width="1em" height="1em" aria-hidden="true" focusable="false">
|
|
|
|
|
<path
|
|
|
|
|
d="M3 13h4l2 3h6l2-3h4M5 6h14l2 7v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5l2-7Z"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="1.6"
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
{:else}
|
|
|
|
|
<svg viewBox="0 0 96 56" width="96" height="56" aria-hidden="true" focusable="false">
|
|
|
|
|
<rect
|
|
|
|
|
x="6"
|
|
|
|
|
y="6"
|
|
|
|
|
width="84"
|
|
|
|
|
height="44"
|
|
|
|
|
rx="8"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
opacity="0.4"
|
|
|
|
|
/>
|
|
|
|
|
<circle cx="30" cy="24" r="7" fill="currentColor" opacity="0.35" />
|
|
|
|
|
<path
|
|
|
|
|
d="M16 44l16-14 12 10 14-12 22 16"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
opacity="0.5"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
{/if}
|
|
|
|
|
</EmptyState.Media>
|
|
|
|
|
{/if}
|
|
|
|
|
<EmptyState.Title {level}>No projects yet</EmptyState.Title>
|
|
|
|
|
{#if withDescription}
|
|
|
|
|
<EmptyState.Description>
|
|
|
|
|
Create your first project to get started. Imported projects appear here too.
|
|
|
|
|
</EmptyState.Description>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if withActions}
|
|
|
|
|
<EmptyState.Actions label="Empty state actions">
|
|
|
|
|
<Button>Create project</Button>
|
|
|
|
|
{#if withSecondary}
|
|
|
|
|
<Button variant="ghost">Learn more</Button>
|
|
|
|
|
{/if}
|
|
|
|
|
</EmptyState.Actions>
|
|
|
|
|
{/if}
|
|
|
|
|
</EmptyState>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
<div data-uix-canvas-inner>
|
|
|
|
|
<header>
|
|
|
|
|
<div data-uix-eyebrow>Status · Empty state</div>
|
|
|
|
|
<h1 data-uix-page-title>Empty state</h1>
|
|
|
|
|
<p data-uix-page-lede>
|
|
|
|
|
Passive "nothing here yet / no results" surface: media + title + description + actions,
|
|
|
|
|
centered. Display-only and eidos-native — the composed
|
|
|
|
|
<a href="/uix/components/button"><Button></a>s carry the behavior, and announcing an
|
|
|
|
|
empty's appearance belongs to the collection that renders it. <code>size="sm"</code> is the
|
|
|
|
|
compact in-collection density (tables, selects, command palettes); <code>lg</code> the
|
|
|
|
|
full-page hero. For terminal flow states (success / error / 404) use the upcoming
|
|
|
|
|
<code>Result</code> instead.
|
|
|
|
|
</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>
|
|
|
|
|
<span data-uix-meta-pill>
|
|
|
|
|
<span data-uix-meta-key>sizes</span>{sizes.length}
|
|
|
|
|
</span>
|
|
|
|
|
<span data-uix-meta-pill>
|
|
|
|
|
<span data-uix-meta-key>scope</span>eidos
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- Always-on stage — carries the System axes -->
|
|
|
|
|
<div data-uix-stage>
|
|
|
|
|
<div
|
|
|
|
|
data-uix-stage-area
|
|
|
|
|
bind:this={stageRef}
|
|
|
|
|
data-density={density}
|
|
|
|
|
data-theme={mode === 'inherit' ? undefined : mode}
|
|
|
|
|
data-mode={mode === 'inherit' ? undefined : mode}
|
|
|
|
|
{dir}
|
|
|
|
|
style={`--scaling: ${scaling}; --border-width: ${borderWidth}px;`}
|
|
|
|
|
>
|
|
|
|
|
{#if inCollection}
|
|
|
|
|
<Card style="inline-size: min(100%, 34rem);">
|
|
|
|
|
{@render demoEmptyState()}
|
|
|
|
|
</Card>
|
|
|
|
|
{:else}
|
|
|
|
|
{@render demoEmptyState()}
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-stage-trace>
|
|
|
|
|
<span data-uix-stage-trace-key>trace</span>
|
|
|
|
|
<span>
|
|
|
|
|
{trace.entries.length === 0 ? 'no semantic events' : `${trace.entries.length} event(s)`}
|
|
|
|
|
</span>
|
|
|
|
|
<span style="color: var(--uix-text-faint)">·</span>
|
|
|
|
|
<span data-uix-stage-trace-key>size</span>
|
|
|
|
|
<span>{size}</span>
|
|
|
|
|
<span style="margin-inline-start: auto;">
|
|
|
|
|
<span data-uix-stage-trace-key>media</span>
|
|
|
|
|
{withMedia ? kind : 'off'} ·
|
|
|
|
|
<span data-uix-stage-trace-key>level</span> h{level}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-tabs role="tablist">
|
|
|
|
|
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'system'} onclick={() => (tab = 'system')}>
|
|
|
|
|
System
|
|
|
|
|
</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'motion'} onclick={() => (tab = 'motion')}>
|
|
|
|
|
Motion
|
|
|
|
|
</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
|
|
|
|
|
<span data-uix-layer-badge="sema">sema</span>
|
|
|
|
|
<span data-uix-tab-count>{events.length}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'services'} onclick={() => (tab = 'services')}>
|
|
|
|
|
Services
|
|
|
|
|
</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>API</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
|
|
|
|
|
<span data-uix-layer-badge="morfo">morfo</span>
|
|
|
|
|
<span data-uix-tab-count>{partsList.length}p · {events.length}e</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>
|
|
|
|
|
Recipe
|
|
|
|
|
</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if tab === 'live'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Controls</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
EmptyState is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. It has
|
|
|
|
|
no <code>color</code> prop by design: it is a neutral display surface (media plate colors
|
|
|
|
|
are theme tokens, overridable via <code>recipes.empty-state.*</code>).
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>
|
|
|
|
|
<span data-uix-layer-badge="eidos">eidos</span> props
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-controls>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>size</span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each sizes as opt (opt)}
|
|
|
|
|
<button data-uix-chip data-active={size === opt} onclick={() => (size = opt)}>
|
|
|
|
|
{opt}
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>media kind</span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each kinds as opt (opt)}
|
|
|
|
|
<button data-uix-chip data-active={kind === opt} onclick={() => (kind = opt)}>
|
|
|
|
|
{opt}
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>title level</span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each levels as opt (opt)}
|
|
|
|
|
<button data-uix-chip data-active={level === opt} onclick={() => (level = opt)}>
|
|
|
|
|
h{opt}
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>Composition</div>
|
|
|
|
|
<div data-uix-controls>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>media</span>
|
|
|
|
|
<input type="checkbox" bind:checked={withMedia} />
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>description</span>
|
|
|
|
|
<input type="checkbox" bind:checked={withDescription} />
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>actions</span>
|
|
|
|
|
<input type="checkbox" bind:checked={withActions} />
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>secondary action</span>
|
|
|
|
|
<input type="checkbox" bind:checked={withSecondary} />
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>inside a Card (in-collection)</span>
|
|
|
|
|
<input type="checkbox" bind:checked={inCollection} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-code>
|
|
|
|
|
<div data-uix-code-head>
|
|
|
|
|
<span data-uix-layer-badge="eidos">eidos</span>
|
|
|
|
|
<span>composition · size / media kind / heading level</span>
|
|
|
|
|
<span data-uix-code-lang>svelte</span>
|
|
|
|
|
</div>
|
|
|
|
|
<pre><code>{eidosSnippet}</code></pre>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'system'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>System axes</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
Foundation knobs applied to the stage — density, scaling, mode, direction and border width.
|
|
|
|
|
EmptyState's typography rides the <code>--size-*</code> bundle, so density and scaling re-derive
|
|
|
|
|
it coherently.
|
|
|
|
|
</p>
|
|
|
|
|
<SystemAxes bind:density bind:scaling bind:mode bind:dir bind:borderWidth />
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'motion'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Motion</h2>
|
|
|
|
|
<MotionPanel
|
|
|
|
|
note="EmptyState has no motion prop and no entrance animation of its own — collections decide how an empty appears (usually a plain swap). For choreographed entrances compose Cascade around it."
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'sema'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>
|
|
|
|
|
<span data-uix-layer-badge="sema">sema</span> · events
|
|
|
|
|
</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
EmptyState declares no semantic events. It is a passive surface: appearing/disappearing is a
|
|
|
|
|
fact of the COLLECTION that renders it (table / list / select), and announcing it belongs to
|
|
|
|
|
that collection's live region — mirroring how headless references model empties as a render
|
|
|
|
|
slot (<code>renderEmptyState</code> in React Aria, <code>emptyContent</code> in HeroUI). The
|
|
|
|
|
composed <code><Button></code>s in <code>.Actions</code> own their own sema.
|
|
|
|
|
</p>
|
|
|
|
|
<SemaPanel
|
|
|
|
|
actions={events}
|
|
|
|
|
{uix}
|
|
|
|
|
getTarget={() => stageRef?.querySelector('[data-empty-state]') ?? stageRef}
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'services'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Services</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
EmptyState consumes <strong>langs</strong> only for its catalog display name (<code
|
|
|
|
|
>texts.label</code
|
|
|
|
|
>
|
|
|
|
|
→ «{uix.langs.ts('#?components.empty-state.label|Empty state')}» — switch the language in
|
|
|
|
|
the topbar to see it follow). All VISIBLE copy (title, description, action labels) is app
|
|
|
|
|
content by design — the component bakes no strings, so it is translation-ready by
|
|
|
|
|
construction. No format / announce / clipboard consumption.
|
|
|
|
|
</p>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'api'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>API reference</h2>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>EmptyState props</div>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Prop</th><th>Type</th><th>Notes</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">size</td>
|
|
|
|
|
<td class="type">{sizes.join(' | ')}</td>
|
|
|
|
|
<td>
|
|
|
|
|
Density. <code>sm</code> = compact in-collection empty, <code>lg</code> = full-page
|
|
|
|
|
hero. Default <code>md</code>. Responsive.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>Parts</div>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Part</th><th>Prop</th><th>Notes</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">EmptyState.Media</td>
|
|
|
|
|
<td class="type">kind: {kinds.join(' | ')}</td>
|
|
|
|
|
<td>
|
|
|
|
|
<code>icon</code> paints the tinted icon box; <code>media</code> is a free slot for
|
|
|
|
|
illustrations / avatar groups. Default <code>icon</code>.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">EmptyState.Title</td>
|
|
|
|
|
<td class="type">level: 2 | 3 | 4 | 5 | 6</td>
|
|
|
|
|
<td>
|
|
|
|
|
Real <code><h{'{level}'}></code> (Atlaskit model). Semantic level and visual
|
|
|
|
|
size are decoupled — the look rides <code>size</code>. Default
|
|
|
|
|
<code>3</code>.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">EmptyState.Description</td>
|
|
|
|
|
<td class="type">—</td>
|
|
|
|
|
<td>Muted <code><p></code>, capped to a readable measure (45ch token).</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">EmptyState.Actions</td>
|
|
|
|
|
<td class="type">label?: string</td>
|
|
|
|
|
<td>
|
|
|
|
|
Hosts consumer <code><Button></code>s — one primary (verb-first), optional
|
|
|
|
|
secondary/ghost. <code>label</code> names the group (<code>role="group"</code> +
|
|
|
|
|
<code>aria-label</code>).
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>Reference comparison</div>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr><th>Library</th><th>Closest equivalent</th><th>Difference</th></tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">shadcn/ui</td>
|
|
|
|
|
<td><code>Empty / EmptyMedia / EmptyTitle / …</code></td>
|
|
|
|
|
<td>
|
|
|
|
|
Same anatomy incl. <code>EmptyMedia variant="icon"</code> (our
|
|
|
|
|
<code>kind</code>); shadcn adds an <code>EmptyHeader</code> wrapper we don't need (the
|
|
|
|
|
root centers directly).
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">chakra v3</td>
|
|
|
|
|
<td><code>EmptyState.Root/Indicator/Title/Description</code></td>
|
|
|
|
|
<td>Same slot recipe + <code>size</code>; no heading-level control.</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">atlaskit</td>
|
|
|
|
|
<td><code>EmptyState</code></td>
|
|
|
|
|
<td>
|
|
|
|
|
Source of <code>headingLevel</code> (their default h4; ours h3) and
|
|
|
|
|
<code>buttonGroupLabel</code> (our <code>Actions label</code>).
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">antd</td>
|
|
|
|
|
<td><code>Empty</code></td>
|
|
|
|
|
<td>
|
|
|
|
|
Ships image presets + <code>ConfigProvider.renderEmpty</code> global injection — collection
|
|
|
|
|
integration is a tracked gap here (see README §Gaps).
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'morfo'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Morfo contract</h2>
|
|
|
|
|
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Field</th><th>Value</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr><td class="name">name</td><td>{emptyStateMorfo.name}</td></tr>
|
|
|
|
|
<tr><td class="name">kebab</td><td><code>{emptyStateMorfo.kebab}</code></td></tr>
|
|
|
|
|
<tr><td class="name">scope</td><td>{emptyStateMorfo.scope.join(', ')}</td></tr>
|
|
|
|
|
<tr><td class="name">parts</td><td>{partsList.length}</td></tr>
|
|
|
|
|
<tr><td class="name">events</td><td>{events.length}</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>Parts</div>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>kebab</th>
|
|
|
|
|
<th>marker</th>
|
|
|
|
|
<th>element</th>
|
|
|
|
|
<th>archetype</th>
|
|
|
|
|
<th>optional</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each partsList as part (part.kebab)}
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">{part.kebab}</td>
|
|
|
|
|
<td><code data-uix-part-marker>[{part.marker}]</code></td>
|
|
|
|
|
<td class="type"><{part.defaultElement}></td>
|
|
|
|
|
<td class="type">{part.archetype}</td>
|
|
|
|
|
<td class="default">{part.optional ? 'yes' : 'no'}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p data-uix-section-desc style="margin-top: var(--uix-space-4);">
|
|
|
|
|
Title declares <code>role: 'heading'</code> intent in the contract; the eidos wrapper
|
|
|
|
|
renders the real <code><h{'{level}'}></code>. The wrapper-only visual attrs (<code
|
|
|
|
|
>data-size</code
|
|
|
|
|
>
|
|
|
|
|
on the root, <code>data-kind</code> on media) are eidos-only by design — they never cross to another
|
|
|
|
|
layer.
|
|
|
|
|
</p>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'recipe'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Eidos recipe</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
Recipe lives in <code>src/uix/eidos/components/empty-state/empty-state.css</code>. Public
|
|
|
|
|
tokens:
|
|
|
|
|
<code
|
|
|
|
|
>--empty-state-gap · actions-gap · media-bg · media-fg · media-radius ·
|
|
|
|
|
description-measure</code
|
|
|
|
|
>
|
|
|
|
|
(overridable via <code>recipes.empty-state.*</code>). Typography rides the
|
|
|
|
|
<code>--size-{'{k}'}-*</code> bundle — the title reads one discrete size up from the component
|
|
|
|
|
size.
|
|
|
|
|
</p>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name"><code>[data-empty-state]</code></td>
|
|
|
|
|
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
|
|
|
|
<td>Provider marker — centered column, gap + padding.</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name"><code>[data-empty-state][data-size='X']</code></td>
|
|
|
|
|
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
|
|
|
|
|
<td>Density — paddings + title/body font sizes + media glyph size.</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name"><code>[data-empty-state-media][data-kind='icon']</code></td>
|
|
|
|
|
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
|
|
|
|
|
<td>The tinted icon plate (2× glyph box, neutral track background).</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name"><code>[data-empty-state-title]</code></td>
|
|
|
|
|
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
|
|
|
|
<td>Semibold heading, one bundle size above the component size.</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name"><code>[data-empty-state-description]</code></td>
|
|
|
|
|
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
|
|
|
|
<td>Muted body copy, <code>max-inline-size</code> reading measure.</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name"><code>[data-empty-state-actions]</code></td>
|
|
|
|
|
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
|
|
|
|
|
<td>Wrapping centered action row.</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'a11y'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Accessibility</h2>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">Role</td>
|
|
|
|
|
<td>
|
|
|
|
|
None on the root — a static display surface, not a live region and not a landmark.
|
|
|
|
|
Announcing "no results" belongs to the collection that renders it.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">Heading</td>
|
|
|
|
|
<td>
|
|
|
|
|
Title is a real <code><h{'{level}'}></code> (default h3) so the empty participates
|
|
|
|
|
correctly in the document outline; pick the level that fits the page hierarchy.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">Actions group</td>
|
|
|
|
|
<td>
|
|
|
|
|
<code>Actions label</code> renders <code>role="group"</code> +
|
|
|
|
|
<code>aria-label</code> — an accessible name for multi-action rows (Atlaskit
|
|
|
|
|
<code>buttonGroupLabel</code>).
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">Media</td>
|
|
|
|
|
<td>
|
|
|
|
|
Decorative by default — pass <code>aria-hidden</code> glyphs or meaningful
|
|
|
|
|
<code>alt</code>s on real images; the plate itself carries no semantics.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="name">Content doctrine</td>
|
|
|
|
|
<td>
|
|
|
|
|
One primary action, verb-first ("Create project", never "OK"); description states
|
|
|
|
|
the reason + the next step (Polaris / Atlassian writing guidance).
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|