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.
295 lines
8.0 KiB
295 lines
8.0 KiB
<script lang="ts">
|
|
import { TestLayout, StateItem, PropsTable, DemoSection } from '../_components';
|
|
import { Terra, TimeField } from '$uix/terra';
|
|
import { createTerraTranslator, createTerraDateTimeFormatter } from '$uix/terra/system';
|
|
import { Time, type TimeValue, type HourCycle } from '$lib/util/dias';
|
|
|
|
const LOCALES = [
|
|
{ id: 'es-ES', label: 'es-ES' },
|
|
{ id: 'en-US', label: 'en-US' },
|
|
{ id: 'en-GB', label: 'en-GB' },
|
|
{ id: 'de-DE', label: 'de-DE' },
|
|
{ id: 'ja-JP', label: 'ja-JP' }
|
|
];
|
|
|
|
let locale = $state('es-ES');
|
|
const translator = createTerraTranslator({
|
|
locale: () => locale
|
|
});
|
|
let timeFormat = $state<HourCycle>(24);
|
|
const dateTimeFormatter = createTerraDateTimeFormatter({
|
|
timeFormat: () => timeFormat
|
|
});
|
|
|
|
let value1 = $state<Time | undefined>(new Time(9, 30, 0));
|
|
let value2 = $state<Time | undefined>(new Time(14, 45, 20));
|
|
let value3 = $state<Time | undefined>(new Time(8, 0, 0));
|
|
let value4 = $state<Time | undefined>(new Time(18, 15, 0));
|
|
|
|
function fmt(value: Time | undefined) {
|
|
if (!value) return '—';
|
|
return [value.hour, value.minute, value.second]
|
|
.map((part) => String(part).padStart(2, '0'))
|
|
.join(':');
|
|
}
|
|
|
|
const timeFieldProps = [
|
|
{
|
|
name: 'locale',
|
|
type: 'string',
|
|
default: '"es-ES"',
|
|
description: 'Locale para formato de hora'
|
|
},
|
|
{
|
|
name: 'timeFormat',
|
|
type: 'HourCycle',
|
|
default: '24',
|
|
description: 'Formato de hora: 12 o 24'
|
|
},
|
|
{
|
|
name: 'granularity',
|
|
type: '"hour" | "minute" | "second"',
|
|
default: '"minute"',
|
|
description: 'Nivel de precisión'
|
|
},
|
|
{
|
|
name: 'readonlySegments',
|
|
type: 'Segment[]',
|
|
default: '[]',
|
|
description: 'Segmentos en solo lectura'
|
|
},
|
|
{ name: 'hourCycle', type: 'HourCycle', default: '24', description: 'Ciclo de hora (12h/24h)' }
|
|
];
|
|
|
|
const breadcrumbs = [{ label: 'Test', href: '/test' }, { label: 'TimeField' }];
|
|
</script>
|
|
|
|
<TestLayout
|
|
title="TimeField"
|
|
description="Campo de entrada de hora con segmentos editables. Soporta locale, formato 12/24h y diferentes niveles de precisión."
|
|
badges={['Terra', 'v1.0']}
|
|
{breadcrumbs}
|
|
>
|
|
{#snippet demos()}
|
|
<!-- Controls -->
|
|
<div class="locale-bar">
|
|
<span class="locale-label">Locale:</span>
|
|
{#each LOCALES as option}
|
|
<button
|
|
class="locale-btn {locale === option.id ? 'locale-btn--active' : ''}"
|
|
onclick={() => (locale = option.id)}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="locale-bar">
|
|
<span class="locale-label">Time format:</span>
|
|
<button
|
|
class="locale-btn {timeFormat === 24 ? 'locale-btn--active' : ''}"
|
|
onclick={() => (timeFormat = 24)}>24h</button
|
|
>
|
|
<button
|
|
class="locale-btn {timeFormat === 12 ? 'locale-btn--active' : ''}"
|
|
onclick={() => (timeFormat = 12)}>12h</button
|
|
>
|
|
</div>
|
|
|
|
<!-- Demo 1: Básico -->
|
|
<DemoSection title="Básico" description="Campo simple con segmentos autoformateados.">
|
|
<TimeField.Provider bind:value={value1} {locale} {timeFormat}>
|
|
<TimeField.Label class="tf-label">Hora de inicio</TimeField.Label>
|
|
<TimeField.Input class="tf-input" name="start-time">
|
|
{#snippet children({ segments })}
|
|
{#each segments as { part, value }}
|
|
<TimeField.Segment {part} class="tf-seg tf-seg--{part}">{value}</TimeField.Segment>
|
|
{/each}
|
|
{/snippet}
|
|
</TimeField.Input>
|
|
</TimeField.Provider>
|
|
|
|
<div class="val-display">{fmt(value1)}</div>
|
|
</DemoSection>
|
|
|
|
<!-- Demo 2: Granularity second -->
|
|
<DemoSection
|
|
title="Granularity second"
|
|
description="Incluye segundos. Ideal para validar cómo cambia AM/PM."
|
|
variant="bordered"
|
|
>
|
|
<TimeField.Provider bind:value={value2} {locale} {timeFormat} granularity="second">
|
|
<TimeField.Label class="tf-label">Hora exacta</TimeField.Label>
|
|
<TimeField.Input class="tf-input">
|
|
{#snippet children({ segments })}
|
|
{#each segments as { part, value }}
|
|
<TimeField.Segment {part} class="tf-seg tf-seg--{part}">{value}</TimeField.Segment>
|
|
{/each}
|
|
{/snippet}
|
|
</TimeField.Input>
|
|
</TimeField.Provider>
|
|
|
|
<div class="val-display">{fmt(value2)}</div>
|
|
</DemoSection>
|
|
|
|
<!-- Demo 3: Readonly parcial -->
|
|
<DemoSection
|
|
title="Readonly parcial"
|
|
description="Deja minutos editables mientras bloquea segundos."
|
|
>
|
|
<TimeField.Provider
|
|
bind:value={value3}
|
|
{locale}
|
|
{timeFormat}
|
|
granularity="second"
|
|
readonlySegments={['second']}
|
|
>
|
|
<TimeField.Label class="tf-label">Hora parcial</TimeField.Label>
|
|
<TimeField.Input class="tf-input">
|
|
{#snippet children({ segments })}
|
|
{#each segments as { part, value }}
|
|
<TimeField.Segment {part} class="tf-seg tf-seg--{part}">{value}</TimeField.Segment>
|
|
{/each}
|
|
{/snippet}
|
|
</TimeField.Input>
|
|
</TimeField.Provider>
|
|
|
|
<div class="val-display">{fmt(value3)}</div>
|
|
</DemoSection>
|
|
|
|
<!-- Demo 4: Terra -->
|
|
<DemoSection
|
|
title="Vía TerraConfig"
|
|
description="Este ejemplo hereda locale y timeFormat desde TerraConfig."
|
|
variant="bordered"
|
|
>
|
|
<Terra {translator} presentation={{ dateTimeFormatter }}>
|
|
<TimeField.Provider bind:value={value4} granularity="minute">
|
|
<TimeField.Label class="tf-label">Hora heredada</TimeField.Label>
|
|
<TimeField.Input class="tf-input">
|
|
{#snippet children({ segments })}
|
|
{#each segments as { part, value }}
|
|
<TimeField.Segment {part} class="tf-seg tf-seg--{part}">{value}</TimeField.Segment>
|
|
{/each}
|
|
{/snippet}
|
|
</TimeField.Input>
|
|
</TimeField.Provider>
|
|
</Terra>
|
|
|
|
<div class="val-display">{fmt(value4)}</div>
|
|
</DemoSection>
|
|
{/snippet}
|
|
|
|
{#snippet state()}
|
|
<StateItem label="Locale" value={locale} type="string" icon="🌐" />
|
|
<StateItem label="Time format" value={timeFormat + 'h'} type="string" icon="🕐" />
|
|
<StateItem label="Hora 1" value={fmt(value1)} type="string" icon="⏰" />
|
|
<StateItem label="Hora 2" value={fmt(value2)} type="string" icon="⏱️" />
|
|
{/snippet}
|
|
|
|
{#snippet props()}
|
|
<PropsTable title="TimeField.Provider" props={timeFieldProps} />
|
|
{/snippet}
|
|
</TestLayout>
|
|
|
|
<style>
|
|
/* Locale bar */
|
|
.locale-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-3);
|
|
padding: var(--spacing-3) var(--spacing-4);
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
margin-bottom: var(--spacing-4);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.locale-label {
|
|
font-size: var(--text-xs);
|
|
color: var(--color-text-muted);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.locale-btn {
|
|
font-family: inherit;
|
|
font-size: var(--text-xs);
|
|
padding: var(--spacing-1) var(--spacing-3);
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
background: var(--color-background);
|
|
border: 1px solid var(--color-border);
|
|
color: var(--color-text-muted);
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.locale-btn:hover {
|
|
border-color: var(--color-primary);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.locale-btn--active {
|
|
border-color: var(--color-primary);
|
|
background: var(--color-primary-bg);
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
/* TimeField */
|
|
:global(.tf-label) {
|
|
display: block;
|
|
font-size: var(--text-sm);
|
|
color: var(--color-text-muted);
|
|
margin-bottom: var(--spacing-2);
|
|
font-weight: 500;
|
|
}
|
|
|
|
:global(.tf-input) {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-1);
|
|
padding: var(--spacing-3) var(--spacing-4);
|
|
border-radius: var(--radius-md);
|
|
border: 1px solid var(--color-border);
|
|
background: var(--color-surface-elevated);
|
|
min-height: 44px;
|
|
width: 100%;
|
|
max-width: 280px;
|
|
}
|
|
|
|
:global(.tf-seg) {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 1.7em;
|
|
padding: var(--spacing-1) var(--spacing-2);
|
|
border-radius: var(--radius-sm);
|
|
outline: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
:global(.tf-seg[data-type='literal']) {
|
|
min-width: auto;
|
|
padding: 0;
|
|
color: var(--color-text-dim);
|
|
}
|
|
|
|
:global(.tf-seg:focus),
|
|
:global(.tf-seg[data-focused]) {
|
|
background: var(--color-primary);
|
|
color: var(--color-background);
|
|
}
|
|
|
|
:global(.tf-seg[data-readonly]) {
|
|
opacity: 0.55;
|
|
}
|
|
|
|
/* Value display */
|
|
.val-display {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-sm);
|
|
color: var(--color-primary);
|
|
margin-top: var(--spacing-3);
|
|
}
|
|
</style>
|