@agentero/design-system 0.31.8 → 0.32.1

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.
@@ -2007,7 +2007,7 @@
2007
2007
  "displayName": "Combobox.Root",
2008
2008
  "exportName": "Combobox",
2009
2009
  "filePath": "/home/runner/work/design-system/design-system/src/combobox/index.ts",
2010
- "description": "Root of a combobox: a text input that filters a list of options and writes the\nchosen one back into itself. With `Input` as the only trigger, the list opens on\nfocus or on the first keystroke.\n\nPass the options as `items`. For a list that comes from the server, set\n`filter={null}`, hand the already-filtered rows to `filteredItems`, and request\nthem from `onInputValueChange`; `Status` then announces the wait.\n\nWith `multiple`, the value is an array and picking a row toggles it. The list\nstays open and keeps the typed query between picks, so several rows can be\nticked from one search; closing the list (Escape, a click outside) still resets\nthe query. Each row then shows whether it is selected — see `Item`. Nothing is\nwritten back into the input: render the selection next to the field.",
2010
+ "description": "Root of a combobox: a text input that filters a list of options and writes the\nchosen one back into itself. The list opens on focus, on the first keystroke\nor from the chevron of `Input`'s `select` variant.\n\nFor a search against an API, set `minInputLength` so the list stays shut until\nthe query is worth sending: a field that opens an empty list on focus reads as\nbroken. For a pick from a short fixed list with no typing at all, prefer a\nselect.\n\nPass the options as `items`. For a list that comes from the server, set\n`filter={null}`, hand the already-filtered rows to `filteredItems`, and request\nthem from `onInputValueChange`; `Status` then announces the wait.\n\nEvery Base UI root prop is accepted and forwarded — `items`, `filter`,\n`value`, `onValueChange` and so on. `open` stays the consumer's to control;\n`minInputLength` only refuses to open below the minimum.\n\nWith `multiple`, the value is an array and picking a row toggles it. The list\nstays open and keeps the typed query between picks, so several rows can be\nticked from one search; closing the list (Escape, a click outside) still resets\nthe query. Each row then shows whether it is selected — see `Item`. Nothing is\nwritten back into the input: render the selection next to the field.",
2011
2011
  "jsDocTags": {
2012
2012
  "summary": [
2013
2013
  "Root provider for an input-triggered filterable list"
@@ -2769,6 +2769,27 @@
2769
2769
  "fileName": "node_modules/@base-ui/react/combobox/root/ComboboxRoot.d.mts"
2770
2770
  }
2771
2771
  ]
2772
+ },
2773
+ "minInputLength": {
2774
+ "name": "minInputLength",
2775
+ "required": false,
2776
+ "type": {
2777
+ "name": "number"
2778
+ },
2779
+ "description": "Characters the query needs, spaces aside, before the list opens. Below it\nfocusing, clicking or pressing the chevron leaves the list shut, and\ndeleting back under it closes the list. Defaults to `0`: the list opens on\nfocus.",
2780
+ "defaultValue": {
2781
+ "value": "0"
2782
+ },
2783
+ "parent": {
2784
+ "name": "ComboboxRootProps",
2785
+ "fileName": "src/combobox/combobox.tsx"
2786
+ },
2787
+ "declarations": [
2788
+ {
2789
+ "name": "TypeLiteral",
2790
+ "fileName": "src/combobox/combobox.tsx"
2791
+ }
2792
+ ]
2772
2793
  }
2773
2794
  }
2774
2795
  },
@@ -2777,7 +2798,29 @@
2777
2798
  "id": "components-combobox--default",
2778
2799
  "name": "Default",
2779
2800
  "snippet": "const Default = () => (\n <Combobox.Root items={STATES}>\n <Combobox.Input placeholder=\"Search states\" aria-label=\"Search states\" />\n <Combobox.Content>\n <Combobox.Empty>No matches found</Combobox.Empty>\n <Combobox.List>\n {(state: string) => (\n <Combobox.Item key={state} value={state}>\n {state}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n);",
2780
- "description": "The common case: options are already in memory and Base UI filters them against\nwhat you type. Selecting a row fills the input and closes the list."
2801
+ "description": "The common case: options are already in memory and Base UI filters them against\nwhat you type. Selecting a row fills the input and closes the list. The field is\na search out of the box — a magnifier in front, a clear button once something is\npicked — and the list hangs from the whole field.",
2802
+ "summary": "Search field with a magnifier and clear button over an in-memory list"
2803
+ },
2804
+ {
2805
+ "id": "components-combobox--plain",
2806
+ "name": "Plain",
2807
+ "snippet": "const Plain = () => (\n <Combobox.Root items={STATES}>\n <Combobox.Input variant=\"plain\" placeholder=\"State\" aria-label=\"State\" />\n <Combobox.Content>\n <Combobox.Empty>No matches found</Combobox.Empty>\n <Combobox.List>\n {(state: string) => (\n <Combobox.Item key={state} value={state}>\n {state}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n);",
2808
+ "description": "`variant=\"plain\"`: the input alone, with no magnifier and no clear button, for a\nfield that needs no affordance — a FEIN lookup, a template name. The list still\nmatches the field's width.",
2809
+ "summary": "Bare input with no icon and no clear button"
2810
+ },
2811
+ {
2812
+ "id": "components-combobox--select",
2813
+ "name": "Select",
2814
+ "snippet": "const Select = () => (\n <Combobox.Root items={STATES}>\n <Combobox.Input variant=\"select\" placeholder=\"Choose a state\" aria-label=\"State\" />\n <Combobox.Content>\n <Combobox.Empty>No matches found</Combobox.Empty>\n <Combobox.List>\n {(state: string) => (\n <Combobox.Item key={state} value={state}>\n {state}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n);",
2815
+ "description": "`variant=\"select\"`: a chevron after the input opens and closes the list, for a\nshort known list the user may want to browse before typing. It has no clear\nbutton by default. The chevron is out of the tab order — the input already\nopens the list from the keyboard with the arrow keys.",
2816
+ "summary": "Field with a chevron that toggles the list, for short known lists"
2817
+ },
2818
+ {
2819
+ "id": "components-combobox--select-with-clear",
2820
+ "name": "Select With Clear",
2821
+ "snippet": "const SelectWithClear = () => (\n <Combobox.Root items={STATES} defaultValue=\"Florida\">\n <Combobox.Input variant=\"select\" showClear placeholder=\"Choose a state\" aria-label=\"State\" />\n <Combobox.Content>\n <Combobox.Empty>No matches found</Combobox.Empty>\n <Combobox.List>\n {(state: string) => (\n <Combobox.Item key={state} value={state}>\n {state}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n);",
2822
+ "description": "`showClear` overrides the variant's default: here it adds the clear button to a\n`select`, so a picked option can be removed without selecting the text and\ndeleting it. While there is a value it takes the chevron's place; clearing\nbrings the chevron back. Passing `false` drops it from `search`.",
2823
+ "summary": "Select field with a clear button added through showClear"
2781
2824
  },
2782
2825
  {
2783
2826
  "id": "components-combobox--no-matches",
@@ -2800,8 +2843,9 @@
2800
2843
  {
2801
2844
  "id": "components-combobox--async-search-with-status",
2802
2845
  "name": "Async Search With Status",
2803
- "snippet": "const AsyncSearchWithStatus = () => {\n const [results, setResults] = useState<Agency[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n // Only the latest request may land; fast typing returns responses out of order.\n const latestRequest = useRef(0);\n\n const onInputValueChange = async (query: string) => {\n const request = ++latestRequest.current;\n\n // The previous rows go before the wait starts: a list that no longer matches what is typed\n // must not sit under the loading state.\n setResults([]);\n setIsLoading(true);\n const agencies = await searchAgencies(query);\n if (request !== latestRequest.current) return;\n\n setResults(agencies);\n setIsLoading(false);\n };\n\n return (\n // `filter={null}` hands filtering to the server; `filteredItems` is what came back.\n <Combobox.Root\n items={results}\n filteredItems={results}\n filter={null}\n itemToStringLabel={(agency: Agency) => agency.name}\n isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}\n onInputValueChange={onInputValueChange}>\n <Combobox.Input placeholder=\"Search agencies\" aria-label=\"Search agencies\" />\n <Combobox.Content aria-busy={isLoading || undefined}>\n {/* The rows show the wait; `Status` only announces it. */}\n <Combobox.Status className=\"sr-only\">\n {isLoading ? 'Searching…' : undefined}\n </Combobox.Status>\n {isLoading && <SkeletonRows />}\n <Combobox.Empty>{isLoading ? undefined : 'No agencies found'}</Combobox.Empty>\n <Combobox.List>\n {(agency: Agency) => (\n <Combobox.Item key={agency.id} value={agency}>\n {agency.name}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n );\n};",
2804
- "description": "The same search without Suspense, for a consumer that gets a `loading` flag from\nits data layer instead: `filter={null}`, the rows arrive through `filteredItems`,\n`Content` is marked busy and `Status` announces the wait. The list is cleared\nbefore each request and a request that is no longer the latest is dropped."
2846
+ "snippet": "const AsyncSearchWithStatus = () => {\n const [results, setResults] = useState<Agency[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n // Only the latest request may land; fast typing returns responses out of order.\n const latestRequest = useRef(0);\n\n const onInputValueChange = async (query: string) => {\n const request = ++latestRequest.current;\n if (query.trim().length < MIN_QUERY_LENGTH) {\n setResults([]);\n setIsLoading(false);\n return;\n }\n\n // The previous rows go before the wait starts: a list that no longer matches what is typed\n // must not sit under the loading state.\n setResults([]);\n setIsLoading(true);\n const agencies = await searchAgencies(query);\n if (request !== latestRequest.current) return;\n\n setResults(agencies);\n setIsLoading(false);\n };\n\n return (\n // `filter={null}` hands filtering to the server; `filteredItems` is what came back.\n <Combobox.Root\n items={results}\n filteredItems={results}\n filter={null}\n minInputLength={MIN_QUERY_LENGTH}\n itemToStringLabel={(agency: Agency) => agency.name}\n isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}\n onInputValueChange={onInputValueChange}>\n <Combobox.Input placeholder=\"Search agencies\" aria-label=\"Search agencies\" />\n <Combobox.Content aria-busy={isLoading || undefined}>\n {/* The rows show the wait; `Status` only announces it. */}\n <Combobox.Status className=\"sr-only\">\n {isLoading ? 'Searching…' : undefined}\n </Combobox.Status>\n {isLoading && <SkeletonRows />}\n <Combobox.Empty>{isLoading ? undefined : 'No agencies found'}</Combobox.Empty>\n <Combobox.List>\n {(agency: Agency) => (\n <Combobox.Item key={agency.id} value={agency}>\n {agency.name}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n );\n};",
2847
+ "description": "The same search without Suspense, for a consumer that gets a `loading` flag from\nits data layer instead: `filter={null}`, the rows arrive through `filteredItems`,\n`Content` is marked busy and `Status` announces the wait. The list is cleared\nbefore each request and a request that is no longer the latest is dropped.\n\n`minInputLength` keeps the list shut until two characters are typed — focusing\nthe field opens nothing, and deleting back under the minimum closes it — and\nno request goes out for a shorter query either.",
2848
+ "summary": "API search that waits for two characters, with loading status"
2805
2849
  },
2806
2850
  {
2807
2851
  "id": "components-combobox--inside-field-text",
@@ -2809,12 +2853,6 @@
2809
2853
  "snippet": "const InsideFieldText = () => (\n <FieldText invalid required>\n <Label>Agency</Label>\n <Combobox.Root items={STATES}>\n <Combobox.Input placeholder=\"Search agencies\" />\n <Combobox.Content>\n <Combobox.Empty>No matches found</Combobox.Empty>\n <Combobox.List>\n {(state: string) => (\n <Combobox.Item key={state} value={state}>\n {state}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n <Field.Description>The agency this agent will be added to.</Field.Description>\n <Field.Error errors={[{ message: 'Pick an agency from the list' }]} />\n </FieldText>\n);",
2810
2854
  "description": "Dropped into a `FieldText`, the input takes the label, the description and\nthe error from the field with nothing wired by hand: `Combobox.Input` reads the\nsame `InputContext` the plain `Input` does. It also points `aria-labelledby`\nat the label, so the name survives the list opening: Base UI then hides the\nrest of the page from assistive technology, label included, and a\n`<label for>` alone would stop naming the input."
2811
2855
  },
2812
- {
2813
- "id": "components-combobox--with-leading-icon",
2814
- "name": "With Leading Icon",
2815
- "snippet": "const WithLeadingIcon = () => (\n <Combobox.Root items={STATES}>\n <div className=\"relative\">\n <IconSearch\n aria-hidden\n className=\"pointer-events-none absolute top-1/2 left-3 -z-hide size-6 -translate-y-1/2 [&>path]:fill-icon-default-base-tertiary\"\n />\n <Combobox.Input className=\"pl-11\" placeholder=\"Search states\" aria-label=\"Search states\" />\n </div>\n <Combobox.Content>\n <Combobox.Empty>No matches found</Combobox.Empty>\n <Combobox.List>\n {(state: string) => (\n <Combobox.Item key={state} value={state}>\n {state}\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Content>\n </Combobox.Root>\n);",
2816
- "description": "A leading icon is composed around the input, not baked in: not every combobox\nis a search (a FEIN lookup or a template picker are not), so the component\nships no icon of its own. Until `InputGroup` lands in the design system, the\nmarketplace search fields get theirs like this — the icon overlaid, the input\npadded past it."
2817
- },
2818
2856
  {
2819
2857
  "id": "components-combobox--inside-modal",
2820
2858
  "name": "Inside Modal",
@@ -2822,7 +2860,7 @@
2822
2860
  "description": "Inside a `Modal`, which is a Radix Dialog. The surface portals to the body, so\nthis checks that the dialog's modal layer neither blocks pointer events on the\nlist nor treats a click in it as a click outside and closes."
2823
2861
  }
2824
2862
  ],
2825
- "import": "import {\n Button,\n Combobox,\n Field,\n FieldText,\n IconSearch,\n Label,\n Modal,\n Skeleton,\n Tag,\n} from \"@agentero/design-system\";\nimport { Suspense } from \"react\";"
2863
+ "import": "import { Button, Combobox, Field, FieldText, Label, Modal, Skeleton, Tag } from \"@agentero/design-system\";\nimport { Suspense } from \"react\";"
2826
2864
  },
2827
2865
  "components-command": {
2828
2866
  "id": "components-command",
@@ -6460,6 +6498,132 @@
6460
6498
  ],
6461
6499
  "import": "import { Avatar, Button, HoverCard } from \"@agentero/design-system\";"
6462
6500
  },
6501
+ "components-inputgroup": {
6502
+ "id": "components-inputgroup",
6503
+ "name": "InputGroup.Root",
6504
+ "path": "./src/input-group/input-group.stories.tsx",
6505
+ "jsDocTags": {
6506
+ "summary": [
6507
+ "Bordered frame drawing an Input and its addons as one control"
6508
+ ],
6509
+ "dataAttribute": [
6510
+ "{string} data-slot - Always set to \"input-group\""
6511
+ ],
6512
+ "example": [
6513
+ "<InputGroup.Root>\n <InputGroup.Text>$</InputGroup.Text>\n <Input inputMode=\"decimal\" placeholder=\"0.00\" />\n <InputGroup.Text>USD</InputGroup.Text>\n</InputGroup.Root>"
6514
+ ]
6515
+ },
6516
+ "description": "InputGroup draws an `Input` and what sits beside it — an icon, a currency or\nunit affix, a trailing action — as one bordered control. The frame owns the\nborder, the shadow and the focus ring; the `Input` inside renders bare and\nhands its state to the frame through `:has()`, so focus, `disabled`,\n`aria-invalid` and `size` are never wired by hand. Inside a `FieldText` the\n`Input` keeps taking its label and messages from the field.",
6517
+ "summary": "Bordered frame drawing an Input and its addons as one control",
6518
+ "reactComponentMeta": {
6519
+ "displayName": "InputGroup.Root",
6520
+ "exportName": "InputGroup",
6521
+ "filePath": "/home/runner/work/design-system/design-system/src/input-group/index.ts",
6522
+ "description": "The bordered frame of an input group: one box around an `Input` and its\naddons, drawn as a single control. It carries the border, the shadow, the\nfocus ring and the horizontal padding; the `Input` inside renders bare and\ngives the frame its state — focus, `disabled`, `aria-invalid` and `size`\nall propagate through `:has()`, so nothing is passed to the frame by hand.\n\nPut the `Input` and any `InputGroup.Addon` or `InputGroup.Text` inside, in\nreading order. Inside a `FieldText` the `Input` still takes its label and\nmessages from the field, at any depth; the frame needs nothing.\n\nUse it for an input with a leading icon, a currency or unit affix, or a\ntrailing action. A plain input with nothing around it is just `Input`, and a\nbutton beside an input that submits on its own is a form layout, not a group.\nNot a `role=\"group\"`, like `Field`.",
6523
+ "jsDocTags": {
6524
+ "summary": [
6525
+ "Bordered frame drawing an Input and its addons as one control"
6526
+ ],
6527
+ "dataAttribute": [
6528
+ "{string} data-slot - Always set to \"input-group\""
6529
+ ],
6530
+ "example": [
6531
+ "<InputGroup.Root>\n <InputGroup.Text>$</InputGroup.Text>\n <Input inputMode=\"decimal\" placeholder=\"0.00\" />\n <InputGroup.Text>USD</InputGroup.Text>\n</InputGroup.Root>"
6532
+ ]
6533
+ },
6534
+ "props": {}
6535
+ },
6536
+ "stories": [
6537
+ {
6538
+ "id": "components-inputgroup--default",
6539
+ "name": "Default",
6540
+ "snippet": "const Default = () => (\n <InputGroup.Root>\n <InputGroup.Addon>\n <IconSearch />\n </InputGroup.Addon>\n <Input type=\"search\" placeholder=\"Search agencies\" aria-label=\"Search agencies\" />\n </InputGroup.Root>\n);",
6541
+ "description": "A search field: the icon leads and the input takes the rest of the frame.\nPressing the icon focuses the input, and the frame — not the input — draws the\nfocus ring, so the two never show a double border.",
6542
+ "summary": "Leading icon addon with the frame drawing the focus ring"
6543
+ },
6544
+ {
6545
+ "id": "components-inputgroup--text-affixes",
6546
+ "name": "Text Affixes",
6547
+ "snippet": "const TextAffixes = () => (\n <InputGroup.Root>\n <InputGroup.Text>$</InputGroup.Text>\n <Input inputMode=\"decimal\" placeholder=\"0.00\" aria-label=\"Premium in US dollars\" />\n <InputGroup.Text>USD</InputGroup.Text>\n </InputGroup.Root>\n);",
6548
+ "description": "Text affixes on both sides. They are decoration only — a screen reader does\nnot read them as part of the value — so the unit is also in the label.",
6549
+ "summary": "Currency prefix and unit suffix around an amount"
6550
+ },
6551
+ {
6552
+ "id": "components-inputgroup--with-divider",
6553
+ "name": "With Divider",
6554
+ "snippet": "const WithDivider = () => (\n <InputGroup.Root>\n <InputGroup.Text>https://</InputGroup.Text>\n <Divider orientation=\"vertical\" />\n <Input\n type=\"text\"\n inputMode=\"url\"\n autoCapitalize=\"none\"\n placeholder=\"www.agentero\"\n aria-label=\"Agency website\"\n />\n <Divider orientation=\"vertical\" />\n <InputGroup.Text>.com</InputGroup.Text>\n </InputGroup.Root>\n);",
6555
+ "description": "A `Divider` separates an affix from the value when it reads as a distinct\nsegment, like a protocol before a domain. It spans the full height of the\nframe, meeting its border at both ends.",
6556
+ "summary": "Protocol prefix and domain suffix split off by vertical dividers"
6557
+ },
6558
+ {
6559
+ "id": "components-inputgroup--with-tags",
6560
+ "name": "With Tags",
6561
+ "snippet": "const WithTags = () => (\n <div className=\"flex flex-col gap-4\">\n <InputGroup.Root>\n <InputGroup.Addon>\n <Tag color=\"informative\">FEIN</Tag>\n </InputGroup.Addon>\n <Input placeholder=\"XX-XXXXXXX\" aria-label=\"FEIN\" />\n </InputGroup.Root>\n <InputGroup.Root>\n <InputGroup.Addon>\n <IconSearch />\n </InputGroup.Addon>\n <Input placeholder=\"Describe your filters\" aria-label=\"Describe your filters\" />\n <InputGroup.Addon>\n <Tag color=\"informative\">Beta</Tag>\n </InputGroup.Addon>\n </InputGroup.Root>\n </div>\n);",
6562
+ "description": "A `Tag` in an addon labels what the value is. It tucks into the frame's\npadding on the side it touches and takes a tighter radius, so it sits inside\nthe corner instead of competing with it.",
6563
+ "summary": "Leading and trailing tags tucked into the frame's padding"
6564
+ },
6565
+ {
6566
+ "id": "components-inputgroup--with-button",
6567
+ "name": "With Button",
6568
+ "snippet": "const WithButton = () => (\n <InputGroup.Root>\n <Input value=\"https://agentero.com/r/abc123\" readOnly aria-label=\"Referral link\" />\n <Divider orientation=\"vertical\" />\n <InputGroup.Addon variant=\"action\">\n <Button type=\"button\" variant=\"ghost\" size=\"md\" onClick={handleCopy}>\n Copy link\n </Button>\n </InputGroup.Addon>\n </InputGroup.Root>\n);"
6569
+ },
6570
+ {
6571
+ "id": "components-inputgroup--disabled",
6572
+ "name": "Disabled",
6573
+ "snippet": "const Disabled = () => (\n <InputGroup.Root>\n <InputGroup.Addon>\n <IconSearch />\n </InputGroup.Addon>\n <Input placeholder=\"Search agencies\" aria-label=\"Search agencies\" disabled />\n <InputGroup.Text>USD</InputGroup.Text>\n </InputGroup.Root>\n);",
6574
+ "description": "`disabled` on the input greys out the whole frame — background, border, icon\nand affix — and the frame stops offering the text cursor.",
6575
+ "summary": "Disabled input greying out the frame and its addons"
6576
+ },
6577
+ {
6578
+ "id": "components-inputgroup--invalid",
6579
+ "name": "Invalid",
6580
+ "snippet": "const Invalid = () => (\n <InputGroup.Root>\n <InputGroup.Text>$</InputGroup.Text>\n <Input defaultValue=\"-40\" aria-invalid aria-label=\"Premium in US dollars\" />\n </InputGroup.Root>\n);",
6581
+ "description": "`aria-invalid` on the input moves the destructive border to the frame, and\nthe ring follows it on focus.",
6582
+ "summary": "Invalid input driving the frame's destructive border"
6583
+ },
6584
+ {
6585
+ "id": "components-inputgroup--all-sizes",
6586
+ "name": "All Sizes",
6587
+ "snippet": "const AllSizes = () => (\n <div className=\"flex flex-col gap-4\">\n {SIZES.map(size => (\n <InputGroup.Root key={size}>\n <InputGroup.Addon>\n <IconSearch />\n </InputGroup.Addon>\n <Input size={size} placeholder={`Size ${size}`} aria-label={`Size ${size}`} />\n </InputGroup.Root>\n ))}\n </div>\n);",
6588
+ "description": "The frame follows the input's `size`: the same height as a standalone\n`Input` of that size, and the larger corner radius with `lg`.",
6589
+ "summary": "The three input sizes inside the frame, heights matching Input"
6590
+ },
6591
+ {
6592
+ "id": "components-inputgroup--inside-field-text",
6593
+ "name": "Inside Field Text",
6594
+ "snippet": "const InsideFieldText = () => (\n <FieldText required>\n <Label>Agency website</Label>\n <InputGroup.Root>\n <InputGroup.Text>https://</InputGroup.Text>\n <Input type=\"text\" inputMode=\"url\" autoCapitalize=\"none\" placeholder=\"www.agentero.com\" />\n </InputGroup.Root>\n <Field.Description>Shown on your public profile.</Field.Description>\n </FieldText>\n);",
6595
+ "description": "Inside a `FieldText` nothing changes for the group: the `Input` reads the\nfield's context at any depth, so the label names it and the description\ndescribes it with no ids written by hand. The frame is not in the way.",
6596
+ "summary": "Label and description reaching the input through the frame"
6597
+ },
6598
+ {
6599
+ "id": "components-inputgroup--search-field",
6600
+ "name": "Search Field",
6601
+ "snippet": "const SearchField = () => (\n <form onSubmit={handleSearchSubmit}>\n <FieldStates\n label=\"Agency\"\n tooltip=\"Search by agency name or NPN.\"\n required\n error=\"Pick an agency from the list.\">\n <SearchInput />\n </FieldStates>\n </form>\n);"
6602
+ },
6603
+ {
6604
+ "id": "components-inputgroup--email-field",
6605
+ "name": "Email Field",
6606
+ "snippet": "const EmailField = () => (\n <FieldStates label=\"Email\" required error=\"Enter a valid email address.\">\n <InputGroup.Root>\n <InputGroup.Addon>\n <IconMail />\n </InputGroup.Addon>\n <Input type=\"email\" placeholder=\"me@email.com\" />\n </InputGroup.Root>\n </FieldStates>\n);",
6607
+ "description": "The email field from the Portal UI library: a leading icon is all it adds\nto the input.",
6608
+ "summary": "Email field with a leading icon, valid and invalid"
6609
+ },
6610
+ {
6611
+ "id": "components-inputgroup--price-field",
6612
+ "name": "Price Field",
6613
+ "snippet": "const PriceField = () => (\n <FieldStates label=\"Price\" required error=\"Enter a price above zero.\">\n <PriceInput />\n </FieldStates>\n);",
6614
+ "description": "The price field from the Portal UI library: a currency symbol before the\nvalue and a currency picker after it. The picker is a `DropdownMenu` whose\ntrigger fills an `action` addon, so it keeps the frame's height and an inset\nfocus ring.",
6615
+ "summary": "Price field with a symbol and a currency picker, valid and invalid"
6616
+ },
6617
+ {
6618
+ "id": "components-inputgroup--phone-field",
6619
+ "name": "Phone Field",
6620
+ "snippet": "const PhoneField = () => (\n <FieldStates label=\"Phone number\" error=\"Enter a 10-digit phone number.\">\n <PhoneInput />\n </FieldStates>\n);",
6621
+ "description": "The optional phone field from the Portal UI library: a country code picker\nbefore the number. The picker leads, so the `action` addon rounds its\nbutton's start corners with the frame.",
6622
+ "summary": "Phone field with a leading country code picker, valid and invalid"
6623
+ }
6624
+ ],
6625
+ "import": "import {\n Button,\n Divider,\n DropdownMenu,\n Field,\n FieldText,\n IconCancel,\n IconKeyboardArrowDown,\n IconSearch,\n Input,\n InputGroup,\n Label,\n Tag,\n} from \"@agentero/design-system\";"
6626
+ },
6463
6627
  "components-input": {
6464
6628
  "id": "components-input",
6465
6629
  "name": "Input",
@@ -9426,6 +9590,6 @@
9426
9590
  },
9427
9591
  "meta": {
9428
9592
  "docgen": "react-component-meta",
9429
- "durationMs": 4635
9593
+ "durationMs": 3999
9430
9594
  }
9431
9595
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentero/design-system",
3
- "version": "0.31.8",
3
+ "version": "0.32.1",
4
4
  "description": "A React component library built with Tailwind CSS v4 and Radix UI primitives",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -93,6 +93,10 @@
93
93
  "types": "./src/input/index.d.ts",
94
94
  "import": "./src/input/index.js"
95
95
  },
96
+ "./input-group": {
97
+ "types": "./src/input-group/index.d.ts",
98
+ "import": "./src/input-group/index.js"
99
+ },
96
100
  "./label": {
97
101
  "types": "./src/label/index.d.ts",
98
102
  "import": "./src/label/index.js"
@@ -2,8 +2,13 @@ import { ComponentPropsWithRef } from 'react';
2
2
  import { Combobox as ComboboxPrimitive } from '@base-ui/react/combobox';
3
3
  import { InputSize } from '../input';
4
4
  /**
5
- * Style recipe for Combobox. The surface carries its own chrome — border,
6
- * background and shadow — because the list it holds is plain content.
5
+ * Style recipe for Combobox. Slots: `action` and `trigger` (the clear and
6
+ * chevron buttons inside the input's frame, which is `InputGroup`'s),
7
+ * `triggerReplaced` (hides the chevron once the clear button has a value to
8
+ * clear), `content`,
9
+ * `list`, `item`, `indicator` and `message`. The surface carries its
10
+ * own chrome — border, background and shadow — because the list it holds is
11
+ * plain content.
7
12
  *
8
13
  * @summary tailwind-variants recipe backing the Combobox surface and list styles
9
14
  */
@@ -12,11 +17,12 @@ export declare const comboboxRecipe: import('tailwind-variants').TVReturnType<{
12
17
  [key: string]: import('tailwind-merge').ClassNameValue | {
13
18
  content?: import('tailwind-merge').ClassNameValue;
14
19
  list?: import('tailwind-merge').ClassNameValue;
15
- clear?: import('tailwind-merge').ClassNameValue;
16
20
  item?: import('tailwind-merge').ClassNameValue;
17
21
  indicator?: import('tailwind-merge').ClassNameValue;
18
22
  message?: import('tailwind-merge').ClassNameValue;
19
- field?: import('tailwind-merge').ClassNameValue;
23
+ action?: import('tailwind-merge').ClassNameValue;
24
+ trigger?: import('tailwind-merge').ClassNameValue;
25
+ triggerReplaced?: import('tailwind-merge').ClassNameValue;
20
26
  };
21
27
  };
22
28
  } | {
@@ -24,16 +30,18 @@ export declare const comboboxRecipe: import('tailwind-variants').TVReturnType<{
24
30
  [x: string]: import('tailwind-merge').ClassNameValue | {
25
31
  content?: import('tailwind-merge').ClassNameValue;
26
32
  list?: import('tailwind-merge').ClassNameValue;
27
- clear?: import('tailwind-merge').ClassNameValue;
28
33
  item?: import('tailwind-merge').ClassNameValue;
29
34
  indicator?: import('tailwind-merge').ClassNameValue;
30
35
  message?: import('tailwind-merge').ClassNameValue;
31
- field?: import('tailwind-merge').ClassNameValue;
36
+ action?: import('tailwind-merge').ClassNameValue;
37
+ trigger?: import('tailwind-merge').ClassNameValue;
38
+ triggerReplaced?: import('tailwind-merge').ClassNameValue;
32
39
  };
33
40
  };
34
41
  } | {}, {
35
- field: string;
36
- clear: string[];
42
+ action: string[];
43
+ trigger: string;
44
+ triggerReplaced: string;
37
45
  content: string[];
38
46
  list: string[];
39
47
  item: string[];
@@ -44,39 +52,60 @@ export declare const comboboxRecipe: import('tailwind-variants').TVReturnType<{
44
52
  [key: string]: import('tailwind-merge').ClassNameValue | {
45
53
  content?: import('tailwind-merge').ClassNameValue;
46
54
  list?: import('tailwind-merge').ClassNameValue;
47
- clear?: import('tailwind-merge').ClassNameValue;
48
55
  item?: import('tailwind-merge').ClassNameValue;
49
56
  indicator?: import('tailwind-merge').ClassNameValue;
50
57
  message?: import('tailwind-merge').ClassNameValue;
51
- field?: import('tailwind-merge').ClassNameValue;
58
+ action?: import('tailwind-merge').ClassNameValue;
59
+ trigger?: import('tailwind-merge').ClassNameValue;
60
+ triggerReplaced?: import('tailwind-merge').ClassNameValue;
52
61
  };
53
62
  };
54
63
  } | {}, {
55
- field: string;
56
- clear: string[];
64
+ action: string[];
65
+ trigger: string;
66
+ triggerReplaced: string;
57
67
  content: string[];
58
68
  list: string[];
59
69
  item: string[];
60
70
  indicator: string[];
61
71
  message: string[];
62
72
  }, import('tailwind-variants').TVReturnType<unknown, {
63
- field: string;
64
- clear: string[];
73
+ action: string[];
74
+ trigger: string;
75
+ triggerReplaced: string;
65
76
  content: string[];
66
77
  list: string[];
67
78
  item: string[];
68
79
  indicator: string[];
69
80
  message: string[];
70
81
  }, undefined, unknown, unknown, undefined>>;
82
+ export type ComboboxRootProps<Value, Multiple extends boolean | undefined, Item> = ComboboxPrimitive.Root.Props<Value, Multiple, Item> & {
83
+ /**
84
+ * Characters the query needs, spaces aside, before the list opens. Below it
85
+ * focusing, clicking or pressing the chevron leaves the list shut, and
86
+ * deleting back under it closes the list. Defaults to `0`: the list opens on
87
+ * focus.
88
+ */
89
+ minInputLength?: number;
90
+ };
71
91
  /**
72
92
  * Root of a combobox: a text input that filters a list of options and writes the
73
- * chosen one back into itself. With `Input` as the only trigger, the list opens on
74
- * focus or on the first keystroke.
93
+ * chosen one back into itself. The list opens on focus, on the first keystroke
94
+ * or from the chevron of `Input`'s `select` variant.
95
+ *
96
+ * For a search against an API, set `minInputLength` so the list stays shut until
97
+ * the query is worth sending: a field that opens an empty list on focus reads as
98
+ * broken. For a pick from a short fixed list with no typing at all, prefer a
99
+ * select.
75
100
  *
76
101
  * Pass the options as `items`. For a list that comes from the server, set
77
102
  * `filter={null}`, hand the already-filtered rows to `filteredItems`, and request
78
103
  * them from `onInputValueChange`; `Status` then announces the wait.
79
104
  *
105
+ * Every Base UI root prop is accepted and forwarded — `items`, `filter`,
106
+ * `value`, `onValueChange` and so on. `open` stays the consumer's to control;
107
+ * `minInputLength` only refuses to open below the minimum.
108
+ *
80
109
  * With `multiple`, the value is an array and picking a row toggles it. The list
81
110
  * stays open and keeps the typed query between picks, so several rows can be
82
111
  * ticked from one search; closing the list (Escape, a click outside) still resets
@@ -102,20 +131,37 @@ export declare const comboboxRecipe: import('tailwind-variants').TVReturnType<{
102
131
  * ```
103
132
  */
104
133
  export declare const Root: {
105
- <Value, Multiple extends boolean | undefined = false, Item = Value>({ onOpenChange, ...props }: ComboboxPrimitive.Root.Props<Value, Multiple, Item>): import("react/jsx-runtime").JSX.Element;
134
+ <Value, Multiple extends boolean | undefined = false, Item = Value>({ minInputLength, open: openProp, defaultOpen, onOpenChange, onOpenChangeComplete, onInputValueChange, ...props }: ComboboxRootProps<Value, Multiple, Item>): import("react/jsx-runtime").JSX.Element;
106
135
  displayName: string;
107
136
  };
108
137
  type InputProps = Omit<ComponentPropsWithRef<typeof ComboboxPrimitive.Input>, 'size'> & {
109
138
  /** Control height, matching `Input`. Defaults to `'md'`. */
110
139
  size?: InputSize;
140
+ /**
141
+ * What surrounds the input. Defaults to `'search'`.
142
+ * - `search` — magnifier in front, clear button after: a lookup the user types
143
+ * - `plain` — the input alone
144
+ * - `select` — a chevron that toggles the list: a short list to browse
145
+ */
146
+ variant?: 'search' | 'plain' | 'select';
147
+ /**
148
+ * Whether a clear button follows the input once an option is picked.
149
+ * Defaults to `true` for `search` and `false` for `plain` and `select`; pass
150
+ * it to override the variant. On a `select` it replaces the chevron while
151
+ * there is a value. Ignored under a `multiple` root, which never
152
+ * shows one.
153
+ */
154
+ showClear?: boolean;
111
155
  /**
112
156
  * Accessible name for the clear button. Defaults to `'Clear'`; name it after
113
157
  * the field when a page carries several comboboxes.
114
158
  */
115
159
  clearLabel?: string;
160
+ /** Accessible name for the chevron of the `select` variant. Defaults to `'Show options'`. */
161
+ triggerLabel?: string;
116
162
  };
117
163
  /**
118
- * The search field, which doubles as the trigger: focusing or typing opens the
164
+ * The text field, which doubles as the trigger: focusing or typing opens the
119
165
  * list. It wears the same styling as `Input` and takes the same wiring from
120
166
  * `InputContext`, so inside a `FieldText` the label, the hint and the error point
121
167
  * at it with nothing passed by hand. Its own props win over the context.
@@ -127,21 +173,38 @@ type InputProps = Omit<ComponentPropsWithRef<typeof ComboboxPrimitive.Input>, 's
127
173
  * reference keeps the name. Outside a field, give it `aria-label` or an
128
174
  * `aria-labelledby` of your own.
129
175
  *
130
- * It carries a clear button, which Base UI mounts only once there is something
131
- * to clear, so an untouched field shows nothing. Name it with `clearLabel` when
132
- * a page has more than one combobox. Under a `multiple` root there is no clear
133
- * button: Base UI's would drop the whole selection, while a cross inside a search
134
- * field reads as "clear what I typed" — and the selection is rendered outside
135
- * the field, with its own way to remove each entry.
176
+ * It is drawn as an `InputGroup` frame, and the list anchors to that whole
177
+ * frame, so it is as wide as the field and starts at its left edge whatever
178
+ * sits beside the input. `variant` picks what does:
179
+ *
180
+ * - `search` (default): a magnifier in front and a clear button. For any field
181
+ * where the user types to look something up.
182
+ * - `plain`: the input alone, with no icon and no button. When the field needs
183
+ * no affordance at all.
184
+ * - `select`: a chevron that opens and closes the list. For a short known list
185
+ * the user may want to browse before typing.
186
+ *
187
+ * Only `search` carries a clear button by default; `showClear` adds it to the
188
+ * other two or drops it from `search`. On a `select`, the clear button takes the
189
+ * chevron's place once an option is picked; the list still opens from the input. Base UI mounts it only once there is
190
+ * something to clear — a picked option — so an untouched field shows nothing. Name it with
191
+ * `clearLabel`, and the chevron with `triggerLabel`, when a page has more than
192
+ * one combobox.
193
+ *
194
+ * Under a `multiple` root there is neither: the list stays open between picks,
195
+ * so a chevron has nothing to toggle, and Base UI's clear button would drop the
196
+ * whole selection while a cross inside a search field reads as "clear what I
197
+ * typed". The selection is rendered outside the field, with its own way to
198
+ * remove each entry.
136
199
  *
137
- * Pass `render` to swap the element for a richer control — a field with a leading
138
- * search icon, for instance — and the combobox wiring rides along.
200
+ * Every native input attribute is accepted and forwarded to the `<input>` —
201
+ * `placeholder`, `disabled`, `autoComplete` — and so is `className`.
139
202
  *
140
- * @summary Search field that opens the list on focus and filters it as you type
203
+ * @summary Search, plain or select field that opens and filters the list
141
204
  * @dataAttribute {string} data-slot - Always set to "combobox-input"
142
205
  */
143
206
  export declare const Input: {
144
- ({ clearLabel, ...props }: InputProps): import("react/jsx-runtime").JSX.Element;
207
+ ({ variant, showClear, clearLabel, triggerLabel, ...props }: InputProps): import("react/jsx-runtime").JSX.Element;
145
208
  displayName: string;
146
209
  };
147
210
  type ContentProps = ComponentPropsWithRef<typeof ComboboxPrimitive.Popup> & Pick<ComponentPropsWithRef<typeof ComboboxPrimitive.Positioner>, 'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor' | 'collisionPadding'>;