@vipengele/react-ui 0.0.0 → 0.1.0

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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +463 -3
  3. package/dist/Avatar/Avatar.d.ts +21 -0
  4. package/dist/Avatar/Avatar.d.ts.map +1 -0
  5. package/dist/Avatar/Avatar.stylesheet.d.ts +17 -0
  6. package/dist/Avatar/Avatar.stylesheet.d.ts.map +1 -0
  7. package/dist/Button/Button.d.ts +39 -0
  8. package/dist/Button/Button.d.ts.map +1 -0
  9. package/dist/Button/Button.stylesheet.d.ts +17 -0
  10. package/dist/Button/Button.stylesheet.d.ts.map +1 -0
  11. package/dist/ButtonGroup/ButtonGroup.d.ts +16 -0
  12. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -0
  13. package/dist/ButtonGroup/ButtonGroup.stylesheet.d.ts +12 -0
  14. package/dist/ButtonGroup/ButtonGroup.stylesheet.d.ts.map +1 -0
  15. package/dist/Card/Card.d.ts +36 -0
  16. package/dist/Card/Card.d.ts.map +1 -0
  17. package/dist/Card/Card.stylesheet.d.ts +17 -0
  18. package/dist/Card/Card.stylesheet.d.ts.map +1 -0
  19. package/dist/Dropdown/Dropdown.d.ts +205 -0
  20. package/dist/Dropdown/Dropdown.d.ts.map +1 -0
  21. package/dist/Dropdown/Dropdown.stylesheet.d.ts +24 -0
  22. package/dist/Dropdown/Dropdown.stylesheet.d.ts.map +1 -0
  23. package/dist/FieldSet/FieldSet.d.ts +22 -0
  24. package/dist/FieldSet/FieldSet.d.ts.map +1 -0
  25. package/dist/FieldSet/FieldSet.stylesheet.d.ts +34 -0
  26. package/dist/FieldSet/FieldSet.stylesheet.d.ts.map +1 -0
  27. package/dist/FieldShell/FieldShell.d.ts +42 -0
  28. package/dist/FieldShell/FieldShell.d.ts.map +1 -0
  29. package/dist/FieldShell/FieldShell.stylesheet.d.ts +32 -0
  30. package/dist/FieldShell/FieldShell.stylesheet.d.ts.map +1 -0
  31. package/dist/FormField/FormField.d.ts +45 -0
  32. package/dist/FormField/FormField.d.ts.map +1 -0
  33. package/dist/FormField/FormField.stylesheet.d.ts +24 -0
  34. package/dist/FormField/FormField.stylesheet.d.ts.map +1 -0
  35. package/dist/PasswordInput/PasswordInput.d.ts +15 -0
  36. package/dist/PasswordInput/PasswordInput.d.ts.map +1 -0
  37. package/dist/PasswordInput/PasswordInput.stylesheet.d.ts +24 -0
  38. package/dist/PasswordInput/PasswordInput.stylesheet.d.ts.map +1 -0
  39. package/dist/Popover/Popover.d.ts +47 -0
  40. package/dist/Popover/Popover.d.ts.map +1 -0
  41. package/dist/Popover/Popover.stylesheet.d.ts +21 -0
  42. package/dist/Popover/Popover.stylesheet.d.ts.map +1 -0
  43. package/dist/Progress/Progress.d.ts +18 -0
  44. package/dist/Progress/Progress.d.ts.map +1 -0
  45. package/dist/Progress/Progress.stylesheet.d.ts +18 -0
  46. package/dist/Progress/Progress.stylesheet.d.ts.map +1 -0
  47. package/dist/RadioButton/RadioButton.d.ts +18 -0
  48. package/dist/RadioButton/RadioButton.d.ts.map +1 -0
  49. package/dist/RadioButton/RadioButton.stylesheet.d.ts +15 -0
  50. package/dist/RadioButton/RadioButton.stylesheet.d.ts.map +1 -0
  51. package/dist/RadioGroup/RadioGroup.d.ts +40 -0
  52. package/dist/RadioGroup/RadioGroup.d.ts.map +1 -0
  53. package/dist/RadioGroup/RadioGroup.stylesheet.d.ts +11 -0
  54. package/dist/RadioGroup/RadioGroup.stylesheet.d.ts.map +1 -0
  55. package/dist/Skeleton/Skeleton.d.ts +18 -0
  56. package/dist/Skeleton/Skeleton.d.ts.map +1 -0
  57. package/dist/Skeleton/Skeleton.stylesheet.d.ts +25 -0
  58. package/dist/Skeleton/Skeleton.stylesheet.d.ts.map +1 -0
  59. package/dist/Slider/Slider.d.ts +12 -0
  60. package/dist/Slider/Slider.d.ts.map +1 -0
  61. package/dist/Slider/Slider.stylesheet.d.ts +18 -0
  62. package/dist/Slider/Slider.stylesheet.d.ts.map +1 -0
  63. package/dist/Spinner/Spinner.d.ts +19 -0
  64. package/dist/Spinner/Spinner.d.ts.map +1 -0
  65. package/dist/Spinner/Spinner.stylesheet.d.ts +14 -0
  66. package/dist/Spinner/Spinner.stylesheet.d.ts.map +1 -0
  67. package/dist/Tabs/Tabs.d.ts +46 -0
  68. package/dist/Tabs/Tabs.d.ts.map +1 -0
  69. package/dist/Tabs/Tabs.stylesheet.d.ts +15 -0
  70. package/dist/Tabs/Tabs.stylesheet.d.ts.map +1 -0
  71. package/dist/TextField/TextField.d.ts +24 -0
  72. package/dist/TextField/TextField.d.ts.map +1 -0
  73. package/dist/TextField/TextField.stylesheet.d.ts +29 -0
  74. package/dist/TextField/TextField.stylesheet.d.ts.map +1 -0
  75. package/dist/Toggle/Toggle.d.ts +12 -0
  76. package/dist/Toggle/Toggle.d.ts.map +1 -0
  77. package/dist/Toggle/Toggle.stylesheet.d.ts +16 -0
  78. package/dist/Toggle/Toggle.stylesheet.d.ts.map +1 -0
  79. package/dist/Tooltip/Tooltip.d.ts +34 -0
  80. package/dist/Tooltip/Tooltip.d.ts.map +1 -0
  81. package/dist/Tooltip/Tooltip.stylesheet.d.ts +22 -0
  82. package/dist/Tooltip/Tooltip.stylesheet.d.ts.map +1 -0
  83. package/dist/Typography/Typography.d.ts +28 -0
  84. package/dist/Typography/Typography.d.ts.map +1 -0
  85. package/dist/Typography/Typography.stylesheet.d.ts +16 -0
  86. package/dist/Typography/Typography.stylesheet.d.ts.map +1 -0
  87. package/dist/index.d.ts +22 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +2947 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/internal/listbox.stylesheet.d.ts +24 -0
  92. package/dist/internal/listbox.stylesheet.d.ts.map +1 -0
  93. package/dist/internal/useListboxKeyboard.d.ts +90 -0
  94. package/dist/internal/useListboxKeyboard.d.ts.map +1 -0
  95. package/package.json +50 -5
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The floating listbox, the panel a search row turns it into, its options, a group's heading and
3
+ * the line between two groups, the option checkbox and
4
+ * the multi-select chip row — the parts every combobox-shaped component in this package renders
5
+ * identically. It lives here, injected
6
+ * under its own `<style href>` by each component that needs it, rather than being duplicated per
7
+ * component or imported from one component's directory into another's.
8
+ *
9
+ * Injected as an inline `<style>` rather than a `.css` import so the package can stay
10
+ * `"sideEffects": false` (same approach as `Card`'s and `Popover`'s stylesheets).
11
+ *
12
+ * Every `--vpg-*` property is *read* here through `var()` and never assigned inline by a
13
+ * component: an inline style declaration always wins over a stylesheet rule for the same property
14
+ * on the same element, so an inline `--vpg-surface-raised` would permanently shadow the
15
+ * dark-mode reassignment in `@vipengele/react-tokens`'s base stylesheet and the listbox would stop
16
+ * adapting to colour mode. The only inline styles any of these elements carry are floating-ui's
17
+ * computed `position`/`top`/`left`/`width`, which are plain CSS properties holding a per-instance
18
+ * coordinate and the panel's match to the field it anchors to.
19
+ *
20
+ * An option's own text is a control's text, so it takes the type scale's `sm` step rather than the
21
+ * prose `md` one. The panel carries the elevation family's medium step, one below the popover's.
22
+ */
23
+ export declare const listboxStylesheet = "\n.vpg-listbox {\n position: absolute;\n z-index: var(--vpg-layer-listbox);\n box-sizing: border-box;\n margin: 0;\n padding: var(--vpg-space-1);\n /* The size of a container, not steps of anything: no scale carries a measurement this large,\n and a `--vpg-*` name the theme never assigns advertises a theming hook that doesn't\n exist. Width is floating-ui's inline style, matching the field exactly \u2014 this rule must\n never impose a floor wider than a narrow field, or the panel outgrows what it's anchored to. */\n max-height: 16rem;\n overflow-y: auto;\n background-color: var(--vpg-surface-raised);\n border: 1px solid var(--vpg-border);\n border-radius: var(--vpg-radius);\n box-shadow: var(--vpg-shadow-med);\n color: var(--vpg-ink);\n font-family: var(--vpg-font-sans);\n font-size: var(--vpg-font-size-sm);\n line-height: 1.5;\n}\n\n.vpg-listbox:focus-visible {\n /* The listbox takes DOM focus from nothing \u2014 the highlight is virtual \u2014 but it is still a\n focus target for a stray programmatic focus() call, and an invisible one reads as broken. */\n outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);\n outline-offset: var(--vpg-focus-ring-offset);\n}\n\n/* A search row turns the listbox into a panel holding two parts, and the floating element is then\n the panel rather than the listbox: it draws the surface `.vpg-listbox` draws for a listbox\n that is the floating element itself, and holds the search row still while the options scroll\n under it. `overflow: hidden` keeps the scrolling options inside the panel's rounded corners. */\n.vpg-listbox-panel {\n position: absolute;\n z-index: var(--vpg-layer-listbox);\n box-sizing: border-box;\n overflow: hidden;\n background-color: var(--vpg-surface-raised);\n border: 1px solid var(--vpg-border);\n border-radius: var(--vpg-radius);\n box-shadow: var(--vpg-shadow-med);\n color: var(--vpg-ink);\n font-family: var(--vpg-font-sans);\n font-size: var(--vpg-font-size-sm);\n line-height: 1.5;\n}\n\n/* The scrolling half of the panel, at the same height `.vpg-listbox` stands at \u2014 the size of\n a container, not a step of anything, so no `--vpg-*` name stands for it. The search row\n above it is outside this box and so never scrolls out of reach. */\n.vpg-listbox-options {\n padding: var(--vpg-space-1);\n max-height: 16rem;\n overflow-y: auto;\n}\n\n/* The panel's first row: the magnifier, then the input. Its lower border is the divider between\n the search and the options under it. The row stands at the same height as an option, so the\n panel's first two rows read as one rhythm. */\n.vpg-listbox-search {\n display: flex;\n align-items: center;\n gap: var(--vpg-space-2);\n box-sizing: border-box;\n min-height: var(--vpg-size-md);\n padding-inline: var(--vpg-space-3);\n border-bottom: 1px solid var(--vpg-border);\n}\n\n.vpg-listbox-search-icon {\n flex: none;\n width: var(--vpg-icon-md);\n height: var(--vpg-icon-md);\n color: var(--vpg-ink-muted);\n}\n\n/* The input carries none of a field's chrome: the panel's own border is the box around it, and a\n border or focus ring here would draw a second box inside that one. The panel opens with the\n caret already in this input, which is what marks it as the focused element. */\n.vpg-listbox-search-input {\n flex: 1;\n min-width: 0;\n appearance: none;\n padding: 0;\n background: none;\n border: none;\n outline: none;\n color: inherit;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n.vpg-listbox-search-input::placeholder {\n color: var(--vpg-ink-subtle);\n}\n\n/* Not an option: it carries no role, is never highlighted and cannot be selected \u2014 it exists so\n a query matching nothing says so instead of leaving the panel blank. Its padding is an option's\n block padding, so the message stands as tall as the option it stands in for. */\n.vpg-listbox-empty {\n padding: var(--vpg-space-2);\n color: var(--vpg-ink-subtle);\n}\n\n/* A heading, never a row the keyboard can reach: it is the group's name, and the options under it\n keep the indices they hold with no group around them. Its inline padding is an option's, so the\n heading and the labels beneath it share one left edge, and it takes the type scale's smallest\n step to read as a label over the rows rather than as one of them. */\n.vpg-listbox-group-label {\n padding-block: var(--vpg-space-1);\n padding-inline: var(--vpg-space-3);\n color: var(--vpg-ink-subtle);\n font-size: var(--vpg-font-size-xs);\n font-weight: var(--vpg-font-weight-medium);\n}\n\n/* The line between one group and the next, drawn by the group that follows another. A rule rather\n than a border on the group itself: the line spans the padding the options sit inside, so it\n reaches the full width of the list rather than stopping at an option's edge. */\n.vpg-listbox-separator {\n height: 1px;\n margin-block: var(--vpg-space-1);\n margin-inline: calc(var(--vpg-space-1) * -1);\n background-color: var(--vpg-border);\n}\n\n.vpg-listbox-option {\n display: flex;\n align-items: center;\n gap: var(--vpg-space-2);\n box-sizing: border-box;\n /* A row's height is a token read, not the sum of a padding and a line-height: min-height\n plus centred content holds it at the control scale's md step regardless of what the label's\n font metrics or an icon's box happen to add up to. */\n min-height: var(--vpg-size-md);\n padding-inline: var(--vpg-space-3);\n border-radius: var(--vpg-radius-sm);\n cursor: pointer;\n transition: background-color var(--vpg-duration-fast) var(--vpg-ease-standard);\n /* The highlight moves with the keyboard, not with the pointer, so an option must never look\n selectable-by-drag. */\n user-select: none;\n}\n\n/* The highlight is virtual \u2014 the option never takes DOM focus, so :focus/:hover cannot express\n it and the component sets this attribute from its own highlighted index instead. Highlight\n (background) and selection (a checkbox fill or a trailing check, never colour) read on\n different visual channels, so a row can carry both at once without either one washing out the\n other. */\n.vpg-listbox-option[data-highlighted] {\n background-color: var(--vpg-surface-hover);\n}\n\n.vpg-listbox-option[aria-disabled=\"true\"] {\n color: var(--vpg-ink-subtle);\n cursor: default;\n}\n\n.vpg-listbox-option-icon {\n flex: none;\n width: var(--vpg-icon-md);\n height: var(--vpg-icon-md);\n color: var(--vpg-ink-muted);\n}\n\n/* A single-select row's only selection signal: colour alone would fail WCAG 1.4.1, and\n `aria-selected` reaches assistive technology but not a sighted reader. Trailing edge matches\n the shadcn/Radix select convention. A multi-select row already carries the checkbox as its\n one encoding, so this mark never renders alongside it \u2014 `Dropdown` renders it only for a\n selected option outside `multiple`. */\n.vpg-listbox-option-check {\n flex: none;\n width: var(--vpg-icon-md);\n height: var(--vpg-icon-md);\n color: var(--vpg-accent);\n}\n\n.vpg-listbox-option-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* A drawn box rather than an <input type=\"checkbox\">: the option already carries\n role=\"option\" + aria-selected, and a real checkbox inside it would be an interactive\n element nested in one. */\n.vpg-listbox-checkbox {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n box-sizing: border-box;\n width: 1.125rem;\n height: 1.125rem;\n border: 1px solid var(--vpg-border-strong);\n border-radius: var(--vpg-radius-sm);\n color: var(--vpg-accent-contrast);\n transition: background-color var(--vpg-duration-fast) var(--vpg-ease-standard),\n border-color var(--vpg-duration-fast) var(--vpg-ease-standard);\n}\n\n.vpg-listbox-checkbox[data-checked] {\n background-color: var(--vpg-accent);\n border-color: var(--vpg-accent);\n}\n\n/* The row of chips a multi-select field shows beside its control. It is a direct child of the\n field's shell, sitting before the control, within the width the shell leaves it. Wrapping is\n the shape it takes when the field asks for more than one row: each further line grows the field\n downwards rather than overflowing it, because the shell's height is a floor.\n\n One row of chips stands the field at the control step, the same height it has with no chips,\n so selecting the first option never makes the field jump. The field's content box is the\n control step less its border \u2014 30px at the default scale \u2014 and a chip is 24px, leaving 3px on\n each side. The spacing scale's smallest step is 4px, which would grow the field to 34px, so the\n row pads each edge by half of it: 2px, which a single row fits inside and the shell centres, and\n which keeps the top and bottom rows of a wrapped field clear of its border. The rows themselves\n are a whole step apart. */\n.vpg-listbox-chips {\n display: flex;\n flex-wrap: wrap;\n gap: var(--vpg-space-1);\n padding-block: calc(var(--vpg-space-1) / 2);\n}\n\n/* One row, with the chips that do not fit measured out of it and an indicator standing for them.\n Which chips those are is the component's read, not a rule here: the attribute says the row\n collapses, and each chip the read excluded carries `data-hidden`.\n\n `nowrap` is what holds the field at the control step in the one case the read cannot resolve:\n a chip too wide to share the row with the indicator still shows, and wrapping would put the two\n on separate rows rather than shrinking the chip to the room the indicator leaves. */\n.vpg-listbox-chips[data-collapsing] {\n flex-wrap: nowrap;\n}\n\n.vpg-listbox-chips[data-collapsing] > [data-hidden] {\n display: none;\n}\n\n/* The read that decides which chips fit needs every one of them on the row at its own width: a\n hidden chip has no width to weigh, a shrunk one reports the row's constraint rather than its\n label, and the row itself shrinks to whatever is left in it once some are hidden. This\n attribute is set and removed inside one synchronous measurement, so the state it describes\n never paints. */\n.vpg-listbox-chips[data-collapsing][data-measuring] > * {\n display: inline-flex;\n flex: none;\n}\n\n/* A chip's height is a token read, not the sum of a padding and a line-height: a fixed height\n plus centred content holds it at the control scale's xs step regardless of the label's font\n metrics. Its end carries no padding, so the remove button's round hover fill sits concentric\n with the chip's own rounded end. */\n.vpg-listbox-chip {\n display: inline-flex;\n align-items: center;\n gap: var(--vpg-space-1);\n box-sizing: border-box;\n height: var(--vpg-size-xs);\n padding-inline: var(--vpg-space-2) 0;\n background-color: var(--vpg-accent-wash);\n border: 1px solid transparent;\n border-radius: var(--vpg-radius-full);\n color: var(--vpg-ink);\n font-family: var(--vpg-font-sans);\n font-size: var(--vpg-font-size-sm);\n line-height: 1.5;\n}\n\n/* A chip never outgrows the row that holds it: a label too long for the field is cut short\n rather than pushing the chip, and the field with it, past the field's border.\n\n `min-width` is what lets it be cut short at all. A flex item's automatic minimum size is its\n content's, which for a chip is its label's longest word \u2014 so a chip holding one long word takes\n the whole row and pushes the overflow indicator out past the field's border. The label carries\n the ellipsis that keeps the shorter box readable. */\n.vpg-listbox-chips > .vpg-listbox-chip {\n min-width: 0;\n max-width: 100%;\n}\n\n/* The indicator standing for the chips the row has no width for. A chip's box without a chip's\n remove button: the selection it covers is unpicked in the listbox, since the chip carrying it\n is not on screen to remove it from \u2014 so it pads both ends alike and keeps its own width, which\n is the width the read reserves before any chip is counted onto the row. */\n.vpg-listbox-overflow-chip {\n display: inline-flex;\n align-items: center;\n flex: none;\n box-sizing: border-box;\n height: var(--vpg-size-xs);\n padding-inline: var(--vpg-space-2);\n background-color: var(--vpg-accent-wash);\n border: 1px solid transparent;\n border-radius: var(--vpg-radius-full);\n color: var(--vpg-ink-muted);\n font-family: var(--vpg-font-sans);\n font-size: var(--vpg-font-size-sm);\n line-height: 1.5;\n white-space: nowrap;\n}\n\n.vpg-listbox-chip-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* A square as tall as the chip's inside: the glyph plus a spacing step on every side, which at the\n default scale is 22px \u2014 the whole of the chip's height inside its border, so the target is as\n large as a 24px chip allows. */\n.vpg-listbox-chip-remove {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n appearance: none;\n padding: var(--vpg-space-1);\n background: none;\n border: none;\n border-radius: var(--vpg-radius-full);\n color: var(--vpg-ink-muted);\n cursor: pointer;\n transition: background-color var(--vpg-duration-fast) var(--vpg-ease-standard),\n color var(--vpg-duration-fast) var(--vpg-ease-standard);\n}\n\n.vpg-listbox-chip-remove-icon {\n flex: none;\n width: var(--vpg-icon-sm);\n height: var(--vpg-icon-sm);\n}\n\n.vpg-listbox-chip-remove:hover {\n background-color: var(--vpg-accent-wash);\n color: var(--vpg-ink);\n}\n\n.vpg-listbox-chip-remove:focus-visible {\n outline: var(--vpg-focus-ring-width) solid var(--vpg-accent-ring);\n outline-offset: var(--vpg-focus-ring-offset);\n}\n";
24
+ //# sourceMappingURL=listbox.stylesheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listbox.stylesheet.d.ts","sourceRoot":"","sources":["../../src/internal/listbox.stylesheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB,8+aA2V7B,CAAC"}
@@ -0,0 +1,90 @@
1
+ import { type UseFloatingReturn, type UseInteractionsReturn } from "@floating-ui/react";
2
+ import { type MouseEvent, type RefObject } from "react";
3
+ export interface UseListboxKeyboardOptions {
4
+ /** The option elements, in the order they are rendered — each option registers itself at its
5
+ * own index. Arrow-key navigation and type-ahead both read the list from here, so it must hold
6
+ * the elements currently in the document, not a snapshot. */
7
+ listRef: RefObject<Array<HTMLElement | null>>;
8
+ /** The highlighted option's index, or `null` for none. State lives in the calling component,
9
+ * never in this hook: `Dropdown` re-scopes it on every query change through its own
10
+ * `applyQuery`, which a hook owning the state would make impossible without widening this
11
+ * signature. */
12
+ activeIndex: number | null;
13
+ /** Called with the new highlighted index on every arrow-key move, `Home`/`End` jump, hover, and
14
+ * type-ahead match. */
15
+ onNavigate: (index: number | null) => void;
16
+ /** Indices of options excluded from arrow-key traversal and from type-ahead matching. */
17
+ disabledIndices: number[];
18
+ /** Whether typing a character jumps the highlight to the next matching label. Off for a
19
+ * filtering combobox, where typing goes to the text input instead. */
20
+ typeahead: boolean;
21
+ /** `"select"` puts `role="combobox"` and `aria-haspopup="listbox"` on the reference element —
22
+ * what a component rendering its own non-input trigger wants. `"combobox"` leaves the role to
23
+ * the reference element itself, for a component whose trigger is already an `<input>`. */
24
+ role: "select" | "combobox";
25
+ /** Whether the caller renders a search input inside the floating element and gives it real
26
+ * focus, with `getSearchProps()` and a non-modal `FloatingFocusManager`. Off, the reference
27
+ * element keeps real focus and `getSearchProps()` is never called. */
28
+ search?: boolean;
29
+ open: boolean;
30
+ onOpenChange: (open: boolean) => void;
31
+ }
32
+ export interface UseListboxKeyboardReturn extends UseInteractionsReturn, Pick<UseFloatingReturn, "refs" | "floatingStyles" | "context" | "elements"> {
33
+ /** The nearest `.vpg-root` ancestor of the reference element, or `null` when there is none
34
+ * to portal the listbox into. */
35
+ themeRoot: Element | null;
36
+ /** A callback ref for the field — the bordered box around the reference element. The listbox is
37
+ * positioned against it and sized to its width, and a press anywhere inside it is not a press
38
+ * outside the listbox. Left unattached, the listbox anchors to the reference element and only a
39
+ * press outside the reference and the listbox closes it. */
40
+ fieldRef: (node: HTMLElement | null) => void;
41
+ /** The field's `mousedown` handler. A press on the field outside the reference element keeps
42
+ * focus where it is, so the reference element never loses focus to the field's own padding, a
43
+ * chip row or an adornment. A primary press there that does not land on a button also focuses
44
+ * the reference element and opens the listbox; a button — a chip's remove button — keeps its
45
+ * press for its own `click`. */
46
+ onFieldMouseDown: (event: MouseEvent<HTMLElement>) => void;
47
+ /** Props for the search input inside the floating element, in `search` mode: the listbox role
48
+ * wiring and the arrow, `Home` and `End` handling, so the input is what drives the list and
49
+ * what carries `aria-activedescendant`. `useClick` is left out of them: its press handler
50
+ * toggles the listbox, and a press on the input to place the caret would close the popover the
51
+ * input lives in. */
52
+ getSearchProps: UseInteractionsReturn["getReferenceProps"];
53
+ }
54
+ /**
55
+ * The floating-listbox glue shared by every combobox-shaped component in this package: positions
56
+ * the listbox, and composes `@floating-ui/react`'s `useListNavigation({ virtual: true })`,
57
+ * `useTypeahead` and `useRole` into one set of prop getters.
58
+ *
59
+ * Virtual focus is the whole point — the highlighted option is tracked through
60
+ * `aria-activedescendant`, and real DOM focus never enters the listbox itself (see
61
+ * `docs/adr/0004-aria-activedescendant-for-dropdown-and-autocomplete.md`). The element holding
62
+ * real focus is the reference element, and in `search` mode a search input inside the floating
63
+ * element instead — a `role="combobox"` of its own, taking `getSearchProps()`. That mode is the
64
+ * only one where the caller wraps the floating element in a **non-modal** `FloatingFocusManager`:
65
+ * the manager moves real focus into the floating element, which is what puts the caret in the
66
+ * search input, and what returns focus to the reference element when the listbox closes. Modal
67
+ * would be wrong — the trigger and the page behind it stay reachable.
68
+ *
69
+ * Two elements anchor the listbox. The reference element — the trigger or input the consumer
70
+ * focuses — carries the interactions, and stays `elements.domReference` for the whole lifecycle,
71
+ * `search` mode included: floating-ui reads it for its typeable-combobox handling, its focus
72
+ * restore and `useClick`'s space key, none of which a reference that moved between open and shut
73
+ * would survive.
74
+ *
75
+ * The field around it is only the position reference: the listbox sits below the field's border
76
+ * and matches its width, where
77
+ * anchoring to the reference element would start it wherever that element starts — after the
78
+ * field's padding and any chips beside it — and size it to nothing in particular.
79
+ *
80
+ * The field is also one press target. A non-focusable part of it — its padding, the gaps in a chip
81
+ * row, a chevron — takes focus off the reference element when pressed and hands it to `<body>`.
82
+ * Since that press is not outside the listbox, the listbox would stay open with nothing left to
83
+ * receive the arrow keys; `onFieldMouseDown` keeps focus on the reference element instead.
84
+ *
85
+ * `useClick`'s keyboard handlers are switched off. They toggle the listbox on `Enter` and `Space`,
86
+ * which are the keys that select the highlighted option once it is open — the caller owns both
87
+ * keys and decides what they mean for the current open state.
88
+ */
89
+ export declare function useListboxKeyboard({ listRef, activeIndex, onNavigate, disabledIndices, typeahead, role, search, open, onOpenChange, }: UseListboxKeyboardOptions): UseListboxKeyboardReturn;
90
+ //# sourceMappingURL=useListboxKeyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useListboxKeyboard.d.ts","sourceRoot":"","sources":["../../src/internal/useListboxKeyboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAQ3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAgC,MAAM,OAAO,CAAC;AAgBtF,MAAM,WAAW,yBAAyB;IACxC;;iEAE6D;IAC7D,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9C;;;oBAGgB;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;2BACuB;IACvB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,yFAAyF;IACzF,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;0EACsE;IACtE,SAAS,EAAE,OAAO,CAAC;IACnB;;8FAE0F;IAC1F,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC5B;;0EAEsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,wBACf,SAAQ,qBAAqB,EAC3B,IAAI,CAAC,iBAAiB,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,GAAG,UAAU,CAAC;IAC7E;qCACiC;IACjC,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B;;;gEAG4D;IAC5D,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C;;;;oCAIgC;IAChC,gBAAgB,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAC3D;;;;yBAIqB;IACrB,cAAc,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAC;CAC5D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,OAAO,EACP,WAAW,EACX,UAAU,EACV,eAAe,EACf,SAAS,EACT,IAAI,EACJ,MAAc,EACd,IAAI,EACJ,YAAY,GACb,EAAE,yBAAyB,GAAG,wBAAwB,CAkKtD"}
package/package.json CHANGED
@@ -1,9 +1,54 @@
1
1
  {
2
2
  "name": "@vipengele/react-ui",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserving the @vipengele/react-ui name. Install a released version.",
3
+ "version": "0.1.0",
4
+ "description": "vipengele component library themeable React atoms styled entirely from --vpg-* CSS custom properties",
5
5
  "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/vipengele/react.git" },
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "provenance": true
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/vipengele/react.git",
15
+ "directory": "source/react-ui/packages/ui"
16
+ },
7
17
  "homepage": "https://vipengele.github.io/react/react-ui/",
8
- "publishConfig": { "access": "public" }
9
- }
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "types": "./dist/index.d.ts",
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "dependencies": {
31
+ "@floating-ui/react": "^0.27.20",
32
+ "@vipengele/react-icons": "0.1.0"
33
+ },
34
+ "peerDependencies": {
35
+ "@vipengele/react-tokens": "^0.1.0",
36
+ "react": "^19",
37
+ "react-dom": "^19"
38
+ },
39
+ "devDependencies": {
40
+ "@vipengele/react-tokens": "0.1.0",
41
+ "@types/react": "^19.0.0",
42
+ "@types/react-dom": "^19.0.0",
43
+ "@vitest/browser-playwright": "5.0.0",
44
+ "playwright": "1.63.0",
45
+ "react": "^19.0.0",
46
+ "react-dom": "^19.0.0",
47
+ "vite": "^8.2.2"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup && tsc -p tsconfig.build.json",
51
+ "type-check": "tsc -p tsconfig.json --noEmit",
52
+ "test": "vitest run --coverage && node bundle-check/run.mjs"
53
+ }
54
+ }