@agentero/design-system 0.34.1 → 0.35.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.
@@ -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. 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.",
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`, request\nthem from `onInputValueChange`, and pass the request's state as `loading`:\nthe combobox announces the wait and marks the list busy on its own.\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"
@@ -2790,6 +2790,46 @@
2790
2790
  "fileName": "src/combobox/combobox.tsx"
2791
2791
  }
2792
2792
  ]
2793
+ },
2794
+ "loading": {
2795
+ "name": "loading",
2796
+ "required": false,
2797
+ "type": {
2798
+ "name": "boolean"
2799
+ },
2800
+ "description": "Whether the options are being fetched. Once the list has been open and\nloading for half a second, `loadingLabel` is announced once: sooner, a\nscreen reader would cut it off to echo the key just typed, and a faster\nanswer has no wait to announce. `Content` is marked `aria-busy`, and\n`Empty` shows nothing, so \"no results\" is not read out before the results\narrive. The announcement comes from a visually hidden live region the root\nrenders next to the field, so it is in place before the list opens. On a search\nagainst an API, pass it from the first render, as `false` while idle: a root\nthat leaves it out gets no live region. A list filtered in memory never\nwaits and leaves it out. While loading with no rows yet, the surface shows\nwhatever placeholder `Content` holds — `Empty` stays quiet.",
2801
+ "defaultValue": null,
2802
+ "parent": {
2803
+ "name": "ComboboxRootProps",
2804
+ "fileName": "src/combobox/combobox.tsx"
2805
+ },
2806
+ "declarations": [
2807
+ {
2808
+ "name": "TypeLiteral",
2809
+ "fileName": "src/combobox/combobox.tsx"
2810
+ }
2811
+ ]
2812
+ },
2813
+ "loadingLabel": {
2814
+ "name": "loadingLabel",
2815
+ "required": false,
2816
+ "type": {
2817
+ "name": "string"
2818
+ },
2819
+ "description": "What a screen reader hears when `loading` turns `true`. Announced only; any\nloading visuals — placeholder rows, a spinner — are yours to render inside\n`Content`, hidden from assistive technology. Defaults to `'Loading…'`.",
2820
+ "defaultValue": {
2821
+ "value": "'Loading…'"
2822
+ },
2823
+ "parent": {
2824
+ "name": "ComboboxRootProps",
2825
+ "fileName": "src/combobox/combobox.tsx"
2826
+ },
2827
+ "declarations": [
2828
+ {
2829
+ "name": "TypeLiteral",
2830
+ "fileName": "src/combobox/combobox.tsx"
2831
+ }
2832
+ ]
2793
2833
  }
2794
2834
  }
2795
2835
  },
@@ -2837,15 +2877,15 @@
2837
2877
  {
2838
2878
  "id": "components-combobox--async-search",
2839
2879
  "name": "Async Search",
2840
- "snippet": "const AsyncSearch = () => {\n const [query, setQuery] = useState('');\n // The rows read the deferred query: while the new one suspends, React keeps rendering the\n // old rows instead of falling back to the placeholders.\n const deferredQuery = useDeferredValue(query);\n const isStale = query !== deferredQuery;\n\n return (\n // No `items` and `filter={null}`: the server did the filtering, the child renders what came\n // back, and `isItemEqualToValue` keeps a re-fetched copy counting as the selected one.\n <Combobox.Root\n filter={null}\n itemToStringLabel={(agency: Agency) => agency.name}\n isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}\n onInputValueChange={(value, { reason }) => {\n // Picking a row writes its label into the input; that is not a new search.\n if (reason !== 'item-press') setQuery(value);\n }}>\n <Combobox.Input placeholder=\"Search agencies\" aria-label=\"Search agencies\" />\n <Combobox.Content aria-busy={isStale || undefined}>\n <Suspense fallback={<SkeletonRows />}>\n <AgencyItems query={deferredQuery} />\n </Suspense>\n </Combobox.Content>\n </Combobox.Root>\n );\n};",
2841
- "description": "Options fetched as you type, the way the apps load data: the rows are a component\nthat suspends on the request, so a `Suspense` boundary inside `Content` shows\nplaceholder rows until the first results land. The rows read a deferred copy of\nthe query, so a new search keeps the previous rows on screen while it loads and\n`Content` is marked busy until they are replaced; the placeholders never come\nback. Nothing tracks `loading` by hand, and reading the data by query is what\nmakes out-of-order responses harmless. With react-query, drop a\n`useSuspenseQuery` where `use()` is, or use `placeholderData: keepPreviousData`\nwith a plain `useQuery` for the same behaviour."
2880
+ "snippet": "const AsyncSearch = () => {\n const [query, setQuery] = useState('');\n // The rows read the deferred query: while the new one suspends, React keeps rendering the\n // old rows instead of falling back to the placeholders.\n const deferredQuery = useDeferredValue(query);\n const isStale = query !== deferredQuery;\n\n return (\n // No `items` and `filter={null}`: the server did the filtering, the child renders what came\n // back, and `isItemEqualToValue` keeps a re-fetched copy counting as the selected one.\n <Combobox.Root\n filter={null}\n itemToStringLabel={(agency: Agency) => agency.name}\n isItemEqualToValue={(a: Agency, b: Agency) => a.id === b.id}\n loading={isStale}\n loadingLabel=\"Searching agencies…\"\n onInputValueChange={(value, { reason }) => {\n // Picking a row writes its label into the input; that is not a new search.\n if (reason !== 'item-press') setQuery(value);\n }}>\n <Combobox.Input placeholder=\"Search agencies\" aria-label=\"Search agencies\" />\n <Combobox.Content>\n <Suspense fallback={<SkeletonRows />}>\n <AgencyItems query={deferredQuery} />\n </Suspense>\n </Combobox.Content>\n </Combobox.Root>\n );\n};",
2881
+ "description": "Options fetched as you type, the way the apps load data: the rows are a component\nthat suspends on the request, so a `Suspense` boundary inside `Content` shows\nplaceholder rows until the first results land. The rows read a deferred copy of\nthe query, so a new search keeps the previous rows on screen while it loads, and\npassing that staleness as `loading` announces the wait and marks `Content` busy\nuntil they are replaced; the placeholders never come back. Nothing tracks a\nrequest by hand, and reading the data by query is what\nmakes out-of-order responses harmless. With react-query, drop a\n`useSuspenseQuery` where `use()` is, or use `placeholderData: keepPreviousData`\nwith a plain `useQuery` for the same behaviour."
2842
2882
  },
2843
2883
  {
2844
- "id": "components-combobox--async-search-with-status",
2845
- "name": "Async Search With Status",
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 stay through the wait, marked busy, rather than blinking out and back.\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 && results.length === 0 && <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. Placeholder rows show\nonly while there is nothing to show yet; after that, the previous rows stay\nuntil the new ones replace them. A request that is no longer the latest is\ndropped.\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"
2884
+ "id": "components-combobox--async-search-with-loading",
2885
+ "name": "Async Search With Loading",
2886
+ "snippet": "const AsyncSearchWithLoading = () => {\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 stay through the wait, marked busy, rather than blinking out and back.\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 loading={isLoading}\n loadingLabel=\"Searching agencies…\"\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>\n {isLoading && results.length === 0 && <SkeletonRows />}\n <Combobox.Empty>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};",
2887
+ "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`,\nand the flag goes to `Root` as `loading`. That is all the wiring the wait needs:\n`Content` is marked busy, `loadingLabel` is announced once when the request\nstarts, and `Empty` holds back \"no results\" until the answer is in. Placeholder\nrows show only while there is nothing to show yet, hidden from assistive\ntechnology since the label already speaks for them; after that, the previous\nrows stay until the new ones replace them. A request that is no longer the\nlatest 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.",
2888
+ "summary": "API search that waits for two characters and announces its loading state"
2849
2889
  },
2850
2890
  {
2851
2891
  "id": "components-combobox--inside-field-text",
@@ -6901,6 +6941,154 @@
6901
6941
  ],
6902
6942
  "import": "import { Field, FieldText, InputSearch, Label } from \"@agentero/design-system\";"
6903
6943
  },
6944
+ "components-inputtel": {
6945
+ "id": "components-inputtel",
6946
+ "name": "InputTel",
6947
+ "path": "./src/input-tel/input-tel.stories.tsx",
6948
+ "jsDocTags": {
6949
+ "summary": [
6950
+ "US phone input with a +1 prefix that masks as you type"
6951
+ ],
6952
+ "dataAttribute": [
6953
+ "{string} data-slot - Always set to \"input-tel\" on the frame"
6954
+ ],
6955
+ "example": [
6956
+ "<FieldText>\n <Label>Phone number</Label>\n <InputTel name=\"phone\" autoComplete=\"tel-national\" placeholder=\"(555) 000-0000\" />\n</FieldText>"
6957
+ ]
6958
+ },
6959
+ "description": "InputTel is a US phone number field that masks as you type: the digits are\nlaid out as `(555) 123-4567`, anything else is dropped and the caret stays\nnext to the digit just edited. It is an `Input` of `type=\"tel\"` inside an\n`InputGroup` frame with a `+1` prefix; `onChange` receives the native event\nwith the masked text already in `event.target.value`.",
6960
+ "summary": "US phone input with a +1 prefix that masks as you type",
6961
+ "reactComponentMeta": {
6962
+ "displayName": "InputTel",
6963
+ "exportName": "InputTel",
6964
+ "filePath": "/home/runner/work/design-system/design-system/src/input-tel/input-tel.tsx",
6965
+ "description": "InputTel is a US phone number field that masks as you type: the digits the\nuser enters are laid out as `(555) 123-4567` while typing, pasting or\ndeleting, anything that is not a digit is dropped, and the caret stays next\nto the digit just edited instead of jumping to the end. A leading `+1`\ncountry code, as pasted or filled in by the browser's autofill, is dropped\ntoo; past that, the eleventh digit and beyond are ignored. `onChange`\nreceives the native change event with the masked text already in\n`event.target.value`, so a controlled field, an uncontrolled one and one\nregistered with react-hook-form all store `(555) 123-4567`, never the bare\ndigits.\n\nIt is an [Input](?path=/docs/components-input--docs) of `type=\"tel\"` inside\nan [InputGroup](?path=/docs/components-inputgroup--docs) frame that always\nshows a `+1` prefix in front of the number. The prefix is part of the frame,\nnot of the value. `className` styles the frame. Every other prop goes to the\n`<input>`: the `ref`, `id`, `name`, `value`, `onChange`, `aria-*` and every\nother standard attribute, such as `placeholder`, `autoComplete` or\n`required`. Only `type` is not accepted. Inside a `FieldText` the input\ntakes its label, messages and states from the field as a plain `Input` does.\nThe invalid state comes from `aria-invalid`, and the frame paints the\ndestructive border.\n\nDo not use it for international numbers or extensions; the mask only knows\nthe ten-digit US format. For free-form text that happens to hold a number,\nuse `Input`.",
6966
+ "jsDocTags": {
6967
+ "summary": [
6968
+ "US phone input with a +1 prefix that masks as you type"
6969
+ ],
6970
+ "dataAttribute": [
6971
+ "{string} data-slot - Always set to \"input-tel\" on the frame"
6972
+ ],
6973
+ "example": [
6974
+ "<FieldText>\n <Label>Phone number</Label>\n <InputTel name=\"phone\" autoComplete=\"tel-national\" placeholder=\"(555) 000-0000\" />\n</FieldText>"
6975
+ ]
6976
+ },
6977
+ "props": {
6978
+ "size": {
6979
+ "name": "size",
6980
+ "required": false,
6981
+ "type": {
6982
+ "name": "enum",
6983
+ "raw": "\"sm\" | \"md\" | \"lg\"",
6984
+ "value": [
6985
+ {
6986
+ "value": "\"sm\""
6987
+ },
6988
+ {
6989
+ "value": "\"md\""
6990
+ },
6991
+ {
6992
+ "value": "\"lg\""
6993
+ }
6994
+ ]
6995
+ },
6996
+ "description": "Control height. Defaults to `'md'`.\n- `sm` (32px) — dense layouts where vertical space is tight.\n- `md` (40px) — standard form density.\n- `lg` (48px) — low-density forms and larger touch targets. Also raises\n the text to `base` and the corner radius to `lg`.",
6997
+ "defaultValue": null,
6998
+ "parent": {
6999
+ "name": "InputProps",
7000
+ "fileName": "src/input/input.tsx"
7001
+ },
7002
+ "declarations": [
7003
+ {
7004
+ "name": "TypeLiteral",
7005
+ "fileName": "src/input/input.tsx"
7006
+ }
7007
+ ]
7008
+ }
7009
+ }
7010
+ },
7011
+ "stories": [
7012
+ {
7013
+ "id": "components-inputtel--default",
7014
+ "name": "Default",
7015
+ "snippet": "const Default = () => <InputTel\n aria-label=\"Phone number\"\n onChange={fn((event: ChangeEvent<HTMLInputElement>) => event.target.value)} />;",
7016
+ "description": "An uncontrolled phone field with its `+1` prefix. Typing the ten digits lays\nthem out as `(555) 123-4567`, and every `onChange` call carries the masked\ntext in `event.target.value`; the prefix never enters the value.",
7017
+ "summary": "Phone field with a +1 prefix masking the digits as they are typed"
7018
+ },
7019
+ {
7020
+ "id": "components-inputtel--controlled",
7021
+ "name": "Controlled",
7022
+ "snippet": "const Controlled = () => {\n const [phone, setPhone] = useState('');\n\n return (\n <InputTel\n aria-label=\"Phone number\"\n value={phone}\n onChange={event => {\n setPhone(event.target.value);\n args.onChange?.(event);\n }} />\n );\n};",
7023
+ "description": "A controlled phone field. The state that owns the value stores what\n`event.target.value` reports, which is already masked, and the input shows\nit back unchanged. A partial number carries the separator that comes before\nthe next digit, so six digits read `(555) 123-`.",
7024
+ "summary": "Controlled phone field storing the masked value"
7025
+ },
7026
+ {
7027
+ "id": "components-inputtel--caret-in-the-middle",
7028
+ "name": "Caret In The Middle",
7029
+ "snippet": "const CaretInTheMiddle = () => <InputTel\n aria-label=\"Phone number\"\n onChange={fn((event: ChangeEvent<HTMLInputElement>) => event.target.value)} />;",
7030
+ "description": "Editing in the middle of a full number: a digit typed after `(5` shifts the\nrest right, the eleventh digit falls off the end and the caret stays after\nthe digit just typed instead of jumping to the end.",
7031
+ "summary": "Caret staying put when a digit is inserted mid-number"
7032
+ },
7033
+ {
7034
+ "id": "components-inputtel--backspace-over-mask-characters",
7035
+ "name": "Backspace Over Mask Characters",
7036
+ "snippet": "const BackspaceOverMaskCharacters = () => <InputTel\n aria-label=\"Phone number\"\n onChange={fn((event: ChangeEvent<HTMLInputElement>) => event.target.value)} />;",
7037
+ "description": "Backspace over the mask's own characters: after `555` the field shows\n`(555) `, and deleting backwards trims the separators along with the digit\nunder the caret instead of leaving a dangling `) `.",
7038
+ "summary": "Backspace trimming the separators the mask added"
7039
+ },
7040
+ {
7041
+ "id": "components-inputtel--paste",
7042
+ "name": "Paste",
7043
+ "snippet": "const Paste = () => <InputTel\n aria-label=\"Phone number\"\n onChange={fn((event: ChangeEvent<HTMLInputElement>) => event.target.value)} />;",
7044
+ "description": "A pasted number in any format is reduced to its digits and masked, so\n`555-123-4567` from a clipboard ends up as `(555) 123-4567`.",
7045
+ "summary": "Pasted number reduced to its digits and masked"
7046
+ },
7047
+ {
7048
+ "id": "components-inputtel--paste-with-country-code",
7049
+ "name": "Paste With Country Code",
7050
+ "snippet": "const PasteWithCountryCode = () => <InputTel\n aria-label=\"Phone number\"\n onChange={fn((event: ChangeEvent<HTMLInputElement>) => event.target.value)} />;",
7051
+ "description": "A number that carries the `+1` country code, as copied from a signature or\nfilled in by the browser's autofill, loses the code and keeps its ten\nnational digits: `+1 (555) 123-4567` ends up as `(555) 123-4567`.",
7052
+ "summary": "Pasted number with a +1 country code"
7053
+ },
7054
+ {
7055
+ "id": "components-inputtel--inside-field-text",
7056
+ "name": "Inside Field Text",
7057
+ "snippet": "const InsideFieldText = () => (\n <FieldText>\n <Label>Phone number</Label>\n <InputTel />\n <Field.Description>We only call about your account.</Field.Description>\n </FieldText>\n);",
7058
+ "description": "Inside a `FieldText` the input takes its label, description and states from\nthe field, and the masked value still reaches a handler the field provides.",
7059
+ "summary": "Label and description reaching the input through the field"
7060
+ },
7061
+ {
7062
+ "id": "components-inputtel--with-react-hook-form",
7063
+ "name": "With React Hook Form",
7064
+ "snippet": "const WithReactHookForm = () => {\n const { register, handleSubmit } = useForm<ContactValues>({\n defaultValues: { phone: '' }\n });\n\n return (\n <form\n aria-label=\"Contact\"\n noValidate\n onSubmit={handleSubmit(values => onSubmit(values))}\n style={{ display: 'grid', gap: '1rem' }}>\n <InputTel aria-label=\"Phone number\" {...register('phone')} />\n <Button type=\"submit\">Save</Button>\n </form>\n );\n};",
7065
+ "description": "Registered with react-hook-form, the form receives the masked value: the\n`register` handler reads `event.target.value` like any other change.",
7066
+ "summary": "Masked value reaching react-hook-form through register"
7067
+ },
7068
+ {
7069
+ "id": "components-inputtel--disabled",
7070
+ "name": "Disabled",
7071
+ "snippet": "const Disabled = () => <InputTel\n aria-label=\"Phone number\"\n onChange={fn((event: ChangeEvent<HTMLInputElement>) => event.target.value)}\n disabled\n defaultValue=\"(555) 123-4567\" />;",
7072
+ "description": "A disabled phone field keeps its masked value and takes no input.",
7073
+ "summary": "Disabled phone field"
7074
+ },
7075
+ {
7076
+ "id": "components-inputtel--invalid",
7077
+ "name": "Invalid",
7078
+ "snippet": "const Invalid = () => (\n <FieldText invalid>\n <Label>Phone number</Label>\n <InputTel defaultValue=\"(555) 12\" />\n <Field.Error errors={[{ message: 'Enter a 10-digit phone number.' }]} />\n </FieldText>\n);",
7079
+ "description": "`invalid` on the surrounding `FieldText` sets `aria-invalid` on the input,\nwhich paints the frame's destructive border, and `Field.Error` is announced\nwith the input.",
7080
+ "summary": "Invalid field driving the frame's destructive border"
7081
+ },
7082
+ {
7083
+ "id": "components-inputtel--all-sizes",
7084
+ "name": "All Sizes",
7085
+ "snippet": "const AllSizes = () => <div style={{ display: 'grid', gap: '1rem' }}>\n <InputTel\n onChange={fn((event: ChangeEvent<HTMLInputElement>) => event.target.value)}\n size=\"sm\"\n aria-label=\"Small\"\n defaultValue=\"(555) 123-4567\" />\n <InputTel\n onChange={fn((event: ChangeEvent<HTMLInputElement>) => event.target.value)}\n size=\"md\"\n aria-label=\"Medium\"\n defaultValue=\"(555) 123-4567\" />\n <InputTel\n onChange={fn((event: ChangeEvent<HTMLInputElement>) => event.target.value)}\n size=\"lg\"\n aria-label=\"Large\"\n defaultValue=\"(555) 123-4567\" />\n</div>;",
7086
+ "description": "The three sizes match Input's: the frame is 32, 40 or 48px tall.",
7087
+ "summary": "The three sizes side by side"
7088
+ }
7089
+ ],
7090
+ "import": "import { Button, Field, FieldText, InputContext, InputTel, Label } from \"@agentero/design-system\";"
7091
+ },
6904
7092
  "components-input": {
6905
7093
  "id": "components-input",
6906
7094
  "name": "Input",
@@ -9867,6 +10055,6 @@
9867
10055
  },
9868
10056
  "meta": {
9869
10057
  "docgen": "react-component-meta",
9870
- "durationMs": 5001
10058
+ "durationMs": 4990
9871
10059
  }
9872
10060
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentero/design-system",
3
- "version": "0.34.1",
3
+ "version": "0.35.0",
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",
@@ -105,6 +105,10 @@
105
105
  "types": "./src/input-search/index.d.ts",
106
106
  "import": "./src/input-search/index.js"
107
107
  },
108
+ "./input-tel": {
109
+ "types": "./src/input-tel/index.d.ts",
110
+ "import": "./src/input-tel/index.js"
111
+ },
108
112
  "./label": {
109
113
  "types": "./src/label/index.d.ts",
110
114
  "import": "./src/label/index.js"
@@ -87,6 +87,26 @@ export type ComboboxRootProps<Value, Multiple extends boolean | undefined, Item>
87
87
  * focus.
88
88
  */
89
89
  minInputLength?: number;
90
+ /**
91
+ * Whether the options are being fetched. Once the list has been open and
92
+ * loading for half a second, `loadingLabel` is announced once: sooner, a
93
+ * screen reader would cut it off to echo the key just typed, and a faster
94
+ * answer has no wait to announce. `Content` is marked `aria-busy`, and
95
+ * `Empty` shows nothing, so "no results" is not read out before the results
96
+ * arrive. The announcement comes from a visually hidden live region the root
97
+ * renders next to the field, so it is in place before the list opens. On a search
98
+ * against an API, pass it from the first render, as `false` while idle: a root
99
+ * that leaves it out gets no live region. A list filtered in memory never
100
+ * waits and leaves it out. While loading with no rows yet, the surface shows
101
+ * whatever placeholder `Content` holds — `Empty` stays quiet.
102
+ */
103
+ loading?: boolean;
104
+ /**
105
+ * What a screen reader hears when `loading` turns `true`. Announced only; any
106
+ * loading visuals — placeholder rows, a spinner — are yours to render inside
107
+ * `Content`, hidden from assistive technology. Defaults to `'Loading…'`.
108
+ */
109
+ loadingLabel?: string;
90
110
  };
91
111
  /**
92
112
  * Root of a combobox: a text input that filters a list of options and writes the
@@ -99,8 +119,9 @@ export type ComboboxRootProps<Value, Multiple extends boolean | undefined, Item>
99
119
  * select.
100
120
  *
101
121
  * Pass the options as `items`. For a list that comes from the server, set
102
- * `filter={null}`, hand the already-filtered rows to `filteredItems`, and request
103
- * them from `onInputValueChange`; `Status` then announces the wait.
122
+ * `filter={null}`, hand the already-filtered rows to `filteredItems`, request
123
+ * them from `onInputValueChange`, and pass the request's state as `loading`:
124
+ * the combobox announces the wait and marks the list busy on its own.
104
125
  *
105
126
  * Every Base UI root prop is accepted and forwarded — `items`, `filter`,
106
127
  * `value`, `onValueChange` and so on. `open` stays the consumer's to control;
@@ -131,7 +152,7 @@ export type ComboboxRootProps<Value, Multiple extends boolean | undefined, Item>
131
152
  * ```
132
153
  */
133
154
  export declare const Root: {
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;
155
+ <Value, Multiple extends boolean | undefined = false, Item = Value>({ minInputLength, loading, loadingLabel, open: openProp, defaultOpen, onOpenChange, onOpenChangeComplete, onInputValueChange, children, ...props }: ComboboxRootProps<Value, Multiple, Item>): import("react/jsx-runtime").JSX.Element;
135
156
  displayName: string;
136
157
  };
137
158
  type InputProps = Omit<ComponentPropsWithRef<typeof ComboboxPrimitive.Input>, 'size'> & {
@@ -223,11 +244,14 @@ type ContentProps = ComponentPropsWithRef<typeof ComboboxPrimitive.Popup> & Pick
223
244
  * one sits under a focused input whose ring is already two near-black pixels, and
224
245
  * at 8 the two borders fight.
225
246
  *
247
+ * Under a root that passes `loading`, the surface is `aria-busy` while `loading`
248
+ * is `true`; the root announces the wait. An `aria-busy` passed here wins.
249
+ *
226
250
  * @summary Floating surface anchored to the input, holding the list
227
251
  * @dataAttribute {string} data-slot - Always set to "combobox-content"
228
252
  */
229
253
  export declare const Content: {
230
- ({ className, side, align, sideOffset, alignOffset, anchor, collisionPadding, ...props }: ContentProps): import("react/jsx-runtime").JSX.Element;
254
+ ({ className, side, align, sideOffset, alignOffset, anchor, collisionPadding, children, ...props }: ContentProps): import("react/jsx-runtime").JSX.Element;
231
255
  displayName: string;
232
256
  };
233
257
  type ListProps = ComponentPropsWithRef<typeof ComboboxPrimitive.List>;
@@ -270,21 +294,26 @@ type EmptyProps = ComponentPropsWithRef<typeof ComboboxPrimitive.Empty>;
270
294
  /**
271
295
  * Shown in place of the list when nothing matches, and announced politely. It
272
296
  * needs `items` on `Root` to know the list is empty, and it must stay mounted —
273
- * change its children rather than rendering it conditionally.
297
+ * change its children rather than rendering it conditionally. While the root is
298
+ * `loading` it shows nothing: the list is empty because the results have not
299
+ * arrived yet, not because there are none. With no rows to show either, the
300
+ * surface would open as an empty frame, so render placeholder rows inside
301
+ * `Content` for the wait.
274
302
  *
275
303
  * @summary Message shown when no item matches the query
276
304
  * @dataAttribute {string} data-slot - Always set to "combobox-empty"
277
305
  */
278
306
  export declare const Empty: {
279
- ({ className, ...props }: EmptyProps): import("react/jsx-runtime").JSX.Element;
307
+ ({ className, children, ...props }: EmptyProps): import("react/jsx-runtime").JSX.Element;
280
308
  displayName: string;
281
309
  };
282
310
  type StatusProps = ComponentPropsWithRef<typeof ComboboxPrimitive.Status>;
283
311
  /**
284
- * Announces the state of a list that is still loading. Use it for the wait on a
285
- * remote search: keep it mounted and swap its children. It does not mark the
286
- * surface busy on its own — pass `aria-busy` to `Content` while the request is in
287
- * flight, as Base UI's own examples do.
312
+ * A politely announced message about the list. For the wait on a remote search,
313
+ * pass `loading` to `Root` instead: it announces and marks the surface busy with
314
+ * nothing else wired. Reach for `Status` for any other message — a count of
315
+ * results, an error from the request — and keep it mounted, swapping its
316
+ * children: a live region that appears already filled in is often skipped.
288
317
  *
289
318
  * @summary Politely announced status message for an async list
290
319
  * @dataAttribute {string} data-slot - Always set to "combobox-status"