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.
423 lines
19 KiB
423 lines
19 KiB
<script lang="ts">
|
|
import {
|
|
FormatDate,
|
|
type FormatDateMode,
|
|
type FormatDateStyle,
|
|
type FormatTimeStyle
|
|
} from '$uix/eidos/components/format-date';
|
|
import { compileMorfo } from '$uix/morfo';
|
|
import { formatDateMorfo } from '@/uix/morfo/components/format-date';
|
|
import { getActiveUix } from '$active-uix';
|
|
|
|
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
|
|
let tab = $state<Tab>('live');
|
|
|
|
const uix = getActiveUix();
|
|
const compiled = compileMorfo(formatDateMorfo);
|
|
const partsList = $derived([...compiled.parts.byKebab.values()]);
|
|
|
|
// ── Live state ────────────────────────────────────────────────────────
|
|
// The demo owns its own `locale` state so the stage and pattern rows
|
|
// stay decoupled from the toolbar's global locale — otherwise every
|
|
// page on the site would re-render when this picker moves. The
|
|
// "Locale override" pattern row keeps its own hard-coded list so the
|
|
// reader can compare languages side-by-side independent of this
|
|
// picker.
|
|
type DemoLocale = 'es-ES' | 'en-US' | 'en-GB' | 'de-DE' | 'fr-FR' | 'ja-JP' | 'ar-EG';
|
|
const DEMO_LOCALES: readonly DemoLocale[] = [
|
|
'es-ES',
|
|
'en-US',
|
|
'en-GB',
|
|
'de-DE',
|
|
'fr-FR',
|
|
'ja-JP',
|
|
'ar-EG'
|
|
];
|
|
|
|
const SAMPLE_ISO = '2026-05-25T14:30:00Z';
|
|
let value = $state(SAMPLE_ISO);
|
|
let demoLocale = $state<DemoLocale>('es-ES');
|
|
let mode = $state<FormatDateMode>('date');
|
|
let dateStyle = $state<FormatDateStyle | ''>('medium');
|
|
let timeStyle = $state<FormatTimeStyle | ''>('');
|
|
let timeZone = $state('');
|
|
let timeZoneName = $state<'short' | 'long' | 'shortOffset' | 'longOffset' | ''>('');
|
|
let hourCycle = $state<'h11' | 'h12' | 'h23' | 'h24' | ''>('');
|
|
|
|
const sampleDates = {
|
|
now: new Date(),
|
|
yesterday: new Date(Date.now() - 86_400_000),
|
|
nextWeek: new Date(Date.now() + 7 * 86_400_000),
|
|
ancient: new Date('1969-07-20T20:17:40Z'),
|
|
future: new Date('2099-12-31T23:59:59Z')
|
|
};
|
|
</script>
|
|
|
|
<div data-uix-canvas-inner>
|
|
<header>
|
|
<div data-uix-eyebrow>Utilities · FormatDate</div>
|
|
<h1 data-uix-page-title>FormatDate</h1>
|
|
<p data-uix-page-lede>
|
|
Service component wrapping <code>getActiveUix().format.dates</code> for locale-aware
|
|
date / time / dateTime rendering. Renders as
|
|
<code><time datetime="ISO 8601"></code> by default for assistive tech. Same
|
|
family as <a href="/uix/components/trans">Trans</a>,
|
|
<a href="/uix/components/format-number">FormatNumber</a> and
|
|
<a href="/uix/components/relative-time">RelativeTime</a>.
|
|
</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>modes</span>3
|
|
</span>
|
|
<span data-uix-meta-pill>
|
|
<span data-uix-meta-key>locale</span>
|
|
<code>{demoLocale}</code>
|
|
</span>
|
|
<span data-uix-meta-pill>
|
|
<span data-uix-meta-key>scope</span>eidos
|
|
</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Live preview -->
|
|
<div data-uix-stage>
|
|
<div data-uix-stage-area>
|
|
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: var(--uix-space-6); inline-size: 100%;">
|
|
<div style="font-size: var(--uix-font-size-2xl); font-weight: 600;">
|
|
<FormatDate
|
|
{value}
|
|
{mode}
|
|
locale={demoLocale}
|
|
dateStyle={mode === 'time' || dateStyle === '' ? undefined : dateStyle}
|
|
timeStyle={mode === 'date' || timeStyle === '' ? undefined : timeStyle}
|
|
timeZone={timeZone === '' ? undefined : timeZone}
|
|
timeZoneName={timeZoneName === '' ? undefined : timeZoneName}
|
|
hourCycle={hourCycle === '' ? undefined : hourCycle}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div data-uix-stage-trace>
|
|
<span data-uix-stage-trace-key>value</span>
|
|
<code>{value}</code>
|
|
<span style="color: var(--uix-text-faint)">·</span>
|
|
<span data-uix-stage-trace-key>mode</span>
|
|
<span>{mode}</span>
|
|
<span style="margin-inline-start: auto;">
|
|
<span data-uix-stage-trace-key>locale</span>
|
|
<code>{demoLocale}</code>
|
|
</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>18</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>0</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>
|
|
|
|
<div data-uix-controls>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>value (ISO 8601)</span>
|
|
<input
|
|
type="text"
|
|
bind:value
|
|
style="inline-size: 18rem; font-family: var(--uix-font-mono);"
|
|
/>
|
|
</label>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>locale</span>
|
|
<span data-uix-chips role="radiogroup">
|
|
{#each DEMO_LOCALES as l}
|
|
<button
|
|
data-uix-chip
|
|
data-active={demoLocale === l}
|
|
onclick={() => (demoLocale = l)}>{l}</button
|
|
>
|
|
{/each}
|
|
</span>
|
|
</label>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>mode</span>
|
|
<span data-uix-chips role="radiogroup">
|
|
{#each ['date', 'time', 'dateTime'] as m}
|
|
<button
|
|
data-uix-chip
|
|
data-active={mode === m}
|
|
onclick={() => (mode = m as FormatDateMode)}>{m}</button
|
|
>
|
|
{/each}
|
|
</span>
|
|
</label>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>dateStyle</span>
|
|
<span data-uix-chips role="radiogroup">
|
|
{#each ['', 'short', 'medium', 'long', 'full'] as ds}
|
|
<button
|
|
data-uix-chip
|
|
data-active={dateStyle === ds}
|
|
onclick={() => (dateStyle = ds as FormatDateStyle | '')}>{ds || 'unset'}</button
|
|
>
|
|
{/each}
|
|
</span>
|
|
</label>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>timeStyle</span>
|
|
<span data-uix-chips role="radiogroup">
|
|
{#each ['', 'short', 'medium', 'long', 'full'] as ts}
|
|
<button
|
|
data-uix-chip
|
|
data-active={timeStyle === ts}
|
|
onclick={() => (timeStyle = ts as FormatTimeStyle | '')}>{ts || 'unset'}</button
|
|
>
|
|
{/each}
|
|
</span>
|
|
</label>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>timeZone</span>
|
|
<span data-uix-chips role="radiogroup">
|
|
{#each ['', 'UTC', 'Europe/Madrid', 'America/New_York', 'Asia/Tokyo'] as tz}
|
|
<button
|
|
data-uix-chip
|
|
data-active={timeZone === tz}
|
|
onclick={() => (timeZone = tz)}>{tz || 'local'}</button
|
|
>
|
|
{/each}
|
|
</span>
|
|
</label>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>timeZoneName</span>
|
|
<span data-uix-chips role="radiogroup">
|
|
{#each ['', 'short', 'long', 'shortOffset', 'longOffset'] as n}
|
|
<button
|
|
data-uix-chip
|
|
data-active={timeZoneName === n}
|
|
onclick={() => (timeZoneName = n as typeof timeZoneName)}>{n || 'unset'}</button
|
|
>
|
|
{/each}
|
|
</span>
|
|
</label>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>hourCycle</span>
|
|
<span data-uix-chips role="radiogroup">
|
|
{#each ['', 'h11', 'h12', 'h23', 'h24'] as hc}
|
|
<button
|
|
data-uix-chip
|
|
data-active={hourCycle === hc}
|
|
onclick={() => (hourCycle = hc as typeof hourCycle)}>{hc || 'unset'}</button
|
|
>
|
|
{/each}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div data-uix-subsection-head>Patterns · common shapes</div>
|
|
<div data-uix-pattern-stack>
|
|
<div data-uix-pattern>
|
|
<h3 data-uix-pattern-title>Preset styles · same instant rendered four ways</h3>
|
|
<div data-uix-pattern-row>
|
|
<table data-uix-table>
|
|
<thead><tr><th>Style</th><th>Date</th><th>Time</th><th>DateTime</th></tr></thead>
|
|
<tbody>
|
|
{#each ['short', 'medium', 'long', 'full'] as s}
|
|
<tr>
|
|
<td><code>{s}</code></td>
|
|
<td><strong><FormatDate value={SAMPLE_ISO} mode="date" dateStyle={s as FormatDateStyle} locale={demoLocale} /></strong></td>
|
|
<td><strong><FormatDate value={SAMPLE_ISO} mode="time" timeStyle={s as FormatTimeStyle} locale={demoLocale} /></strong></td>
|
|
<td><strong><FormatDate value={SAMPLE_ISO} mode="dateTime" dateStyle={s as FormatDateStyle} timeStyle={s as FormatTimeStyle} locale={demoLocale} /></strong></td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-uix-pattern>
|
|
<h3 data-uix-pattern-title>TimeZone override · same instant, different zones</h3>
|
|
<div data-uix-pattern-row>
|
|
<table data-uix-table>
|
|
<thead><tr><th>timeZone</th><th>per-axis + short zone</th></tr></thead>
|
|
<tbody>
|
|
{#each ['UTC', 'Europe/Madrid', 'America/New_York', 'Asia/Tokyo', 'Pacific/Auckland'] as tz}
|
|
<tr>
|
|
<td><code>{tz}</code></td>
|
|
<td><strong><FormatDate value={SAMPLE_ISO} year="numeric" month="short" day="numeric" hour="2-digit" minute="2-digit" timeZone={tz} timeZoneName="short" locale={demoLocale} /></strong></td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
<p data-uix-pattern-note>
|
|
<code>timeZoneName</code> is incompatible with <code>dateStyle</code> /
|
|
<code>timeStyle</code> presets per the Intl spec — use per-axis fields
|
|
(<code>year</code>, <code>month</code>, <code>hour</code>, …) when you
|
|
need a visible zone label. FormatDate drops the conflicting preset
|
|
silently if you mix them, so the call never throws.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-uix-pattern>
|
|
<h3 data-uix-pattern-title>Locale override · same instant, different formatting rules</h3>
|
|
<div data-uix-pattern-row>
|
|
<table data-uix-table>
|
|
<thead><tr><th>locale</th><th>full date</th><th>full dateTime</th></tr></thead>
|
|
<tbody>
|
|
{#each ['es-ES', 'en-US', 'de-DE', 'ja-JP', 'ar-EG'] as l}
|
|
<tr>
|
|
<td><code>{l}</code></td>
|
|
<td><strong><FormatDate value={SAMPLE_ISO} dateStyle="full" locale={l} /></strong></td>
|
|
<td><strong><FormatDate value={SAMPLE_ISO} mode="dateTime" dateStyle="medium" timeStyle="short" locale={l} /></strong></td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-uix-pattern>
|
|
<h3 data-uix-pattern-title>Per-axis overrides · explicit field formats</h3>
|
|
<div data-uix-pattern-row>
|
|
<table data-uix-table>
|
|
<thead><tr><th>Markup</th><th>Output</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>weekday="long" day="numeric" month="long"</code></td><td><strong><FormatDate value={SAMPLE_ISO} weekday="long" day="numeric" month="long" locale={demoLocale} /></strong></td></tr>
|
|
<tr><td><code>year="2-digit" month="2-digit" day="2-digit"</code></td><td><strong><FormatDate value={SAMPLE_ISO} year="2-digit" month="2-digit" day="2-digit" locale={demoLocale} /></strong></td></tr>
|
|
<tr><td><code>hour="2-digit" minute="2-digit" hourCycle="h23"</code></td><td><strong><FormatDate value={SAMPLE_ISO} mode="time" hour="2-digit" minute="2-digit" hourCycle="h23" locale={demoLocale} /></strong></td></tr>
|
|
<tr><td><code>era="long" year="numeric"</code></td><td><strong><FormatDate value={sampleDates.ancient} era="long" year="numeric" locale={demoLocale} /></strong></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-uix-pattern>
|
|
<h3 data-uix-pattern-title>Edge cases · null, invalid, fallback</h3>
|
|
<div data-uix-pattern-row>
|
|
<table data-uix-table>
|
|
<thead><tr><th>Markup</th><th>Output</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>value={'{null}'}</code></td><td>"<FormatDate value={null} locale={demoLocale} />" (empty)</td></tr>
|
|
<tr><td><code>value={'{null}'} fallback="—"</code></td><td><strong><FormatDate value={null} fallback="—" locale={demoLocale} /></strong></td></tr>
|
|
<tr><td><code>value="not a date" fallback="invalid"</code></td><td><strong><FormatDate value="not a date" fallback="invalid" locale={demoLocale} /></strong></td></tr>
|
|
<tr><td><code>value={'{new Date()}'} dateStyle="medium"</code></td><td><strong><FormatDate value={sampleDates.now} dateStyle="medium" locale={demoLocale} /></strong></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if tab === 'api'}
|
|
<section data-uix-section>
|
|
<h2 data-uix-section-title>API reference</h2>
|
|
<div data-uix-table-wrap>
|
|
<table data-uix-table>
|
|
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="name">value</td><td class="type">Date | number | string | null</td><td class="default empty">—</td><td>Date, epoch ms, or ISO 8601 string. Null/invalid → fallback.</td></tr>
|
|
<tr><td class="name">mode</td><td class="type">'date' | 'time' | 'dateTime'</td><td class="default">'date'</td><td>Runtime method dispatch.</td></tr>
|
|
<tr><td class="name">dateStyle</td><td class="type">'short' | 'medium' | 'long' | 'full'</td><td class="default empty">—</td><td>Preset date style.</td></tr>
|
|
<tr><td class="name">timeStyle</td><td class="type">'short' | 'medium' | 'long' | 'full'</td><td class="default empty">—</td><td>Preset time style.</td></tr>
|
|
<tr><td class="name">weekday / era / year / month / day</td><td class="type">'numeric' | '2-digit' | 'long' | 'short' | 'narrow'</td><td class="default empty">—</td><td>Per-axis overrides — override the preset.</td></tr>
|
|
<tr><td class="name">hour / minute / second</td><td class="type">'numeric' | '2-digit'</td><td class="default empty">—</td><td>Time axes.</td></tr>
|
|
<tr><td class="name">dayPeriod</td><td class="type">'narrow' | 'short' | 'long'</td><td class="default empty">—</td><td>AM/PM display flavour.</td></tr>
|
|
<tr><td class="name">fractionalSecondDigits</td><td class="type">1 | 2 | 3</td><td class="default empty">—</td><td>Subsecond precision.</td></tr>
|
|
<tr><td class="name">timeZone</td><td class="type">string</td><td class="default empty">— local</td><td>IANA zone (`Europe/Madrid`, …).</td></tr>
|
|
<tr><td class="name">timeZoneName</td><td class="type">'short' | 'long' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric'</td><td class="default empty">—</td><td>Zone label flavour.</td></tr>
|
|
<tr><td class="name">hourCycle</td><td class="type">'h11' | 'h12' | 'h23' | 'h24'</td><td class="default empty">—</td><td>12 vs 24 hour cycle.</td></tr>
|
|
<tr><td class="name">calendar</td><td class="type">string</td><td class="default empty">— gregory</td><td>Calendar override (`islamic`, `buddhist`, …).</td></tr>
|
|
<tr><td class="name">numberingSystem</td><td class="type">string</td><td class="default empty">—</td><td>Numbering system override.</td></tr>
|
|
<tr><td class="name">locale</td><td class="type">SupportedLocale</td><td class="default empty">— uix.format.dates locale</td><td>Optional locale override.</td></tr>
|
|
<tr><td class="name">fallback</td><td class="type">string</td><td class="default">''</td><td>Render when value is null/invalid.</td></tr>
|
|
<tr><td class="name">as</td><td class="type">keyof HTMLElementTagNameMap</td><td class="default">'time'</td><td>HTML tag. Default <code>'time'</code> + <code>datetime</code> attr for a11y.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if tab === 'morfo'}
|
|
<section data-uix-section>
|
|
<h2 data-uix-section-title>
|
|
<span data-uix-layer-badge="morfo">morfo</span> · declarative 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>{formatDateMorfo.name}</td></tr>
|
|
<tr><td class="name">kebab</td><td><code>{formatDateMorfo.kebab}</code></td></tr>
|
|
<tr><td class="name">scope</td><td>{formatDateMorfo.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>
|
|
</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>FormatDate declares no semantic events.</p>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if tab === 'recipe'}
|
|
<section data-uix-section>
|
|
<h2 data-uix-section-title>Eidos recipe</h2>
|
|
<p data-uix-section-desc>
|
|
FormatDate ships <strong>no CSS recipe</strong>. The rendered element is
|
|
<code><time></code> with a <code>datetime</code> attribute (ISO 8601);
|
|
the <code>[data-format-date]</code> marker exists only for tooling.
|
|
</p>
|
|
</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"><time> element</td><td>Default <code>as="time"</code> with <code>datetime="ISO 8601"</code> attribute exposes the canonical machine-readable timestamp alongside the human-readable rendering.</td></tr>
|
|
<tr><td class="name">Locale + timeZone</td><td>Both can be overridden per-instance, but defaults flow from the active runtime — keep that consistent across the app so assistive tech announces predictable phrasing.</td></tr>
|
|
<tr><td class="name">Invalid value</td><td>Invalid <code>value</code> renders the <code>fallback</code> (default empty). Screen readers skip empty <code><time></code> elements — set a visible fallback ("—", "unknown") if the absence carries meaning.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
[data-uix-pattern-stack] { display: flex; flex-direction: column; gap: var(--uix-space-5); }
|
|
[data-uix-pattern-row] { padding: var(--uix-space-4); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); }
|
|
h3[data-uix-pattern-title] { margin: 0 0 var(--uix-space-3); font-family: var(--uix-font-ui); font-size: var(--uix-font-size-sm); font-weight: 600; color: var(--uix-text-faint); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
</style>
|