|
|
<script lang="ts" module>
|
|
|
/**
|
|
|
* Two visual modes:
|
|
|
*
|
|
|
* - 'square' — for box-like components (Icon, Avatar). Each size
|
|
|
* renders as an actual-size square in a baseline-aligned row, so
|
|
|
* the user sees the proportional difference at a glance: xxs is
|
|
|
* visibly tiny next to xxl.
|
|
|
* - 'bar' — for panel-like components (Dialog, Drawer, Popover).
|
|
|
* Each size is a horizontal bar whose width scales with the size,
|
|
|
* stacked vertically so the user reads it like a histogram.
|
|
|
*/
|
|
|
export type DemoSizesShape = 'square' | 'bar';
|
|
|
|
|
|
export type DemoSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'full';
|
|
|
|
|
|
// Absolute sizes for the comparative row. Cap the largest at ~22px
|
|
|
// so that with 1.8rem (~29 px) cell floors the squares always sit
|
|
|
// fully inside their cells — bigger squares would overflow into the
|
|
|
// gap and visually merge with neighbours. The proportional truth is
|
|
|
// still preserved (xxl ≈ 7× xxs).
|
|
|
const SQUARE_PX: Record<DemoSize, number> = {
|
|
|
xxs: 3,
|
|
|
xs: 5,
|
|
|
sm: 8,
|
|
|
md: 12,
|
|
|
lg: 16,
|
|
|
xl: 19,
|
|
|
xxl: 22,
|
|
|
full: 24
|
|
|
};
|
|
|
|
|
|
const BAR_PCT: Record<DemoSize, number> = {
|
|
|
xxs: 12,
|
|
|
xs: 22,
|
|
|
sm: 35,
|
|
|
md: 50,
|
|
|
lg: 65,
|
|
|
xl: 80,
|
|
|
xxl: 95,
|
|
|
full: 100
|
|
|
};
|
|
|
|
|
|
const ORDER: DemoSize[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl', 'full'];
|
|
|
</script>
|
|
|
|
|
|
<script lang="ts" generics="T extends DemoSize">
|
|
|
/**
|
|
|
* Size picker for demos. Renders a unified comparative scale (not a
|
|
|
* chip-per-size grid), so the magnitude relationship between values
|
|
|
* is the FIRST thing the reader perceives.
|
|
|
*
|
|
|
* - shape='square' → row of actual-size squares, baseline-aligned;
|
|
|
* active square fills with the accent.
|
|
|
* - shape='bar' → stacked horizontal bars, widths proportional
|
|
|
* to the size; active bar fills + name highlights.
|
|
|
*
|
|
|
* Generic over the size vocabulary so the consumer's narrower type
|
|
|
* (e.g. Drawer's 'sm'|'md'|'lg'|'full') flows through `bind:value`
|
|
|
* without widening casts.
|
|
|
*/
|
|
|
type Props = {
|
|
|
/** Currently selected size. Bindable. */
|
|
|
value: T;
|
|
|
/** Subset of sizes the component supports. Defaults to all eight. */
|
|
|
available?: readonly T[];
|
|
|
/** Visual mode — 'square' for icon-like, 'bar' for panel-like. */
|
|
|
shape?: DemoSizesShape;
|
|
|
/** Optional caption above the scale. */
|
|
|
label?: string;
|
|
|
};
|
|
|
|
|
|
let {
|
|
|
value = $bindable(),
|
|
|
available,
|
|
|
shape = 'square',
|
|
|
label
|
|
|
}: Props = $props();
|
|
|
|
|
|
const visible = $derived(
|
|
|
ORDER.filter((s) =>
|
|
|
(available ?? (ORDER as unknown as readonly T[])).includes(s as unknown as T)
|
|
|
)
|
|
|
);
|
|
|
</script>
|
|
|
|
|
|
<div class="control">
|
|
|
{#if label}
|
|
|
<span class="control-label">{label}</span>
|
|
|
{/if}
|
|
|
|
|
|
{#if shape === 'square'}
|
|
|
<div class="sizes-square-row sizes-box" role="radiogroup" aria-label={label ?? 'Size'}>
|
|
|
{#each visible as size (size)}
|
|
|
<button
|
|
|
type="button"
|
|
|
class="sq-cell"
|
|
|
class:active={(value as string) === size}
|
|
|
role="radio"
|
|
|
aria-checked={(value as string) === size}
|
|
|
onclick={() => (value = size as T)}
|
|
|
>
|
|
|
{#if size === 'full'}
|
|
|
<span class="sq-full" aria-hidden="true">
|
|
|
<span class="sq-full-cap"></span>
|
|
|
<span class="sq-full-bar"></span>
|
|
|
<span class="sq-full-cap"></span>
|
|
|
</span>
|
|
|
{:else}
|
|
|
<span
|
|
|
class="sq-shape"
|
|
|
style="--side: {SQUARE_PX[size]}px;"
|
|
|
aria-hidden="true"
|
|
|
></span>
|
|
|
{/if}
|
|
|
<span class="sq-name">{size}</span>
|
|
|
</button>
|
|
|
{/each}
|
|
|
</div>
|
|
|
{:else}
|
|
|
<div class="sizes-bar-stack sizes-box" role="radiogroup" aria-label={label ?? 'Size'}>
|
|
|
{#each visible as size (size)}
|
|
|
<button
|
|
|
type="button"
|
|
|
class="bar-row"
|
|
|
class:active={(value as string) === size}
|
|
|
role="radio"
|
|
|
aria-checked={(value as string) === size}
|
|
|
onclick={() => (value = size as T)}
|
|
|
>
|
|
|
<span class="bar-name">{size}</span>
|
|
|
<span class="bar-track" aria-hidden="true">
|
|
|
<span class="bar-fill" style="--bar-width: {BAR_PCT[size]}%;"></span>
|
|
|
</span>
|
|
|
</button>
|
|
|
{/each}
|
|
|
</div>
|
|
|
{/if}
|
|
|
</div>
|
|
|
|
|
|
<style>
|
|
|
/* Box wrapping the picker grid/stack — sits below `.control-label`,
|
|
|
* so the label stays outside the box (uniform with DemoText etc). */
|
|
|
.sizes-box {
|
|
|
padding: 0.5rem 0.6rem;
|
|
|
border: 1px solid var(--gray-200);
|
|
|
border-radius: var(--radius-md);
|
|
|
background: var(--gray-0);
|
|
|
}
|
|
|
|
|
|
/* ── Square shape — comparative row, baseline-aligned ───────────── */
|
|
|
.sizes-square-row {
|
|
|
display: flex;
|
|
|
align-items: end;
|
|
|
gap: 0.4rem;
|
|
|
min-inline-size: 0;
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
.sq-cell {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
gap: 0.4rem;
|
|
|
/* Cells size to their content (label or square — whichever wider)
|
|
|
* + a safety floor that fits the 3-char mono labels comfortably.
|
|
|
* `flex: 0 0 auto` prevents the equal-distribution collapse that
|
|
|
* would compress big-square cells into their neighbours. */
|
|
|
flex: 0 0 auto;
|
|
|
min-inline-size: 1.9rem;
|
|
|
padding: 0.3rem 0.25rem 0.4rem;
|
|
|
border: none;
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
cursor: pointer;
|
|
|
border-radius: var(--radius-sm);
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
|
|
}
|
|
|
.sq-cell:hover:not(.active) {
|
|
|
background: var(--gray-50);
|
|
|
}
|
|
|
.sq-cell.active {
|
|
|
background: color-mix(in srgb, var(--accent-500) 12%, transparent);
|
|
|
}
|
|
|
.sq-shape {
|
|
|
display: block;
|
|
|
inline-size: var(--side);
|
|
|
block-size: var(--side);
|
|
|
background: var(--gray-400);
|
|
|
border-radius: var(--radius-sm);
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
|
|
}
|
|
|
.sq-cell.active .sq-shape {
|
|
|
background: var(--accent-600);
|
|
|
}
|
|
|
.sq-cell:hover:not(.active) .sq-shape {
|
|
|
background: var(--gray-600);
|
|
|
}
|
|
|
.sq-name {
|
|
|
font-family: var(--font-mono);
|
|
|
font-size: 0.7rem;
|
|
|
color: var(--gray-500);
|
|
|
letter-spacing: 0.02em;
|
|
|
}
|
|
|
.sq-cell.active .sq-name {
|
|
|
color: var(--accent-700);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
/* `full` glyph — visually distinct from a fixed square: a horizontal
|
|
|
* track with end caps, conveying "fills its container". */
|
|
|
.sq-full {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 2px;
|
|
|
inline-size: 36px;
|
|
|
block-size: 8px;
|
|
|
}
|
|
|
.sq-full-cap {
|
|
|
flex: 0 0 3px;
|
|
|
block-size: 100%;
|
|
|
background: var(--gray-400);
|
|
|
border-radius: 2px;
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
|
|
}
|
|
|
.sq-full-bar {
|
|
|
flex: 1;
|
|
|
block-size: 4px;
|
|
|
background: var(--gray-400);
|
|
|
border-radius: var(--radius-full);
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
|
|
}
|
|
|
.sq-cell.active .sq-full-cap,
|
|
|
.sq-cell.active .sq-full-bar {
|
|
|
background: var(--accent-600);
|
|
|
}
|
|
|
.sq-cell:hover:not(.active) .sq-full-cap,
|
|
|
.sq-cell:hover:not(.active) .sq-full-bar {
|
|
|
background: var(--gray-600);
|
|
|
}
|
|
|
|
|
|
/* ── Bar shape — stacked histogram ──────────────────────────────── */
|
|
|
.sizes-bar-stack {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 0.18rem;
|
|
|
}
|
|
|
.bar-row {
|
|
|
display: grid;
|
|
|
grid-template-columns: 2.4rem 1fr;
|
|
|
align-items: center;
|
|
|
gap: 0.6rem;
|
|
|
padding: 0.3rem 0.5rem;
|
|
|
border: none;
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
cursor: pointer;
|
|
|
border-radius: var(--radius-sm);
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
|
|
}
|
|
|
.bar-row:hover:not(.active) {
|
|
|
background: var(--gray-50);
|
|
|
}
|
|
|
.bar-row.active {
|
|
|
background: color-mix(in srgb, var(--accent-500) 12%, transparent);
|
|
|
}
|
|
|
.bar-name {
|
|
|
font-family: var(--font-mono);
|
|
|
font-size: 0.72rem;
|
|
|
color: var(--gray-500);
|
|
|
letter-spacing: 0.02em;
|
|
|
text-align: end;
|
|
|
}
|
|
|
.bar-row.active .bar-name {
|
|
|
color: var(--accent-700);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
.bar-track {
|
|
|
display: block;
|
|
|
block-size: 10px;
|
|
|
border-radius: var(--radius-full);
|
|
|
background: var(--gray-100);
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
.bar-fill {
|
|
|
display: block;
|
|
|
block-size: 100%;
|
|
|
inline-size: var(--bar-width);
|
|
|
background: var(--gray-400);
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
|
|
}
|
|
|
.bar-row.active .bar-fill {
|
|
|
background: var(--accent-600);
|
|
|
}
|
|
|
.bar-row:hover:not(.active) .bar-fill {
|
|
|
background: var(--gray-600);
|
|
|
}
|
|
|
</style>
|