import type { Meta, StoryObj } from '@storybook/react-vite' import { useState, type ComponentProps } from 'react' import { Avatar } from './Avatar' import { Field } from './Field' import { Select } from './Select' import { TextInput } from './TextInput' /** * A select drawn by the app rather than by the operating system. * * The trigger is `TextInput`'s field, so a select and a text field sitting in * the same form are the same control with different contents. * * Open one and try the keyboard: arrows move, Home and End jump, Enter and Space * pick, Escape closes and puts focus back on the trigger. That is what a native * ` } /** Field height, matching `TextInput` — the size for a form. */ export const Default: Story = { render: (args) => } /** Nothing chosen yet: the placeholder is muted, so an empty select does not read as one already holding a value. */ export const Empty: Story = { render: (args) => , // axe color-contrast is off here until PLAN.md stage 0.10 is ruled: // the placeholder is muted text, 3.21:1 on the field in signal (AA 4.5:1). parameters: { a11y: { config: { rules: [{ id: 'color-contrast', enabled: false }] } } }, } /** The dense size, matching `Button`'s small — for property rows, where a full-height field would dominate the label beside it. */ export const Small: Story = { render: (args) => } /** An option may carry a `leading` node — a face beside a person's name — shown in the trigger and in the list. Ship's addition. */ export const WithLeading: Story = { render: (args) => ( ({ value: name.split(' ')[0].toLowerCase(), label: name, leading: })), ]} /> ), } /** In a `Field`, which is how a select appears in a form. */ export const InAField: Story = { render: (args) => (
), } /** Unavailable — the same disabled treatment as every other control. To say why, `WithAReason`. */ export const Disabled: Story = { render: (args) => } /** `disabledReason`: it looks disabled and will not open, but Tab reaches it and the reason shows on hover and on focus. */ export const WithAReason: Story = { render: (args) => } /** Longer labels than the trigger is wide: the trigger truncates rather than growing; the menu takes the trigger's width as a minimum and grows past it. */ export const LongLabels: Story = { render: (args) => ( ), } /** Enough options to scroll — the menu caps its height rather than running off. */ export const ManyOptions: Story = { render: (args) => ({ value: `p${i}`, label: `Project ${i + 1}` }))} />, } /** * The files-panel bug, reproduced (Katerina, 2026-09-01): a trigger in the * bottom-right corner with a long people list. The menu used to run off the * right edge, past the bottom, and close itself on its own scroll. Now it * clamps to the viewport, opens upward when the room below is worse, and * scrolls — open it and try the wheel and the arrow keys. */ export const CorneredBottomRight: Story = { parameters: { layout: 'fullscreen' }, render: (args) => (
{/* `fixed`, escaping the meta decorator's 360px column: the point is the REAL viewport corner, where both clamps have to earn their keep. */}
({ value: `p${i}`, label: name, leading: , })), ]} />
), } /** The same list from a trigger high on screen — stays below, capped, scrollable. */ export const LongPeopleList: Story = { render: (args) => ( ({ value: `p${i}`, label: `Person ${i + 1}`, leading: , })), ]} /> ), } /** * A long label in a labelled property row (Katerina, 2026-09-01): the trigger * used to size to its content and shove the row past its card. `min-w-0 * max-w-full` lets the flex row shrink it, so the label truncates instead. */ export const LongLabelInPropertyRow: Story = { render: (args) => (
Lead , }, ]} />
), }