@flood-md/react 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  Original React 18.3/19 components for TypeScript and Next.js App Router. Native
4
4
  HTML semantics and refs, no third-party component library or Tailwind dependency.
5
5
 
6
- This package documents the **0.2.2** API. Keep Flood package versions aligned
7
- and follow the guide matching your installed version.
6
+ This package documents the **0.2.3** API. Keep Flood package versions aligned and follow the guide matching
7
+ your installed declarations; source changes do not modify an existing npm release.
8
8
 
9
9
  ```tsx
10
10
  import "@flood-md/react/styles.css";
@@ -30,10 +30,20 @@ included. Legacy controls outside FloodRoot keep their earlier theme behavior.
30
30
 
31
31
  Exports: FloodRoot, Text, Heading, Stack, Inline, Surface, Divider, IconSlot,
32
32
  IconButton, CodeSnippet, Button, Input, Textarea, Checkbox, Radio, Switch, Field and Link,
33
- plus TypeScript prop types. Button variants are solid/subtle/outline/ghost;
33
+ plus TypeScript prop types. Version 0.2.3 also exports Label, Fieldset, RadioGroup, CheckboxGroup, ButtonGroup, InputGroup, Toggle, ToggleGroup, SegmentedControl, Collapsible, Accordion, Tooltip, Popover, Dialog, Tabs, Breadcrumb, Pagination, Select
34
+ and ScrollArea. Button variants are solid/subtle/outline/ghost;
34
35
  sizes are 2xs/xs/sm/md/lg/xl/2xl. Prefer subtle for standalone secondary actions;
35
36
  ghost belongs in a clear parent context. Loading disables activation.
36
37
 
38
+ Version 0.2.3 adds LinkProps with `variant="text" | "solid" | "subtle" |
39
+ "outline" | "ghost"` and the same seven `size` values as Button. The default
40
+ text link keeps its existing appearance and inherits text size; size affects
41
+ only button variants. These variants reuse Button's geometry, tokens and states
42
+ while retaining a real anchor, href, ref, target and browser modifier navigation.
43
+ Use Button for actions; do not add role=button or nest a Button in a Link.
44
+ There is no disabled/loading navigation API. This compatible opt-in extension
45
+ is available in 0.2.3.
46
+
37
47
  Heading adds `size="display"` for large marketing headings: 5.5rem / 1.12,
38
48
  weight 500, tracking -.04em. Existing heading sizes retain their metrics.
39
49
  Text adds `size="lead"` for introductory copy: 1.375rem / 1.45, weight 400,
@@ -44,11 +54,54 @@ font stack; the host owns copy actions and status messages. Both additions are
44
54
  compatible extensions in 0.2.2; they do not replace existing defaults.
45
55
 
46
56
  Input/Textarea controlSize is sm/md/lg/xl; native Input size is preserved.
57
+ Version 0.2.3 adds `status="error" | "warning" | "success" | "info"` to Input,
58
+ Textarea, Select and Field. Use Field's `message` for a description associated
59
+ with the control; spreading its callback props forwards status too. Existing
60
+ `error`, `invalid` or affirmative `aria-invalid` override positive statuses.
61
+ Only error sets aria-invalid; warning, success and info do not reject a value.
62
+ These visual/ARIA states do not replace native constraints or host validation.
63
+
64
+ Input/Textarea `readOnly` preserves focus, selection, copying and form submission
65
+ while preventing edits. Inside dark foundations it uses a canvas background and
66
+ quiet dashed perimeter at rest; focus retains the solid 2px accent. It differs
67
+ from disabled, which prevents ordinary focus and omits the value from submission.
68
+ Native HTML readOnly applies to text-like input types and Textarea; it is not a
69
+ Select or checkbox state. Explain why a value is read only in the surrounding UI.
70
+
71
+ Dark fields use one quiet uniform `color.field.border` perimeter; focus replaces it with a 2px
72
+ accent without a gap or layout shift. Textarea scrolls its content without a
73
+ native resize grip by default; hosts can opt into resizing through the native
74
+ style prop, for example `style={{ resize: "vertical" }}`.
47
75
  Field uses `{props => <Input {...props} />}`. Its required prop forwards native
48
76
  required and displays a marker; describedBy merges external IDs with hint/error
49
77
  associations. Checkbox/Radio/Switch require a visible label. IconSlot accepts
50
78
  host SVG artwork without bundling an icon pack.
51
79
 
80
+ ## Select and choice layout
81
+
82
+ Version 0.2.3 adds **Select**. It accepts native select props/ref, options and optgroups,
83
+ plus `invalid` and `controlSize="sm" | "md" | "lg" | "xl"` (md by default).
84
+ Use Field's callback props on Select to associate the label and descriptions.
85
+ Single selection uses a custom Flood trigger/listbox popup with a native select
86
+ bridge for form values and the HTMLSelectElement ref. Native numeric `size` and
87
+ `multiple` retain an inline listbox without a dropdown chevron or OS popup.
88
+ For single selection, arrows/Home/End highlight without committing; Enter,
89
+ Space or Tab commits, and Escape closes without changing the value. Typeahead
90
+ matches option labels. Focus stays on the trigger; `ref.current.focus()` targets
91
+ it while the ref itself remains the native select. Read changes from
92
+ `event.currentTarget.value` and keep required/disabled/form props on Select.
93
+ Use ordinary option/optgroup children with text labels; an option is not an
94
+ arbitrary interactive slot. See the usage guide for reset and controlled values.
95
+
96
+ Inside FloodRoot, Checkbox/Radio use Flood's custom tick/dot appearance on real
97
+ native inputs. Values, checked/defaultChecked, keyboard, forms and refs remain
98
+ native; forced colors restores native indicators. The glyph stays 1.125rem square,
99
+ with a .5rem label gap and centered alignment. Switch also keeps its label close.
100
+ Use `Inline gap="4"` for a 1rem gap between separate choices; Inline's default
101
+ gap="2" is unchanged. Buttons retain intrinsic width; full width is a deliberate
102
+ host opt-in. See [layout rules](../../docs/layout.md) and
103
+ [Select usage](../../docs/components.md#select).
104
+
52
105
  Import CSS once in Next app/layout.tsx. Add "use client" to host components
53
106
  using state/callbacks or Field's function child. Override semantic --flood-sys-*
54
107
  variables on the scope after the import. Surface overlay is visual elevation,
@@ -57,3 +110,400 @@ not dialog behavior. The focus accent remains configurable and provisional.
57
110
  See [components](../../docs/components.md), [foundations](../../docs/foundations.md)
58
111
  and [Inter provenance](src/fonts/README.md). Verify contrast, focus, keyboard,
59
112
  narrow layouts and text scaling in your actual application.
113
+
114
+
115
+ ## Label and Fieldset
116
+
117
+ Label forwards native label attributes and an HTMLLabelElement ref. Connect its
118
+ htmlFor to a unique control ID, or nest one labelable control. Its required marker
119
+ is decorative; required belongs on the actual control. Field composes this Label.
120
+
121
+ Fieldset forwards native fieldset attributes/ref and takes a required legend.
122
+ It provides hint/message/error/status and merges their IDs with aria-describedby.
123
+ Error wins over advisory feedback. Descriptions belong to the group rather than
124
+ being copied onto every child. Disabled uses native descendant behavior: controls
125
+ in the first legend are exempt. Avoid interactive legend content unless intended.
126
+ Disabled values are omitted from native FormData; nested fieldsets preserve HTML
127
+ rules. Styling removes the browser border but retains the actual fieldset/legend.
128
+ It does not own choice values, implement a selection model or validate children.
129
+
130
+ ```tsx
131
+ <Fieldset legend="Notifications" hint="Choose the updates you want.">
132
+ <Checkbox label="Product updates" name="updates" defaultChecked />
133
+ <Checkbox label="Release notes" name="releases" />
134
+ </Fieldset>
135
+ ```
136
+
137
+ ## ButtonGroup and InputGroup
138
+
139
+ ButtonGroup requires a label and direct Button/IconButton children of compatible
140
+ sizes. It joins visible edges with finite field radii and one full separator along the joined edge, supports
141
+ horizontal/vertical orientation and keeps normal Tab/disabled/action behavior.
142
+ It does not implement a toolbar or selection model. Do not wrap actions in extra
143
+ DOM elements; fragments are fine. Avoid overflow clipping around focused actions.
144
+
145
+ InputGroup accepts text-like Input props plus start/end ReactNode adornments.
146
+ The ref, id, name, value, handlers and ARIA attributes target the actual input;
147
+ className/style target the wrapper. dir/lang/hidden apply to the shell as well.
148
+ Use Field's callback or an associated Label. Adornments are visual and are not
149
+ included in the submitted value or used as a replacement for a label.
150
+ Native required, readOnly and disabled remain input states; host-owned addon
151
+ buttons need their own names, callbacks and disabled policy. Direct Flood Button/IconButton addons inherit their height from the group, with
152
+ an equal space-1 inset against the top, bottom and nearest logical edge; their
153
+ finite radius follows the shell radius minus that inset. Their size prop still
154
+ controls text, icon and horizontal padding. Coarse pointers grow the shell to
155
+ preserve the nested action's touch target. Arbitrary addon content can grow it too.
156
+ Input focus uses the shared perimeter, while an addon action keeps its own focus.
157
+ Use scoped host layout for group width. Textarea/Select require other compositions.
158
+ Both exports are available in 0.2.3.
159
+
160
+ ```tsx
161
+ <Field label="Website">
162
+ {control => <InputGroup {...control} start="https://" placeholder="example.com" />}
163
+ </Field>
164
+ ```
165
+
166
+ ## RadioGroup and CheckboxGroup
167
+
168
+ Both compose Fieldset and the existing native Radio/Checkbox controls. Supply a
169
+ legend, a name for the inputs and an options array with unique string values,
170
+ visible labels and optional disabled flags. The ref targets HTMLFieldSetElement.
171
+ Orientation is vertical by default; horizontal wraps. Disabled, hint, status,
172
+ message and error follow Fieldset. These exports are available in 0.2.3.
173
+ Choice groups use compact sm-height rows without an additional vertical gap.
174
+ Coarse pointers retain the touch-target minimum. Wrapping labels can grow a row;
175
+ the indicator remains explicitly sized. Horizontal groups keep space-4 between
176
+ columns. Legend/helper spacing belongs to the group rather than each option.
177
+
178
+ RadioGroup uses a string value/defaultValue and onValueChange(string).
179
+ CheckboxGroup uses an array value/defaultValue and onValueChange(string[]).
180
+ Omitting value selects uncontrolled mode; otherwise update value in the host.
181
+ Native form reset restores the latest defaultValue in uncontrolled mode and
182
+ preserves the host's controlled value. A canceled reset does nothing; reset does
183
+ not call onValueChange. Values for missing options remain in state but are not
184
+ submitted. Disabled options are omitted from FormData.
185
+
186
+ Name applies to inputs, not the fieldset. CheckboxGroup submits repeated entries;
187
+ read them with FormData.getAll(name). The form prop associates every input with
188
+ an external form. RadioGroup required uses native single-choice validity.
189
+ CheckboxGroup intentionally has no required/min/max prop: validate the array in
190
+ the host and describe errors through status/message. Empty/fully disabled options
191
+ need a host decision about whether submission is meaningful.
192
+
193
+ ```tsx
194
+ <RadioGroup name="visibility" legend="Visibility" defaultValue="private"
195
+ options={[{ value: "private", label: "Private" }, { value: "team", label: "Team" }]} />
196
+ ```
197
+
198
+ ## overlay ScrollArea
199
+
200
+ ScrollArea keeps native wheel, touch and keyboard scrolling while drawing inset
201
+ tracks above content. Set height/maxHeight on its viewport through style; choose
202
+ orientation vertical (default), horizontal or both. The HTMLDivElement ref and
203
+ native div props/className/style target that actual scroll viewport. Supply an
204
+ accessible label when it is a standalone scroll region. Tracks do not reserve
205
+ layout width; inner content padding keeps text clear of them by default. Forced colors and unavailable
206
+ enhancement restore native scrollbars rather than hide scrolling access.
207
+
208
+ Rails automatically shorten near rounded corners on the viewport or a clipping
209
+ ancestor, including asymmetric corners. This does not add a gutter. The default
210
+ `contentInset="scrollbar"` protects text and full-width interactive rows: while
211
+ an overlay rail is present, the inner content receives logical end padding for
212
+ that axis (rail size plus two insets). The viewport keeps its full width; row
213
+ backgrounds and text stop before the rail. Explicit `contentInset="none"` preserves
214
+ edge-to-edge content when the host already provides safe padding. The default
215
+ padding can wrap text and change content height. Native
216
+ fallback scrollbars retain their platform behavior.
217
+
218
+ ```tsx
219
+ <ScrollArea aria-label="Activity" style={{ maxHeight: "12rem" }}>
220
+ <Stack gap="4">{/* Long activity content */}</Stack>
221
+ </ScrollArea>
222
+ ```
223
+
224
+ This export is available in 0.2.3. The website
225
+ uses the same component for code, tables and navigation; Select reuses its internal
226
+ scroll viewport. See [layout](../../docs/layout.md) for constrained compositions.
227
+
228
+ InputGroup start/end are independent slots: use short prefixes (`https://`, `+7`,
229
+ `@`, currency), suffixes (`kg`, `%`, `USD`) or named Button/IconButton actions on
230
+ either side. Plain addons use subtle content color; actions retain their action
231
+ color. Include necessary units/format in the accessible label or description.
232
+ Prefixes are not masks and are not prepended to submitted values automatically;
233
+ normalization and complete phone/URL values belong to the host. Do not hide an
234
+ interactive addon from assistive technology. ButtonGroup supports text buttons,
235
+ IconButton or a mixture; every icon-only action needs its own accessible name.
236
+
237
+
238
+ ## Toggle and ToggleGroup
239
+
240
+ Toggle uses a native button with aria-pressed and type=button. Use defaultPressed
241
+ for local state or pressed/onPressedChange for controlled state. Keep its visible
242
+ or accessible name stable across changes. Native onClick runs first; calling
243
+ preventDefault cancels the state change. Disabled/loading prevent activation.
244
+ iconOnly wraps host artwork in IconSlot; supply aria-label for that action.
245
+ Sizes and variants follow Button. The ref targets HTMLButtonElement.
246
+
247
+ ToggleGroup takes a label and unique-value options with label, optional icon and
248
+ disabled. selectionMode is multiple by default; single permits at most one value
249
+ and allows clearing it. Both modes use string arrays: defaultValue for local
250
+ state, value/onValueChange for host state. Controlled values do not update unless
251
+ the host updates them. Keep the selection mode stable, or remount/reset state
252
+ when changing it. Missing-option values remain in host/local state; trim them in
253
+ the host if needed. Empty options render an empty named group.
254
+
255
+ Tab visits each enabled toggle. Orientation-aware arrows (including RTL), Home
256
+ and End move focus, skip disabled options and do not select. Enter/Space activate.
257
+ The group retains intrinsic action widths rather than breaking words or compressing
258
+ icons. In constrained layouts use an enclosing horizontal ScrollArea, icon-only
259
+ actions or vertical orientation. The ref targets HTMLDivElement. iconOnly requires icons and uses option labels as
260
+ accessible names. Text+icon and text-only options are supported too.
261
+
262
+ These are pressed action buttons, not tabs, radio inputs or switches. No hidden
263
+ inputs, automatic form submission or native form-reset behavior is added. For
264
+ form fields use RadioGroup/CheckboxGroup; for panels use Tabs.
265
+ Uncontrolled default props initialize state only. Both exports are available in 0.2.3.
266
+
267
+ ```tsx
268
+ <Toggle defaultPressed onPressedChange={active => console.log(active)}>Bold</Toggle>
269
+ <ToggleGroup label="Text formatting" defaultValue={["bold"]}
270
+ options={[{value: "bold", label: "Bold"}, {value: "italic", label: "Italic"}]} />
271
+ ```
272
+
273
+
274
+ ## SegmentedControl
275
+
276
+ SegmentedControl is a segmented presentation of Flood RadioGroup with native
277
+ radio inputs, a visible label/legend and shared inset surface. Supply label,
278
+ name and unique-value options (string label, optional icon/disabled). Use short
279
+ labels for a small set of modes. Default orientation is horizontal; vertical,
280
+ iconOnly and fullWidth are supported. iconOnly requires each option's icon and
281
+ keeps its label available to assistive technology. fullWidth distributes equal
282
+ width to horizontal segments. controlSize is sm/md/lg/xl; touch targets may grow
283
+ the shared shell. Intrinsic groups retain readable labels rather than compressing
284
+ icons; use a constrained ScrollArea or another orientation if needed.
285
+
286
+ value/onValueChange use a single string; defaultValue initializes local state.
287
+ An absent or unmatched initial value leaves the group unselected. Once selected,
288
+ clicking the current segment cannot clear it; controlled host state can clear it.
289
+ Keep names unique among independent groups in the same form. Native name, form,
290
+ required, disabled fieldsets and reset are preserved through RadioGroup. Reset
291
+ restores the latest defaultValue for local state, preserves controlled host
292
+ values, respects cancellation and does not call onValueChange. Empty/missing
293
+ options are not invented. No automatic fallback to the first option is applied.
294
+
295
+ Tab reaches the selected enabled radio (browser defaults apply if none is
296
+ selected). Arrows focus and select, wrapping and skipping disabled options;
297
+ horizontal left/right follow RTL. Home/End select the first/last enabled item.
298
+ Space selects without clearing. A canceled native onKeyDown skips Flood's
299
+ keyboard enhancement. Enter retains native radio/form behavior. The ref targets
300
+ HTMLFieldSetElement. Hint/message/error/status follow Fieldset.
301
+
302
+ Use SegmentedControl for one value/mode, ToggleGroup for optional pressed actions
303
+ and actual tabs for associated tab panels. This does not implement tab panels.
304
+ Available in 0.2.3. Use matching installed declarations.
305
+
306
+ ```tsx
307
+ <SegmentedControl label="Time range" name="period" defaultValue="day"
308
+ options={[{value: "day", label: "Day"}, {value: "week", label: "Week"},
309
+ {value: "month", label: "Month"}]} />
310
+ ```
311
+
312
+
313
+ ## visually hidden external labels
314
+
315
+ Field and Fieldset accept hideLabel (false by default). It visually hides the real
316
+ Label or legend, preserving its association and accessible name, while removing
317
+ the label's layout row and spacing. The required label/legend prop remains required.
318
+ RadioGroup, CheckboxGroup and SegmentedControl inherit hideLabel. Required native
319
+ controls, descriptions, error/status, group names and form behavior are unchanged.
320
+ Keep essential instructions in a visible hint/message when they are needed.
321
+ Use textual label content; do not put interactive elements inside hidden labels.
322
+
323
+ For Input/Textarea/Select/InputGroup apply hideLabel to their surrounding Field,
324
+ not the native control. Input aria-label/aria-labelledby remain available when
325
+ the host deliberately provides its own labeling. Placeholder is not a label.
326
+ ButtonGroup/ToggleGroup already expose their group label as an accessible name;
327
+ this flag does not hide button text or the per-option labels of Checkbox/Radio/
328
+ Switch. Standalone icon actions still need an accessible name.
329
+
330
+ ```tsx
331
+ <Field label="Search projects" hideLabel>
332
+ {control => <Input {...control} type="search" />}
333
+ </Field>
334
+ <SegmentedControl label="Time range" hideLabel name="period" defaultValue="day"
335
+ options={[{value:"day",label:"Day"},{value:"week",label:"Week"}]} />
336
+ ```
337
+
338
+ This compatible prop addition is available in 0.2.3. It requires Flood CSS.
339
+
340
+
341
+ ## Collapsible and Accordion
342
+
343
+ Collapsible takes a plain-text title, React children and open/onOpenChange or
344
+ local defaultOpen. Default closed; native Enter/Space activate its named button.
345
+ The trigger owns aria-expanded and aria-controls with stable instance IDs. Its
346
+ optional headingLevel adds a native h2–h6 around the button. Disabled affects the
347
+ trigger only; content inside an open panel remains interactive. controlSize is
348
+ sm/md/lg/xl. Native div props, className/style and HTMLDivElement ref target the
349
+ root; title names the trigger rather than the root's HTML title attribute.
350
+
351
+ Accordion takes an accessible group label and unique-value items with title,
352
+ content and optional disabled. expansionMode is single by default (one or none
353
+ open), or multiple. Both use string-array value/onValueChange or defaultValue.
354
+ The default headingLevel is 3; choose 2–6 to match surrounding headings. Up/Down,
355
+ Home/End focus enabled top-level triggers without changing expansion. Tab reaches
356
+ triggers and open panel controls normally; Enter/Space open/close. Nested accordions
357
+ keep their own keyboard navigation. The ref targets the group div. There is no
358
+ visible group caption to hide; headings and trigger titles remain visible.
359
+
360
+ Defaults initialize local state only. Controlled state needs host updates; keep
361
+ expansionMode stable or reconcile/remount when switching. Duplicate item values
362
+ and multiple expanded values in single mode are rejected. Missing-item values
363
+ remain in state until the host removes them. Disabled does not force panels shut.
364
+
365
+ Both share the same original disclosure anatomy. Panels stay mounted with native
366
+ hidden when closed, preserving local content state while removing hidden content
367
+ from focus/navigation. Existing form values and constraints remain native even
368
+ when hidden; manage inactive form fields in the host. If programmatic closing
369
+ hides focused content, focus returns to the enabled trigger unless already moved
370
+ elsewhere. Hosts own focus if the trigger is disabled or its whole item is removed.
371
+ There is no built-in Escape-to-close, form reset or persistence. Panels change
372
+ visibility immediately; only the chevron animates, with reduced motion disabling
373
+ that transition. No extra landmark regions are created per panel.
374
+
375
+ ```tsx
376
+ <Collapsible title="Advanced settings"><Input aria-label="Project prefix" /></Collapsible>
377
+ <Accordion label="Questions" headingLevel={2} items={[
378
+ {value:"install",title:"How do I install Flood?",content:"Install the packages."},
379
+ ]} />
380
+ ```
381
+
382
+ Available in 0.2.3. Import Flood CSS and use FloodRoot for dark foundations.
383
+
384
+ Disclosure corner geometry: Accordion headers have square internal seams. Only
385
+ the first header has upper shell corners, and only a closed last header has lower
386
+ shell corners. An open Collapsible header has square lower corners where its
387
+ panel begins. Hover backgrounds follow that same shape without clipping focus.
388
+
389
+
390
+ ## Tooltip, Popover and Dialog
391
+
392
+ All three use a trigger render function: `trigger={props => <Button {...props}>Open</Button>}`.
393
+ Forward every supplied prop and ref to one actual Button/native button in the same
394
+ document. Keep it mounted and visible while its overlay is open. Compose any host
395
+ handlers with the supplied ones, rather than replacing them; merge existing
396
+ aria-describedby with Tooltip's supplied value if adding another description.
397
+ These client interactions require modern native Popover API / dialog showModal
398
+ support. No older-browser polyfill is bundled. Native top-layer placement keeps
399
+ the panel in its original DOM/theme scope and avoids overflow clipping.
400
+
401
+ Tooltip takes text-only content, side (top default), align (center default),
402
+ delay in milliseconds (300 default) and disabled. Keyboard focus shows immediately;
403
+ mouse/pen hover uses the delay. Moving onto the tooltip keeps it visible. Escape
404
+ dismisses until a new focus/hover interaction. It never moves focus; content is
405
+ associated via aria-describedby. disabled suppresses the tooltip, not its trigger
406
+ action. Touch hover is not relied on; essential instructions belong in visible
407
+ content. Use Popover for interactive content; tooltip text is not a button's name.
408
+
409
+ Popover takes a visible title, children, trigger, side (bottom), align (start),
410
+ open/onOpenChange or local defaultOpen. It is a nonmodal dialog: focus enters its
411
+ content, but Tab is not trapped. Escape, outside pointer and focus leaving request
412
+ closing. The upper-end close action is an IconButton with a decorative cross; closeLabel
413
+ configures its accessible name. initialFocusRef can
414
+ choose an enabled element inside the panel; otherwise content gets initial focus.
415
+ Disabled prevents trigger activation. The div ref/className/style/native props
416
+ target the actual panel. Side flips and coordinates clamp near viewport edges;
417
+ scroll, viewport resize and layout changes reposition it. Native hidePopover requests state changes and a controlled owner can refuse
418
+ closure. Engine owns top/left,
419
+ position and available-size variables. Logical start/end alignment follows RTL.
420
+
421
+ Dialog uses native showModal for backdrop, inert background and nested modal
422
+ behavior. It takes title, optional plain description, children, trigger and the
423
+ same controlled/local state, disabled trigger, closeLabel and initialFocusRef.
424
+ Its upper-end close action also uses IconButton.
425
+ Escape requests closure of the topmost layer. closeOnOutsideClick defaults true;
426
+ turning it off preserves Escape and the explicit close action. Tab/Shift+Tab cycle
427
+ inside the modal, including nested nonmodal content; background scrolling is
428
+ locked without changing layout width and restored when the last modal closes.
429
+ Root props/style and HTMLDialogElement ref target the actual dialog. Native
430
+ close/method=dialog requests state changes; a controlled owner can refuse them.
431
+
432
+ Popover/Dialog children mount while open and unmount when closed. Keep persistent
433
+ form data in the host. Defaults initialize once; controlled owners must update
434
+ open. On closing, focus returns to the connected enabled trigger when appropriate;
435
+ focus deliberately moved elsewhere is preserved for nonmodal dismissal. Hosts own
436
+ fallback focus if a trigger disappears or is disabled. Choose safe initial focus
437
+ for consequential actions. Complex dialog content need not be flattened into a
438
+ single description; use its visible structure. No menu/listbox semantics are
439
+ implied by Popover. No opening/closing animation is applied in this first slice.
440
+
441
+ ```tsx
442
+ <Tooltip content="Additional context" trigger={p => <Button {...p}>Help</Button>} />
443
+ <Popover title="Settings" trigger={p => <Button {...p}>Settings</Button>}>
444
+ <Input aria-label="Project name" />
445
+ </Popover>
446
+ <Dialog title="Create a project" trigger={p => <Button {...p}>Open dialog</Button>}>
447
+ <Field label="Project name">{p => <Input {...p} />}</Field>
448
+ </Dialog>
449
+ ```
450
+
451
+ Available in 0.2.3. Use Flood CSS and FloodRoot. These are original native
452
+ implementations, without a third-party component/positioning library.
453
+
454
+
455
+ ## Tabs, Breadcrumb and Pagination
456
+
457
+ Tabs connects unique-value items (label, content, optional icon/disabled) to a
458
+ named tablist and retained panels. Use defaultValue for local state or
459
+ value/onValueChange for controlled state. Missing, invalid or individually disabled
460
+ selection derives the first available item without a change callback. Reconcile
461
+ host values when changing items. Global disabled disables tab controls while
462
+ leaving the selected panel visible. iconOnly requires all icons and keeps labels
463
+ as accessible names. Native div props/ref target the root.
464
+
465
+ One enabled tab is in the Tab sequence. Orientation-aware arrows, Home/End move
466
+ focus, wrapping and skipping disabled tabs; horizontal start/end follows RTL.
467
+ activationMode is automatic (arrow navigation activates) or manual (Enter/Space
468
+ activate). Manual focus resets to active tab on leaving the list. Panels are
469
+ focusable, labelled by their tabs and retained with hidden when inactive, preserving
470
+ state and native form constraints. Hosts own inactive-field validation. External
471
+ selection changes return focus from a hidden panel to the selected tab if needed.
472
+ Horizontal overflow uses Flood ScrollArea; vertical layout wraps its tab labels.
473
+ Sizes are controlSize sm/md/lg/xl. There is no tab deletion/loading/fetch model.
474
+ The dark recipe is one contained surface: the active tab shares the panel's
475
+ background, while inactive tabs sit on the recessed navigation strip. Both
476
+ orientations share a single outer perimeter, without a separate panel border or
477
+ selection underline. Keep panel content free of redundant enclosing surfaces.
478
+
479
+ Breadcrumb uses an accessible nav label (Breadcrumb by default), an ordered items
480
+ array of label/optional href, and a decorative separator (/ by default). The last
481
+ item always has aria-current=page, as a native link or text. Parent links preserve
482
+ browser navigation; host routes and client interception remain host-owned. It
483
+ wraps and supports long labels. Native nav props/ref target HTMLElement.
484
+
485
+ Pagination is controlled: page/pageCount are required positive safe integers,
486
+ 1 <= page <= pageCount; omit the component for empty datasets. Host must reconcile
487
+ page when count changes. onPageChange requests local updates; getHref renders
488
+ native route links instead of buttons. In link mode onPageChange is notification,
489
+ not navigation cancellation; modifier clicks retain native behavior. Host routing
490
+ can intercept the bubbling nav click. Current page uses aria-current, and boundary
491
+ Previous/Next controls have no active href or disabled native buttons. disabled
492
+ makes every control noninteractive. No callback fires for the current page.
493
+
494
+ siblingCount 0/1/2 gives at most 5/7/9 numeric/ellipsis slots plus Previous/Next,
495
+ independent of pageCount magnitude. Ellipses are decorative. controlSize matches
496
+ field sizes; wrapping prevents narrow-width overflow and touch controls retain
497
+ 44px targets. label, previousLabel, nextLabel and pageLabel localize naming.
498
+ The native nav ref is HTMLElement. This component does not fetch data or own URLs.
499
+
500
+ ```tsx
501
+ <Tabs label="Project sections" items={[
502
+ {value:"overview",label:"Overview",content:"Project context."},
503
+ {value:"activity",label:"Activity",content:"Recent updates."},
504
+ ]} />
505
+ <Breadcrumb items={[{label:"Projects",href:"/projects"},{label:"Flood"}]} />
506
+ <Pagination page={1} pageCount={20} getHref={page => `?page=${page}`} />
507
+ ```
508
+
509
+ All three are available in 0.2.3. Use Flood styles and FloodRoot for the neutral dark profile.
@@ -0,0 +1,16 @@
1
+ import type { Checkbox, Fieldset, FieldsetProps, ChoiceOption } from "./index.js";
2
+ interface Props extends Omit<FieldsetProps, "children" | "defaultValue"> {
3
+ name: string;
4
+ options: readonly ChoiceOption[];
5
+ kind: "radio" | "checkbox";
6
+ selected?: readonly string[];
7
+ initial: readonly string[];
8
+ onSelectionChange?: (value: string[]) => void;
9
+ required?: boolean;
10
+ orientation: "vertical" | "horizontal";
11
+ fieldsetComponent: typeof Fieldset;
12
+ choiceComponent: typeof Checkbox;
13
+ }
14
+ /** Private selection model; native inputs own keyboard, focus, forms and validity. */
15
+ export declare const ChoiceGroup: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLFieldSetElement>>;
16
+ export {};
@@ -0,0 +1,52 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useLayoutEffect, useImperativeHandle, useRef, useState } from "react";
3
+ const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
4
+ /** Private selection model; native inputs own keyboard, focus, forms and validity. */
5
+ export const ChoiceGroup = forwardRef(function ChoiceGroup({ kind, name, options, selected, initial, onSelectionChange, required, orientation, fieldsetComponent: Group, choiceComponent: Choice, legend, ...props }, forwardedRef) {
6
+ if (new Set(options.map(option => option.value)).size !== options.length)
7
+ throw new Error("Flood choice group option values must be unique");
8
+ const root = useRef(null);
9
+ const inputs = useRef(new Map());
10
+ const [local, setLocal] = useState(() => [...new Set(initial)]);
11
+ const controlled = selected !== undefined;
12
+ const values = controlled ? selected : local;
13
+ const latest = useRef({ controlled, selected, initial });
14
+ useBrowserLayoutEffect(() => { latest.current = { controlled, selected, initial }; });
15
+ useImperativeHandle(forwardedRef, () => root.current, []);
16
+ useEffect(() => {
17
+ const fieldset = root.current;
18
+ if (!fieldset)
19
+ return;
20
+ const eventRoot = fieldset.getRootNode();
21
+ const timers = new Set();
22
+ const reset = (event) => {
23
+ if (!fieldset.form || event.target !== fieldset.form)
24
+ return;
25
+ // Wait for the whole dispatch/default action, including React's delegated
26
+ // reset handler. A microtask can run between native event listeners.
27
+ const timer = setTimeout(() => {
28
+ timers.delete(timer);
29
+ if (event.defaultPrevented || root.current !== fieldset || event.target !== fieldset.form)
30
+ return;
31
+ const current = latest.current;
32
+ const next = [...new Set(current.controlled ? current.selected : current.initial)];
33
+ if (!current.controlled)
34
+ setLocal(next);
35
+ // Controlled groups must also undo native DOM reset if the host keeps its value.
36
+ inputs.current.forEach(input => { input.checked = next.includes(input.value); });
37
+ }, 0);
38
+ timers.add(timer);
39
+ };
40
+ eventRoot.addEventListener("reset", reset, true);
41
+ return () => { eventRoot.removeEventListener("reset", reset, true); timers.forEach(clearTimeout); };
42
+ }, []);
43
+ return _jsx(Group, { ...props, ref: root, legend: required ? _jsxs(_Fragment, { children: [legend, _jsx("span", { "aria-hidden": "true", children: " *" })] }) : legend, role: kind === "radio" ? "radiogroup" : "group", "aria-required": kind === "radio" ? required || undefined : undefined, "aria-orientation": kind === "radio" ? orientation : undefined, children: _jsxs("div", { className: "flood-choice-group", "data-orientation": orientation, children: [options.length === 0 && _jsx("span", { className: "flood-hint", children: "No options available." }), options.map(option => _jsx(Choice, { ref: input => { if (input)
44
+ inputs.current.set(option.value, input);
45
+ else
46
+ inputs.current.delete(option.value); }, name: name, value: option.value, label: option.label, form: props.form, disabled: option.disabled, required: kind === "radio" ? required : undefined, checked: values.includes(option.value), onChange: event => {
47
+ const next = kind === "radio" ? [option.value] : [...new Set(event.currentTarget.checked ? [...values, option.value] : values.filter(value => value !== option.value))];
48
+ if (!controlled)
49
+ setLocal(next);
50
+ onSelectionChange?.([...next]);
51
+ } }, option.value))] }) });
52
+ });
@@ -0,0 +1,12 @@
1
+ import { type HTMLAttributes } from "react";
2
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
3
+ title: string;
4
+ expanded: boolean;
5
+ onExpandedChange: (open: boolean) => void;
6
+ disabled?: boolean;
7
+ controlSize: "sm" | "md" | "lg" | "xl";
8
+ headingLevel?: 2 | 3 | 4 | 5 | 6;
9
+ }
10
+ /** Shared original disclosure anatomy. The closed panel stays mounted but hidden. */
11
+ export declare const Disclosure: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLDivElement>>;
12
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createElement, forwardRef, useEffect, useId, useLayoutEffect, useRef, useImperativeHandle } from "react";
3
+ const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
4
+ /** Shared original disclosure anatomy. The closed panel stays mounted but hidden. */
5
+ export const Disclosure = forwardRef(function Disclosure({ title, expanded, onExpandedChange, disabled, controlSize, headingLevel, className, children, id: explicitId, ...props }, ref) {
6
+ const generatedId = useId(), id = explicitId ?? `flood-disclosure-${generatedId}`;
7
+ const root = useRef(null), panel = useRef(null), trigger = useRef(null);
8
+ const focusedInside = useRef(false);
9
+ useImperativeHandle(ref, () => root.current, []);
10
+ useBrowserLayoutEffect(() => {
11
+ const active = panel.current?.ownerDocument.activeElement;
12
+ if (!expanded && focusedInside.current && !disabled && (panel.current?.contains(active ?? null) || active === panel.current?.ownerDocument.body))
13
+ trigger.current?.focus();
14
+ if (!expanded)
15
+ focusedInside.current = false;
16
+ }, [expanded, disabled]);
17
+ const button = _jsxs("button", { ref: trigger, id: `${id}-trigger`, type: "button", disabled: disabled, "aria-expanded": expanded, "aria-controls": `${id}-panel`, className: `flood-button flood-button--ghost flood-button--${controlSize} flood-disclosure__trigger`, onClick: () => onExpandedChange(!expanded), children: [_jsx("span", { className: "flood-disclosure__title", children: title }), _jsx("span", { className: "flood-disclosure__indicator", "aria-hidden": "true" })] });
18
+ return _jsxs("div", { ...props, id: id, ref: root, "data-open": expanded, className: ["flood-disclosure", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), children: [createElement(headingLevel ? `h${headingLevel}` : "div", { className: "flood-disclosure__heading" }, button), _jsx("div", { ref: panel, id: `${id}-panel`, className: "flood-disclosure__panel", hidden: !expanded, onFocusCapture: () => { focusedInside.current = true; }, onBlurCapture: event => { if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget))
19
+ focusedInside.current = false; }, children: children })] });
20
+ });
@@ -0,0 +1,6 @@
1
+ import type { AriaAttributes, ReactNode } from "react";
2
+ import type { FieldStatus } from "./index.js";
3
+ /** Existing invalid semantics always take precedence over a positive status. */
4
+ export declare function resolveFieldStatus(status?: FieldStatus, invalid?: boolean, ariaInvalid?: AriaAttributes["aria-invalid"]): FieldStatus | undefined;
5
+ export declare const hasFieldContent: (value: ReactNode) => boolean;
6
+ export declare function mergeDescriptions(...values: Array<string | false | undefined>): string | undefined;
@@ -0,0 +1,8 @@
1
+ /** Existing invalid semantics always take precedence over a positive status. */
2
+ export function resolveFieldStatus(status, invalid, ariaInvalid) {
3
+ return invalid || (ariaInvalid !== undefined && ariaInvalid !== false && ariaInvalid !== "false") ? "error" : status;
4
+ }
5
+ export const hasFieldContent = (value) => value !== null && value !== undefined && value !== false && value !== true && value !== "";
6
+ export function mergeDescriptions(...values) {
7
+ return [...new Set(values.filter(Boolean).join(" ").split(/\s+/).filter(Boolean))].join(" ") || undefined;
8
+ }