@godxjp/ui 33.1.0 → 33.2.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.
Files changed (37) hide show
  1. package/README.md +4 -4
  2. package/agent/START-HERE.md +6 -6
  3. package/agent/components/Command.json +9 -1
  4. package/agent/components/TranslatableInput.json +92 -0
  5. package/agent/components/TranslatableTextarea.json +84 -0
  6. package/agent/components-index.json +10 -0
  7. package/agent/components.json +185 -1
  8. package/agent/index.json +7 -7
  9. package/agent/llms.txt +7 -7
  10. package/agent/patterns/list-filters.json +1 -1
  11. package/agent/patterns.json +1 -1
  12. package/agent/tokens.json +12 -0
  13. package/dist/components/data-entry/command.d.ts +22 -1
  14. package/dist/components/data-entry/command.js +14 -1
  15. package/dist/components/data-entry/form-field.js +10 -4
  16. package/dist/components/data-entry/index.d.ts +4 -2
  17. package/dist/components/data-entry/index.js +6 -1
  18. package/dist/components/data-entry/translatable-input.d.ts +29 -0
  19. package/dist/components/data-entry/translatable-input.js +184 -0
  20. package/dist/contracts/measurement.json +1 -1
  21. package/dist/i18n/messages/en.json +4 -0
  22. package/dist/i18n/messages/ja.json +4 -0
  23. package/dist/i18n/messages/vi.json +4 -0
  24. package/dist/lib/field-a11y.d.ts +15 -0
  25. package/dist/lib/field-a11y.js +13 -1
  26. package/dist/lib/intl-cache.d.ts +1 -0
  27. package/dist/lib/intl-cache.js +4 -0
  28. package/dist/props/components/data-entry.prop.d.ts +46 -0
  29. package/dist/props/components/index.d.ts +1 -1
  30. package/dist/props/registry.d.ts +51 -0
  31. package/dist/props/registry.js +79 -0
  32. package/dist/styles/control.css +15 -0
  33. package/dist/styles/data-entry-layout.css +25 -0
  34. package/dist/styles/layers.json +19 -1
  35. package/dist/styles.css +1 -1
  36. package/dist/tokens/components/data-entry.css +4 -0
  37. package/package.json +2 -2
@@ -6,7 +6,7 @@
6
6
  "advanced-filters",
7
7
  "add-filter"
8
8
  ],
9
- "code": "// Research basis: Shopify Polaris Filters (\"include no more than 2 or 3 promoted filters\"; a\n// More button opens a sheet with the rest), Linear (one Filter menu), IBM Carbon filtering\n// (filters hidden in a drawer/menu show how many are applied), WCAG 2.2 SC 1.4.10 Reflow\n// (search fields and controls are not in the 2-D exception: they reflow, they do not scroll).\n//\n// RULE #51: a filter bar WRAPS — stacked below 640px, extra rows above — at every width.\n// ❌ FilterBar overflow=\"scroll\" (removed in 33.0.0), <ScrollArea orientation=\"horizontal\"> or an\n// overflow-x-auto box around filters: the strip runs off-screen and clips its first label.\n// ✅ the common filters inline + ONE \"More filters\" Sheet for the rest + wrapping chips.\nimport { useState } from \"react\";\nimport { SlidersHorizontal } from \"lucide-react\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport {\n Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader,\n SheetTitle, SheetTrigger,\n} from \"@godxjp/ui/feedback\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { FilterBar, FilterBarGroup } from \"@godxjp/ui/navigation\";\n\ntype Option = { value: string; label: string };\ntype Filters = Record<\"q\" | \"status\" | \"owner\" | \"region\" | \"channel\" | \"period\", string>;\nexport const NO_FILTERS: Filters = { q: \"\", status: \"\", owner: \"\", region: \"\", channel: \"\", period: \"\" };\n\nconst STATUS: Option[] = [{ value: \"open\", label: \"Open\" }, { value: \"paid\", label: \"Paid\" }, { value: \"void\", label: \"Void\" }];\nconst OWNER: Option[] = [{ value: \"me\", label: \"Me\" }, { value: \"team\", label: \"My team\" }];\n// Everything people reach for less often. Order = the order of the sheet.\nconst MORE: { key: \"region\" | \"channel\" | \"period\"; label: string; options: Option[] }[] = [\n { key: \"region\", label: \"Region\", options: [{ value: \"kanto\", label: \"Kanto\" }, { value: \"kansai\", label: \"Kansai\" }] },\n { key: \"channel\", label: \"Sales channel\", options: [{ value: \"web\", label: \"Web\" }, { value: \"store\", label: \"Store\" }] },\n { key: \"period\", label: \"Period\", options: [{ value: \"month\", label: \"This month\" }, { value: \"quarter\", label: \"This quarter\" }] },\n];\nconst LABELS: Record<string, { label: string; options: Option[] }> = {\n status: { label: \"Status\", options: STATUS },\n owner: { label: \"Owner\", options: OWNER },\n ...Object.fromEntries(MORE.map((f) => [f.key, f])),\n};\n\nexport function OrderFilters({ value, onValueChange, resultCount }: {\n value: Filters; onValueChange: (next: Filters) => void; resultCount: number;\n}) {\n const [moreOpen, setMoreOpen] = useState(false);\n const set = (key: keyof Filters, v: string) => onValueChange({ ...value, [key]: v });\n\n // ONE chip per applied filter — inline AND hidden ones, so nothing applied is out of sight.\n const chips = Object.entries(LABELS)\n .filter(([key]) => value[key as keyof Filters] !== \"\")\n .map(([key, f]) => ({\n value: key,\n label: `${f.label}: ${f.options.find((o) => o.value === value[key as keyof Filters])?.label ?? \"\"}`,\n }));\n if (value.q) chips.unshift({ value: \"q\", label: `Search: ${value.q}` });\n const hiddenApplied = MORE.filter((f) => value[f.key] !== \"\").length;\n\n return (\n <FilterBar\n search={{ value: value.q, onValueChange: (v) => set(\"q\", v), placeholder: \"Search orders\", ariaLabel: \"Search orders\" }}\n // The 2–4 most-used filters, INLINE. Typed filters wire each caption as the real <label>.\n filters={[\n { value: \"status\", label: \"Status\", options: STATUS, selected: value.status, onSelectedChange: (v) => set(\"status\", v), placeholder: \"All\" },\n { value: \"owner\", label: \"Owner\", options: OWNER, selected: value.owner, onSelectedChange: (v) => set(\"owner\", v), placeholder: \"Anyone\" },\n ]}\n chips={chips}\n onChipRemove={(key) => set(key as keyof Filters, \"\")}\n hasActiveFilters={chips.length > 0}\n onClear={() => onValueChange(NO_FILTERS)}\n resultCount={resultCount}\n >\n {/* Children render after the typed filters, before the reset: ONE \"More filters\" control.\n count = hidden filters applied (0 renders nothing), so the closed sheet still says so. */}\n <Sheet open={moreOpen} onOpenChange={setMoreOpen}>\n <SheetTrigger asChild>\n <Button variant=\"outline\" count={hiddenApplied} countLabel=\"applied\">\n <SlidersHorizontal aria-hidden=\"true\" />\n More filters\n </Button>\n </SheetTrigger>\n <SheetContent side=\"right\">\n <SheetHeader>\n <SheetTitle>More filters</SheetTitle>\n <SheetDescription>Applied filters also show as chips above the list.</SheetDescription>\n </SheetHeader>\n <SheetBody>\n {/* A Sheet scrolls VERTICALLY; the fields stack. The same FilterBarGroup labels them. */}\n <Flex direction=\"col\" gap=\"md\">\n {MORE.map((f) => (\n <FilterBarGroup key={f.key} label={f.label} controlId={`more-${f.key}`}>\n <Select value={value[f.key]} onValueChange={(v: string) => set(f.key, v)}>\n <SelectTrigger id={`more-${f.key}`}><SelectValue placeholder=\"All\" /></SelectTrigger>\n <SelectContent>\n {f.options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}\n </SelectContent>\n </Select>\n </FilterBarGroup>\n ))}\n </Flex>\n </SheetBody>\n <SheetFooter>\n <SheetClose asChild><Button>Show results</Button></SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n </FilterBar>\n );\n}\n// Phone width: the bar stacks (search, each filter, More filters, reset) and the chips wrap.\n// A row of filter CHIPS (ToggleGroup) takes `wrap` — it does not scroll sideways either.\n// Few filters (≤ 4)? Drop the Sheet: the plain bar in the data-table-page pattern is enough.",
9
+ "code": "// Research basis: Shopify Polaris Filters (\"include no more than 2 or 3 promoted filters\"; a\n// More button opens a sheet with the rest), Linear (one Filter menu), IBM Carbon filtering\n// (filters hidden in a drawer/menu show how many are applied), WCAG 2.2 SC 1.4.10 Reflow\n// (search fields and controls are not in the 2-D exception: they reflow, they do not scroll).\n//\n// RULE #51: a filter bar WRAPS — stacked below 640px, extra rows above — at every width.\n// ❌ FilterBar overflow=\"scroll\" (removed in 33.0.0), <ScrollArea orientation=\"horizontal\"> or an\n// overflow-x-auto box around filters: the strip runs off-screen and clips its first label.\n// ✅ the common filters inline + ONE \"More filters\" Sheet for the rest + wrapping chips.\nimport { useState } from \"react\";\nimport { SlidersHorizontal } from \"lucide-react\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport {\n Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader,\n SheetTitle, SheetTrigger,\n} from \"@godxjp/ui/feedback\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { FilterBar, FilterBarGroup } from \"@godxjp/ui/navigation\";\n\ntype Option = { value: string; label: string };\ntype Filters = Record<\"q\" | \"status\" | \"owner\" | \"region\" | \"channel\" | \"period\", string>;\nexport const NO_FILTERS: Filters = { q: \"\", status: \"\", owner: \"\", region: \"\", channel: \"\", period: \"\" };\n\nconst STATUS: Option[] = [{ value: \"open\", label: \"Open\" }, { value: \"paid\", label: \"Paid\" }, { value: \"void\", label: \"Void\" }];\nconst OWNER: Option[] = [{ value: \"me\", label: \"Me\" }, { value: \"team\", label: \"My team\" }];\n// Everything people reach for less often. Order = the order of the sheet.\nconst MORE: { key: \"region\" | \"channel\" | \"period\"; label: string; options: Option[] }[] = [\n { key: \"region\", label: \"Region\", options: [{ value: \"kanto\", label: \"Kanto\" }, { value: \"kansai\", label: \"Kansai\" }] },\n { key: \"channel\", label: \"Sales channel\", options: [{ value: \"web\", label: \"Web\" }, { value: \"store\", label: \"Store\" }] },\n { key: \"period\", label: \"Period\", options: [{ value: \"month\", label: \"This month\" }, { value: \"quarter\", label: \"This quarter\" }] },\n];\nconst LABELS: Record<string, { label: string; options: Option[] }> = {\n status: { label: \"Status\", options: STATUS },\n owner: { label: \"Owner\", options: OWNER },\n ...Object.fromEntries(MORE.map((f) => [f.key, f])),\n};\n\nexport function OrderFilters({ value, onValueChange, resultCount }: {\n value: Filters; onValueChange: (next: Filters) => void; resultCount: number;\n}) {\n const [moreOpen, setMoreOpen] = useState(false);\n const set = (key: keyof Filters, v: string) => onValueChange({ ...value, [key]: v });\n\n // ONE chip per applied filter — inline AND hidden ones, so nothing applied is out of sight.\n const chips = Object.entries(LABELS)\n .filter(([key]) => value[key as keyof Filters] !== \"\")\n .map(([key, f]) => ({\n value: key,\n label: `${f.label}: ${f.options.find((o) => o.value === value[key as keyof Filters])?.label ?? \"\"}`,\n }));\n if (value.q) chips.unshift({ value: \"q\", label: `Search: ${value.q}` });\n const hiddenApplied = MORE.filter((f) => value[f.key] !== \"\").length;\n\n return (\n <FilterBar\n search={{ value: value.q, onValueChange: (v) => set(\"q\", v), placeholder: \"Search orders\", ariaLabel: \"Search orders\" }}\n // The 2–4 most-used filters, INLINE. Typed filters wire each caption as the real <label>.\n filters={[\n { value: \"status\", label: \"Status\", options: STATUS, selected: value.status, onSelectedChange: (v) => set(\"status\", v), placeholder: \"All\" },\n { value: \"owner\", label: \"Owner\", options: OWNER, selected: value.owner, onSelectedChange: (v) => set(\"owner\", v), placeholder: \"Anyone\" },\n ]}\n chips={chips}\n onChipRemove={(key) => set(key as keyof Filters, \"\")}\n hasActiveFilters={chips.length > 0}\n onClear={() => onValueChange(NO_FILTERS)}\n resultCount={resultCount}\n >\n {/* Children render after the typed filters, before the reset: ONE \"More filters\" control.\n count = hidden filters applied, so the closed sheet still says so. Button paints a 0 pill by\n default (showZero = true) — showZero={false} hides it when nothing hidden is applied (#1333). */}\n <Sheet open={moreOpen} onOpenChange={setMoreOpen}>\n <SheetTrigger asChild>\n <Button variant=\"outline\" count={hiddenApplied} showZero={false} countLabel=\"applied\">\n <SlidersHorizontal aria-hidden=\"true\" />\n More filters\n </Button>\n </SheetTrigger>\n <SheetContent side=\"right\">\n <SheetHeader>\n <SheetTitle>More filters</SheetTitle>\n <SheetDescription>Applied filters also show as chips above the list.</SheetDescription>\n </SheetHeader>\n <SheetBody>\n {/* A Sheet scrolls VERTICALLY; the fields stack. The same FilterBarGroup labels them. */}\n <Flex direction=\"col\" gap=\"md\">\n {MORE.map((f) => (\n <FilterBarGroup key={f.key} label={f.label} controlId={`more-${f.key}`}>\n <Select value={value[f.key]} onValueChange={(v: string) => set(f.key, v)}>\n <SelectTrigger id={`more-${f.key}`}><SelectValue placeholder=\"All\" /></SelectTrigger>\n <SelectContent>\n {f.options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}\n </SelectContent>\n </Select>\n </FilterBarGroup>\n ))}\n </Flex>\n </SheetBody>\n <SheetFooter>\n <SheetClose asChild><Button>Show results</Button></SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n </FilterBar>\n );\n}\n// Phone width: the bar stacks (search, each filter, More filters, reset) and the chips wrap.\n// A row of filter CHIPS (ToggleGroup) takes `wrap` — it does not scroll sideways either.\n// Few filters (≤ 4)? Drop the Sheet: the plain bar in the data-table-page pattern is enough.",
10
10
  "name": "list-filters",
11
11
  "tagline": "A list page with MANY filters (cardinal rule #51) — FilterBar WRAPS; the 2–4 most-used filters stay inline, the rest sit behind ONE 'More filters' Button (count = hidden filters applied) that opens a Sheet; every applied filter is a removable chip. Never a sideways-scrolling strip.",
12
12
  "tags": [
@@ -280,7 +280,7 @@
280
280
  "advanced-filters",
281
281
  "add-filter"
282
282
  ],
283
- "code": "// Research basis: Shopify Polaris Filters (\"include no more than 2 or 3 promoted filters\"; a\n// More button opens a sheet with the rest), Linear (one Filter menu), IBM Carbon filtering\n// (filters hidden in a drawer/menu show how many are applied), WCAG 2.2 SC 1.4.10 Reflow\n// (search fields and controls are not in the 2-D exception: they reflow, they do not scroll).\n//\n// RULE #51: a filter bar WRAPS — stacked below 640px, extra rows above — at every width.\n// ❌ FilterBar overflow=\"scroll\" (removed in 33.0.0), <ScrollArea orientation=\"horizontal\"> or an\n// overflow-x-auto box around filters: the strip runs off-screen and clips its first label.\n// ✅ the common filters inline + ONE \"More filters\" Sheet for the rest + wrapping chips.\nimport { useState } from \"react\";\nimport { SlidersHorizontal } from \"lucide-react\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport {\n Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader,\n SheetTitle, SheetTrigger,\n} from \"@godxjp/ui/feedback\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { FilterBar, FilterBarGroup } from \"@godxjp/ui/navigation\";\n\ntype Option = { value: string; label: string };\ntype Filters = Record<\"q\" | \"status\" | \"owner\" | \"region\" | \"channel\" | \"period\", string>;\nexport const NO_FILTERS: Filters = { q: \"\", status: \"\", owner: \"\", region: \"\", channel: \"\", period: \"\" };\n\nconst STATUS: Option[] = [{ value: \"open\", label: \"Open\" }, { value: \"paid\", label: \"Paid\" }, { value: \"void\", label: \"Void\" }];\nconst OWNER: Option[] = [{ value: \"me\", label: \"Me\" }, { value: \"team\", label: \"My team\" }];\n// Everything people reach for less often. Order = the order of the sheet.\nconst MORE: { key: \"region\" | \"channel\" | \"period\"; label: string; options: Option[] }[] = [\n { key: \"region\", label: \"Region\", options: [{ value: \"kanto\", label: \"Kanto\" }, { value: \"kansai\", label: \"Kansai\" }] },\n { key: \"channel\", label: \"Sales channel\", options: [{ value: \"web\", label: \"Web\" }, { value: \"store\", label: \"Store\" }] },\n { key: \"period\", label: \"Period\", options: [{ value: \"month\", label: \"This month\" }, { value: \"quarter\", label: \"This quarter\" }] },\n];\nconst LABELS: Record<string, { label: string; options: Option[] }> = {\n status: { label: \"Status\", options: STATUS },\n owner: { label: \"Owner\", options: OWNER },\n ...Object.fromEntries(MORE.map((f) => [f.key, f])),\n};\n\nexport function OrderFilters({ value, onValueChange, resultCount }: {\n value: Filters; onValueChange: (next: Filters) => void; resultCount: number;\n}) {\n const [moreOpen, setMoreOpen] = useState(false);\n const set = (key: keyof Filters, v: string) => onValueChange({ ...value, [key]: v });\n\n // ONE chip per applied filter — inline AND hidden ones, so nothing applied is out of sight.\n const chips = Object.entries(LABELS)\n .filter(([key]) => value[key as keyof Filters] !== \"\")\n .map(([key, f]) => ({\n value: key,\n label: `${f.label}: ${f.options.find((o) => o.value === value[key as keyof Filters])?.label ?? \"\"}`,\n }));\n if (value.q) chips.unshift({ value: \"q\", label: `Search: ${value.q}` });\n const hiddenApplied = MORE.filter((f) => value[f.key] !== \"\").length;\n\n return (\n <FilterBar\n search={{ value: value.q, onValueChange: (v) => set(\"q\", v), placeholder: \"Search orders\", ariaLabel: \"Search orders\" }}\n // The 2–4 most-used filters, INLINE. Typed filters wire each caption as the real <label>.\n filters={[\n { value: \"status\", label: \"Status\", options: STATUS, selected: value.status, onSelectedChange: (v) => set(\"status\", v), placeholder: \"All\" },\n { value: \"owner\", label: \"Owner\", options: OWNER, selected: value.owner, onSelectedChange: (v) => set(\"owner\", v), placeholder: \"Anyone\" },\n ]}\n chips={chips}\n onChipRemove={(key) => set(key as keyof Filters, \"\")}\n hasActiveFilters={chips.length > 0}\n onClear={() => onValueChange(NO_FILTERS)}\n resultCount={resultCount}\n >\n {/* Children render after the typed filters, before the reset: ONE \"More filters\" control.\n count = hidden filters applied (0 renders nothing), so the closed sheet still says so. */}\n <Sheet open={moreOpen} onOpenChange={setMoreOpen}>\n <SheetTrigger asChild>\n <Button variant=\"outline\" count={hiddenApplied} countLabel=\"applied\">\n <SlidersHorizontal aria-hidden=\"true\" />\n More filters\n </Button>\n </SheetTrigger>\n <SheetContent side=\"right\">\n <SheetHeader>\n <SheetTitle>More filters</SheetTitle>\n <SheetDescription>Applied filters also show as chips above the list.</SheetDescription>\n </SheetHeader>\n <SheetBody>\n {/* A Sheet scrolls VERTICALLY; the fields stack. The same FilterBarGroup labels them. */}\n <Flex direction=\"col\" gap=\"md\">\n {MORE.map((f) => (\n <FilterBarGroup key={f.key} label={f.label} controlId={`more-${f.key}`}>\n <Select value={value[f.key]} onValueChange={(v: string) => set(f.key, v)}>\n <SelectTrigger id={`more-${f.key}`}><SelectValue placeholder=\"All\" /></SelectTrigger>\n <SelectContent>\n {f.options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}\n </SelectContent>\n </Select>\n </FilterBarGroup>\n ))}\n </Flex>\n </SheetBody>\n <SheetFooter>\n <SheetClose asChild><Button>Show results</Button></SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n </FilterBar>\n );\n}\n// Phone width: the bar stacks (search, each filter, More filters, reset) and the chips wrap.\n// A row of filter CHIPS (ToggleGroup) takes `wrap` — it does not scroll sideways either.\n// Few filters (≤ 4)? Drop the Sheet: the plain bar in the data-table-page pattern is enough.",
283
+ "code": "// Research basis: Shopify Polaris Filters (\"include no more than 2 or 3 promoted filters\"; a\n// More button opens a sheet with the rest), Linear (one Filter menu), IBM Carbon filtering\n// (filters hidden in a drawer/menu show how many are applied), WCAG 2.2 SC 1.4.10 Reflow\n// (search fields and controls are not in the 2-D exception: they reflow, they do not scroll).\n//\n// RULE #51: a filter bar WRAPS — stacked below 640px, extra rows above — at every width.\n// ❌ FilterBar overflow=\"scroll\" (removed in 33.0.0), <ScrollArea orientation=\"horizontal\"> or an\n// overflow-x-auto box around filters: the strip runs off-screen and clips its first label.\n// ✅ the common filters inline + ONE \"More filters\" Sheet for the rest + wrapping chips.\nimport { useState } from \"react\";\nimport { SlidersHorizontal } from \"lucide-react\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport {\n Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader,\n SheetTitle, SheetTrigger,\n} from \"@godxjp/ui/feedback\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { FilterBar, FilterBarGroup } from \"@godxjp/ui/navigation\";\n\ntype Option = { value: string; label: string };\ntype Filters = Record<\"q\" | \"status\" | \"owner\" | \"region\" | \"channel\" | \"period\", string>;\nexport const NO_FILTERS: Filters = { q: \"\", status: \"\", owner: \"\", region: \"\", channel: \"\", period: \"\" };\n\nconst STATUS: Option[] = [{ value: \"open\", label: \"Open\" }, { value: \"paid\", label: \"Paid\" }, { value: \"void\", label: \"Void\" }];\nconst OWNER: Option[] = [{ value: \"me\", label: \"Me\" }, { value: \"team\", label: \"My team\" }];\n// Everything people reach for less often. Order = the order of the sheet.\nconst MORE: { key: \"region\" | \"channel\" | \"period\"; label: string; options: Option[] }[] = [\n { key: \"region\", label: \"Region\", options: [{ value: \"kanto\", label: \"Kanto\" }, { value: \"kansai\", label: \"Kansai\" }] },\n { key: \"channel\", label: \"Sales channel\", options: [{ value: \"web\", label: \"Web\" }, { value: \"store\", label: \"Store\" }] },\n { key: \"period\", label: \"Period\", options: [{ value: \"month\", label: \"This month\" }, { value: \"quarter\", label: \"This quarter\" }] },\n];\nconst LABELS: Record<string, { label: string; options: Option[] }> = {\n status: { label: \"Status\", options: STATUS },\n owner: { label: \"Owner\", options: OWNER },\n ...Object.fromEntries(MORE.map((f) => [f.key, f])),\n};\n\nexport function OrderFilters({ value, onValueChange, resultCount }: {\n value: Filters; onValueChange: (next: Filters) => void; resultCount: number;\n}) {\n const [moreOpen, setMoreOpen] = useState(false);\n const set = (key: keyof Filters, v: string) => onValueChange({ ...value, [key]: v });\n\n // ONE chip per applied filter — inline AND hidden ones, so nothing applied is out of sight.\n const chips = Object.entries(LABELS)\n .filter(([key]) => value[key as keyof Filters] !== \"\")\n .map(([key, f]) => ({\n value: key,\n label: `${f.label}: ${f.options.find((o) => o.value === value[key as keyof Filters])?.label ?? \"\"}`,\n }));\n if (value.q) chips.unshift({ value: \"q\", label: `Search: ${value.q}` });\n const hiddenApplied = MORE.filter((f) => value[f.key] !== \"\").length;\n\n return (\n <FilterBar\n search={{ value: value.q, onValueChange: (v) => set(\"q\", v), placeholder: \"Search orders\", ariaLabel: \"Search orders\" }}\n // The 2–4 most-used filters, INLINE. Typed filters wire each caption as the real <label>.\n filters={[\n { value: \"status\", label: \"Status\", options: STATUS, selected: value.status, onSelectedChange: (v) => set(\"status\", v), placeholder: \"All\" },\n { value: \"owner\", label: \"Owner\", options: OWNER, selected: value.owner, onSelectedChange: (v) => set(\"owner\", v), placeholder: \"Anyone\" },\n ]}\n chips={chips}\n onChipRemove={(key) => set(key as keyof Filters, \"\")}\n hasActiveFilters={chips.length > 0}\n onClear={() => onValueChange(NO_FILTERS)}\n resultCount={resultCount}\n >\n {/* Children render after the typed filters, before the reset: ONE \"More filters\" control.\n count = hidden filters applied, so the closed sheet still says so. Button paints a 0 pill by\n default (showZero = true) — showZero={false} hides it when nothing hidden is applied (#1333). */}\n <Sheet open={moreOpen} onOpenChange={setMoreOpen}>\n <SheetTrigger asChild>\n <Button variant=\"outline\" count={hiddenApplied} showZero={false} countLabel=\"applied\">\n <SlidersHorizontal aria-hidden=\"true\" />\n More filters\n </Button>\n </SheetTrigger>\n <SheetContent side=\"right\">\n <SheetHeader>\n <SheetTitle>More filters</SheetTitle>\n <SheetDescription>Applied filters also show as chips above the list.</SheetDescription>\n </SheetHeader>\n <SheetBody>\n {/* A Sheet scrolls VERTICALLY; the fields stack. The same FilterBarGroup labels them. */}\n <Flex direction=\"col\" gap=\"md\">\n {MORE.map((f) => (\n <FilterBarGroup key={f.key} label={f.label} controlId={`more-${f.key}`}>\n <Select value={value[f.key]} onValueChange={(v: string) => set(f.key, v)}>\n <SelectTrigger id={`more-${f.key}`}><SelectValue placeholder=\"All\" /></SelectTrigger>\n <SelectContent>\n {f.options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}\n </SelectContent>\n </Select>\n </FilterBarGroup>\n ))}\n </Flex>\n </SheetBody>\n <SheetFooter>\n <SheetClose asChild><Button>Show results</Button></SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n </FilterBar>\n );\n}\n// Phone width: the bar stacks (search, each filter, More filters, reset) and the chips wrap.\n// A row of filter CHIPS (ToggleGroup) takes `wrap` — it does not scroll sideways either.\n// Few filters (≤ 4)? Drop the Sheet: the plain bar in the data-table-page pattern is enough.",
284
284
  "name": "list-filters",
285
285
  "tagline": "A list page with MANY filters (cardinal rule #51) — FilterBar WRAPS; the 2–4 most-used filters stay inline, the rest sit behind ONE 'More filters' Button (count = hidden filters applied) that opens a Sheet; every applied filter is a removable chip. Never a sideways-scrolling strip.",
286
286
  "tags": [
package/agent/tokens.json CHANGED
@@ -6503,6 +6503,18 @@
6503
6503
  "tier": "component",
6504
6504
  "value": "16rem"
6505
6505
  },
6506
+ {
6507
+ "description": "TranslatableInput / TranslatableTextarea (gh#1336). The rhythm between the locale tab strip and the field it switches — a service aligning its forms to a tighter grid sets it once. 4px",
6508
+ "name": "--translatable-field-gap",
6509
+ "tier": "component",
6510
+ "value": "var(--space-1)"
6511
+ },
6512
+ {
6513
+ "description": "The error mark beside a locale's name on its tab. The ICON scale one step below the tab's own 14px text, so it reads as a mark on the label rather than a second word. 12px",
6514
+ "name": "--translatable-field-error-icon-size",
6515
+ "tier": "component",
6516
+ "value": "var(--icon-size-xs)"
6517
+ },
6506
6518
  {
6507
6519
  "description": "Width of the label column when <Descriptions layout=\"horizontal\">. Labels align to this shared column so the values line up (the horizontal-detail look, mirroring <Form layout>). A rem value gives a fixed aligned column; set `max-content` to size each label to its text. (rule #44/#45 — a service theme tunes it here instead of forking CSS.)",
6508
6520
  "name": "--descriptions-label-width",
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
- import type { CommandProp } from "../../props/components/data-entry.prop.js";
2
+ import type { CommandProp, CommandSeparatorProp } from "../../props/components/data-entry.prop.js";
3
3
  export type { CommandProp, CommandProp as CommandProps };
4
+ export type { CommandSeparatorProp, CommandSeparatorProp as CommandSeparatorProps };
4
5
  export declare const Command: React.ForwardRefExoticComponent<Omit<{
5
6
  children?: React.ReactNode;
6
7
  } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
@@ -66,3 +67,23 @@ export declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
66
67
  keywords?: string[];
67
68
  forceMount?: boolean;
68
69
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
70
+ /**
71
+ * A hairline between two groups of one list — e.g. the matches, then `+ Create "<q>"` (gh#1335).
72
+ * It is cmdk's own `Command.Separator`, so it must sit inside the same `Command` as its groups.
73
+ *
74
+ * `alwaysRender` defaults to `true`, the OPPOSITE of cmdk: cmdk drops a separator as soon as the
75
+ * search is non-empty, because in a palette the groups collapse under filtering. A "Create" group
76
+ * exists only BECAUSE the user typed, so with cmdk's default the rule vanished exactly when it
77
+ * was needed. Pass `alwaysRender={false}` for cmdk's palette behaviour.
78
+ *
79
+ * `aria-hidden` by default: the rule is decorative, and a `role="separator"` child is not allowed
80
+ * in the `listbox` CommandList renders (ARIA owns only `option` / `group` there); the groups'
81
+ * headings carry the structure.
82
+ */
83
+ export declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
84
+ ref?: React.Ref<HTMLDivElement>;
85
+ } & {
86
+ asChild?: boolean;
87
+ }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
88
+ alwaysRender?: boolean;
89
+ } & React.RefAttributes<HTMLDivElement>, "ref"> & CommandSeparatorProp & React.RefAttributes<HTMLDivElement>>;
@@ -50,11 +50,24 @@ const CommandGroup = React.forwardRef(({ className, ...props }, ref) => /* @__PU
50
50
  CommandGroup.displayName = CommandPrimitive.Group.displayName;
51
51
  const CommandItem = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(CommandPrimitive.Item, { ref, className: cn("ui-command-item", className), ...props }));
52
52
  CommandItem.displayName = CommandPrimitive.Item.displayName;
53
+ const CommandSeparator = React.forwardRef(({ className, alwaysRender = true, ...props }, ref) => /* @__PURE__ */ jsx(
54
+ CommandPrimitive.Separator,
55
+ {
56
+ ref,
57
+ className: cn("ui-command-separator", className),
58
+ "data-slot": "command-separator",
59
+ "aria-hidden": "true",
60
+ alwaysRender,
61
+ ...props
62
+ }
63
+ ));
64
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
53
65
  export {
54
66
  Command,
55
67
  CommandEmpty,
56
68
  CommandGroup,
57
69
  CommandInput,
58
70
  CommandItem,
59
- CommandList
71
+ CommandList,
72
+ CommandSeparator
60
73
  };
@@ -5,7 +5,12 @@ import { CheckCircle, CircleAlert, LoaderCircle, TriangleAlert } from "lucide-re
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { Label } from "../data-entry/label.js";
7
7
  import { cn } from "../../lib/utils.js";
8
- import { FieldIdentityContext, FieldNameContext, mergeAriaIds } from "../../lib/field-a11y.js";
8
+ import {
9
+ FieldErrorReportContext,
10
+ FieldIdentityContext,
11
+ FieldNameContext,
12
+ mergeAriaIds
13
+ } from "../../lib/field-a11y.js";
9
14
  import { useOptionalAppContext } from "../../app/app-provider.js";
10
15
  import { useFormLayout } from "./form.js";
11
16
  import { firstBagMessage, useClaimErrorKey, useFormErrorsRegistry } from "./form-errors.js";
@@ -42,7 +47,8 @@ function FormField({
42
47
  const collapseBelow = form?.collapseBelow ?? "md";
43
48
  const errorsRegistry = useFormErrorsRegistry();
44
49
  useClaimErrorKey(name);
45
- const error = errorProp ?? (name && errorsRegistry ? firstBagMessage(errorsRegistry.errors[name]) : void 0);
50
+ const [reportedError, setReportedError] = React.useState(void 0);
51
+ const error = errorProp ?? (name && errorsRegistry ? firstBagMessage(errorsRegistry.errors[name]) : void 0) ?? (reportedError === null || reportedError === false || reportedError === "" ? void 0 : reportedError);
46
52
  const autoId = React.useId();
47
53
  const resolvedId = id ?? autoId;
48
54
  const labelId = `${resolvedId}-label`;
@@ -165,7 +171,7 @@ function FormField({
165
171
  ] }) : null,
166
172
  /* @__PURE__ */ jsxs("div", { "data-slot": "form-field-control", className: "ui-form-field-control", children: [
167
173
  helperPlacement === "before" ? helperNode : null,
168
- isStatic ? childWithA11y : /* @__PURE__ */ jsx(FieldNameContext.Provider, { value: fieldNameContext, children: /* @__PURE__ */ jsx(FieldIdentityContext.Provider, { value: fieldIdentityContext, children: /* @__PURE__ */ jsx(
174
+ isStatic ? childWithA11y : /* @__PURE__ */ jsx(FieldNameContext.Provider, { value: fieldNameContext, children: /* @__PURE__ */ jsx(FieldIdentityContext.Provider, { value: fieldIdentityContext, children: /* @__PURE__ */ jsx(FieldErrorReportContext.Provider, { value: setReportedError, children: /* @__PURE__ */ jsx(
169
175
  "fieldset",
170
176
  {
171
177
  role: "presentation",
@@ -173,7 +179,7 @@ function FormField({
173
179
  disabled: form?.disabled && childProps?.disabled !== false,
174
180
  children: childWithA11y
175
181
  }
176
- ) }) }),
182
+ ) }) }) }),
177
183
  hasFeedback && validationStatus ? /* @__PURE__ */ jsxs(
178
184
  "span",
179
185
  {
@@ -9,6 +9,8 @@ export { CheckboxGroup } from "./checkbox-group.js";
9
9
  export { Radio, RadioGroup, RadioItem, RadioGroupRoot } from "./radio.js";
10
10
  export { Textarea } from "./textarea.js";
11
11
  export type { TextareaProps } from "./textarea.js";
12
+ export { TranslatableInput, TranslatableTextarea } from "./translatable-input.js";
13
+ export type { TranslatableValue, TranslatableInputProp, TranslatableInputProps, TranslatableTextareaProp, TranslatableTextareaProps, } from "./translatable-input.js";
12
14
  export { Form, useFormLayout, type FormLayoutContextValue } from "./form.js";
13
15
  export type { FormProp, FormProps } from "./form.js";
14
16
  export { FormField } from "./form-field.js";
@@ -47,8 +49,8 @@ export { TreeSelect, SHOW_CHILD, SHOW_PARENT, SHOW_ALL } from "./tree-select.js"
47
49
  export type { TreeSelectProps } from "./tree-select.js";
48
50
  export { Transfer } from "./transfer.js";
49
51
  export type { TransferProps, TransferItemProp } from "./transfer.js";
50
- export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command.js";
51
- export type { CommandProps } from "./command.js";
52
+ export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "./command.js";
53
+ export type { CommandProps, CommandSeparatorProps } from "./command.js";
52
54
  export { CommandPalette } from "./command-palette.js";
53
55
  export type { CommandPaletteGroup, CommandPaletteItem, CommandPaletteLabels, CommandPaletteProps, CommandPaletteRecent, CommandPaletteSelectModifiers, } from "./command-palette.js";
54
56
  export { PasswordInput } from "./password-input.js";
@@ -17,6 +17,7 @@ import { Checkbox } from "./checkbox.js";
17
17
  import { CheckboxGroup } from "./checkbox-group.js";
18
18
  import { Radio, RadioGroup, RadioItem, RadioGroupRoot } from "./radio.js";
19
19
  import { Textarea } from "./textarea.js";
20
+ import { TranslatableInput, TranslatableTextarea } from "./translatable-input.js";
20
21
  import { Form, useFormLayout } from "./form.js";
21
22
  import { FormField } from "./form-field.js";
22
23
  import { FormErrors, FormErrorsProvider } from "./form-errors.js";
@@ -48,7 +49,8 @@ import {
48
49
  CommandGroup,
49
50
  CommandInput,
50
51
  CommandItem,
51
- CommandList
52
+ CommandList,
53
+ CommandSeparator
52
54
  } from "./command.js";
53
55
  import { CommandPalette } from "./command-palette.js";
54
56
  import { PasswordInput } from "./password-input.js";
@@ -77,6 +79,7 @@ export {
77
79
  CommandItem,
78
80
  CommandList,
79
81
  CommandPalette,
82
+ CommandSeparator,
80
83
  DatePicker,
81
84
  DayButton,
82
85
  Field,
@@ -123,6 +126,8 @@ export {
123
126
  ToggleGroup,
124
127
  ToggleGroupItem,
125
128
  Transfer,
129
+ TranslatableInput,
130
+ TranslatableTextarea,
126
131
  TreeSelect,
127
132
  UPLOAD_LIST_IGNORE,
128
133
  Upload,
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ import type { TranslatableValue } from "../../props/components/data-entry.prop.js";
3
+ export type { TranslatableValue, TranslatableInputProp, TranslatableInputProp as TranslatableInputProps, TranslatableTextareaProp, TranslatableTextareaProp as TranslatableTextareaProps, } from "../../props/components/data-entry.prop.js";
4
+ /**
5
+ * TranslatableInput — ONE text field that holds a value per locale (`{ ja, en, vi }`), switched by
6
+ * locale tabs above it (gh#1336). The locale list, the first tab and the fallback come from
7
+ * `AppProvider` and are overridable per field; an empty locale shows the fallback locale's text as
8
+ * its placeholder; a locale with an error is marked on its tab. Every other `Input` prop applies to
9
+ * the single control, and `ref` is that control's `<input>`.
10
+ */
11
+ export declare const TranslatableInput: React.ForwardRefExoticComponent<Omit<import("./input.js").InputProp, "defaultValue" | "lang" | "onValueChange" | "value"> & {
12
+ value?: import("../../props/index.js").ValueProp<TranslatableValue>;
13
+ defaultValue?: import("../../props/index.js").DefaultValueProp<TranslatableValue>;
14
+ onValueChange?: import("../../props/index.js").OnValueChangeProp<TranslatableValue>;
15
+ locales?: readonly string[];
16
+ defaultLocale?: string;
17
+ fallbackLocale?: string;
18
+ errors?: Partial<Record<string, import("../../props/index.js").ErrorProp>>;
19
+ } & React.RefAttributes<HTMLInputElement>>;
20
+ /** TranslatableTextarea — {@link TranslatableInput} over a `Textarea`; `ref` is the `<textarea>`. */
21
+ export declare const TranslatableTextarea: React.ForwardRefExoticComponent<Omit<import("./textarea.js").TextareaProp, "defaultValue" | "lang" | "onValueChange" | "value"> & {
22
+ value?: import("../../props/index.js").ValueProp<TranslatableValue>;
23
+ defaultValue?: import("../../props/index.js").DefaultValueProp<TranslatableValue>;
24
+ onValueChange?: import("../../props/index.js").OnValueChangeProp<TranslatableValue>;
25
+ locales?: readonly string[];
26
+ defaultLocale?: string;
27
+ fallbackLocale?: string;
28
+ errors?: Partial<Record<string, import("../../props/index.js").ErrorProp>>;
29
+ } & React.RefAttributes<HTMLTextAreaElement>>;
@@ -0,0 +1,184 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { CircleAlert } from "lucide-react";
5
+ import { useTranslation } from "../../i18n/use-translation.js";
6
+ import { getRegisteredLocales } from "../../i18n/locale-tags.js";
7
+ import { mergeAriaIds, useReportFieldError } from "../../lib/field-a11y.js";
8
+ import { displayNames } from "../../lib/intl-cache.js";
9
+ import { cn } from "../../lib/utils.js";
10
+ import { Tabs, TabsList, TabsTrigger } from "../navigation/tabs.js";
11
+ import { Input } from "./input.js";
12
+ import { Textarea } from "./textarea.js";
13
+ const NO_VALUE = {};
14
+ function localeName(code, uiLocale) {
15
+ try {
16
+ return displayNames(uiLocale, { type: "language" }).of(code) ?? code;
17
+ } catch {
18
+ return code;
19
+ }
20
+ }
21
+ const idPart = (code) => code.replace(/[^A-Za-z0-9_-]/g, "_");
22
+ function TranslatableField({
23
+ kind,
24
+ value: valueProp,
25
+ defaultValue,
26
+ onValueChange,
27
+ locales: localesProp,
28
+ defaultLocale,
29
+ fallbackLocale: fallbackLocaleProp,
30
+ errors,
31
+ size,
32
+ placeholder,
33
+ className,
34
+ forwardedRef,
35
+ ...rest
36
+ }) {
37
+ const { t, locale: uiLocale, fallbackLocale: appFallbackLocale } = useTranslation();
38
+ const locales = React.useMemo(() => {
39
+ const list = localesProp ?? getRegisteredLocales();
40
+ return list.length > 0 ? list : [uiLocale];
41
+ }, [localesProp, uiLocale]);
42
+ const pick = (candidates) => candidates.find((code) => code !== void 0 && locales.includes(code)) ?? locales[0];
43
+ const fallbackLocale = pick([fallbackLocaleProp, appFallbackLocale]);
44
+ const [activeState, setActiveState] = React.useState(
45
+ () => pick([defaultLocale, uiLocale, fallbackLocale])
46
+ );
47
+ const active = locales.includes(activeState) ? activeState : pick([defaultLocale, fallbackLocale]);
48
+ const [innerValue, setInnerValue] = React.useState(defaultValue ?? NO_VALUE);
49
+ const current = valueProp ?? innerValue;
50
+ const currentRef = React.useRef(current);
51
+ currentRef.current = current;
52
+ const handleText = (text) => {
53
+ const next = { ...currentRef.current, [active]: text };
54
+ currentRef.current = next;
55
+ if (valueProp === void 0) setInnerValue(next);
56
+ onValueChange?.(next);
57
+ };
58
+ const activeText = current[active] ?? "";
59
+ const fallbackText = active !== fallbackLocale && activeText === "" ? current[fallbackLocale] : void 0;
60
+ const activeError = errors?.[active];
61
+ const hasActiveError = activeError !== void 0 && activeError !== null && activeError !== false && activeError !== "";
62
+ const insideFormField = useReportFieldError(hasActiveError ? activeError : void 0);
63
+ const baseId = React.useId();
64
+ const tabId = (code) => `${baseId}-tab-${idPart(code)}`;
65
+ const panelId = `${baseId}-panel`;
66
+ const switcherNameId = `${baseId}-languages`;
67
+ const localeNameId = `${baseId}-locale`;
68
+ const ownErrorId = `${baseId}-error`;
69
+ const labelledBy = rest["aria-labelledby"];
70
+ const ariaLabel = rest["aria-label"];
71
+ const showOwnError = hasActiveError && !insideFormField;
72
+ const controlProps = {
73
+ ...rest,
74
+ ref: forwardedRef,
75
+ lang: active,
76
+ value: activeText,
77
+ onValueChange: handleText,
78
+ placeholder: fallbackText ? fallbackText : placeholder,
79
+ // The control is named by the field AND the locale it holds: "商品名 English".
80
+ "aria-labelledby": labelledBy ? `${labelledBy} ${localeNameId}` : void 0,
81
+ "aria-invalid": hasActiveError ? true : rest["aria-invalid"],
82
+ "aria-errormessage": mergeAriaIds(
83
+ rest["aria-errormessage"],
84
+ showOwnError ? ownErrorId : void 0
85
+ ),
86
+ status: hasActiveError ? "error" : rest.status
87
+ };
88
+ return /* @__PURE__ */ jsxs(
89
+ "div",
90
+ {
91
+ "data-slot": "translatable-field",
92
+ "data-size": size,
93
+ className: cn("ui-translatable-field", className),
94
+ children: [
95
+ /* @__PURE__ */ jsx(
96
+ Tabs,
97
+ {
98
+ value: active,
99
+ onValueChange: setActiveState,
100
+ size: size === "lg" ? "md" : "sm",
101
+ controls: panelId,
102
+ className: "ui-translatable-field-tabs",
103
+ ...labelledBy ? { "aria-labelledby": `${labelledBy} ${switcherNameId}` } : {
104
+ "aria-label": [ariaLabel, t("dataEntry.translatable.languages")].filter(Boolean).join(" ")
105
+ },
106
+ children: /* @__PURE__ */ jsx(TabsList, { children: locales.map((code) => {
107
+ const error = errors?.[code];
108
+ const invalid = error !== void 0 && error !== null && error !== false && error !== "";
109
+ return /* @__PURE__ */ jsxs(
110
+ TabsTrigger,
111
+ {
112
+ id: tabId(code),
113
+ value: code,
114
+ "data-locale": code,
115
+ "data-invalid": invalid ? "true" : void 0,
116
+ "data-filled": current[code] ? "true" : void 0,
117
+ children: [
118
+ /* @__PURE__ */ jsx("span", { children: localeName(code, uiLocale) }),
119
+ invalid ? /* @__PURE__ */ jsxs(Fragment, { children: [
120
+ /* @__PURE__ */ jsx(CircleAlert, { className: "ui-translatable-field-error-mark", "aria-hidden": "true" }),
121
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: `, ${t("dataEntry.translatable.hasError")}` })
122
+ ] }) : null
123
+ ]
124
+ },
125
+ code
126
+ );
127
+ }) })
128
+ }
129
+ ),
130
+ /* @__PURE__ */ jsx("span", { id: switcherNameId, hidden: true, children: t("dataEntry.translatable.languages") }),
131
+ /* @__PURE__ */ jsx("span", { id: localeNameId, hidden: true, children: localeName(active, uiLocale) }),
132
+ /* @__PURE__ */ jsx(
133
+ "div",
134
+ {
135
+ role: "tabpanel",
136
+ id: panelId,
137
+ "aria-labelledby": tabId(active),
138
+ className: "ui-translatable-field-panel",
139
+ children: kind === "input" ? /* @__PURE__ */ jsx(
140
+ Input,
141
+ {
142
+ ...controlProps,
143
+ size
144
+ }
145
+ ) : /* @__PURE__ */ jsx(
146
+ Textarea,
147
+ {
148
+ ...controlProps,
149
+ size
150
+ }
151
+ )
152
+ }
153
+ ),
154
+ showOwnError ? /* @__PURE__ */ jsx("p", { id: ownErrorId, role: "alert", className: "ui-form-field-note text-error-strong text-xs", children: activeError }) : null
155
+ ]
156
+ }
157
+ );
158
+ }
159
+ const TranslatableInput = React.forwardRef(
160
+ (props, ref) => /* @__PURE__ */ jsx(
161
+ TranslatableField,
162
+ {
163
+ ...props,
164
+ kind: "input",
165
+ forwardedRef: ref
166
+ }
167
+ )
168
+ );
169
+ TranslatableInput.displayName = "TranslatableInput";
170
+ const TranslatableTextarea = React.forwardRef(
171
+ (props, ref) => /* @__PURE__ */ jsx(
172
+ TranslatableField,
173
+ {
174
+ ...props,
175
+ kind: "textarea",
176
+ forwardedRef: ref
177
+ }
178
+ )
179
+ );
180
+ TranslatableTextarea.displayName = "TranslatableTextarea";
181
+ export {
182
+ TranslatableInput,
183
+ TranslatableTextarea
184
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "33.1.0",
3
+ "version": "33.2.1",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -278,6 +278,10 @@
278
278
  "number": "Contains number",
279
279
  "symbol": "Contains symbol"
280
280
  }
281
+ },
282
+ "translatable": {
283
+ "languages": "Language",
284
+ "hasError": "has an error"
281
285
  }
282
286
  },
283
287
  "feedback": {
@@ -273,6 +273,10 @@
273
273
  "number": "数字を含む",
274
274
  "symbol": "記号を含む"
275
275
  }
276
+ },
277
+ "translatable": {
278
+ "languages": "言語",
279
+ "hasError": "エラーあり"
276
280
  }
277
281
  },
278
282
  "feedback": {
@@ -274,6 +274,10 @@
274
274
  "number": "Có chữ số",
275
275
  "symbol": "Có ký hiệu"
276
276
  }
277
+ },
278
+ "translatable": {
279
+ "languages": "Ngôn ngữ",
280
+ "hasError": "có lỗi"
277
281
  }
278
282
  },
279
283
  "feedback": {
@@ -106,3 +106,18 @@ export declare function pickGroupFieldA11y(props: FieldA11yProps): {
106
106
  "aria-describedby"?: string;
107
107
  "data-field"?: string;
108
108
  };
109
+ /**
110
+ * A control that owns SEVERAL values reports which of its messages is current (gh#1336). A
111
+ * `TranslatableInput` holds one value per locale and gets one message per locale; only the active
112
+ * locale's message describes the text the user is looking at, and only the control knows which
113
+ * locale that is. FormField reads the report as the lowest-precedence source of its `error` — its
114
+ * own `error` prop and the form's error bag both win — so the message renders in FormField's own
115
+ * error line and reaches the control through the same `aria-invalid` / `aria-errormessage` wiring
116
+ * every other field uses. `null` outside a FormField.
117
+ */
118
+ export declare const FieldErrorReportContext: React.Context<((message: React.ReactNode) => void) | null>;
119
+ /**
120
+ * Report `message` (or its absence) to the enclosing FormField. Returns `true` when a FormField is
121
+ * listening — a caller with no FormField above it has to render its own message.
122
+ */
123
+ export declare function useReportFieldError(message: React.ReactNode): boolean;
@@ -78,7 +78,18 @@ function pickGroupFieldA11y(props) {
78
78
  ...props["data-field"] !== void 0 ? { "data-field": props["data-field"] } : {}
79
79
  };
80
80
  }
81
+ const FieldErrorReportContext = React.createContext(null);
82
+ function useReportFieldError(message) {
83
+ const report = React.useContext(FieldErrorReportContext);
84
+ React.useEffect(() => {
85
+ if (!report) return;
86
+ report(message);
87
+ return () => report(void 0);
88
+ }, [report, message]);
89
+ return report !== null;
90
+ }
81
91
  export {
92
+ FieldErrorReportContext,
82
93
  FieldIdentityContext,
83
94
  FieldNameContext,
84
95
  mergeAriaIds,
@@ -88,5 +99,6 @@ export {
88
99
  resolveFieldA11y,
89
100
  useFieldIdentity,
90
101
  useFieldNameFallback,
91
- useMirroredInputAttributes
102
+ useMirroredInputAttributes,
103
+ useReportFieldError
92
104
  };
@@ -30,3 +30,4 @@ export declare function dateTimeFormat(locale: string, options?: Intl.DateTimeFo
30
30
  export declare function numberFormat(locale: string, options?: Intl.NumberFormatOptions): Intl.NumberFormat;
31
31
  export declare function pluralRules(locale: string, options?: Intl.PluralRulesOptions): Intl.PluralRules;
32
32
  export declare function listFormat(locale: string, options?: Intl.ListFormatOptions): Intl.ListFormat;
33
+ export declare function displayNames(locale: string, options: Intl.DisplayNamesOptions): Intl.DisplayNames;
@@ -21,8 +21,12 @@ function pluralRules(locale, options) {
21
21
  function listFormat(locale, options) {
22
22
  return memoise("lf", locale, options, () => new Intl.ListFormat(locale, options));
23
23
  }
24
+ function displayNames(locale, options) {
25
+ return memoise("dn", locale, options, () => new Intl.DisplayNames(locale, options));
26
+ }
24
27
  export {
25
28
  dateTimeFormat,
29
+ displayNames,
26
30
  listFormat,
27
31
  numberFormat,
28
32
  pluralRules