@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.
- package/README.md +4 -4
- package/agent/START-HERE.md +6 -6
- package/agent/components/Command.json +9 -1
- package/agent/components/TranslatableInput.json +92 -0
- package/agent/components/TranslatableTextarea.json +84 -0
- package/agent/components-index.json +10 -0
- package/agent/components.json +185 -1
- package/agent/index.json +7 -7
- package/agent/llms.txt +7 -7
- package/agent/patterns/list-filters.json +1 -1
- package/agent/patterns.json +1 -1
- package/agent/tokens.json +12 -0
- package/dist/components/data-entry/command.d.ts +22 -1
- package/dist/components/data-entry/command.js +14 -1
- package/dist/components/data-entry/form-field.js +10 -4
- package/dist/components/data-entry/index.d.ts +4 -2
- package/dist/components/data-entry/index.js +6 -1
- package/dist/components/data-entry/translatable-input.d.ts +29 -0
- package/dist/components/data-entry/translatable-input.js +184 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +4 -0
- package/dist/i18n/messages/ja.json +4 -0
- package/dist/i18n/messages/vi.json +4 -0
- package/dist/lib/field-a11y.d.ts +15 -0
- package/dist/lib/field-a11y.js +13 -1
- package/dist/lib/intl-cache.d.ts +1 -0
- package/dist/lib/intl-cache.js +4 -0
- package/dist/props/components/data-entry.prop.d.ts +46 -0
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/registry.d.ts +51 -0
- package/dist/props/registry.js +79 -0
- package/dist/styles/control.css +15 -0
- package/dist/styles/data-entry-layout.css +25 -0
- package/dist/styles/layers.json +19 -1
- package/dist/styles.css +1 -1
- package/dist/tokens/components/data-entry.css +4 -0
- 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
|
|
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": [
|
package/agent/patterns.json
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
|
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,
|
package/dist/lib/field-a11y.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/field-a11y.js
CHANGED
|
@@ -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
|
};
|
package/dist/lib/intl-cache.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/intl-cache.js
CHANGED
|
@@ -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
|