@flood-md/react 0.2.3 → 0.2.4

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,7 +3,7 @@
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.3** API. Keep Flood package versions aligned and follow the guide matching
6
+ This package documents the **0.2.4** API. Keep Flood package versions aligned and follow the guide matching
7
7
  your installed declarations; source changes do not modify an existing npm release.
8
8
 
9
9
  ```tsx
@@ -314,11 +314,16 @@ Available in 0.2.3. Use matching installed declarations.
314
314
 
315
315
  Field and Fieldset accept hideLabel (false by default). It visually hides the real
316
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.
317
+ the label's layout row and spacing while it has no focused descendants. The
318
+ required label/legend prop remains required.
318
319
  RadioGroup, CheckboxGroup and SegmentedControl inherit hideLabel. Required native
319
320
  controls, descriptions, error/status, group names and form behavior are unchanged.
320
321
  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
+ Prefer textual label content. Hidden labels/legends reveal while a descendant
323
+ has focus, then hide again when focus leaves. A supporting link in a Field label
324
+ or a control in a Fieldset first legend therefore retains visible keyboard focus;
325
+ React child state, refs and native first-legend disabled exceptions are preserved.
326
+ Do not nest another labelable control (button/input) inside a Field label.
322
327
 
323
328
  For Input/Textarea/Select/InputGroup apply hideLabel to their surrounding Field,
324
329
  not the native control. Input aria-label/aria-labelledby remain available when
@@ -507,3 +512,303 @@ The native nav ref is HTMLElement. This component does not fetch data or own URL
507
512
  ```
508
513
 
509
514
  All three are available in 0.2.3. Use Flood styles and FloodRoot for the neutral dark profile.
515
+
516
+
517
+ ## Badge, Spinner, Skeleton and Avatar · 0.2.4
518
+
519
+ These original primitives are compatible additions in the 0.2.4.
520
+ Import Flood CSS and use FloodRoot. Verify registry publication before installing.
521
+ Native span props and refs target their roots; className/style remain host overrides.
522
+
523
+ Badge is a noninteractive span: size sm/md/lg, tone neutral/accent/error/warning/
524
+ success/info and variant subtle/outline. Its text carries the meaning; color is
525
+ supplementary. It has no selection, dismissal, click-action or live-region model.
526
+ Use a separate named action when needed instead of treating it as a button.
527
+
528
+ Spinner defaults to size md and label="Loading". Localize label for standalone
529
+ status. decorative hides it and its label from assistive technology. Button
530
+ loading uses the same Spinner decoratively, inherits its icon size and retains
531
+ its accessible button name and aria-busy. Announcements belong at the appropriate
532
+ host level; do not create one live region for each decorative loading graphic.
533
+ Reduced motion stops rotation. This indicator does not measure completion.
534
+
535
+ Skeleton is always aria-hidden. shape is text/rounded/circle; width and height
536
+ accept CSS dimensions (numbers are React CSS pixels), and style overrides them.
537
+ Prefer relative dimensions. For a circle supply equal dimensions. animate defaults
538
+ true; reduced motion and forced colors stop animation. Match the final geometry;
539
+ Skeleton does not fetch, detect loading or announce changes. The host owns busy
540
+ state, loading text and replacement with real content. Do not add interactive children.
541
+
542
+ Avatar requires name, accepts src, fallback text, size sm/md/lg, shape circle/
543
+ rounded and decorative. The root exposes one named image; the internal image has
544
+ empty alt and is hidden from assistive technology. Use decorative beside an
545
+ already visible equivalent name. Defaults use the first letters of the first two
546
+ whitespace-separated name parts; override fallback for your locale or naming rules.
547
+ The image overlays the fallback only after successful load; an error retains the
548
+ fallback. A new src remounts the image load state, including returning to an earlier
549
+ source. The root stays the same size. This API does not upload or crop images.
550
+
551
+ ```tsx
552
+ <Badge tone="success">Active</Badge>
553
+ <Spinner label="Loading workspace" />
554
+ <Skeleton width="12rem" />
555
+ <Avatar name="Alex Morgan" src="/team/alex.webp" />
556
+ ```
557
+
558
+
559
+ ## Progress, Meter and Alert · 0.2.4
560
+
561
+ Progress describes task completion. label is required; max defaults to 100 and
562
+ value is a finite number in 0..max. Omit value for indeterminate progress; its
563
+ progressbar then omits aria-valuenow. Meter describes a measurement in a bounded
564
+ range, such as storage or battery. It requires value, with min=0 and max=100 by
565
+ default. Require finite min < max, a finite range span, and min <= value <= max.
566
+ Invalid ranges/values throw RangeError; they are not silently clamped.
567
+
568
+ Both support size sm/md/lg, semantic tone, hideLabel and valueText. Tone defaults
569
+ to accent for Progress and neutral for Meter. valueText supplies localized,
570
+ human-readable units; it also appears beside the caption. hideLabel visually
571
+ hides the caption while retaining the accessible name and valueText. The bar's
572
+ role is progressbar or meter; neither becomes a live region. Native div props,
573
+ className/style and HTMLDivElement ref target the wrapper; the internal bar owns
574
+ its ARIA range and label associations. Wrapper ARIA props stay on the wrapper; use valueText for range-specific wording. No keyboard model,
575
+ threshold calculation, fetching, timers or task cancellation is built in.
576
+ Reduced motion stops indeterminate movement and value transitions. Forced colors
577
+ preserves the track boundary and fill. Use a host loading message when needed.
578
+
579
+ Alert is an inline message, with optional plain-text title and React children.
580
+ Tones are neutral/info/success/warning/error; info is the default. Meaning must
581
+ remain in the text. Original decorative status glyphs are included; icon accepts
582
+ noninteractive host artwork, and icon={null} removes it without an empty column.
583
+ The title is message content, not the root's HTML title tooltip or a document heading.
584
+ Native div props/className/style/ref target the root.
585
+
586
+ announcement defaults to off for static messages. polite uses role=status;
587
+ assertive uses role=alert. Choose urgency deliberately for dynamically inserted
588
+ content. Announcements depend on actual assistive technology; initial static
589
+ markup is not guaranteed to be read aloud. No focus movement, auto-dismiss,
590
+ timeout or built-in close/action button is supplied. Compose named host actions
591
+ inside children when needed. Alert is separate from Dialog and future Toast.
592
+
593
+ See the [WAI Meter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/meter/),
594
+ [Alert pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alert/) and
595
+ [progressbar definition](https://www.w3.org/TR/wai-aria-1.2/#progressbar).
596
+
597
+ ```tsx
598
+ <Progress label="Uploading files" value={60} valueText="60%" />
599
+ <Progress label="Preparing workspace" />
600
+ <Meter label="Storage used" min={0} max={100} value={75} valueText="75 of 100 GB" />
601
+ <Alert tone="warning" title="Storage is almost full">
602
+ Free some space before your next upload.
603
+ </Alert>
604
+ ```
605
+
606
+
607
+ ## DescriptionList and Table · 0.2.4
608
+
609
+ DescriptionList renders native dl groups, each with dt and dd. items is a readonly
610
+ array of {id, term, description}; IDs must be unique and nonempty, and are React
611
+ keys rather than DOM anchors. term is text and description is ReactNode, so a
612
+ value can include a Badge, Link or other meaningful content. The host formats
613
+ values and supplies fallback text for missing data. An empty items array stays
614
+ an empty list, without invented placeholder content. Native dl props,
615
+ className/style and HTMLDListElement ref target the list.
616
+
617
+ layout defaults inline: term and value share a row. It becomes stacked below a
618
+ 22rem component container width, independently of viewport width. layout=stacked
619
+ always puts the value below its term. Density defaults compact; comfortable adds
620
+ space between pairs, without inflating the term/value relationship. dividers is
621
+ false by default and adds quiet separators when enabled. This is not an editable
622
+ form and its terms do not label controls; use Field/Fieldset for form inputs.
623
+
624
+ Table wraps an actual HTML table in Flood's horizontal ScrollArea. caption is
625
+ required and must be meaningful; hideCaption visually hides it without removing
626
+ the accessible table name. children owns native thead/tbody/tfoot/colgroup markup
627
+ and th/td associations. Supply scope=col or scope=row for simple headers, and
628
+ explicit id/headers associations for complex spanning headers. Do not add another
629
+ caption child. See [WAI table guidance](https://www.w3.org/WAI/tutorials/tables/).
630
+
631
+ Table density defaults comfortable; compact reduces vertical cell padding.
632
+ striped is false by default. Native tfoot rows use a quiet tinted background and
633
+ medium-weight text to distinguish summaries; the top separator stays decorative.
634
+ data-numeric on a th or td uses logical end alignment,
635
+ tabular figures and no wrapping for numbers. data-align=start/center/end overrides
636
+ alignment explicitly. Text cells wrap; set style.minWidth to preserve room for a
637
+ wide table, and the containing ScrollArea handles overflow. The host owns dates,
638
+ units, totals, empty rows/colSpan and any cell actions. Avoid interactive whole rows.
639
+
640
+ Native table props, className/style and HTMLTableElement ref target the table,
641
+ not its viewport. dir/lang/hidden also govern the containing scroll area.
642
+ scrollLabel names that viewport independently and defaults to caption. The native
643
+ table keeps its own caption; viewport keyboard scrolling does not turn it into a
644
+ grid widget. Keep action controls inside cells normally focusable. No sorting,
645
+ selection, data fetching, pagination, sticky header or virtualization is implied.
646
+ This is the compositional table primitive, not a DataTable engine.
647
+
648
+ ```tsx
649
+ <DescriptionList items={[
650
+ {id:"project",term:"Project",description:"Flood"},
651
+ {id:"version",term:"Version",description:"0.2.4"},
652
+ ]} />
653
+ <Table caption="Workspace projects" style={{minWidth:"32rem"}}>
654
+ <thead><tr><th scope="col">Project</th><th scope="col" data-numeric>Members</th></tr></thead>
655
+ <tbody><tr><th scope="row">Flood</th><td data-numeric>12</td></tr></tbody>
656
+ </Table>
657
+ ```
658
+
659
+
660
+ ## Menu · 0.2.4
661
+
662
+ Menu is an original action menu using Flood's positioning/layer stack and the
663
+ native Popover API. label names the menu; there is no extra visible caption.
664
+ trigger is a render function receiving OverlayTriggerProps: forward all props
665
+ and ref to one persistent Button/IconButton/native button, and compose existing
666
+ handlers rather than replacing the supplied ones. The trigger exposes
667
+ aria-haspopup=menu, aria-expanded and aria-controls. disabled affects the trigger,
668
+ not actions inside a menu the controlled host keeps open.
669
+
670
+ items is a readonly array of action items {id,label,icon?,shortcut?,disabled?,tone?}
671
+ or separators {id,type:"separator"}. IDs must be unique and nonempty, action
672
+ labels nonempty, and at least one action must exist. The optional item type is
673
+ item. Icons must be decorative, noninteractive artwork. shortcut is display-only,
674
+ not an installed keyboard binding or part of the accessible action name. Destructive
675
+ actions use tone=danger; that visual treatment does not perform confirmation or
676
+ execute any business operation. Disabled items remain focusable with aria-disabled,
677
+ but neither pointer nor keyboard activation calls onAction or closes the menu.
678
+
679
+ Enter/Space/ArrowDown open at the first action; ArrowUp opens at the last.
680
+ Inside: Up/Down wrap, Home/End choose edges, printable label-prefix typeahead
681
+ matches items (repeated characters cycle). Separators are not focusable. Enter
682
+ or Space activates through a native button. Escape closes the top menu and
683
+ returns focus. Tab moves to the next focusable control after the trigger;
684
+ Shift+Tab returns to the trigger. The menu does not trap focus. Outside pointer
685
+ or focus departure requests dismissal and preserves deliberately moved focus.
686
+
687
+ open/onOpenChange is controlled, or use defaultOpen for initial local state.
688
+ The host may refuse a close request. onAction(id) requests closing before calling
689
+ the host; the host owns business logic, asynchronous work and errors. If it moves
690
+ focus elsewhere or opens a Dialog, normal focus restoration must not steal it.
691
+ Actions mount while open and unmount on close. The persistent popup ref and native
692
+ div props/className/style target its panel. Engine-owned coordinates and available
693
+ space variables belong to positioning; side/align follow Popover conventions.
694
+ controlSize is sm/md/lg/xl (sm default); pointer-coarse targets retain touch size.
695
+
696
+ The popup uses Flood ScrollArea for long action lists, bounded to the available
697
+ viewport. Its inner corners follow shell padding and border geometry. Focus is
698
+ inset to stay visible. There is no href/navigation-item, submenu, checkbox/radio,
699
+ context-menu trigger or menubar contract in this first action-menu component.
700
+ Use ordinary Link for ordinary navigation; keep different selection models separate.
701
+
702
+ See [WAI Menu Button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/) and
703
+ [Menu keyboard patterns](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/).
704
+
705
+ ```tsx
706
+ <Menu label="Project actions"
707
+ trigger={props => <Button {...props}>Project actions</Button>}
708
+ items={[{id:"rename",label:"Rename…"},{id:"divider",type:"separator"},
709
+ {id:"delete",label:"Delete project",tone:"danger"}]}
710
+ onAction={id => chooseProjectAction(id)}
711
+ />
712
+ ```
713
+
714
+ ### Toolbar and Kbd (0.2.4)
715
+
716
+ Toolbar groups Button, IconButton, Toggle, native buttons/links and Menu triggers
717
+ under one named action panel. After hydration it uses one Tab entry, directional
718
+ arrows, Home/End and optional looping. Disabled/hidden actions are skipped. Arrows
719
+ move focus, not activation. Horizontal order follows writing direction. In vertical
720
+ toolbars open Menu with Enter/Space; horizontal menus retain Up/Down opening.
721
+ Keep text inputs and independent selection widgets outside this action-only API.
722
+ Native child handlers and refs remain attached; Toolbar owns their tabIndex while
723
+ mounted and restores original attributes on cleanup. Without JavaScript, native
724
+ child Tab order remains available. Use a meaningful required label.
725
+
726
+ Kbd renders native kbd markup with sm/md/lg sizes and relative keycap heights.
727
+ It displays a key or combination; the host implements real shortcuts. It is not
728
+ interactive. Menu shortcut hints reuse Kbd and remain hidden from item names.
729
+
730
+ ```tsx
731
+ <Toolbar label="Editor actions">
732
+ <Button variant="ghost">Undo</Button>
733
+ <Toggle variant="ghost">Bold</Toggle>
734
+ <Button variant="ghost">Share</Button>
735
+ </Toolbar>
736
+ <Kbd>Ctrl</Kbd> + <Kbd>K</Kbd>
737
+ ```
738
+
739
+ ### Card (0.2.4)
740
+
741
+ Card composes Surface into a header, body and footer. Optional title, description,
742
+ actions and footer accept React content; children are the body. Missing slots
743
+ create no empty section. Use Surface when only a background/padding is needed.
744
+
745
+ Card is a noninteractive div. Put Button, Link or Menu in actions/footer and keep
746
+ its container out of the Tab order. Do not make the entire card clickable around
747
+ nested controls. Native div props, styles and HTMLDivElement ref target the root.
748
+ The title prop is heading content, not an HTML tooltip; supply phrasing content.
749
+ headingLevel defaults to 2; choose the correct hierarchy for the host page.
750
+
751
+ Compact/comfortable density controls existing spacing tokens. Header actions wrap
752
+ without clipping menus or focus indicators. Footer uses a quiet separator; a
753
+ footer-only card has no redundant separator. Elevation is default or raised.
754
+ No data access, selection or interactive-card behavior is implied.
755
+
756
+ ```tsx
757
+ <Card title="Project details" description="Keep your team in sync."
758
+ footer={<Button variant="subtle">Open project</Button>}>
759
+ <Text as="p" size="compact">A calm place for the next idea.</Text>
760
+ </Card>
761
+ ```
762
+
763
+ ## Structural, media and accessible primitives · 0.2.4
764
+
765
+ Container centers a max-width box with logical gutters. size sm/md/lg uses
766
+ 40/64/80rem bounds including padding; full removes the max. gutter uses Space.
767
+ Grid defaults to auto-fit with a16rem semantic minimum clamped to available
768
+ width. gap uses Space. minItemWidth overrides the auto minimum; explicit columns
769
+ (1/2/3/4/6/12) remain fixed. Grid is ordinary layout, never an ARIA grid widget.
770
+
771
+ VisuallyHidden preserves content in the accessibility tree using span/div. Default
772
+ is for noninteractive content. Use focusable for a skip link: focus-within reveals
773
+ its wrapper; style the actual link's focus. This is different from aria-hidden or
774
+ conditional rendering. Native captions/legends may use the same hiding utility
775
+ without an invalid generic wrapper. Spinner and hidden indicator captions compose
776
+ VisuallyHidden.
777
+
778
+ AspectRatio reserves responsive media geometry before loading, accepting a finite
779
+ positive ratio (16/9 default). Direct img/video/iframe children fill its box. It
780
+ does not clip focus or make arbitrary long content fit; the host owns alternative
781
+ text, captions, media controls and deliberate cropping. Image is a native img
782
+ with required alt (empty for decorative content), cover/contain fit and preserved
783
+ srcSet/sizes/loading/decoding/dimensions/events/ref. No optimizer, fetch engine or
784
+ fallback is implied. Avatar composes Image for its own loading lifecycle. Supply
785
+ image dimensions or AspectRatio to reserve space.
786
+
787
+ List renders native ul/ol with native li children and token gap. ordered selects
788
+ ol; start/reversed apply there. start must be a safe integer. marker=false retains
789
+ explicit list semantics for platforms that drop lists without visible markers.
790
+ This is content, not ListBox selection or arrow navigation.
791
+
792
+ EmptyState composes Stack, Heading, Text, IconSlot and Inline. Required title,
793
+ optional description, decorative icon and actions explain absent content; choose
794
+ headingLevel for the host hierarchy. It is static, with no automatic focus or
795
+ announcement. compact/comfortable density controls padding. The host implements
796
+ real actions and decides whether asynchronously changed results need announcing.
797
+
798
+ Checkbox adds indeterminate for a host-controlled mixed presentation after
799
+ hydration. checked independently determines form submission; mixed does not mean
800
+ a third submitted value. The host owns aggregate/select-all values. Native reset
801
+ and ref access remain available. Radio and Switch do not expose indeterminate.
802
+
803
+ ButtonGroup/ToggleGroup retain their keyboard contracts and locally scroll when
804
+ a connected row cannot fit. InputGroup actions keep words intact and can wrap to
805
+ a new row while the input retains usable space. Plain string adornments are bidi
806
+ isolated; for protocols/phones with explicit direction pass a bdi dir=ltr node.
807
+ Keep very long addon content concise. Hidden native choices hide their label too.
808
+
809
+ Dark foundations bundle both normal and italic Inter, including Cyrillic, with
810
+ original OFL notices. Color alone never communicates validation or progress.
811
+
812
+ CodeSnippet codeDirection defaults to ltr independently of RTL parent action layout; choose rtl/auto explicitly for content that needs it. Native dir still applies to the wrapper. Protected ScrollArea reveals focused descendants fully when needed, composes the host focus handler and avoids scrolling to nested top-layer controls.
813
+
814
+ Checkbox, Radio and Switch apply native hidden/dir/lang to their visible label wrapper as well as the input, so a component-local language/direction applies to its whole choice.
@@ -0,0 +1,5 @@
1
+ /** Keyed by src in Avatar so each source starts with its own load lifecycle. */
2
+ export declare function AvatarArtwork({ src, fallback }: {
3
+ src?: string;
4
+ fallback: string;
5
+ }): import("react").JSX.Element;
package/dist/avatar.js ADDED
@@ -0,0 +1,8 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Image } from "./index.js";
4
+ /** Keyed by src in Avatar so each source starts with its own load lifecycle. */
5
+ export function AvatarArtwork({ src, fallback }) {
6
+ const [state, setState] = useState("loading");
7
+ return _jsxs(_Fragment, { children: [_jsx("span", { className: "flood-avatar__fallback", "aria-hidden": "true", children: fallback }), src && state !== "error" && _jsx(Image, { src: src, alt: "", "aria-hidden": "true", decoding: "async", className: "flood-avatar__image", "data-ready": state === "ready", onLoad: () => setState("ready"), onError: () => setState("error") })] });
8
+ }
@@ -0,0 +1,4 @@
1
+ import { type InputHTMLAttributes } from "react";
2
+ export declare const MixedCheckbox: import("react").ForwardRefExoticComponent<InputHTMLAttributes<HTMLInputElement> & {
3
+ indeterminate: boolean;
4
+ } & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef } from "react";
3
+ const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
4
+ export const MixedCheckbox = forwardRef(function MixedCheckbox({ indeterminate, onChange, ...props }, ref) {
5
+ const node = useRef(null), mixed = useRef(indeterminate);
6
+ useImperativeHandle(ref, () => node.current, []);
7
+ useBrowserLayoutEffect(() => { mixed.current = indeterminate; if (node.current)
8
+ node.current.indeterminate = indeterminate; });
9
+ return _jsx("input", { ...props, ref: node, type: "checkbox", className: "flood-check", "aria-checked": indeterminate ? "mixed" : props["aria-checked"], onChange: event => { const control = event.currentTarget; try {
10
+ onChange?.(event);
11
+ }
12
+ finally {
13
+ control.indeterminate = mixed.current;
14
+ } } });
15
+ });
@@ -0,0 +1,3 @@
1
+ import type { DescriptionListProps, TableProps } from "./index.js";
2
+ export declare const DescribedData: import("react").ForwardRefExoticComponent<DescriptionListProps & import("react").RefAttributes<HTMLDListElement>>;
3
+ export declare const TabularData: import("react").ForwardRefExoticComponent<TableProps & import("react").RefAttributes<HTMLTableElement>>;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { ScrollArea } from "./index.js";
4
+ export const DescribedData = forwardRef(function DescribedData({ items, layout, density, dividers, className, ...props }, ref) {
5
+ if (items.some(item => !item.id.trim()) || new Set(items.map(item => item.id)).size !== items.length)
6
+ throw new Error("Flood description items require unique nonempty IDs");
7
+ return _jsx("dl", { ...props, ref: ref, className: ["flood-description-list", className].filter(Boolean).join(" "), "data-layout": layout, "data-density": density, "data-dividers": dividers, children: items.map(item => _jsxs("div", { className: "flood-description-list__item", children: [_jsx("dt", { children: item.term }), _jsx("dd", { children: item.description })] }, item.id)) });
8
+ });
9
+ export const TabularData = forwardRef(function TabularData({ caption, children, hideCaption, density, striped, scrollLabel, className, hidden, dir, lang, ...props }, ref) {
10
+ return _jsx("div", { className: "flood-table-container", hidden: hidden, dir: dir, lang: lang, children: _jsx(ScrollArea, { orientation: "horizontal", "aria-label": scrollLabel ?? caption, children: _jsxs("table", { ...props, ref: ref, hidden: hidden, dir: dir, lang: lang, className: ["flood-table", className].filter(Boolean).join(" "), "data-density": density, "data-striped": striped, children: [_jsx("caption", { className: hideCaption ? "flood-visually-hidden" : undefined, children: caption }), children] }) }) });
11
+ });
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createElement, forwardRef, useEffect, useId, useLayoutEffect, useRef, useImperativeHandle } from "react";
3
+ import { Button } from "./index.js";
3
4
  const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
4
5
  /** Shared original disclosure anatomy. The closed panel stays mounted but hidden. */
5
6
  export const Disclosure = forwardRef(function Disclosure({ title, expanded, onExpandedChange, disabled, controlSize, headingLevel, className, children, id: explicitId, ...props }, ref) {
@@ -14,7 +15,7 @@ export const Disclosure = forwardRef(function Disclosure({ title, expanded, onEx
14
15
  if (!expanded)
15
16
  focusedInside.current = false;
16
17
  }, [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
+ const button = _jsxs(Button, { ref: trigger, variant: "ghost", size: controlSize, id: `${id}-trigger`, type: "button", disabled: disabled, "aria-expanded": expanded, "aria-controls": `${id}-panel`, className: "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
19
  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
20
  focusedInside.current = false; }, children: children })] });
20
21
  });
@@ -0,0 +1,8 @@
1
+ import type { AlertProps, ProgressProps } from "./index.js";
2
+ export declare function validateRange(value: number | undefined, min: number, max: number, allowUnknown: boolean): void;
3
+ export declare const ValueIndicator: import("react").ForwardRefExoticComponent<ProgressProps & {
4
+ kind: "progress" | "meter";
5
+ min: number;
6
+ max: number;
7
+ } & import("react").RefAttributes<HTMLDivElement>>;
8
+ export declare const FeedbackMessage: import("react").ForwardRefExoticComponent<AlertProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,29 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useId } from "react";
3
+ import { VisuallyHidden } from "./index.js";
4
+ export function validateRange(value, min, max, allowUnknown) {
5
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min || !Number.isFinite(max - min))
6
+ throw new RangeError("Flood indicators require a finite increasing range");
7
+ if (value === undefined && allowUnknown)
8
+ return;
9
+ if (value === undefined || !Number.isFinite(value) || value < min || value > max)
10
+ throw new RangeError("Flood indicator value must be finite and within its range");
11
+ }
12
+ export const ValueIndicator = forwardRef(function ValueIndicator({ kind, label, value, min, max, valueText, size, tone, hideLabel, className, ...props }, ref) {
13
+ const labelId = `flood-indicator-${useId()}`;
14
+ validateRange(value, min, max, kind === "progress");
15
+ const unknown = value === undefined;
16
+ const percent = unknown ? undefined : ((value - min) / (max - min)) * 100;
17
+ const caption = _jsxs(_Fragment, { children: [_jsx("span", { id: labelId, children: label }), valueText && _jsx("span", { className: "flood-indicator__value", "aria-hidden": "true", children: valueText })] });
18
+ return _jsxs("div", { ...props, ref: ref, className: ["flood-indicator", className].filter(Boolean).join(" "), "data-kind": kind, "data-size": size, "data-tone": tone, "data-indeterminate": unknown, children: [hideLabel ? _jsx(VisuallyHidden, { as: "div", children: caption }) : _jsx("div", { className: "flood-indicator__caption", children: caption }), _jsx("div", { className: "flood-indicator__track", role: kind === "meter" ? "meter" : "progressbar", "aria-labelledby": labelId, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": value, "aria-valuetext": valueText, children: _jsx("span", { className: "flood-indicator__fill", style: { inlineSize: percent === undefined ? undefined : `${percent}%` } }) })] });
19
+ });
20
+ function StatusGlyph({ tone }) {
21
+ return _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", focusable: "false", children: tone === "success" ? _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "m8 12 3 3 5-6" })] }) :
22
+ tone === "warning" ? _jsxs(_Fragment, { children: [_jsx("path", { d: "m12 3 10 17H2L12 3Z" }), _jsx("path", { d: "M12 9v4m0 3h.01" })] }) :
23
+ tone === "error" ? _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "m9 9 6 6m0-6-6 6" })] }) :
24
+ _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 11v5m0-9h.01" })] }) });
25
+ }
26
+ export const FeedbackMessage = forwardRef(function FeedbackMessage({ title, tone, icon, announcement, children, className, ...props }, ref) {
27
+ const artwork = icon === undefined ? _jsx(StatusGlyph, { tone: tone }) : icon;
28
+ return _jsxs("div", { ...props, ref: ref, className: ["flood-alert", className].filter(Boolean).join(" "), "data-tone": tone, role: announcement === "assertive" ? "alert" : announcement === "polite" ? "status" : undefined, "aria-atomic": announcement === "off" ? undefined : true, children: [artwork != null && _jsx("span", { className: "flood-alert__icon", "aria-hidden": "true", children: artwork }), _jsxs("div", { className: "flood-alert__content", children: [title && _jsx("p", { className: "flood-alert__title", children: title }), children != null && _jsx("div", { className: "flood-alert__body", children: children })] })] });
29
+ });
@@ -0,0 +1,3 @@
1
+ import { type Ref, type RefCallback } from "react";
2
+ /** A native ref adapter; does not replace handlers or clone rich child content. */
3
+ export declare function useFocusableHidden<T extends HTMLElement>(enabled: boolean, forwardedRef?: Ref<T>): RefCallback<T>;
@@ -0,0 +1,99 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
2
+ const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
3
+ const heldAttribute = "data-flood-focus-held";
4
+ const scopes = new WeakMap();
5
+ /** Keep in-flow focus reveal stable until the native pointer gesture completes. */
6
+ function createScope(document) {
7
+ const view = document.defaultView;
8
+ const entries = new Set(), held = new Set(), pointers = new Set();
9
+ let releaseTask;
10
+ const cancelTask = () => { if (releaseTask !== undefined)
11
+ view.clearTimeout(releaseTask); releaseTask = undefined; };
12
+ const release = () => { cancelTask(); held.forEach(node => node.removeAttribute(heldAttribute)); held.clear(); };
13
+ const releaseAfterDispatch = () => {
14
+ if (pointers.size || !held.size)
15
+ return;
16
+ cancelTask();
17
+ // A task, rather than a microtask, keeps the reveal through all native click
18
+ // listeners. Pointerup also schedules this fallback when no click is fired.
19
+ releaseTask = view.setTimeout(() => { releaseTask = undefined; if (!pointers.size)
20
+ release(); }, 0);
21
+ };
22
+ const down = (event) => {
23
+ if (event.button !== 0)
24
+ return;
25
+ cancelTask();
26
+ pointers.add(event.pointerId);
27
+ entries.forEach(entry => {
28
+ if (entry.enabled() && entry.node.matches(":focus-within")) {
29
+ entry.node.setAttribute(heldAttribute, "");
30
+ held.add(entry.node);
31
+ }
32
+ });
33
+ };
34
+ const up = (event) => { pointers.delete(event.pointerId); releaseAfterDispatch(); };
35
+ const click = () => releaseAfterDispatch();
36
+ const cancel = (event) => { pointers.delete(event.pointerId); if (!pointers.size)
37
+ release(); };
38
+ const blur = () => { pointers.clear(); release(); };
39
+ document.addEventListener("pointerdown", down, true);
40
+ document.addEventListener("pointerup", up, true);
41
+ document.addEventListener("click", click, true);
42
+ document.addEventListener("pointercancel", cancel, true);
43
+ view.addEventListener("blur", blur);
44
+ const scope = { add(entry) {
45
+ entries.add(entry);
46
+ return () => {
47
+ entries.delete(entry);
48
+ held.delete(entry.node);
49
+ entry.node.removeAttribute(heldAttribute);
50
+ if (entries.size)
51
+ return;
52
+ pointers.clear();
53
+ release();
54
+ document.removeEventListener("pointerdown", down, true);
55
+ document.removeEventListener("pointerup", up, true);
56
+ document.removeEventListener("click", click, true);
57
+ document.removeEventListener("pointercancel", cancel, true);
58
+ view.removeEventListener("blur", blur);
59
+ scopes.delete(document);
60
+ };
61
+ } };
62
+ return scope;
63
+ }
64
+ /** A native ref adapter; does not replace handlers or clone rich child content. */
65
+ export function useFocusableHidden(enabled, forwardedRef) {
66
+ const node = useRef(null), active = useRef(enabled), disconnect = useRef(() => { });
67
+ const forwardedCleanup = useRef(undefined);
68
+ useBrowserLayoutEffect(() => {
69
+ active.current = enabled;
70
+ if (!enabled)
71
+ node.current?.removeAttribute(heldAttribute);
72
+ }, [enabled]);
73
+ return useCallback((element) => {
74
+ disconnect.current();
75
+ disconnect.current = () => { };
76
+ node.current = element;
77
+ if (element?.ownerDocument.defaultView) {
78
+ const document = element.ownerDocument;
79
+ let scope = scopes.get(document);
80
+ if (!scope) {
81
+ scope = createScope(document);
82
+ scopes.set(document, scope);
83
+ }
84
+ disconnect.current = scope.add({ node: element, enabled: () => active.current });
85
+ }
86
+ if (typeof forwardedRef === "function") {
87
+ const previousCleanup = forwardedCleanup.current;
88
+ forwardedCleanup.current = undefined;
89
+ previousCleanup?.();
90
+ if (element !== null || !previousCleanup) {
91
+ const cleanup = forwardedRef(element);
92
+ if (typeof cleanup === "function")
93
+ forwardedCleanup.current = cleanup;
94
+ }
95
+ }
96
+ else if (forwardedRef)
97
+ forwardedRef.current = element;
98
+ }, [forwardedRef]);
99
+ }
@@ -1,18 +1,22 @@
1
1
  # Inter font asset
2
2
 
3
- `InterVariable.woff2` is an unmodified third-party font by the Inter Project
3
+ `InterVariable.woff2` and `InterVariable-Italic.woff2` are unmodified third-party fonts by the Inter Project
4
4
  Authors, distributed under the [SIL Open Font License 1.1](LICENSE.txt).
5
- It is not original Flood artwork. Retain the license with redistributed copies.
5
+ They are not original Flood artwork. Retain the license with redistributed copies.
6
6
 
7
7
  - Official source: <https://github.com/rsms/inter/releases/tag/v4.1>
8
8
  - Archive: <https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip>
9
- - Archive member: `web/InterVariable.woff2`
9
+ - Normal archive member: `web/InterVariable.woff2`
10
+ - Italic archive member: `web/InterVariable-Italic.woff2`
10
11
  - Font internal version: `Version 4.001;git-9221beed3`
11
12
  - Variable axes: weight 100–900; optical size 14–32.
12
13
  - SHA-256: `693b77d4f32ee9b8bfc995589b5fad5e99adf2832738661f5402f9978429a8e3`
14
+ - Italic SHA-256: `e564f652916db6c139570fefb9524a77c4d48f30c92928de9db19b6b5c7a262a`
13
15
  - License SHA-256: `262481e844521b326f5ecd053e59b98c8b2da78c8ee1bdbb6e8174305e54935a`
14
16
 
15
17
  The full upstream font includes Cyrillic; it has not been subsetted or converted.
16
18
  The original `LICENSE.txt` is copied verbatim from the same release archive.
17
- The asset supplies normal variable weights, including the Flood 400/500 roles;
18
- italic text needs a separate face or an explicitly documented fallback.
19
+ Both assets supply weight 100–900 and optical size 14–32, including Flood's 400/500
20
+ roles. CSS registers separate normal and italic faces under Inter; native em/i
21
+ content uses the real italic asset instead of synthetic slant. Both files include
22
+ Latin and Cyrillic and retain the same unmodified upstream license.