You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/uix/components/float/+page.svelte

519 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script lang="ts">
import { Float, type FloatProps, type FloatSide } from '$uix/eidos/components/float';
import { compileMorfo } from '$uix/morfo';
import { floatMorfo } from '@/uix/morfo/components/float';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
// ── Live state ───────────────────────────────────────────────────────
const sides: FloatSide[] = ['start', 'end'];
let side = $state<FloatSide>('start');
let gap = $state<number>(3);
let childWidth = $state<number>(140);
type Demo = 'image' | 'pull-quote' | 'drop-cap';
const demoOptions = ['image', 'pull-quote', 'drop-cap'] as const;
let demo = $state<Demo>('image');
const floatProps = $derived<Partial<FloatProps>>({
side,
gap,
maxWidth: childWidth
});
// ── Compiled morfo ───────────────────────────────────────────────────
const compiled = compileMorfo(floatMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
$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();
});
// ── Snippets ─────────────────────────────────────────────────────────
const somaSnippet = $derived(
[
'<!-- Float is eidos-native — no soma layer. -->',
'<!-- Equivalent semantic markup (not real soma): -->',
'',
'<div',
' data-box',
' data-float',
` data-side="${side}"`,
` style="float: inline-${side}; margin-inline-${side === 'start' ? 'end' : 'start'}: var(--space-${gap}); max-inline-size: ${childWidth}px;"`,
'>',
' …',
'</div>'
].join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Float } from '$uix/eidos/components/float';",
'</' + 'script>',
'',
'<p>',
' <Float',
side !== 'start' && ` side="${side}"`,
gap !== 3 && ` gap={${gap}}`,
childWidth !== 0 && ` maxWidth={${childWidth}}`,
' >',
demo === 'image' && ' <img src="diagram.png" alt="" />',
demo === 'pull-quote' && ' <blockquote>"…"</blockquote>',
demo === 'drop-cap' && ' <span class="drop">A</span>',
' </Float>',
' Long paragraph that wraps around the floated child…',
'</p>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Float</div>
<h1 data-uix-page-title>Float</h1>
<p data-uix-page-lede>
CSS-<code>float</code> primitive: pulls a child to the start or end of the text flow so the
surrounding inline content wraps around it. Composes through
<a href="/uix/components/box">&lt;Box&gt;</a>, so every Box prop (max-width, padding, margin,
…) still works. Typical uses: inline images, pull-quotes, drop caps. Uses logical
<code>float: inline-start / inline-end</code> so the primitive mirrors automatically in RTL.
Note: this is NOT the legacy 9-zone absolute-positioning Float — see the README's Baseline
section for the design history.
</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>0
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>extends</span>Box
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>scope</span>eidos
</span>
</div>
</header>
<!-- Live preview always rendered -->
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<article
style="max-inline-size: 36rem; padding: var(--space-4); border: 1px dashed var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-default); color: var(--color-neutral-text); line-height: 1.55;"
>
<Float {...floatProps}>
{#if demo === 'image'}
<div
style="display: grid; place-items: center; aspect-ratio: 4/3; background: linear-gradient(135deg, var(--color-primary-track), var(--color-affirm-track)); color: var(--color-primary-text); border-radius: var(--radius-sm); font-weight: 600;"
>
{childWidth}×{Math.round((childWidth * 3) / 4)}
</div>
{:else if demo === 'pull-quote'}
<blockquote
style="margin: 0; padding: var(--space-3); border-inline-start: 3px solid var(--color-primary-border); background: var(--color-primary-track); color: var(--color-primary-text); border-radius: var(--radius-sm); font-style: italic;"
>
"A short quotation pulled from the surrounding text to anchor the reader's
attention."
</blockquote>
{:else}
<span
style="display: block; font-size: 4rem; line-height: 0.9; font-weight: 700; color: var(--color-primary-text); padding-block-start: 0.25rem;"
aria-hidden="true">A</span
>
{/if}
</Float>
{#if demo === 'drop-cap'}
ndon a moment to consider how the CSS `float` property has shaped the layout of
long-form articles for two decades. Originally designed exclusively to allow text to
flow around an image, it became the de-facto positioning tool of the early web. Today
it has fallen out of favour for general layout, replaced by flex and grid, but it
remains the right answer for the narrow editorial case it was built for.
{:else}
Long-form prose flowing around the floated child. CSS `float` predates flex and grid
by two decades; it was originally designed exclusively to allow text to wrap around an
inline image. For modern layout, prefer
<code>&lt;Flex&gt;</code> / <code>&lt;Grid&gt;</code>; for inline editorial wraps —
pull-quotes, sidebar notes, drop caps — the CSS-`float` primitive is still the right
tool. The Float wrapper provides logical-side semantics so the same markup mirrors
automatically in RTL contexts.
{/if}
</article>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
<span>{trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`}</span>
<span style="color: var(--uix-text-faint)">·</span>
<span data-uix-stage-trace-key>side</span>
<span>{side}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>gap</span> {gap} ·
<span data-uix-stage-trace-key>maxWidth</span> {childWidth}px
</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 === 'api'} onclick={() => (tab = 'api')}>
API <span data-uix-tab-count>2</span>
</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 · 0e</span>
</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 === '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>
Float is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. The
<code>side</code> prop drives the logical float direction; <code>gap</code> sets the
inline margin against the wrapped text.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>side</span>
<span data-uix-chips role="radiogroup">
{#each sides as opt}
<button data-uix-chip data-active={side === opt} onclick={() => (side = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label
>gap <span data-uix-control-hint>0–8 → var(--space-N), inline margin from text</span></span
>
<input type="number" min="0" max="8" step="1" bind:value={gap} style="inline-size: 6rem;" />
</label>
<label data-uix-control>
<span data-uix-control-label
>maxWidth <span data-uix-control-hint>pixels (inherited from Box)</span></span
>
<input type="range" min="80" max="320" step="10" bind:value={childWidth} />
<span data-uix-control-value>{childWidth}px</span>
</label>
<label data-uix-control>
<span data-uix-control-label>demo content</span>
<span data-uix-chips role="radiogroup">
{#each demoOptions as opt}
<button data-uix-chip data-active={demo === opt} onclick={() => (demo = opt)}
>{opt}</button
>
{/each}
</span>
</label>
</div>
<!-- ── Code snippets per layer ─────────────────────────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>n/a · float is eidos-native — equivalent markup shown</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<div data-uix-code style="margin-top: var(--uix-space-3);">
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · side / gap + inherited Box props</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
Two props on the root + every Box prop inherited. The block-axis margin (top / bottom)
lives on the standard <code>marginTop</code> / <code>marginBottom</code> from Box,
because real-world floats often need a small padding-top to align with the ascender of
the surrounding text.
</p>
<div data-uix-subsection-head>Float 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">side</td>
<td class="type">{sides.join(' | ')}</td>
<td>
Logical side. Maps to <code>float: inline-start / inline-end</code>. Mirrors
in RTL. Default <code>start</code>.
</td>
</tr>
<tr>
<td class="name">gap</td>
<td class="type">number | string</td>
<td>
Inline margin against the wrapped text. Number → <code>var(--space-N)</code>.
Default <code>3</code>.
</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Inherited from Box</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">maxWidth / width / minWidth</td>
<td class="type">number | string</td>
<td>Constrain the float's inline size — essential for readable wrap behaviour.</td>
</tr>
<tr>
<td class="name">marginTop / marginBottom</td>
<td class="type">number | string</td>
<td>Block-axis margin against the surrounding text.</td>
</tr>
<tr>
<td class="name">padding</td>
<td class="type">number | string</td>
<td>Inner padding (e.g. for pull-quote boxes).</td>
</tr>
<tr>
<td class="name">…</td>
<td class="type">BoxProps</td>
<td>
See <a href="/uix/components/box">&lt;Box&gt;</a>. Note: <code>position</code>
is excluded — Float uses the cascade's `float`, not absolute positioning.
</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">radix-themes</td>
<td><code>&lt;Inset&gt;</code></td>
<td>
Radix Inset takes 4 physical sides; UIX Float uses logical
<code>start / end</code> so it mirrors in RTL.
</td>
</tr>
<tr>
<td class="name">mui</td>
<td>Pull-quote pattern</td>
<td>
MUI documents the pattern with manual <code>float: left</code> styling; UIX
wraps it as a typed primitive with logical sides.
</td>
</tr>
<tr>
<td class="name">mantine</td>
<td>n/a</td>
<td>
Mantine ships no float primitive; consumers write
<code>style={`{{ float: 'left' }}`}</code> manually.
</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>{floatMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{floatMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{floatMorfo.scope.join(', ')}</td></tr>
<tr><td class="name">parts</td><td>{partsList.length}</td></tr>
<tr><td class="name">events</td><td>0</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}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</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);">
Float declares a single Provider part. The recipe reads <code>data-side</code> to pick
the logical float direction; the <code>--float-gap</code> variable controls the inline
margin against the wrapped text.
</p>
</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>
Float declares no semantic events. As a passive layout primitive, it changes how its
child participates in the surrounding text flow and nothing else. Animations on float
direction changes (e.g. switching <code>side</code>) belong to the consumer's transition
logic, not to Float itself.
</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/float/float.css</code>. The
<code>data-side</code> attribute switches between <code>float: inline-start</code> and
<code>float: inline-end</code>; the inline margin against the text sits on the side that
faces the wrapped content.
</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-float]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker. Emitted on top of the Box shell.</td>
</tr>
<tr>
<td class="name"
><code>{`[data-box][data-float] { float: inline-start; margin-inline-end: var(--float-gap, var(--space-3)); }`}</code></td
>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Default side — float to start of text flow.</td>
</tr>
<tr>
<td class="name"><code>[data-box][data-float][data-side='end']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Override — float to end of text flow with mirrored gap.</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 implicit. Default element is <code>&lt;div&gt;</code>. When the float
holds a quote, wrap the inner content in <code>&lt;blockquote&gt;</code>;
when it holds an image, the consumer supplies <code>alt</code>.
</td>
</tr>
<tr>
<td class="name">Reading order</td>
<td>
The floated element appears in DOM order but visually wraps with surrounding
text. Screen readers respect DOM order; consider placing pull-quotes
<em>after</em> the paragraph they belong to if visual order matters.
</td>
</tr>
<tr>
<td class="name">Keyboard</td>
<td>Float is not focusable. Tab order follows DOM children.</td>
</tr>
<tr>
<td class="name">Focus visible</td>
<td>Float does not paint a focus ring.</td>
</tr>
<tr>
<td class="name">RTL</td>
<td>
<code>side='start'</code> mirrors automatically — in RTL the float pulls to
the right side. Use logical sides; avoid hardcoded
<code>float: left / right</code>.
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.