@godxjp/ui 32.0.5 → 32.1.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 (62) hide show
  1. package/README.md +2 -2
  2. package/agent/START-HERE.md +7 -7
  3. package/agent/components/Calendar.json +1 -1
  4. package/agent/components/Checkbox.json +1 -1
  5. package/agent/components/CheckboxGroup.json +6 -0
  6. package/agent/components/CommandPalette.json +6 -0
  7. package/agent/components/DatePicker.json +6 -0
  8. package/agent/components/DropdownMenu.json +5 -0
  9. package/agent/components/ListRow.json +2 -1
  10. package/agent/components/PasswordStrength.json +3 -3
  11. package/agent/components/SearchInput.json +6 -0
  12. package/agent/components/Select.json +8 -2
  13. package/agent/components/Toaster.json +15 -4
  14. package/agent/components/Toggle.json +3 -2
  15. package/agent/components/ToggleGroup.json +1 -1
  16. package/agent/components-index.json +1 -1
  17. package/agent/components.json +63 -15
  18. package/agent/index.json +6 -6
  19. package/agent/llms.txt +6 -6
  20. package/agent/patterns/card-grid-selection.json +21 -0
  21. package/agent/patterns/dependent-selects.json +20 -0
  22. package/agent/patterns-index.json +39 -0
  23. package/agent/patterns.json +41 -0
  24. package/agent/tokens.json +60 -0
  25. package/dist/components/data-entry/checkbox-group.d.ts +1 -1
  26. package/dist/components/data-entry/checkbox-group.js +58 -19
  27. package/dist/components/data-entry/command-palette.d.ts +21 -1
  28. package/dist/components/data-entry/command-palette.js +34 -34
  29. package/dist/components/data-entry/date-picker.js +21 -1
  30. package/dist/components/data-entry/index.d.ts +1 -1
  31. package/dist/components/data-entry/password-strength.d.ts +2 -0
  32. package/dist/components/data-entry/password-strength.js +8 -7
  33. package/dist/components/data-entry/search-input.d.ts +1 -1
  34. package/dist/components/data-entry/search-input.js +22 -2
  35. package/dist/components/data-entry/search-select.js +68 -13
  36. package/dist/components/data-entry/toggle.d.ts +2 -2
  37. package/dist/components/data-entry/toggle.js +5 -1
  38. package/dist/components/feedback/sonner.d.ts +7 -1
  39. package/dist/components/feedback/sonner.js +22 -2
  40. package/dist/components/feedback/use-toast.d.ts +3 -1
  41. package/dist/components/feedback/use-toast.js +10 -1
  42. package/dist/components/navigation/dropdown-menu.d.ts +7 -1
  43. package/dist/components/navigation/dropdown-menu.js +18 -2
  44. package/dist/contracts/measurement.json +1 -1
  45. package/dist/i18n/messages/en.json +19 -1
  46. package/dist/i18n/messages/ja.json +18 -1
  47. package/dist/i18n/messages/vi.json +18 -1
  48. package/dist/lib/shortcut.d.ts +17 -0
  49. package/dist/lib/shortcut.js +37 -0
  50. package/dist/props/components/data-entry.prop.d.ts +36 -0
  51. package/dist/styles/control.css +35 -3
  52. package/dist/styles/data-display-layout.css +7 -0
  53. package/dist/styles/layers.json +6 -1
  54. package/dist/styles/navigation-layout.css +26 -0
  55. package/dist/styles/table-layout.css +14 -0
  56. package/dist/styles/toggle.css +14 -0
  57. package/dist/styles.css +1 -1
  58. package/dist/tokens/components/control.css +10 -0
  59. package/dist/tokens/components/navigation.css +6 -0
  60. package/dist/tokens/foundation.css +2 -0
  61. package/package.json +2 -2
  62. package/scripts/ui-audit.mjs +4 -1
package/agent/index.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "counts": {
3
3
  "anti-ai-tells": 26,
4
4
  "components": 161,
5
- "patterns": 22,
5
+ "patterns": 24,
6
6
  "rules": 43,
7
- "tokens": 2183,
7
+ "tokens": 2193,
8
8
  "vocabulary": 14
9
9
  },
10
10
  "files": [
@@ -48,19 +48,19 @@
48
48
  "note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
49
49
  "read": {
50
50
  "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
51
- "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.0.5/agent/index.json"
51
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.1.1/agent/index.json"
52
52
  },
53
53
  "source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
54
54
  "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
55
55
  "tokenTiers": {
56
56
  "component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
57
57
  "counts": {
58
- "component": 1861,
59
- "foundation": 210,
58
+ "component": 1870,
59
+ "foundation": 211,
60
60
  "semantic": 112
61
61
  },
62
62
  "foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
63
63
  "semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
64
64
  },
65
- "version": "32.0.5"
65
+ "version": "32.1.1"
66
66
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 161 components, 2183 design tokens,
4
- > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.0.5.
3
+ > A Japanese-enterprise React design system: 161 components, 2193 design tokens,
4
+ > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.1.1.
5
5
 
6
6
  If your client can run a process, do not read these files — run the MCP server instead
7
- (`npx @godxjp/ui-mcp@32.0.5`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@32.1.1`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -14,11 +14,11 @@ that can only fetch URLs.
14
14
 
15
15
  ## Catalog
16
16
 
17
- - [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 22 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
17
+ - [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 24 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
18
18
  - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 47 KB — all 161 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
19
19
  - [components/&lt;Name&gt;.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–36 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
20
20
  - [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.2 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
21
- - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 210 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 112 `semantic` roles, 1861 `component` knobs.
21
+ - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 112 `semantic` roles, 1870 `component` knobs.
22
22
  - [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
23
23
  - [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 43 cardinal rules.
24
24
  - [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 26 shapes that make generated UI look generated, each with its fix.
@@ -26,7 +26,7 @@ that can only fetch URLs.
26
26
  ## Pinning
27
27
 
28
28
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
29
- the tag: `.../godx-jp/godxjp-ui/v32.0.5/agent/...`. A catalog that does not match the installed
29
+ the tag: `.../godx-jp/godxjp-ui/v32.1.1/agent/...`. A catalog that does not match the installed
30
30
  package describes props that are absent, or hides props that are present, and says nothing either way.
31
31
 
32
32
  Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
@@ -0,0 +1,21 @@
1
+ {
2
+ "aliases": [
3
+ "card-selection",
4
+ "selectable-cards",
5
+ "bulk-delete-cards",
6
+ "grid-bulk-actions"
7
+ ],
8
+ "code": "// Live page: /showcase/card-grid-selection (docs/showcase/card-grid-selection.tsx).\n// DataTable.BulkActions reads the table context only when one exists — outside a table pass\n// `count`: it renders nothing at 0, and the localised \"N selected\" + a labelled region above 0.\n// The Checkbox's inline label is the item name: the name is the click target and the\n// accessible name. Do NOT make the whole Card an onClick div.\nimport * as React from \"react\";\nimport { FolderOpen, Trash2 } from \"lucide-react\";\nimport { Card, CardContent, DataTable, EmptyState } from \"@godxjp/ui/data-display\";\nimport { Checkbox } from \"@godxjp/ui/data-entry\";\nimport { AlertDialog, Toaster, toast } from \"@godxjp/ui/feedback\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { useTranslation } from \"@godxjp/ui/i18n\";\n\ntype Item = { id: string; name: string };\n\nexport function SelectableCardGrid({ source }: { source: Item[] }) {\n const { t } = useTranslation();\n const [items, setItems] = React.useState(source);\n const [selected, setSelected] = React.useState<Set<string>>(new Set());\n const [confirmOpen, setConfirmOpen] = React.useState(false);\n const [confirmCount, setConfirmCount] = React.useState(0); // frozen: selection clears on confirm\n\n const all = items.length > 0 && items.every((i) => selected.has(i.id));\n const selectAll: boolean | \"indeterminate\" = all ? true : selected.size > 0 ? \"indeterminate\" : false;\n\n const deleteSelected = () => {\n const removed = new Set(selected);\n setItems((cur) => cur.filter((i) => !removed.has(i.id)));\n setSelected(new Set());\n toast(t(\"files.deleted\", { count: removed.size }), {\n action: {\n label: t(\"files.undo\"),\n // restore in the SOURCE order, not appended at the end\n onClick: () => setItems((cur) => source.filter((i) => removed.has(i.id) || cur.some((c) => c.id === i.id))),\n },\n });\n };\n\n if (items.length === 0) {\n return (\n <EmptyState\n icon={FolderOpen}\n variant=\"section\"\n title={t(\"files.emptyTitle\")}\n action={<Button variant=\"outline\" onClick={() => setItems(source)}>{t(\"files.reset\")}</Button>}\n />\n );\n }\n\n return (\n <Flex direction=\"col\" gap=\"md\">\n <Flex direction=\"row\" align=\"center\" gap=\"md\" wrap>\n <Checkbox\n checked={selectAll}\n onCheckedChange={(next) => setSelected(next === true ? new Set(items.map((i) => i.id)) : new Set())}\n >\n {t(\"files.selectAll\")}\n </Checkbox>\n <DataTable.BulkActions count={selected.size}>\n <Button variant=\"destructive\" size=\"sm\" onClick={() => { setConfirmCount(selected.size); setConfirmOpen(true); }}>\n <Trash2 aria-hidden=\"true\" />\n {t(\"files.delete\")}\n </Button>\n </DataTable.BulkActions>\n </Flex>\n <ResponsiveGrid columns={{ base: 1, sm: 2, lg: 3 }}>\n {items.map((item) => (\n <Card key={item.id} className={selected.has(item.id) ? \"border-primary\" : undefined}>\n <CardContent>\n <Checkbox\n checked={selected.has(item.id)}\n onCheckedChange={(on) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (on === true) next.add(item.id); else next.delete(item.id);\n return next;\n })\n }\n >\n {item.name}\n </Checkbox>\n </CardContent>\n </Card>\n ))}\n </ResponsiveGrid>\n <AlertDialog\n open={confirmOpen}\n onOpenChange={setConfirmOpen}\n variant=\"destructive\"\n title={t(\"files.confirmTitle\", { count: confirmCount })}\n confirmLabel={t(\"files.delete\")}\n onConfirm={deleteSelected}\n />\n <Toaster />{/* once per app — usually in the root layout, not here */}\n </Flex>\n );\n}",
9
+ "name": "card-grid-selection",
10
+ "tagline": "Select cards in a grid and act on them in bulk — Card + Checkbox per item, the real DataTable.BulkActions driven by count outside a table, indeterminate select-all, AlertDialog before delete, toast with Undo after, EmptyState when nothing is left.",
11
+ "tags": [
12
+ "selection",
13
+ "bulk",
14
+ "cards",
15
+ "grid",
16
+ "delete",
17
+ "undo",
18
+ "empty",
19
+ "checkbox"
20
+ ]
21
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "aliases": [
3
+ "cascading-selects",
4
+ "country-state-city",
5
+ "chained-selects",
6
+ "linked-selects"
7
+ ],
8
+ "code": "// Live page: /showcase/form-dependent-selects (docs/showcase/form-dependent-selects.tsx).\n// Three rules: (1) child disabled until parent has a value + helper saying why;\n// (2) child loading while its options fetch; (3) parent change resets EVERY descendant to \"\".\n// Reset to \"\", Select's controlled empty value — undefined flips it to uncontrolled.\nimport * as React from \"react\";\nimport { Form, FormField, Select } from \"@godxjp/ui/data-entry\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useTranslation } from \"@godxjp/ui/i18n\";\n\ntype Option = { value: string; label: string };\ntype Address = { country: string; region: string; city: string };\n\n// Fetch options for a parent value. The answer is stored WITH the parent it answers, so\n// `loading` is derived and a slow answer for an OLD parent is never shown on the new one.\nfunction useChildOptions(parent: string, fetchOptions: (parent: string) => Promise<Option[]>) {\n const [answer, setAnswer] = React.useState<{ parent: string; options: Option[] }>({ parent: \"\", options: [] });\n React.useEffect(() => {\n if (!parent) return;\n let live = true;\n void fetchOptions(parent).then((options) => { if (live) setAnswer({ parent, options }); });\n return () => { live = false; };\n }, [parent, fetchOptions]);\n const ready = parent !== \"\" && answer.parent === parent;\n return { options: ready ? answer.options : [], loading: parent !== \"\" && !ready };\n}\n\nexport function AddressFields({\n fetchRegions,\n fetchCities,\n}: {\n fetchRegions: (country: string) => Promise<Option[]>; // stable (module-level or useCallback)\n fetchCities: (region: string) => Promise<Option[]>;\n}) {\n const { t, locale } = useTranslation();\n const [value, setValue] = React.useState<Address>({ country: \"\", region: \"\", city: \"\" });\n const regions = useChildOptions(value.country, fetchRegions);\n const cities = useChildOptions(value.region, fetchCities);\n\n // ISO 3166-1 alpha-2 → the reader's language, no catalogue needed.\n const countries = React.useMemo(() => {\n const names = new Intl.DisplayNames([locale], { type: \"region\" });\n return [\"JP\", \"US\", \"VN\"].map((code) => ({ value: code, label: names.of(code) ?? code }));\n }, [locale]);\n\n return (\n <Form controlWidth=\"20rem\">\n <Flex direction=\"col\" gap=\"md\">\n <FormField id=\"country\" label={t(\"address.country\")}>\n <Select\n value={value.country}\n onValueChange={(country: string) => setValue({ country, region: \"\", city: \"\" })}\n options={countries}\n placeholder={t(\"address.countryPlaceholder\")}\n />\n </FormField>\n <FormField\n id=\"region\"\n label={t(\"address.region\")}\n helper={value.country ? undefined : t(\"address.selectCountryFirst\")}\n >\n <Select\n value={value.region}\n onValueChange={(region: string) => setValue({ ...value, region, city: \"\" })}\n options={regions.options}\n disabled={!value.country}\n loading={regions.loading}\n placeholder={t(\"address.regionPlaceholder\")}\n />\n </FormField>\n <FormField\n id=\"city\"\n label={t(\"address.city\")}\n helper={value.region ? undefined : t(\"address.selectRegionFirst\")}\n >\n <Select\n value={value.city}\n onValueChange={(city: string) => setValue({ ...value, city })}\n options={cities.options}\n disabled={!value.region}\n loading={cities.loading}\n placeholder={t(\"address.cityPlaceholder\")}\n />\n </FormField>\n </Flex>\n </Form>\n );\n}",
9
+ "name": "dependent-selects",
10
+ "tagline": "Country → state → city: three Selects where each child is disabled until its parent has a value (FormField helper says why), loading while its options fetch, and reset to \"\" when the parent changes. A composition, not a component — reach for Cascader only when it is ONE field.",
11
+ "tags": [
12
+ "form",
13
+ "select",
14
+ "dependent",
15
+ "cascading",
16
+ "address",
17
+ "loading",
18
+ "reset"
19
+ ]
20
+ }
@@ -91,6 +91,45 @@
91
91
  "delete"
92
92
  ]
93
93
  },
94
+ {
95
+ "aliases": [
96
+ "cascading-selects",
97
+ "country-state-city",
98
+ "chained-selects",
99
+ "linked-selects"
100
+ ],
101
+ "name": "dependent-selects",
102
+ "tagline": "Country → state → city: three Selects where each child is disabled until its parent has a value (FormField helper says why), loading while its options fetch, and reset to \"\" when the parent changes. A composition, not a component — reach for Cascader only when it is ONE field.",
103
+ "tags": [
104
+ "form",
105
+ "select",
106
+ "dependent",
107
+ "cascading",
108
+ "address",
109
+ "loading",
110
+ "reset"
111
+ ]
112
+ },
113
+ {
114
+ "aliases": [
115
+ "card-selection",
116
+ "selectable-cards",
117
+ "bulk-delete-cards",
118
+ "grid-bulk-actions"
119
+ ],
120
+ "name": "card-grid-selection",
121
+ "tagline": "Select cards in a grid and act on them in bulk — Card + Checkbox per item, the real DataTable.BulkActions driven by count outside a table, indeterminate select-all, AlertDialog before delete, toast with Undo after, EmptyState when nothing is left.",
122
+ "tags": [
123
+ "selection",
124
+ "bulk",
125
+ "cards",
126
+ "grid",
127
+ "delete",
128
+ "undo",
129
+ "empty",
130
+ "checkbox"
131
+ ]
132
+ },
94
133
  {
95
134
  "name": "deferred-loading",
96
135
  "tagline": "Inertia deferred props with a Skeleton fallback — SkeletonTable while data loads, then DataTable (real @godxjp/ui API).",
@@ -97,6 +97,47 @@
97
97
  "delete"
98
98
  ]
99
99
  },
100
+ {
101
+ "aliases": [
102
+ "cascading-selects",
103
+ "country-state-city",
104
+ "chained-selects",
105
+ "linked-selects"
106
+ ],
107
+ "code": "// Live page: /showcase/form-dependent-selects (docs/showcase/form-dependent-selects.tsx).\n// Three rules: (1) child disabled until parent has a value + helper saying why;\n// (2) child loading while its options fetch; (3) parent change resets EVERY descendant to \"\".\n// Reset to \"\", Select's controlled empty value — undefined flips it to uncontrolled.\nimport * as React from \"react\";\nimport { Form, FormField, Select } from \"@godxjp/ui/data-entry\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useTranslation } from \"@godxjp/ui/i18n\";\n\ntype Option = { value: string; label: string };\ntype Address = { country: string; region: string; city: string };\n\n// Fetch options for a parent value. The answer is stored WITH the parent it answers, so\n// `loading` is derived and a slow answer for an OLD parent is never shown on the new one.\nfunction useChildOptions(parent: string, fetchOptions: (parent: string) => Promise<Option[]>) {\n const [answer, setAnswer] = React.useState<{ parent: string; options: Option[] }>({ parent: \"\", options: [] });\n React.useEffect(() => {\n if (!parent) return;\n let live = true;\n void fetchOptions(parent).then((options) => { if (live) setAnswer({ parent, options }); });\n return () => { live = false; };\n }, [parent, fetchOptions]);\n const ready = parent !== \"\" && answer.parent === parent;\n return { options: ready ? answer.options : [], loading: parent !== \"\" && !ready };\n}\n\nexport function AddressFields({\n fetchRegions,\n fetchCities,\n}: {\n fetchRegions: (country: string) => Promise<Option[]>; // stable (module-level or useCallback)\n fetchCities: (region: string) => Promise<Option[]>;\n}) {\n const { t, locale } = useTranslation();\n const [value, setValue] = React.useState<Address>({ country: \"\", region: \"\", city: \"\" });\n const regions = useChildOptions(value.country, fetchRegions);\n const cities = useChildOptions(value.region, fetchCities);\n\n // ISO 3166-1 alpha-2 → the reader's language, no catalogue needed.\n const countries = React.useMemo(() => {\n const names = new Intl.DisplayNames([locale], { type: \"region\" });\n return [\"JP\", \"US\", \"VN\"].map((code) => ({ value: code, label: names.of(code) ?? code }));\n }, [locale]);\n\n return (\n <Form controlWidth=\"20rem\">\n <Flex direction=\"col\" gap=\"md\">\n <FormField id=\"country\" label={t(\"address.country\")}>\n <Select\n value={value.country}\n onValueChange={(country: string) => setValue({ country, region: \"\", city: \"\" })}\n options={countries}\n placeholder={t(\"address.countryPlaceholder\")}\n />\n </FormField>\n <FormField\n id=\"region\"\n label={t(\"address.region\")}\n helper={value.country ? undefined : t(\"address.selectCountryFirst\")}\n >\n <Select\n value={value.region}\n onValueChange={(region: string) => setValue({ ...value, region, city: \"\" })}\n options={regions.options}\n disabled={!value.country}\n loading={regions.loading}\n placeholder={t(\"address.regionPlaceholder\")}\n />\n </FormField>\n <FormField\n id=\"city\"\n label={t(\"address.city\")}\n helper={value.region ? undefined : t(\"address.selectRegionFirst\")}\n >\n <Select\n value={value.city}\n onValueChange={(city: string) => setValue({ ...value, city })}\n options={cities.options}\n disabled={!value.region}\n loading={cities.loading}\n placeholder={t(\"address.cityPlaceholder\")}\n />\n </FormField>\n </Flex>\n </Form>\n );\n}",
108
+ "name": "dependent-selects",
109
+ "tagline": "Country → state → city: three Selects where each child is disabled until its parent has a value (FormField helper says why), loading while its options fetch, and reset to \"\" when the parent changes. A composition, not a component — reach for Cascader only when it is ONE field.",
110
+ "tags": [
111
+ "form",
112
+ "select",
113
+ "dependent",
114
+ "cascading",
115
+ "address",
116
+ "loading",
117
+ "reset"
118
+ ]
119
+ },
120
+ {
121
+ "aliases": [
122
+ "card-selection",
123
+ "selectable-cards",
124
+ "bulk-delete-cards",
125
+ "grid-bulk-actions"
126
+ ],
127
+ "code": "// Live page: /showcase/card-grid-selection (docs/showcase/card-grid-selection.tsx).\n// DataTable.BulkActions reads the table context only when one exists — outside a table pass\n// `count`: it renders nothing at 0, and the localised \"N selected\" + a labelled region above 0.\n// The Checkbox's inline label is the item name: the name is the click target and the\n// accessible name. Do NOT make the whole Card an onClick div.\nimport * as React from \"react\";\nimport { FolderOpen, Trash2 } from \"lucide-react\";\nimport { Card, CardContent, DataTable, EmptyState } from \"@godxjp/ui/data-display\";\nimport { Checkbox } from \"@godxjp/ui/data-entry\";\nimport { AlertDialog, Toaster, toast } from \"@godxjp/ui/feedback\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { useTranslation } from \"@godxjp/ui/i18n\";\n\ntype Item = { id: string; name: string };\n\nexport function SelectableCardGrid({ source }: { source: Item[] }) {\n const { t } = useTranslation();\n const [items, setItems] = React.useState(source);\n const [selected, setSelected] = React.useState<Set<string>>(new Set());\n const [confirmOpen, setConfirmOpen] = React.useState(false);\n const [confirmCount, setConfirmCount] = React.useState(0); // frozen: selection clears on confirm\n\n const all = items.length > 0 && items.every((i) => selected.has(i.id));\n const selectAll: boolean | \"indeterminate\" = all ? true : selected.size > 0 ? \"indeterminate\" : false;\n\n const deleteSelected = () => {\n const removed = new Set(selected);\n setItems((cur) => cur.filter((i) => !removed.has(i.id)));\n setSelected(new Set());\n toast(t(\"files.deleted\", { count: removed.size }), {\n action: {\n label: t(\"files.undo\"),\n // restore in the SOURCE order, not appended at the end\n onClick: () => setItems((cur) => source.filter((i) => removed.has(i.id) || cur.some((c) => c.id === i.id))),\n },\n });\n };\n\n if (items.length === 0) {\n return (\n <EmptyState\n icon={FolderOpen}\n variant=\"section\"\n title={t(\"files.emptyTitle\")}\n action={<Button variant=\"outline\" onClick={() => setItems(source)}>{t(\"files.reset\")}</Button>}\n />\n );\n }\n\n return (\n <Flex direction=\"col\" gap=\"md\">\n <Flex direction=\"row\" align=\"center\" gap=\"md\" wrap>\n <Checkbox\n checked={selectAll}\n onCheckedChange={(next) => setSelected(next === true ? new Set(items.map((i) => i.id)) : new Set())}\n >\n {t(\"files.selectAll\")}\n </Checkbox>\n <DataTable.BulkActions count={selected.size}>\n <Button variant=\"destructive\" size=\"sm\" onClick={() => { setConfirmCount(selected.size); setConfirmOpen(true); }}>\n <Trash2 aria-hidden=\"true\" />\n {t(\"files.delete\")}\n </Button>\n </DataTable.BulkActions>\n </Flex>\n <ResponsiveGrid columns={{ base: 1, sm: 2, lg: 3 }}>\n {items.map((item) => (\n <Card key={item.id} className={selected.has(item.id) ? \"border-primary\" : undefined}>\n <CardContent>\n <Checkbox\n checked={selected.has(item.id)}\n onCheckedChange={(on) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (on === true) next.add(item.id); else next.delete(item.id);\n return next;\n })\n }\n >\n {item.name}\n </Checkbox>\n </CardContent>\n </Card>\n ))}\n </ResponsiveGrid>\n <AlertDialog\n open={confirmOpen}\n onOpenChange={setConfirmOpen}\n variant=\"destructive\"\n title={t(\"files.confirmTitle\", { count: confirmCount })}\n confirmLabel={t(\"files.delete\")}\n onConfirm={deleteSelected}\n />\n <Toaster />{/* once per app — usually in the root layout, not here */}\n </Flex>\n );\n}",
128
+ "name": "card-grid-selection",
129
+ "tagline": "Select cards in a grid and act on them in bulk — Card + Checkbox per item, the real DataTable.BulkActions driven by count outside a table, indeterminate select-all, AlertDialog before delete, toast with Undo after, EmptyState when nothing is left.",
130
+ "tags": [
131
+ "selection",
132
+ "bulk",
133
+ "cards",
134
+ "grid",
135
+ "delete",
136
+ "undo",
137
+ "empty",
138
+ "checkbox"
139
+ ]
140
+ },
100
141
  {
101
142
  "code": "// Server (Laravel): defer the heavy prop\n// Inertia::render('crm/coupons/index', [\n// 'coupons' => Inertia::defer(fn () => Coupon::all()),\n// ]);\nimport { Card, CardContent, DataTable } from \"@godxjp/ui/data-display\";\nimport type { ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\n\ntype Coupon = { id: string; name: string };\nconst columns: ColumnDef<Coupon>[] = [{ key: \"name\", header: \"クーポン名\" }];\n\n// coupons is undefined until the deferred request resolves\nexport default function Coupons({ coupons }: { coupons?: Coupon[] }) {\n return (\n <Card>\n <CardContent flush>\n {!coupons\n ? <SkeletonTable rows={10} columns={6} />\n : <DataTable data={coupons} columns={columns} getRowId={(c) => c.id} />}\n </CardContent>\n </Card>\n );\n}",
102
143
  "name": "deferred-loading",
package/agent/tokens.json CHANGED
@@ -1241,6 +1241,12 @@
1241
1241
  "tier": "foundation",
1242
1242
  "value": "160ms"
1243
1243
  },
1244
+ {
1245
+ "description": "How long a toast stays before it dismisses itself (gh#1253). In the MOTION tier, like `--activity-interval`: a component-token name has no property word for a duration. Sonner takes `duration` as a JS number, so `Toaster` reads this ONCE on mount with getComputedStyle — from its ThemeScope host when it has one, else `<html>` — and accepts `ms` or `s`. A theme override applies to toasts raised after the Toaster mounts; a per-call `duration` or the Toaster's own `duration` prop still wins. 4000ms is Sonner's own default.",
1246
+ "name": "--toast-duration",
1247
+ "tier": "foundation",
1248
+ "value": "4000ms"
1249
+ },
1244
1250
  {
1245
1251
  "description": "MARQUEE — the TRAVERSAL member of the loop family, and deliberately NOT `--duration-loop`. `--duration-loop` is 1400ms because it times a BREATH: a dot rising and falling, a bar sweeping a small box. A marquee's cycle is a distance being crossed, two orders of magnitude longer, and the two are not the same knob — binding them would mean a service that calms its typing indicator to 2s also whips its logo wall across the screen twice a second. Same tier, same naming, separate value. It is a duration PER SCREENFUL, not per cycle: the component measures one copy of the content and sets `--marquee-cycle-scale` to (copy width ÷ viewport width), so the duration of one lap is `interval × scale` and the PACE is the same whether the track carries one item or forty. A per-cycle duration would make forty logos forty times faster than one, which is how every pure-CSS marquee behaves and why none of them survive real content. 20s per screenful ≈ 60px/s at 1200px — the resting pace a logo wall reads at. The two ordinals are the `speed` prop's `slow` and `fast`; they are tokens rather than multipliers in the stylesheet so a theme can retune the ladder, not just its middle rung.",
1246
1252
  "name": "--marquee-interval",
@@ -3653,6 +3659,36 @@
3653
3659
  "tier": "component",
3654
3660
  "value": "initial"
3655
3661
  },
3662
+ {
3663
+ "description": "Pressed Toggle LABEL colour — the pair to --toggle-on-background, `initial` like it (default hsl(var(--primary-foreground)) at the call site). Without it a service that quietened the pressed fill to a pale tint was left with white text on it.",
3664
+ "name": "--toggle-on-color",
3665
+ "tier": "component",
3666
+ "value": "initial"
3667
+ },
3668
+ {
3669
+ "description": "`Toggle variant=\"quiet\"` pressed state (gh#1257) — role-mirror knobs, `initial`; defaults at the call site: fill hsl(var(--secondary)), label hsl(var(--foreground)), counter hsl(var(--background)) / hsl(var(--foreground)).",
3670
+ "name": "--toggle-quiet-on-background",
3671
+ "tier": "component",
3672
+ "value": "initial"
3673
+ },
3674
+ {
3675
+ "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
3676
+ "name": "--toggle-quiet-on-color",
3677
+ "tier": "component",
3678
+ "value": "initial"
3679
+ },
3680
+ {
3681
+ "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
3682
+ "name": "--toggle-quiet-on-count-background",
3683
+ "tier": "component",
3684
+ "value": "initial"
3685
+ },
3686
+ {
3687
+ "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
3688
+ "name": "--toggle-quiet-on-count-color",
3689
+ "tier": "component",
3690
+ "value": "initial"
3691
+ },
3656
3692
  {
3657
3693
  "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
3658
3694
  "name": "--slider-track-background",
@@ -4469,6 +4505,18 @@
4469
4505
  "tier": "component",
4470
4506
  "value": "var(--space-1)"
4471
4507
  },
4508
+ {
4509
+ "description": "Text inset of a `width=\"full\"` day cell (gh#1258). The full cell reads from the start edge so a number can sit above badges, and with the unruled cell's `padding: 0` the numeral touched the cell edge — 0px left, 3px top, flagged by the library's own `text-edge-inset` rule (cell: >= 8px inline, >= 4px block). `initial` so the density-scaled defaults (var(--space-2) inline, var(--space-1) block) resolve at the call site, not frozen at the :root density. A shift board that wants a tighter or roomier cell sets these.",
4510
+ "name": "--calendar-full-day-padding-inline",
4511
+ "tier": "component",
4512
+ "value": "initial"
4513
+ },
4514
+ {
4515
+ "description": "Control primitive tokens: heights, horizontal padding, adjacent control sizes.",
4516
+ "name": "--calendar-full-day-padding-block",
4517
+ "tier": "component",
4518
+ "value": "initial"
4519
+ },
4472
4520
  {
4473
4521
  "description": "Colour of the day-grid ruling (`Calendar bordered`, on by default). `initial` on purpose: the role default is read at the call site as `var(--calendar-grid-border-color, hsl(var(--border)))` so it follows a scoped theme. The --border tier: L* 93.80 / 1.149:1 on the popover in light, L* 20.76 / 1.270:1 in dark — a rule exactly as heavy as a Card edge or a DataTable row rule. Asked for three times: 25.3.0 drew the grid at all, 27.6.0 lightened the full --input (3.47 / 3.88) to --input/0.5 (1.74 / 1.95, L* 78.67), and gh#730 measured that still darker than the --border every card and table uses and asked for the --border tier itself. Same tier as the RangeTimeline grid. Set any colour to retint, e.g. `hsl(var(--input) / 0.5)` for the 27.6.0 weight or `hsl(var(--input))` for the heavy line.",
4474
4522
  "name": "--calendar-grid-border-color",
@@ -9317,6 +9365,18 @@
9317
9365
  "tier": "component",
9318
9366
  "value": "var(--font-size-base)"
9319
9367
  },
9368
+ {
9369
+ "description": "`DropdownMenuItem description` (gh#1246): the muted second line takes the shortcut's xs step, and a two-line row swaps its fixed height for this block padding.",
9370
+ "name": "--menu-item-description-font-size",
9371
+ "tier": "component",
9372
+ "value": "var( --font-size-xs, calc(var(--font-size-base) / var(--font-size-ratio)) )"
9373
+ },
9374
+ {
9375
+ "description": "Navigation primitive tokens: pagination, filters, compact pickers.",
9376
+ "name": "--menu-item-description-space-block",
9377
+ "tier": "component",
9378
+ "value": "var(--space-1)"
9379
+ },
9320
9380
  {
9321
9381
  "description": "Inset rows (a checkbox/radio row without its own indicator) reserve the indicator column so labels stay aligned with their checked siblings.",
9322
9382
  "name": "--menu-item-inset-space-inline-start",
@@ -1,4 +1,4 @@
1
1
  import * as React from "react";
2
2
  import type { CheckboxGroupProp } from "../../props/components/data-entry.prop.js";
3
3
  export type { CheckboxGroupProp, CheckboxGroupProp as CheckboxGroupProps, } from "../../props/components/data-entry.prop.js";
4
- export declare function CheckboxGroup({ value: controlledValue, defaultValue, onValueChange, options, orientation, disabled, name, id, className, children, ...ariaProps }: CheckboxGroupProp): React.JSX.Element;
4
+ export declare function CheckboxGroup({ value: controlledValue, defaultValue, onValueChange, options, orientation, disabled, name, id, className, children, selectAll, ...ariaProps }: CheckboxGroupProp): React.JSX.Element;
@@ -1,10 +1,12 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "../../lib/utils.js";
5
5
  import { pickGroupFieldA11y, useFieldIdentity } from "../../lib/field-a11y.js";
6
6
  import { Checkbox } from "./checkbox.js";
7
7
  import { Field } from "./field.js";
8
+ import { Separator } from "../layout/separator.js";
9
+ import { useTranslation } from "../../i18n/use-translation.js";
8
10
  import { choiceGroupClassName } from "./choice-option.js";
9
11
  function useControllableArray(controlled, defaultValue) {
10
12
  const [internal, setInternal] = React.useState(defaultValue);
@@ -28,8 +30,10 @@ function CheckboxGroup({
28
30
  id,
29
31
  className,
30
32
  children,
33
+ selectAll,
31
34
  ...ariaProps
32
35
  }) {
36
+ const { t } = useTranslation();
33
37
  const reactId = React.useId();
34
38
  const [value, setValue] = useControllableArray(controlledValue, defaultValue);
35
39
  const groupA11y = pickGroupFieldA11y(ariaProps);
@@ -42,8 +46,18 @@ function CheckboxGroup({
42
46
  setValue(next);
43
47
  onValueChange?.(next);
44
48
  };
49
+ const enabledValues = (options ?? []).filter((opt) => !opt.disabled).map((opt) => opt.value);
50
+ const selectedEnabled = enabledValues.filter((v) => value.includes(v)).length;
51
+ const allState = enabledValues.length > 0 && selectedEnabled === enabledValues.length ? true : selectedEnabled > 0 ? "indeterminate" : false;
52
+ const toggleAll = () => {
53
+ const next = allState === true ? value.filter((v) => !enabledValues.includes(v)) : [...value, ...enabledValues.filter((v) => !value.includes(v))];
54
+ setValue(next);
55
+ onValueChange?.(next);
56
+ };
57
+ const selectAllId = id ? `${id}-select-all` : `${reactId}-select-all`;
58
+ const selectAllLabel = typeof selectAll === "object" && selectAll.label != null ? selectAll.label : t("dataEntry.checkboxGroup.selectAll");
45
59
  if (options && options.length > 0) {
46
- return /* @__PURE__ */ jsx(
60
+ return /* @__PURE__ */ jsxs(
47
61
  "div",
48
62
  {
49
63
  role: "group",
@@ -53,24 +67,49 @@ function CheckboxGroup({
53
67
  "aria-disabled": disabled ? true : void 0,
54
68
  "data-orientation": orientation,
55
69
  className: choiceGroupClassName(orientation, className),
56
- children: options.map((opt, index) => {
57
- const optionId = optionDomId(opt.value, index);
58
- const checked = value.includes(opt.value);
59
- return /* @__PURE__ */ jsx(Field, { id: optionId, label: opt.label, description: opt.description, children: /* @__PURE__ */ jsx(
60
- Checkbox,
61
- {
62
- id: optionId,
63
- name: resolvedName,
64
- "data-field": resolvedField,
65
- value: opt.value,
66
- checked,
67
- disabled: Boolean(disabled) || Boolean(opt.disabled),
68
- onCheckedChange: () => {
69
- toggle(opt.value);
70
+ children: [
71
+ selectAll ? /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ /* @__PURE__ */ jsx(
73
+ Field,
74
+ {
75
+ id: selectAllId,
76
+ label: selectAllLabel,
77
+ description: t("common.selectedOfTotal", {
78
+ count: selectedEnabled,
79
+ total: enabledValues.length
80
+ }),
81
+ children: /* @__PURE__ */ jsx(
82
+ Checkbox,
83
+ {
84
+ id: selectAllId,
85
+ checked: allState,
86
+ disabled: Boolean(disabled) || enabledValues.length === 0,
87
+ onCheckedChange: toggleAll
88
+ }
89
+ )
90
+ }
91
+ ),
92
+ /* @__PURE__ */ jsx(Separator, {})
93
+ ] }) : null,
94
+ options.map((opt, index) => {
95
+ const optionId = optionDomId(opt.value, index);
96
+ const checked = value.includes(opt.value);
97
+ return /* @__PURE__ */ jsx(Field, { id: optionId, label: opt.label, description: opt.description, children: /* @__PURE__ */ jsx(
98
+ Checkbox,
99
+ {
100
+ id: optionId,
101
+ name: resolvedName,
102
+ "data-field": resolvedField,
103
+ value: opt.value,
104
+ checked,
105
+ disabled: Boolean(disabled) || Boolean(opt.disabled),
106
+ onCheckedChange: () => {
107
+ toggle(opt.value);
108
+ }
70
109
  }
71
- }
72
- ) }, opt.value);
73
- })
110
+ ) }, opt.value);
111
+ })
112
+ ]
74
113
  }
75
114
  );
76
115
  }
@@ -27,6 +27,21 @@ export type CommandPaletteLabels = {
27
27
  select?: React.ReactNode;
28
28
  close?: React.ReactNode;
29
29
  };
30
+ /**
31
+ * Recent queries the CONSUMER keeps (gh#1249). The palette only renders them — it never reads or
32
+ * writes `localStorage` or any other store, and it renders `items` exactly as given (de-duplicate
33
+ * and cap them before passing them in).
34
+ */
35
+ export type CommandPaletteRecent = {
36
+ /** Most recent first. Duplicates are rendered as given. */
37
+ items: string[];
38
+ /** A recent query was chosen. The palette closes, like any other selection. */
39
+ onSelect: (query: string) => void;
40
+ /** The trailing "Clear recent searches" row was chosen. The palette stays open. */
41
+ onClear: () => void;
42
+ /** Group heading. Default: `t("dataEntry.commandPalette.recent")`. */
43
+ label?: React.ReactNode;
44
+ };
30
45
  /** The modifier keys held while an item was chosen (gh#1126). */
31
46
  export type CommandPaletteSelectModifiers = {
32
47
  metaKey: boolean;
@@ -90,6 +105,11 @@ export type CommandPaletteProps = {
90
105
  * `mod+shift+p` are different shortcuts. Never fires during IME composition.
91
106
  */
92
107
  shortcut?: boolean | string;
108
+ /**
109
+ * Recent queries (gh#1249), shown as the FIRST group only while the query is empty, with a
110
+ * trailing "Clear recent searches" row. Storage stays with the consumer.
111
+ */
112
+ recent?: CommandPaletteRecent;
93
113
  };
94
114
  /**
95
115
  * Unchanged. - **`shouldFilter={false}` (server-side)** — the PALETTE owns it, derived
@@ -100,4 +120,4 @@ export type CommandPaletteProps = {
100
120
  * it races. Reading `groups` instead makes "did we render the empty state?" a pure function of the
101
121
  * props at that render, which is what a contract test can assert.
102
122
  */
103
- export declare function CommandPalette({ groups, labels, onSelect, open: controlledOpen, defaultOpen, onOpenChange, search: controlledSearch, defaultSearch, onSearchChange, shouldFilter, loading, error, trigger, shortcut, }: CommandPaletteProps): React.JSX.Element;
123
+ export declare function CommandPalette({ groups, labels, onSelect, open: controlledOpen, defaultOpen, onOpenChange, search: controlledSearch, defaultSearch, onSearchChange, shouldFilter, loading, error, trigger, shortcut, recent, }: CommandPaletteProps): React.JSX.Element;
@@ -12,39 +12,9 @@ import {
12
12
  CommandItem,
13
13
  CommandList
14
14
  } from "./command.js";
15
- import { isApplePlatform } from "../../lib/platform.js";
15
+ import { useTranslation } from "../../i18n/use-translation.js";
16
16
  import { isImeComposing } from "../../lib/ime.js";
17
- function parseShortcut(shortcut) {
18
- if (shortcut === false) return null;
19
- const parts = (shortcut === true ? "mod+k" : shortcut).toLowerCase().split("+").map((p) => p.trim());
20
- const key = parts.pop() ?? "";
21
- if (!key) return null;
22
- return {
23
- key,
24
- mod: parts.includes("mod"),
25
- meta: parts.includes("meta") || parts.includes("cmd"),
26
- ctrl: parts.includes("ctrl"),
27
- shift: parts.includes("shift"),
28
- alt: parts.includes("alt") || parts.includes("option")
29
- };
30
- }
31
- function shortcutMatches(spec, event) {
32
- const keyMatches = event.key.toLowerCase() === spec.key || /^[a-z]$/.test(spec.key) && event.code === `Key${spec.key.toUpperCase()}`;
33
- if (!keyMatches) return false;
34
- if (spec.mod && !(isApplePlatform() ? event.metaKey : event.ctrlKey)) return false;
35
- if (spec.meta && !event.metaKey) return false;
36
- if (spec.ctrl && !event.ctrlKey) return false;
37
- return event.shiftKey === spec.shift && event.altKey === spec.alt;
38
- }
39
- function shortcutHint(spec) {
40
- return [
41
- spec.meta || spec.mod && isApplePlatform() ? "\u2318" : "",
42
- spec.ctrl || spec.mod && !isApplePlatform() ? "Ctrl+" : "",
43
- spec.alt ? "\u2325" : "",
44
- spec.shift ? "\u21E7" : "",
45
- spec.key.length === 1 ? spec.key.toUpperCase() : spec.key
46
- ].join("");
47
- }
17
+ import { parseShortcut, shortcutHint, shortcutMatches } from "../../lib/shortcut.js";
48
18
  const modifiersOf = (event) => ({
49
19
  metaKey: event.metaKey,
50
20
  ctrlKey: event.ctrlKey,
@@ -71,8 +41,10 @@ function CommandPalette({
71
41
  loading = false,
72
42
  error,
73
43
  trigger,
74
- shortcut = true
44
+ shortcut = true,
45
+ recent
75
46
  }) {
47
+ const { t } = useTranslation();
76
48
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
77
49
  const [uncontrolledSearch, setUncontrolledSearch] = React.useState(defaultSearch);
78
50
  const shortcutRestoreFocusRef = React.useRef(null);
@@ -120,6 +92,7 @@ function CommandPalette({
120
92
  return () => document.removeEventListener("keydown", handleKeyDown);
121
93
  }, [open, setOpen, shortcutSpec]);
122
94
  const hasItems = React.useMemo(() => groups.some((group) => group.items.length > 0), [groups]);
95
+ const showRecent = recent != null && recent.items.length > 0 && search === "";
123
96
  const triggerNode = trigger === void 0 ? /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "ui-command-palette-trigger", children: [
124
97
  /* @__PURE__ */ jsx(Search, { "aria-hidden": "true" }),
125
98
  /* @__PURE__ */ jsx("span", { children: labels.open }),
@@ -172,11 +145,38 @@ function CommandPalette({
172
145
  }
173
146
  ),
174
147
  /* @__PURE__ */ jsx(CommandList, { "aria-busy": loading, children: loading ? /* @__PURE__ */ jsx("div", { className: "ui-command-palette-state", role: "status", children: labels.loading }) : error ? /* @__PURE__ */ jsx("div", { className: "ui-command-palette-state", role: "alert", children: error }) : /* @__PURE__ */ jsxs(Fragment, { children: [
175
- shouldFilter ? /* @__PURE__ */ jsx(CommandEmpty, { children: labels.empty }) : hasItems ? null : (
148
+ shouldFilter ? /* @__PURE__ */ jsx(CommandEmpty, { children: labels.empty }) : hasItems || showRecent ? null : (
176
149
  // Same node cmdk would have rendered (class + role), decided from `groups`
177
150
  // instead of from its scheduled DOM-registration count — see the doc block.
178
151
  /* @__PURE__ */ jsx("div", { className: "ui-command-empty", role: "presentation", children: labels.empty })
179
152
  ),
153
+ showRecent ? /* @__PURE__ */ jsxs(CommandGroup, { heading: recent.label ?? t("dataEntry.commandPalette.recent"), children: [
154
+ recent.items.map((query, index) => /* @__PURE__ */ jsx(
155
+ CommandItem,
156
+ {
157
+ value: `recent:${index}:${query}`,
158
+ onSelect: () => {
159
+ modifiersRef.current = NO_MODIFIERS;
160
+ setOpen(false);
161
+ recent.onSelect(query);
162
+ },
163
+ children: /* @__PURE__ */ jsx("span", { className: "ui-command-palette-label", children: query })
164
+ },
165
+ `${index}:${query}`
166
+ )),
167
+ /* @__PURE__ */ jsx(
168
+ CommandItem,
169
+ {
170
+ value: "recent:clear",
171
+ className: "ui-command-palette-recent-clear",
172
+ onSelect: () => {
173
+ modifiersRef.current = NO_MODIFIERS;
174
+ recent.onClear();
175
+ },
176
+ children: t("dataEntry.commandPalette.clearRecent")
177
+ }
178
+ )
179
+ ] }) : null,
180
180
  groups.map((group) => /* @__PURE__ */ jsx(CommandGroup, { heading: group.label, children: group.items.map((item) => /* @__PURE__ */ jsxs(
181
181
  CommandItem,
182
182
  {