@entifix/react-controls 0.19.0 → 0.20.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 (203) hide show
  1. package/README.md +8 -0
  2. package/dist/i18n/catalog/en.d.ts.map +1 -1
  3. package/dist/i18n/catalog/en.js +55 -9
  4. package/dist/i18n/catalog/en.js.map +1 -1
  5. package/dist/i18n/catalog/es.d.ts +64 -5
  6. package/dist/i18n/catalog/es.d.ts.map +1 -1
  7. package/dist/i18n/catalog/es.js +61 -9
  8. package/dist/i18n/catalog/es.js.map +1 -1
  9. package/dist/i18n/catalog/index.d.ts +102 -10
  10. package/dist/i18n/catalog/index.d.ts.map +1 -1
  11. package/dist/i18n/entity-labels.d.ts +14 -0
  12. package/dist/i18n/entity-labels.d.ts.map +1 -1
  13. package/dist/i18n/entity-labels.js +15 -0
  14. package/dist/i18n/entity-labels.js.map +1 -1
  15. package/dist/i18n/i18n-context.js +2 -2
  16. package/dist/i18n/i18n-context.js.map +1 -1
  17. package/dist/index.d.ts +6 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/primitives.d.ts +0 -1
  22. package/dist/primitives.d.ts.map +1 -1
  23. package/dist/primitives.js +0 -1
  24. package/dist/primitives.js.map +1 -1
  25. package/dist/theme/index.d.ts +1 -0
  26. package/dist/theme/index.d.ts.map +1 -1
  27. package/dist/theme/index.js +1 -0
  28. package/dist/theme/index.js.map +1 -1
  29. package/dist/theme/theme-context.d.ts +15 -3
  30. package/dist/theme/theme-context.d.ts.map +1 -1
  31. package/dist/theme/theme-context.js +49 -23
  32. package/dist/theme/theme-context.js.map +1 -1
  33. package/dist/theme/theme-cookie.d.ts +9 -0
  34. package/dist/theme/theme-cookie.d.ts.map +1 -0
  35. package/dist/theme/theme-cookie.js +8 -0
  36. package/dist/theme/theme-cookie.js.map +1 -0
  37. package/dist/theme/theme-script.d.ts +8 -6
  38. package/dist/theme/theme-script.d.ts.map +1 -1
  39. package/dist/theme/theme-script.js +21 -12
  40. package/dist/theme/theme-script.js.map +1 -1
  41. package/dist/ui/actions/action-surfaces.d.ts +1 -1
  42. package/dist/ui/actions/action-surfaces.js +1 -1
  43. package/dist/ui/actions/action-surfaces.js.map +1 -1
  44. package/dist/ui/actions/error-actions.d.ts +32 -0
  45. package/dist/ui/actions/error-actions.d.ts.map +1 -0
  46. package/dist/ui/actions/error-actions.js +40 -0
  47. package/dist/ui/actions/error-actions.js.map +1 -0
  48. package/dist/ui/actions/index.d.ts +3 -0
  49. package/dist/ui/actions/index.d.ts.map +1 -1
  50. package/dist/ui/actions/index.js +3 -0
  51. package/dist/ui/actions/index.js.map +1 -1
  52. package/dist/ui/actions/shortcut.d.ts +13 -0
  53. package/dist/ui/actions/shortcut.d.ts.map +1 -0
  54. package/dist/ui/actions/shortcut.js +18 -0
  55. package/dist/ui/actions/shortcut.js.map +1 -0
  56. package/dist/ui/actions/use-verb-invoker.d.ts +55 -0
  57. package/dist/ui/actions/use-verb-invoker.d.ts.map +1 -0
  58. package/dist/ui/actions/use-verb-invoker.js +98 -0
  59. package/dist/ui/actions/use-verb-invoker.js.map +1 -0
  60. package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
  61. package/dist/ui/atoms/cell-value/cell-value.js +28 -14
  62. package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
  63. package/dist/ui/atoms/field-control/field-control.d.ts +1 -0
  64. package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
  65. package/dist/ui/atoms/field-control/field-control.js +2 -0
  66. package/dist/ui/atoms/field-control/field-control.js.map +1 -1
  67. package/dist/ui/atoms/number-input/quantity-input.d.ts +8 -1
  68. package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -1
  69. package/dist/ui/atoms/number-input/quantity-input.js +2 -1
  70. package/dist/ui/atoms/number-input/quantity-input.js.map +1 -1
  71. package/dist/ui/atoms/table/table.d.ts +12 -3
  72. package/dist/ui/atoms/table/table.d.ts.map +1 -1
  73. package/dist/ui/atoms/table/table.js +59 -10
  74. package/dist/ui/atoms/table/table.js.map +1 -1
  75. package/dist/ui/atoms/text/text.d.ts +2 -2
  76. package/dist/ui/atoms/text/text.d.ts.map +1 -1
  77. package/dist/ui/atoms/text/text.js.map +1 -1
  78. package/dist/ui/molecules/action-bar/action-bar.d.ts +76 -0
  79. package/dist/ui/molecules/action-bar/action-bar.d.ts.map +1 -0
  80. package/dist/ui/molecules/action-bar/action-bar.js +218 -0
  81. package/dist/ui/molecules/action-bar/action-bar.js.map +1 -0
  82. package/dist/ui/molecules/action-bar/index.d.ts +2 -0
  83. package/dist/ui/molecules/action-bar/index.d.ts.map +1 -0
  84. package/dist/ui/molecules/action-bar/index.js +3 -0
  85. package/dist/ui/molecules/action-bar/index.js.map +1 -0
  86. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.d.ts.map +1 -1
  87. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js +8 -10
  88. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js.map +1 -1
  89. package/dist/ui/molecules/bulk-result/bulk-result.d.ts +3 -3
  90. package/dist/ui/molecules/bulk-result/bulk-result.js +4 -4
  91. package/dist/ui/molecules/bulk-result/bulk-result.js.map +1 -1
  92. package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts +24 -9
  93. package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts.map +1 -1
  94. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +36 -11
  95. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
  96. package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts +33 -20
  97. package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts.map +1 -1
  98. package/dist/ui/molecules/entity-record-card/entity-record-card.js +53 -20
  99. package/dist/ui/molecules/entity-record-card/entity-record-card.js.map +1 -1
  100. package/dist/ui/molecules/filter-builder/filter-builder.d.ts +8 -2
  101. package/dist/ui/molecules/filter-builder/filter-builder.d.ts.map +1 -1
  102. package/dist/ui/molecules/filter-builder/filter-builder.js +14 -5
  103. package/dist/ui/molecules/filter-builder/filter-builder.js.map +1 -1
  104. package/dist/ui/molecules/filter-chips/filter-chips.d.ts +16 -0
  105. package/dist/ui/molecules/filter-chips/filter-chips.d.ts.map +1 -0
  106. package/dist/ui/molecules/filter-chips/filter-chips.js +73 -0
  107. package/dist/ui/molecules/filter-chips/filter-chips.js.map +1 -0
  108. package/dist/ui/molecules/filter-chips/index.d.ts +2 -0
  109. package/dist/ui/molecules/filter-chips/index.d.ts.map +1 -0
  110. package/dist/ui/molecules/filter-chips/index.js +3 -0
  111. package/dist/ui/molecules/filter-chips/index.js.map +1 -0
  112. package/dist/ui/molecules/menu/menu.d.ts +11 -1
  113. package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
  114. package/dist/ui/molecules/menu/menu.js +62 -2
  115. package/dist/ui/molecules/menu/menu.js.map +1 -1
  116. package/dist/ui/molecules/pagination/pagination.d.ts +5 -0
  117. package/dist/ui/molecules/pagination/pagination.d.ts.map +1 -1
  118. package/dist/ui/molecules/pagination/pagination.js +35 -18
  119. package/dist/ui/molecules/pagination/pagination.js.map +1 -1
  120. package/dist/ui/molecules/region-state/index.d.ts +2 -0
  121. package/dist/ui/molecules/region-state/index.d.ts.map +1 -0
  122. package/dist/ui/molecules/region-state/index.js +3 -0
  123. package/dist/ui/molecules/region-state/index.js.map +1 -0
  124. package/dist/ui/molecules/region-state/region-state.d.ts +42 -0
  125. package/dist/ui/molecules/region-state/region-state.d.ts.map +1 -0
  126. package/dist/ui/molecules/region-state/region-state.js +83 -0
  127. package/dist/ui/molecules/region-state/region-state.js.map +1 -0
  128. package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts +3 -1
  129. package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts.map +1 -1
  130. package/dist/ui/molecules/table-toolbar/table-toolbar.js +3 -2
  131. package/dist/ui/molecules/table-toolbar/table-toolbar.js.map +1 -1
  132. package/dist/ui/molecules/toast-region/toast-region.d.ts +63 -19
  133. package/dist/ui/molecules/toast-region/toast-region.d.ts.map +1 -1
  134. package/dist/ui/molecules/toast-region/toast-region.js +129 -42
  135. package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
  136. package/dist/ui/organisms/command-palette/command-palette.d.ts.map +1 -1
  137. package/dist/ui/organisms/command-palette/command-palette.js +19 -12
  138. package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
  139. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
  140. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +0 -1
  141. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
  142. package/dist/ui/organisms/entity-form/entity-form.d.ts +1 -1
  143. package/dist/ui/organisms/entity-form/entity-form.d.ts.map +1 -1
  144. package/dist/ui/organisms/entity-form/entity-form.js +322 -214
  145. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  146. package/dist/ui/organisms/entity-form/entity-form.types.d.ts +16 -0
  147. package/dist/ui/organisms/entity-form/entity-form.types.d.ts.map +1 -1
  148. package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
  149. package/dist/ui/organisms/entity-table/entity-table.d.ts +1 -1
  150. package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
  151. package/dist/ui/organisms/entity-table/entity-table.js +332 -197
  152. package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
  153. package/dist/ui/organisms/entity-table/entity-table.types.d.ts +19 -4
  154. package/dist/ui/organisms/entity-table/entity-table.types.d.ts.map +1 -1
  155. package/dist/ui/organisms/entity-table/entity-table.types.js.map +1 -1
  156. package/dist/ui/organisms/entity-table/header-sort.d.ts +18 -0
  157. package/dist/ui/organisms/entity-table/header-sort.d.ts.map +1 -0
  158. package/dist/ui/organisms/entity-table/header-sort.js +54 -0
  159. package/dist/ui/organisms/entity-table/header-sort.js.map +1 -0
  160. package/dist/ui/organisms/entity-table/pivot.d.ts +25 -0
  161. package/dist/ui/organisms/entity-table/pivot.d.ts.map +1 -0
  162. package/dist/ui/organisms/entity-table/pivot.js +106 -0
  163. package/dist/ui/organisms/entity-table/pivot.js.map +1 -0
  164. package/dist/ui/organisms/entity-table/use-result-announcement.d.ts +13 -0
  165. package/dist/ui/organisms/entity-table/use-result-announcement.d.ts.map +1 -0
  166. package/dist/ui/organisms/entity-table/use-result-announcement.js +31 -0
  167. package/dist/ui/organisms/entity-table/use-result-announcement.js.map +1 -0
  168. package/dist/ui/organisms/top-bar/live-indicator.d.ts +11 -0
  169. package/dist/ui/organisms/top-bar/live-indicator.d.ts.map +1 -0
  170. package/dist/ui/organisms/top-bar/live-indicator.js +46 -0
  171. package/dist/ui/organisms/top-bar/live-indicator.js.map +1 -0
  172. package/dist/ui/organisms/top-bar/top-bar.d.ts +11 -2
  173. package/dist/ui/organisms/top-bar/top-bar.d.ts.map +1 -1
  174. package/dist/ui/organisms/top-bar/top-bar.js +14 -5
  175. package/dist/ui/organisms/top-bar/top-bar.js.map +1 -1
  176. package/dist/ui/organisms/wizard/wizard.d.ts +1 -1
  177. package/dist/ui/organisms/wizard/wizard.d.ts.map +1 -1
  178. package/dist/ui/organisms/wizard/wizard.js +7 -1
  179. package/dist/ui/organisms/wizard/wizard.js.map +1 -1
  180. package/dist/ui/organisms/wizard/wizard.types.d.ts +8 -0
  181. package/dist/ui/organisms/wizard/wizard.types.d.ts.map +1 -1
  182. package/dist/ui/organisms/wizard/wizard.types.js.map +1 -1
  183. package/dist/ui/utils/live-updates.d.ts +24 -0
  184. package/dist/ui/utils/live-updates.d.ts.map +1 -0
  185. package/dist/ui/utils/live-updates.js +20 -0
  186. package/dist/ui/utils/live-updates.js.map +1 -0
  187. package/dist/ui/utils/use-focus-on-arrival.d.ts +14 -0
  188. package/dist/ui/utils/use-focus-on-arrival.d.ts.map +1 -0
  189. package/dist/ui/utils/use-focus-on-arrival.js +42 -0
  190. package/dist/ui/utils/use-focus-on-arrival.js.map +1 -0
  191. package/dist/ui/utils/use-hotkey.d.ts +44 -25
  192. package/dist/ui/utils/use-hotkey.d.ts.map +1 -1
  193. package/dist/ui/utils/use-hotkey.js +80 -48
  194. package/dist/ui/utils/use-hotkey.js.map +1 -1
  195. package/package.json +5 -5
  196. package/dist/ui/molecules/split-button/index.d.ts +0 -2
  197. package/dist/ui/molecules/split-button/index.d.ts.map +0 -1
  198. package/dist/ui/molecules/split-button/index.js +0 -3
  199. package/dist/ui/molecules/split-button/index.js.map +0 -1
  200. package/dist/ui/molecules/split-button/split-button.d.ts +0 -21
  201. package/dist/ui/molecules/split-button/split-button.d.ts.map +0 -1
  202. package/dist/ui/molecules/split-button/split-button.js +0 -35
  203. package/dist/ui/molecules/split-button/split-button.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/filter-builder/filter-builder.tsx"],"sourcesContent":["'use client';\n\nimport {\n type Entity,\n type EntityFieldDescriptor,\n type EntityFilter,\n type FilterGroup,\n findQueriedDescriptor,\n type Locale,\n type LogicOperator,\n type MetaAccessorType,\n queryPropertyFor,\n} from '@entifix/core';\nimport { useCallback, useId, useState } from 'react';\n\nimport { useEnumLabel, useLocale, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Select, TextInput } from '../../atoms/field/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport {\n type EntityFilterOperator,\n operatorArity,\n operatorLabelKey,\n operatorsForType,\n} from './filter-operators.js';\n\n/**\n * One in-progress filter row. Values are held as strings because that is what\n * the inputs produce; they are coerced to the member's type only when the row\n * is converted into an {@link EntityFilter}.\n */\ninterface FilterDraft {\n key: string;\n property: string;\n operator: EntityFilterOperator;\n value: string;\n end: string;\n}\n\nfunction coerce(raw: string, type: MetaAccessorType): unknown {\n if (raw === '') return undefined;\n switch (type) {\n case 'number':\n return Number(raw);\n case 'boolean':\n return raw === 'true';\n case 'date':\n return new Date(raw);\n default:\n return raw;\n }\n}\n\n/**\n * Converts a draft row into the wire filter, or `undefined` while the row is\n * still incomplete — a half-typed row must not reach the load request.\n */\nfunction toFilter<TEntity extends Entity>(\n draft: FilterDraft,\n descriptor: EntityFieldDescriptor,\n locale: Locale,\n): EntityFilter<TEntity> | undefined {\n // A localized member is filtered in the reader's own locale.\n const property = queryPropertyFor(descriptor, locale) as keyof TEntity;\n\n switch (operatorArity(draft.operator)) {\n case 'none':\n return { property, operator: draft.operator } as EntityFilter<TEntity>;\n case 'list': {\n const values = draft.value\n .split(',')\n .map(entry => entry.trim())\n .filter(entry => entry !== '')\n .map(entry => coerce(entry, descriptor.type));\n return values.length === 0\n ? undefined\n : ({\n property,\n operator: draft.operator,\n values,\n } as EntityFilter<TEntity>);\n }\n case 'range': {\n const start = coerce(draft.value, descriptor.type);\n const end = coerce(draft.end, descriptor.type);\n return start === undefined || end === undefined\n ? undefined\n : ({\n property,\n operator: draft.operator,\n start,\n end,\n } as EntityFilter<TEntity>);\n }\n default: {\n const value = coerce(draft.value, descriptor.type);\n return value === undefined\n ? undefined\n : ({\n property,\n operator: draft.operator,\n value,\n } as EntityFilter<TEntity>);\n }\n }\n}\n\n/**\n * Rebuilds the editable rows from an already-applied filter, so reopening the\n * panel shows what is actually in effect rather than an empty form. Only the\n * flat rows this builder can express are restored — a group nested by some\n * other means is left to the caller to re-enter.\n */\nfunction toDrafts<TEntity extends Entity>(\n filtering: FilterGroup<TEntity> | undefined,\n idPrefix: string,\n descriptors: readonly EntityFieldDescriptor[],\n): FilterDraft[] {\n if (!filtering) return [];\n\n return filtering.values.flatMap((node, index) => {\n if (!('property' in node)) return [];\n const filter = node as {\n property: unknown;\n operator: EntityFilterOperator;\n value?: unknown;\n values?: unknown[];\n start?: unknown;\n end?: unknown;\n };\n const text = (value: unknown) =>\n value instanceof Date\n ? value.toISOString().slice(0, 10)\n : value === undefined\n ? ''\n : String(value);\n\n return [\n {\n key: `${idPrefix}-applied-${index}`,\n // Rows address members by accessor name; an applied filter carries the\n // wire key, or a locale path into a localized member.\n property:\n findQueriedDescriptor(descriptors, String(filter.property))?.name ??\n String(filter.property),\n operator: filter.operator,\n value: filter.values\n ? filter.values.map(text).join(', ')\n : text(filter.start ?? filter.value),\n end: text(filter.end),\n },\n ];\n });\n}\n\nexport interface FilterBuilderProps<TEntity extends Entity> {\n /** Filterable members, already resolved from the entity metadata. */\n descriptors: readonly EntityFieldDescriptor[];\n /** The filtering currently applied, used to seed the rows. */\n value?: FilterGroup<TEntity>;\n onChange: (filtering: FilterGroup<TEntity>) => void;\n}\n\n/**\n * Builds an `EntityFiltering` value from entity metadata: the member list, the\n * operators offered and the shape of the value control all come from each\n * member's descriptor, so no per-entity filter UI has to be written.\n *\n * Editing is a draft. `onChange` fires only on **Apply**, because the value\n * feeds a load request: emitting per keystroke would put one HTTP request on\n * the wire per character typed.\n */\nexport function FilterBuilder<TEntity extends Entity>({\n descriptors,\n value: applied,\n onChange,\n}: FilterBuilderProps<TEntity>) {\n const t = useT();\n const idPrefix = useId();\n const [logic, setLogic] = useState<LogicOperator>(applied?.operator ?? 'and');\n const [drafts, setDrafts] = useState<FilterDraft[]>(() =>\n toDrafts(applied, idPrefix, descriptors),\n );\n const locale = useLocale();\n\n const descriptorFor = useCallback(\n (name: string) => descriptors.find(entry => entry.name === name),\n [descriptors],\n );\n\n const apply = useCallback(\n (nextDrafts: FilterDraft[], nextLogic: LogicOperator) => {\n const values = nextDrafts\n .map(draft => {\n const descriptor = descriptorFor(draft.property);\n return descriptor\n ? toFilter<TEntity>(draft, descriptor, locale)\n : undefined;\n })\n .filter(\n (filter): filter is EntityFilter<TEntity> => filter !== undefined,\n );\n\n onChange({ operator: nextLogic, values });\n },\n [descriptorFor, locale, onChange],\n );\n\n /** Edits the draft only — nothing reaches the caller until Apply. */\n const update = (nextDrafts: FilterDraft[]) => setDrafts(nextDrafts);\n\n const addRow = () => {\n const first = descriptors[0];\n // Defence in depth: the component returns early for an empty descriptor\n // list, so this button does not exist without a first member.\n /* v8 ignore next */\n if (!first) return;\n update([\n ...drafts,\n {\n key: `${idPrefix}-${drafts.length}-${Date.now()}`,\n property: first.name,\n operator: operatorsForType(first.type)[0],\n value: '',\n end: '',\n },\n ]);\n };\n\n const patch = (key: string, changes: Partial<FilterDraft>) =>\n update(\n drafts.map(draft =>\n draft.key === key ? { ...draft, ...changes } : draft,\n ),\n );\n\n if (descriptors.length === 0) {\n return (\n <Text step={-1} tone=\"muted\">\n {t('filter.none')}\n </Text>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-2xs\">\n <div className=\"flex items-center gap-2xs\">\n <Text step={-2} tone=\"muted\">\n {t('filter.match')}\n </Text>\n <Select\n aria-label={t('filter.matchAria')}\n value={logic}\n onChange={event => {\n setLogic(event.target.value as LogicOperator);\n }}\n >\n <option value=\"and\">{t('filter.all')}</option>\n <option value=\"or\">{t('filter.any')}</option>\n </Select>\n </div>\n\n {drafts.map(draft => {\n const descriptor = descriptorFor(draft.property);\n if (!descriptor) return null;\n const arity = operatorArity(draft.operator);\n\n return (\n <div key={draft.key} className=\"flex flex-wrap items-center gap-2xs\">\n <Select\n aria-label={t('filter.member')}\n value={draft.property}\n onChange={event => {\n const next = descriptorFor(event.target.value);\n // Defence in depth: the options are built from `descriptors`,\n // so a chosen value always resolves.\n /* v8 ignore next */\n if (!next) return;\n patch(draft.key, {\n property: next.name,\n // The previous operator may not exist for the new type.\n operator: operatorsForType(next.type)[0],\n value: '',\n end: '',\n });\n }}\n >\n {descriptors.map(entry => (\n <option key={entry.name} value={entry.name}>\n {entry.label}\n </option>\n ))}\n </Select>\n\n <Select\n aria-label={t('filter.operator')}\n value={draft.operator}\n onChange={event =>\n patch(draft.key, {\n operator: event.target.value as EntityFilterOperator,\n })\n }\n >\n {operatorsForType(descriptor.type).map(operator => (\n <option key={operator} value={operator}>\n {t(operatorLabelKey(operator))}\n </option>\n ))}\n </Select>\n\n {arity !== 'none' && (\n <FilterValueInput\n descriptor={descriptor}\n arity={arity}\n value={draft.value}\n end={draft.end}\n onValueChange={value => patch(draft.key, { value })}\n onEndChange={end => patch(draft.key, { end })}\n />\n )}\n\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('filter.remove')}\n onClick={() =>\n update(drafts.filter(entry => entry.key !== draft.key))\n }\n >\n ✕\n </Button>\n </div>\n );\n })}\n\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={addRow}>\n {t('filter.add')}\n </Button>\n <Button\n type=\"button\"\n variant=\"primary\"\n size=\"sm\"\n aria-label={t('filter.applyAria')}\n onClick={() => apply(drafts, logic)}\n >\n {t('filter.apply')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('filter.clearAria')}\n onClick={() => {\n setDrafts([]);\n setLogic('and');\n // Applying the emptied form is what actually clears the listing;\n // dropping the rows alone would leave the old filter in effect.\n apply([], 'and');\n }}\n >\n {t('filter.clear')}\n </Button>\n </div>\n </div>\n );\n}\n\ninterface FilterValueInputProps {\n descriptor: EntityFieldDescriptor;\n arity: ReturnType<typeof operatorArity>;\n value: string;\n end: string;\n onValueChange: (value: string) => void;\n onEndChange: (end: string) => void;\n}\n\n/** The value control the member's type calls for. */\nfunction FilterValueInput({\n descriptor,\n arity,\n value,\n end,\n onValueChange,\n onEndChange,\n}: FilterValueInputProps) {\n const t = useT();\n const enumLabel = useEnumLabel();\n\n if (descriptor.type === 'boolean') {\n return (\n <Select\n aria-label={t('filter.value')}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n >\n <option value=\"\">—</option>\n <option value=\"true\">{t('value.yes')}</option>\n <option value=\"false\">{t('value.no')}</option>\n </Select>\n );\n }\n\n if (descriptor.type === 'enum' && descriptor.enumValues && arity !== 'list') {\n return (\n <Select\n aria-label={t('filter.value')}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n >\n <option value=\"\">—</option>\n {descriptor.enumValues.map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n const inputType =\n descriptor.type === 'number'\n ? 'number'\n : descriptor.type === 'date'\n ? 'date'\n : 'text';\n\n return (\n <>\n <TextInput\n aria-label={t('filter.value')}\n type={arity === 'list' ? 'text' : inputType}\n placeholder={arity === 'list' ? t('filter.listPlaceholder') : undefined}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n />\n {arity === 'range' && (\n <TextInput\n aria-label={t('filter.rangeEnd')}\n type={inputType}\n value={end}\n onChange={event => onEndChange(event.target.value)}\n />\n )}\n </>\n );\n}\n"],"names":["findQueriedDescriptor","queryPropertyFor","useCallback","useId","useState","useEnumLabel","useLocale","useT","Button","Select","TextInput","Text","operatorArity","operatorLabelKey","operatorsForType","coerce","raw","type","undefined","Number","Date","toFilter","draft","descriptor","locale","property","operator","values","value","split","map","entry","trim","filter","length","start","end","toDrafts","filtering","idPrefix","descriptors","flatMap","node","index","text","toISOString","slice","String","key","name","join","FilterBuilder","applied","onChange","t","logic","setLogic","drafts","setDrafts","descriptorFor","find","apply","nextDrafts","nextLogic","update","addRow","first","now","patch","changes","step","tone","div","className","aria-label","event","target","option","arity","next","label","FilterValueInput","onValueChange","onEndChange","variant","size","onClick","enumLabel","enumValues","inputType","placeholder"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/filter-builder/filter-builder.tsx"],"sourcesContent":["'use client';\n\nimport {\n type Entity,\n type EntityFacetCounts,\n type EntityFacetValue,\n type EntityFieldDescriptor,\n type EntityFilter,\n type FilterGroup,\n findQueriedDescriptor,\n type Locale,\n type LogicOperator,\n type MetaAccessorType,\n queryPropertyFor,\n} from '@entifix/core';\nimport { useCallback, useId, useState } from 'react';\n\nimport { useEnumLabel, useLocale, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Select, TextInput } from '../../atoms/field/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport {\n type EntityFilterOperator,\n operatorArity,\n operatorLabelKey,\n operatorsForType,\n} from './filter-operators.js';\n\n/**\n * One in-progress filter row. Values are held as strings because that is what\n * the inputs produce; they are coerced to the member's type only when the row\n * is converted into an {@link EntityFilter}.\n */\ninterface FilterDraft {\n key: string;\n property: string;\n operator: EntityFilterOperator;\n value: string;\n end: string;\n}\n\nfunction coerce(raw: string, type: MetaAccessorType): unknown {\n if (raw === '') return undefined;\n switch (type) {\n case 'number':\n return Number(raw);\n case 'boolean':\n return raw === 'true';\n case 'date':\n return new Date(raw);\n default:\n return raw;\n }\n}\n\n/**\n * Converts a draft row into the wire filter, or `undefined` while the row is\n * still incomplete — a half-typed row must not reach the load request.\n */\nfunction toFilter<TEntity extends Entity>(\n draft: FilterDraft,\n descriptor: EntityFieldDescriptor,\n locale: Locale,\n): EntityFilter<TEntity> | undefined {\n // A localized member is filtered in the reader's own locale.\n const property = queryPropertyFor(descriptor, locale) as keyof TEntity;\n\n switch (operatorArity(draft.operator)) {\n case 'none':\n return { property, operator: draft.operator } as EntityFilter<TEntity>;\n case 'list': {\n const values = draft.value\n .split(',')\n .map(entry => entry.trim())\n .filter(entry => entry !== '')\n .map(entry => coerce(entry, descriptor.type));\n return values.length === 0\n ? undefined\n : ({\n property,\n operator: draft.operator,\n values,\n } as EntityFilter<TEntity>);\n }\n case 'range': {\n const start = coerce(draft.value, descriptor.type);\n const end = coerce(draft.end, descriptor.type);\n return start === undefined || end === undefined\n ? undefined\n : ({\n property,\n operator: draft.operator,\n start,\n end,\n } as EntityFilter<TEntity>);\n }\n default: {\n const value = coerce(draft.value, descriptor.type);\n return value === undefined\n ? undefined\n : ({\n property,\n operator: draft.operator,\n value,\n } as EntityFilter<TEntity>);\n }\n }\n}\n\n/**\n * Rebuilds the editable rows from an already-applied filter, so reopening the\n * panel shows what is actually in effect rather than an empty form. Only the\n * flat rows this builder can express are restored — a group nested by some\n * other means is left to the caller to re-enter.\n */\nfunction toDrafts<TEntity extends Entity>(\n filtering: FilterGroup<TEntity> | undefined,\n idPrefix: string,\n descriptors: readonly EntityFieldDescriptor[],\n): FilterDraft[] {\n if (!filtering) return [];\n\n return filtering.values.flatMap((node, index) => {\n if (!('property' in node)) return [];\n const filter = node as {\n property: unknown;\n operator: EntityFilterOperator;\n value?: unknown;\n values?: unknown[];\n start?: unknown;\n end?: unknown;\n };\n const text = (value: unknown) =>\n value instanceof Date\n ? value.toISOString().slice(0, 10)\n : value === undefined\n ? ''\n : String(value);\n\n return [\n {\n key: `${idPrefix}-applied-${index}`,\n // Rows address members by accessor name; an applied filter carries the\n // wire key, or a locale path into a localized member.\n property:\n findQueriedDescriptor(descriptors, String(filter.property))?.name ??\n String(filter.property),\n operator: filter.operator,\n value: filter.values\n ? filter.values.map(text).join(', ')\n : text(filter.start ?? filter.value),\n end: text(filter.end),\n },\n ];\n });\n}\n\nexport interface FilterBuilderProps<TEntity extends Entity> {\n /** Filterable members, already resolved from the entity metadata. */\n descriptors: readonly EntityFieldDescriptor[];\n /** The filtering currently applied, used to seed the rows. */\n value?: FilterGroup<TEntity>;\n onChange: (filtering: FilterGroup<TEntity>) => void;\n /**\n * How many records hold each value, by member — shown beside an enum's or a\n * boolean's choices, so a reader sees what a choice would keep before\n * applying it ([ADR 0012](../../../../../../../docs/adr/0012-facet-counts-are-a-port-of-their-own.md)).\n */\n facets?: EntityFacetCounts;\n}\n\n/**\n * Builds an `EntityFiltering` value from entity metadata: the member list, the\n * operators offered and the shape of the value control all come from each\n * member's descriptor, so no per-entity filter UI has to be written.\n *\n * Editing is a draft. `onChange` fires only on **Apply**, because the value\n * feeds a load request: emitting per keystroke would put one HTTP request on\n * the wire per character typed.\n */\nexport function FilterBuilder<TEntity extends Entity>({\n descriptors,\n value: applied,\n onChange,\n facets,\n}: FilterBuilderProps<TEntity>) {\n const t = useT();\n const idPrefix = useId();\n const [logic, setLogic] = useState<LogicOperator>(applied?.operator ?? 'and');\n const [drafts, setDrafts] = useState<FilterDraft[]>(() =>\n toDrafts(applied, idPrefix, descriptors),\n );\n const locale = useLocale();\n\n const descriptorFor = useCallback(\n (name: string) => descriptors.find(entry => entry.name === name),\n [descriptors],\n );\n\n const apply = useCallback(\n (nextDrafts: FilterDraft[], nextLogic: LogicOperator) => {\n const values = nextDrafts\n .map(draft => {\n const descriptor = descriptorFor(draft.property);\n return descriptor\n ? toFilter<TEntity>(draft, descriptor, locale)\n : undefined;\n })\n .filter(\n (filter): filter is EntityFilter<TEntity> => filter !== undefined,\n );\n\n onChange({ operator: nextLogic, values });\n },\n [descriptorFor, locale, onChange],\n );\n\n /** Edits the draft only — nothing reaches the caller until Apply. */\n const update = (nextDrafts: FilterDraft[]) => setDrafts(nextDrafts);\n\n const addRow = () => {\n const first = descriptors[0];\n // Defence in depth: the component returns early for an empty descriptor\n // list, so this button does not exist without a first member.\n /* v8 ignore next */\n if (!first) return;\n update([\n ...drafts,\n {\n key: `${idPrefix}-${drafts.length}-${Date.now()}`,\n property: first.name,\n operator: operatorsForType(first.type)[0],\n value: '',\n end: '',\n },\n ]);\n };\n\n const patch = (key: string, changes: Partial<FilterDraft>) =>\n update(\n drafts.map(draft =>\n draft.key === key ? { ...draft, ...changes } : draft,\n ),\n );\n\n if (descriptors.length === 0) {\n return (\n <Text step={-1} tone=\"muted\">\n {t('filter.none')}\n </Text>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-2xs\">\n <div className=\"flex items-center gap-2xs\">\n <Text step={-2} tone=\"muted\">\n {t('filter.match')}\n </Text>\n <Select\n aria-label={t('filter.matchAria')}\n value={logic}\n onChange={event => {\n setLogic(event.target.value as LogicOperator);\n }}\n >\n <option value=\"and\">{t('filter.all')}</option>\n <option value=\"or\">{t('filter.any')}</option>\n </Select>\n </div>\n\n {drafts.map(draft => {\n const descriptor = descriptorFor(draft.property);\n if (!descriptor) return null;\n const arity = operatorArity(draft.operator);\n\n return (\n <div key={draft.key} className=\"flex flex-wrap items-center gap-2xs\">\n <Select\n aria-label={t('filter.member')}\n value={draft.property}\n onChange={event => {\n const next = descriptorFor(event.target.value);\n // Defence in depth: the options are built from `descriptors`,\n // so a chosen value always resolves.\n /* v8 ignore next */\n if (!next) return;\n patch(draft.key, {\n property: next.name,\n // The previous operator may not exist for the new type.\n operator: operatorsForType(next.type)[0],\n value: '',\n end: '',\n });\n }}\n >\n {descriptors.map(entry => (\n <option key={entry.name} value={entry.name}>\n {entry.label}\n </option>\n ))}\n </Select>\n\n <Select\n aria-label={t('filter.operator')}\n value={draft.operator}\n onChange={event =>\n patch(draft.key, {\n operator: event.target.value as EntityFilterOperator,\n })\n }\n >\n {operatorsForType(descriptor.type).map(operator => (\n <option key={operator} value={operator}>\n {t(operatorLabelKey(operator))}\n </option>\n ))}\n </Select>\n\n {arity !== 'none' && (\n <FilterValueInput\n descriptor={descriptor}\n counts={facets?.[descriptor.name]}\n arity={arity}\n value={draft.value}\n end={draft.end}\n onValueChange={value => patch(draft.key, { value })}\n onEndChange={end => patch(draft.key, { end })}\n />\n )}\n\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('filter.remove')}\n onClick={() =>\n update(drafts.filter(entry => entry.key !== draft.key))\n }\n >\n ✕\n </Button>\n </div>\n );\n })}\n\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={addRow}>\n {t('filter.add')}\n </Button>\n <Button\n type=\"button\"\n variant=\"primary\"\n size=\"sm\"\n aria-label={t('filter.applyAria')}\n onClick={() => apply(drafts, logic)}\n >\n {t('filter.apply')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('filter.clearAria')}\n onClick={() => {\n setDrafts([]);\n setLogic('and');\n // Applying the emptied form is what actually clears the listing;\n // dropping the rows alone would leave the old filter in effect.\n apply([], 'and');\n }}\n >\n {t('filter.clear')}\n </Button>\n </div>\n </div>\n );\n}\n\ninterface FilterValueInputProps {\n descriptor: EntityFieldDescriptor;\n counts?: readonly EntityFacetValue[];\n arity: ReturnType<typeof operatorArity>;\n value: string;\n end: string;\n onValueChange: (value: string) => void;\n onEndChange: (end: string) => void;\n}\n\n/** The value control the member's type calls for. */\nfunction FilterValueInput({\n descriptor,\n counts,\n arity,\n value,\n end,\n onValueChange,\n onEndChange,\n}: FilterValueInputProps) {\n const t = useT();\n const enumLabel = useEnumLabel();\n /** A choice's label, with how many records hold it when that is known. */\n const counted = (label: string, option: string) => {\n const count = counts?.find(entry => String(entry.value) === option)?.count;\n return count === undefined\n ? label\n : t('filter.withCount', { label, count });\n };\n\n if (descriptor.type === 'boolean') {\n return (\n <Select\n aria-label={t('filter.value')}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n >\n <option value=\"\">—</option>\n <option value=\"true\">{counted(t('value.yes'), 'true')}</option>\n <option value=\"false\">{counted(t('value.no'), 'false')}</option>\n </Select>\n );\n }\n\n if (descriptor.type === 'enum' && descriptor.enumValues && arity !== 'list') {\n return (\n <Select\n aria-label={t('filter.value')}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n >\n <option value=\"\">—</option>\n {descriptor.enumValues.map(option => (\n <option key={option} value={option}>\n {counted(enumLabel(descriptor, option), option)}\n </option>\n ))}\n </Select>\n );\n }\n\n const inputType =\n descriptor.type === 'number'\n ? 'number'\n : descriptor.type === 'date'\n ? 'date'\n : 'text';\n\n return (\n <>\n <TextInput\n aria-label={t('filter.value')}\n type={arity === 'list' ? 'text' : inputType}\n placeholder={arity === 'list' ? t('filter.listPlaceholder') : undefined}\n value={value}\n onChange={event => onValueChange(event.target.value)}\n />\n {arity === 'range' && (\n <TextInput\n aria-label={t('filter.rangeEnd')}\n type={inputType}\n value={end}\n onChange={event => onEndChange(event.target.value)}\n />\n )}\n </>\n );\n}\n"],"names":["findQueriedDescriptor","queryPropertyFor","useCallback","useId","useState","useEnumLabel","useLocale","useT","Button","Select","TextInput","Text","operatorArity","operatorLabelKey","operatorsForType","coerce","raw","type","undefined","Number","Date","toFilter","draft","descriptor","locale","property","operator","values","value","split","map","entry","trim","filter","length","start","end","toDrafts","filtering","idPrefix","descriptors","flatMap","node","index","text","toISOString","slice","String","key","name","join","FilterBuilder","applied","onChange","facets","t","logic","setLogic","drafts","setDrafts","descriptorFor","find","apply","nextDrafts","nextLogic","update","addRow","first","now","patch","changes","step","tone","div","className","aria-label","event","target","option","arity","next","label","FilterValueInput","counts","onValueChange","onEndChange","variant","size","onClick","enumLabel","counted","count","enumValues","inputType","placeholder"],"mappings":"AAAA;;;AAEA,SAOEA,qBAAqB,EAIrBC,gBAAgB,QACX,gBAAgB;AACvB,SAASC,WAAW,EAAEC,KAAK,EAAEC,QAAQ,QAAQ,QAAQ;AAErD,SAASC,YAAY,EAAEC,SAAS,EAAEC,IAAI,QAAQ,yBAAyB;AACvE,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,MAAM,EAAEC,SAAS,QAAQ,6BAA6B;AAC/D,SAASC,IAAI,QAAQ,4BAA4B;AACjD,SAEEC,aAAa,EACbC,gBAAgB,EAChBC,gBAAgB,QACX,wBAAwB;AAe/B,SAASC,OAAOC,GAAW,EAAEC,IAAsB;IACjD,IAAID,QAAQ,IAAI,OAAOE;IACvB,OAAQD;QACN,KAAK;YACH,OAAOE,OAAOH;QAChB,KAAK;YACH,OAAOA,QAAQ;QACjB,KAAK;YACH,OAAO,IAAII,KAAKJ;QAClB;YACE,OAAOA;IACX;AACF;AAEA;;;CAGC,GACD,SAASK,SACPC,KAAkB,EAClBC,UAAiC,EACjCC,MAAc;IAEd,6DAA6D;IAC7D,MAAMC,WAAWxB,iBAAiBsB,YAAYC;IAE9C,OAAQZ,cAAcU,MAAMI,QAAQ;QAClC,KAAK;YACH,OAAO;gBAAED;gBAAUC,UAAUJ,MAAMI,QAAQ;YAAC;QAC9C,KAAK;YAAQ;gBACX,MAAMC,SAASL,MAAMM,KAAK,CACvBC,KAAK,CAAC,KACNC,GAAG,CAACC,CAAAA,QAASA,MAAMC,IAAI,IACvBC,MAAM,CAACF,CAAAA,QAASA,UAAU,IAC1BD,GAAG,CAACC,CAAAA,QAAShB,OAAOgB,OAAOR,WAAWN,IAAI;gBAC7C,OAAOU,OAAOO,MAAM,KAAK,IACrBhB,YACC;oBACCO;oBACAC,UAAUJ,MAAMI,QAAQ;oBACxBC;gBACF;YACN;QACA,KAAK;YAAS;gBACZ,MAAMQ,QAAQpB,OAAOO,MAAMM,KAAK,EAAEL,WAAWN,IAAI;gBACjD,MAAMmB,MAAMrB,OAAOO,MAAMc,GAAG,EAAEb,WAAWN,IAAI;gBAC7C,OAAOkB,UAAUjB,aAAakB,QAAQlB,YAClCA,YACC;oBACCO;oBACAC,UAAUJ,MAAMI,QAAQ;oBACxBS;oBACAC;gBACF;YACN;QACA;YAAS;gBACP,MAAMR,QAAQb,OAAOO,MAAMM,KAAK,EAAEL,WAAWN,IAAI;gBACjD,OAAOW,UAAUV,YACbA,YACC;oBACCO;oBACAC,UAAUJ,MAAMI,QAAQ;oBACxBE;gBACF;YACN;IACF;AACF;AAEA;;;;;CAKC,GACD,SAASS,SACPC,SAA2C,EAC3CC,QAAgB,EAChBC,WAA6C;IAE7C,IAAI,CAACF,WAAW,OAAO,EAAE;IAEzB,OAAOA,UAAUX,MAAM,CAACc,OAAO,CAAC,CAACC,MAAMC;kBA4BxBV;YALPjC;QAtBN,IAAI,CAAE,CAAA,cAAc0C,IAAG,GAAI,OAAO,EAAE;QACpC,MAAMT,SAASS;QAQf,MAAME,OAAO,CAAChB,QACZA,iBAAiBR,OACbQ,MAAMiB,WAAW,GAAGC,KAAK,CAAC,GAAG,MAC7BlB,UAAUV,YACR,KACA6B,OAAOnB;QAEf,OAAO;YACL;gBACEoB,KAAK,GAAGT,SAAS,SAAS,EAAEI,OAAO;gBACnC,uEAAuE;gBACvE,sDAAsD;gBACtDlB,QAAQ,WACNzB,yBAAAA,sBAAsBwC,aAAaO,OAAOd,OAAOR,QAAQ,uBAAzDzB,uBAA6DiD,IAAI,mBACjEF,OAAOd,OAAOR,QAAQ;gBACxBC,UAAUO,OAAOP,QAAQ;gBACzBE,OAAOK,OAAON,MAAM,GAChBM,OAAON,MAAM,CAACG,GAAG,CAACc,MAAMM,IAAI,CAAC,QAC7BN,MAAKX,gBAAAA,OAAOE,KAAK,YAAZF,gBAAgBA,OAAOL,KAAK;gBACrCQ,KAAKQ,KAAKX,OAAOG,GAAG;YACtB;SACD;IACH;AACF;AAgBA;;;;;;;;CAQC,GACD,OAAO,SAASe,cAAsC,EACpDX,WAAW,EACXZ,OAAOwB,OAAO,EACdC,QAAQ,EACRC,MAAM,EACsB;;IAC5B,MAAMC,IAAIhD;IACV,MAAMgC,WAAWpC;IACjB,MAAM,CAACqD,OAAOC,SAAS,GAAGrD,iBAAwBgD,2BAAAA,QAAS1B,QAAQ,mBAAI;IACvE,MAAM,CAACgC,QAAQC,UAAU,GAAGvD,SAAwB,IAClDiC,SAASe,SAASb,UAAUC;IAE9B,MAAMhB,SAASlB;IAEf,MAAMsD,gBAAgB1D,YACpB,CAAC+C,OAAiBT,YAAYqB,IAAI,CAAC9B,CAAAA,QAASA,MAAMkB,IAAI,KAAKA,OAC3D;QAACT;KAAY;IAGf,MAAMsB,QAAQ5D,YACZ,CAAC6D,YAA2BC;QAC1B,MAAMrC,SAASoC,WACZjC,GAAG,CAACR,CAAAA;YACH,MAAMC,aAAaqC,cAActC,MAAMG,QAAQ;YAC/C,OAAOF,aACHF,SAAkBC,OAAOC,YAAYC,UACrCN;QACN,GACCe,MAAM,CACL,CAACA,SAA4CA,WAAWf;QAG5DmC,SAAS;YAAE3B,UAAUsC;YAAWrC;QAAO;IACzC,GACA;QAACiC;QAAepC;QAAQ6B;KAAS;IAGnC,mEAAmE,GACnE,MAAMY,SAAS,CAACF,aAA8BJ,UAAUI;IAExD,MAAMG,SAAS;QACb,MAAMC,QAAQ3B,WAAW,CAAC,EAAE;QAC5B,wEAAwE;QACxE,8DAA8D;QAC9D,kBAAkB,GAClB,IAAI,CAAC2B,OAAO;QACZF,OAAO;eACFP;YACH;gBACEV,KAAK,GAAGT,SAAS,CAAC,EAAEmB,OAAOxB,MAAM,CAAC,CAAC,EAAEd,KAAKgD,GAAG,IAAI;gBACjD3C,UAAU0C,MAAMlB,IAAI;gBACpBvB,UAAUZ,iBAAiBqD,MAAMlD,IAAI,CAAC,CAAC,EAAE;gBACzCW,OAAO;gBACPQ,KAAK;YACP;SACD;IACH;IAEA,MAAMiC,QAAQ,CAACrB,KAAasB,UAC1BL,OACEP,OAAO5B,GAAG,CAACR,CAAAA,QACTA,MAAM0B,GAAG,KAAKA,MAAM,aAAK1B,OAAUgD,WAAYhD;IAIrD,IAAIkB,YAAYN,MAAM,KAAK,GAAG;QAC5B,qBACE,KAACvB;YAAK4D,MAAM,CAAC;YAAGC,MAAK;sBAClBjB,EAAE;;IAGT;IAEA,qBACE,MAACkB;QAAIC,WAAU;;0BACb,MAACD;gBAAIC,WAAU;;kCACb,KAAC/D;wBAAK4D,MAAM,CAAC;wBAAGC,MAAK;kCAClBjB,EAAE;;kCAEL,MAAC9C;wBACCkE,cAAYpB,EAAE;wBACd3B,OAAO4B;wBACPH,UAAUuB,CAAAA;4BACRnB,SAASmB,MAAMC,MAAM,CAACjD,KAAK;wBAC7B;;0CAEA,KAACkD;gCAAOlD,OAAM;0CAAO2B,EAAE;;0CACvB,KAACuB;gCAAOlD,OAAM;0CAAM2B,EAAE;;;;;;YAIzBG,OAAO5B,GAAG,CAACR,CAAAA;gBACV,MAAMC,aAAaqC,cAActC,MAAMG,QAAQ;gBAC/C,IAAI,CAACF,YAAY,OAAO;gBACxB,MAAMwD,QAAQnE,cAAcU,MAAMI,QAAQ;gBAE1C,qBACE,MAAC+C;oBAAoBC,WAAU;;sCAC7B,KAACjE;4BACCkE,cAAYpB,EAAE;4BACd3B,OAAON,MAAMG,QAAQ;4BACrB4B,UAAUuB,CAAAA;gCACR,MAAMI,OAAOpB,cAAcgB,MAAMC,MAAM,CAACjD,KAAK;gCAC7C,8DAA8D;gCAC9D,qCAAqC;gCACrC,kBAAkB,GAClB,IAAI,CAACoD,MAAM;gCACXX,MAAM/C,MAAM0B,GAAG,EAAE;oCACfvB,UAAUuD,KAAK/B,IAAI;oCACnB,wDAAwD;oCACxDvB,UAAUZ,iBAAiBkE,KAAK/D,IAAI,CAAC,CAAC,EAAE;oCACxCW,OAAO;oCACPQ,KAAK;gCACP;4BACF;sCAECI,YAAYV,GAAG,CAACC,CAAAA,sBACf,KAAC+C;oCAAwBlD,OAAOG,MAAMkB,IAAI;8CACvClB,MAAMkD,KAAK;mCADDlD,MAAMkB,IAAI;;sCAM3B,KAACxC;4BACCkE,cAAYpB,EAAE;4BACd3B,OAAON,MAAMI,QAAQ;4BACrB2B,UAAUuB,CAAAA,QACRP,MAAM/C,MAAM0B,GAAG,EAAE;oCACftB,UAAUkD,MAAMC,MAAM,CAACjD,KAAK;gCAC9B;sCAGDd,iBAAiBS,WAAWN,IAAI,EAAEa,GAAG,CAACJ,CAAAA,yBACrC,KAACoD;oCAAsBlD,OAAOF;8CAC3B6B,EAAE1C,iBAAiBa;mCADTA;;wBAMhBqD,UAAU,wBACT,KAACG;4BACC3D,YAAYA;4BACZ4D,MAAM,EAAE7B,0BAAAA,MAAQ,CAAC/B,WAAW0B,IAAI,CAAC;4BACjC8B,OAAOA;4BACPnD,OAAON,MAAMM,KAAK;4BAClBQ,KAAKd,MAAMc,GAAG;4BACdgD,eAAexD,CAAAA,QAASyC,MAAM/C,MAAM0B,GAAG,EAAE;oCAAEpB;gCAAM;4BACjDyD,aAAajD,CAAAA,MAAOiC,MAAM/C,MAAM0B,GAAG,EAAE;oCAAEZ;gCAAI;;sCAI/C,KAAC5B;4BACCS,MAAK;4BACLqE,SAAQ;4BACRC,MAAK;4BACLZ,cAAYpB,EAAE;4BACdiC,SAAS,IACPvB,OAAOP,OAAOzB,MAAM,CAACF,CAAAA,QAASA,MAAMiB,GAAG,KAAK1B,MAAM0B,GAAG;sCAExD;;;mBA9DO1B,MAAM0B,GAAG;YAmEvB;0BAEA,MAACyB;gBAAIC,WAAU;;kCACb,KAAClE;wBAAOS,MAAK;wBAASqE,SAAQ;wBAAYC,MAAK;wBAAKC,SAAStB;kCAC1DX,EAAE;;kCAEL,KAAC/C;wBACCS,MAAK;wBACLqE,SAAQ;wBACRC,MAAK;wBACLZ,cAAYpB,EAAE;wBACdiC,SAAS,IAAM1B,MAAMJ,QAAQF;kCAE5BD,EAAE;;kCAEL,KAAC/C;wBACCS,MAAK;wBACLqE,SAAQ;wBACRC,MAAK;wBACLZ,cAAYpB,EAAE;wBACdiC,SAAS;4BACP7B,UAAU,EAAE;4BACZF,SAAS;4BACT,iEAAiE;4BACjE,gEAAgE;4BAChEK,MAAM,EAAE,EAAE;wBACZ;kCAECP,EAAE;;;;;;AAKb;AAYA,mDAAmD,GACnD,SAAS2B,iBAAiB,EACxB3D,UAAU,EACV4D,MAAM,EACNJ,KAAK,EACLnD,KAAK,EACLQ,GAAG,EACHgD,aAAa,EACbC,WAAW,EACW;IACtB,MAAM9B,IAAIhD;IACV,MAAMkF,YAAYpF;IAClB,wEAAwE,GACxE,MAAMqF,UAAU,CAACT,OAAeH;YAChBK;QAAd,MAAMQ,QAAQR,2BAAAA,eAAAA,OAAQtB,IAAI,CAAC9B,CAAAA,QAASgB,OAAOhB,MAAMH,KAAK,MAAMkD,4BAA9CK,aAAuDQ,KAAK;QAC1E,OAAOA,UAAUzE,YACb+D,QACA1B,EAAE,oBAAoB;YAAE0B;YAAOU;QAAM;IAC3C;IAEA,IAAIpE,WAAWN,IAAI,KAAK,WAAW;QACjC,qBACE,MAACR;YACCkE,cAAYpB,EAAE;YACd3B,OAAOA;YACPyB,UAAUuB,CAAAA,QAASQ,cAAcR,MAAMC,MAAM,CAACjD,KAAK;;8BAEnD,KAACkD;oBAAOlD,OAAM;8BAAG;;8BACjB,KAACkD;oBAAOlD,OAAM;8BAAQ8D,QAAQnC,EAAE,cAAc;;8BAC9C,KAACuB;oBAAOlD,OAAM;8BAAS8D,QAAQnC,EAAE,aAAa;;;;IAGpD;IAEA,IAAIhC,WAAWN,IAAI,KAAK,UAAUM,WAAWqE,UAAU,IAAIb,UAAU,QAAQ;QAC3E,qBACE,MAACtE;YACCkE,cAAYpB,EAAE;YACd3B,OAAOA;YACPyB,UAAUuB,CAAAA,QAASQ,cAAcR,MAAMC,MAAM,CAACjD,KAAK;;8BAEnD,KAACkD;oBAAOlD,OAAM;8BAAG;;gBAChBL,WAAWqE,UAAU,CAAC9D,GAAG,CAACgD,CAAAA,uBACzB,KAACA;wBAAoBlD,OAAOkD;kCACzBY,QAAQD,UAAUlE,YAAYuD,SAASA;uBAD7BA;;;IAMrB;IAEA,MAAMe,YACJtE,WAAWN,IAAI,KAAK,WAChB,WACAM,WAAWN,IAAI,KAAK,SAClB,SACA;IAER,qBACE;;0BACE,KAACP;gBACCiE,cAAYpB,EAAE;gBACdtC,MAAM8D,UAAU,SAAS,SAASc;gBAClCC,aAAaf,UAAU,SAASxB,EAAE,4BAA4BrC;gBAC9DU,OAAOA;gBACPyB,UAAUuB,CAAAA,QAASQ,cAAcR,MAAMC,MAAM,CAACjD,KAAK;;YAEpDmD,UAAU,yBACT,KAACrE;gBACCiE,cAAYpB,EAAE;gBACdtC,MAAM4E;gBACNjE,OAAOQ;gBACPiB,UAAUuB,CAAAA,QAASS,YAAYT,MAAMC,MAAM,CAACjD,KAAK;;;;AAK3D"}
@@ -0,0 +1,16 @@
1
+ import { type Entity, type EntityFieldDescriptor, type FilterGroup } from '@entifix/core';
2
+ export interface FilterChipsProps<TEntity extends Entity> {
3
+ /** Filterable members, to name each filter by its column. */
4
+ descriptors: readonly EntityFieldDescriptor[];
5
+ /** The filtering in effect. */
6
+ value?: FilterGroup<TEntity>;
7
+ onChange: (filtering: FilterGroup<TEntity>) => void;
8
+ }
9
+ /**
10
+ * The filters in effect, one removable chip each, and Clear all — so what
11
+ * narrowed the list is visible without opening the builder, and each can be
12
+ * dropped in one step. A nested group, which the builder cannot express, has
13
+ * no chip; Clear all still removes it.
14
+ */
15
+ export declare function FilterChips<TEntity extends Entity>({ descriptors, value, onChange, }: FilterChipsProps<TEntity>): import("react").JSX.Element | null;
16
+ //# sourceMappingURL=filter-chips.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-chips.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/filter-chips/filter-chips.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AASvB,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,MAAM;IACtD,6DAA6D;IAC7D,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,+BAA+B;IAC/B,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrD;AAWD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,EAClD,WAAW,EACX,KAAK,EACL,QAAQ,GACT,EAAE,gBAAgB,CAAC,OAAO,CAAC,sCA6E3B"}
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { findQueriedDescriptor } from '@entifix/core';
4
+ import { useEnumLabel, useT } from '../../../i18n/index.js';
5
+ import { Button } from '../../atoms/button/index.js';
6
+ import { operatorLabelKey } from '../filter-builder/filter-operators.js';
7
+ /**
8
+ * The filters in effect, one removable chip each, and Clear all — so what
9
+ * narrowed the list is visible without opening the builder, and each can be
10
+ * dropped in one step. A nested group, which the builder cannot express, has
11
+ * no chip; Clear all still removes it.
12
+ */ export function FilterChips({ descriptors, value, onChange }) {
13
+ const t = useT();
14
+ const enumLabel = useEnumLabel();
15
+ if (value === undefined || value.values.length === 0) return null;
16
+ const text = (descriptor, raw)=>raw instanceof Date ? raw.toISOString().slice(0, 10) : typeof raw === 'boolean' ? t(raw ? 'value.yes' : 'value.no') : (descriptor == null ? void 0 : descriptor.type) === 'enum' ? enumLabel(descriptor, String(raw)) : String(raw);
17
+ const chips = value.values.flatMap((node, index)=>{
18
+ var _ref;
19
+ if (!('property' in node)) return [];
20
+ const filter = node;
21
+ const descriptor = findQueriedDescriptor(descriptors, String(filter.property));
22
+ const operand = filter.values !== undefined ? filter.values.map((entry)=>text(descriptor, entry)).join(', ') : filter.start !== undefined ? `${text(descriptor, filter.start)} – ${text(descriptor, filter.end)}` : filter.value === undefined ? '' : text(descriptor, filter.value);
23
+ const label = [
24
+ (_ref = descriptor == null ? void 0 : descriptor.label) != null ? _ref : String(filter.property),
25
+ t(operatorLabelKey(filter.operator)),
26
+ operand
27
+ ].filter((part)=>part !== '').join(' ');
28
+ return [
29
+ {
30
+ index,
31
+ label
32
+ }
33
+ ];
34
+ });
35
+ return /*#__PURE__*/ _jsxs("div", {
36
+ role: "group",
37
+ "aria-label": t('filter.applied'),
38
+ className: "flex flex-wrap items-center gap-3xs",
39
+ children: [
40
+ chips.map((chip)=>/*#__PURE__*/ _jsxs("span", {
41
+ className: "inline-flex items-center gap-3xs rounded-full border border-border bg-surface ps-xs text-step-sm text-content",
42
+ children: [
43
+ chip.label,
44
+ /*#__PURE__*/ _jsx(Button, {
45
+ type: "button",
46
+ variant: "ghost",
47
+ size: "sm",
48
+ "aria-label": t('filter.removeChip', {
49
+ filter: chip.label
50
+ }),
51
+ onClick: ()=>onChange({
52
+ operator: value.operator,
53
+ values: value.values.filter((_, at)=>at !== chip.index)
54
+ }),
55
+ children: "✕"
56
+ })
57
+ ]
58
+ }, chip.index)),
59
+ /*#__PURE__*/ _jsx(Button, {
60
+ type: "button",
61
+ variant: "ghost",
62
+ size: "sm",
63
+ onClick: ()=>onChange({
64
+ operator: value.operator,
65
+ values: []
66
+ }),
67
+ children: t('filter.clearAll')
68
+ })
69
+ ]
70
+ });
71
+ }
72
+
73
+ //# sourceMappingURL=filter-chips.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/ui/molecules/filter-chips/filter-chips.tsx"],"sourcesContent":["'use client';\n\nimport {\n type Entity,\n type EntityFieldDescriptor,\n type FilterGroup,\n findQueriedDescriptor,\n} from '@entifix/core';\n\nimport { useEnumLabel, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport {\n type EntityFilterOperator,\n operatorLabelKey,\n} from '../filter-builder/filter-operators.js';\n\nexport interface FilterChipsProps<TEntity extends Entity> {\n /** Filterable members, to name each filter by its column. */\n descriptors: readonly EntityFieldDescriptor[];\n /** The filtering in effect. */\n value?: FilterGroup<TEntity>;\n onChange: (filtering: FilterGroup<TEntity>) => void;\n}\n\ninterface AppliedFilter {\n readonly property: unknown;\n readonly operator: EntityFilterOperator;\n readonly value?: unknown;\n readonly values?: readonly unknown[];\n readonly start?: unknown;\n readonly end?: unknown;\n}\n\n/**\n * The filters in effect, one removable chip each, and Clear all — so what\n * narrowed the list is visible without opening the builder, and each can be\n * dropped in one step. A nested group, which the builder cannot express, has\n * no chip; Clear all still removes it.\n */\nexport function FilterChips<TEntity extends Entity>({\n descriptors,\n value,\n onChange,\n}: FilterChipsProps<TEntity>) {\n const t = useT();\n const enumLabel = useEnumLabel();\n if (value === undefined || value.values.length === 0) return null;\n\n const text = (descriptor: EntityFieldDescriptor | undefined, raw: unknown) =>\n raw instanceof Date\n ? raw.toISOString().slice(0, 10)\n : typeof raw === 'boolean'\n ? t(raw ? 'value.yes' : 'value.no')\n : descriptor?.type === 'enum'\n ? enumLabel(descriptor, String(raw))\n : String(raw);\n\n const chips = value.values.flatMap((node, index) => {\n if (!('property' in node)) return [];\n const filter = node as AppliedFilter;\n const descriptor = findQueriedDescriptor(\n descriptors,\n String(filter.property),\n );\n const operand =\n filter.values !== undefined\n ? filter.values.map(entry => text(descriptor, entry)).join(', ')\n : filter.start !== undefined\n ? `${text(descriptor, filter.start)} – ${text(descriptor, filter.end)}`\n : filter.value === undefined\n ? ''\n : text(descriptor, filter.value);\n const label = [\n descriptor?.label ?? String(filter.property),\n t(operatorLabelKey(filter.operator)),\n operand,\n ]\n .filter(part => part !== '')\n .join(' ');\n return [{ index, label }];\n });\n\n return (\n <div\n role=\"group\"\n aria-label={t('filter.applied')}\n className=\"flex flex-wrap items-center gap-3xs\"\n >\n {chips.map(chip => (\n <span\n key={chip.index}\n className=\"inline-flex items-center gap-3xs rounded-full border border-border bg-surface ps-xs text-step-sm text-content\"\n >\n {chip.label}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('filter.removeChip', { filter: chip.label })}\n onClick={() =>\n onChange({\n operator: value.operator,\n values: value.values.filter((_, at) => at !== chip.index),\n })\n }\n >\n ✕\n </Button>\n </span>\n ))}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => onChange({ operator: value.operator, values: [] })}\n >\n {t('filter.clearAll')}\n </Button>\n </div>\n );\n}\n"],"names":["findQueriedDescriptor","useEnumLabel","useT","Button","operatorLabelKey","FilterChips","descriptors","value","onChange","t","enumLabel","undefined","values","length","text","descriptor","raw","Date","toISOString","slice","type","String","chips","flatMap","node","index","filter","property","operand","map","entry","join","start","end","label","operator","part","div","role","aria-label","className","chip","span","variant","size","onClick","_","at"],"mappings":"AAAA;;AAEA,SAIEA,qBAAqB,QAChB,gBAAgB;AAEvB,SAASC,YAAY,EAAEC,IAAI,QAAQ,yBAAyB;AAC5D,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAEEC,gBAAgB,QACX,wCAAwC;AAmB/C;;;;;CAKC,GACD,OAAO,SAASC,YAAoC,EAClDC,WAAW,EACXC,KAAK,EACLC,QAAQ,EACkB;IAC1B,MAAMC,IAAIP;IACV,MAAMQ,YAAYT;IAClB,IAAIM,UAAUI,aAAaJ,MAAMK,MAAM,CAACC,MAAM,KAAK,GAAG,OAAO;IAE7D,MAAMC,OAAO,CAACC,YAA+CC,MAC3DA,eAAeC,OACXD,IAAIE,WAAW,GAAGC,KAAK,CAAC,GAAG,MAC3B,OAAOH,QAAQ,YACbP,EAAEO,MAAM,cAAc,cACtBD,CAAAA,8BAAAA,WAAYK,IAAI,MAAK,SACnBV,UAAUK,YAAYM,OAAOL,QAC7BK,OAAOL;IAEjB,MAAMM,QAAQf,MAAMK,MAAM,CAACW,OAAO,CAAC,CAACC,MAAMC;;QACxC,IAAI,CAAE,CAAA,cAAcD,IAAG,GAAI,OAAO,EAAE;QACpC,MAAME,SAASF;QACf,MAAMT,aAAaf,sBACjBM,aACAe,OAAOK,OAAOC,QAAQ;QAExB,MAAMC,UACJF,OAAOd,MAAM,KAAKD,YACde,OAAOd,MAAM,CAACiB,GAAG,CAACC,CAAAA,QAAShB,KAAKC,YAAYe,QAAQC,IAAI,CAAC,QACzDL,OAAOM,KAAK,KAAKrB,YACf,GAAGG,KAAKC,YAAYW,OAAOM,KAAK,EAAE,GAAG,EAAElB,KAAKC,YAAYW,OAAOO,GAAG,GAAG,GACrEP,OAAOnB,KAAK,KAAKI,YACf,KACAG,KAAKC,YAAYW,OAAOnB,KAAK;QACvC,MAAM2B,QAAQ;oBACZnB,8BAAAA,WAAYmB,KAAK,mBAAIb,OAAOK,OAAOC,QAAQ;YAC3ClB,EAAEL,iBAAiBsB,OAAOS,QAAQ;YAClCP;SACD,CACEF,MAAM,CAACU,CAAAA,OAAQA,SAAS,IACxBL,IAAI,CAAC;QACR,OAAO;YAAC;gBAAEN;gBAAOS;YAAM;SAAE;IAC3B;IAEA,qBACE,MAACG;QACCC,MAAK;QACLC,cAAY9B,EAAE;QACd+B,WAAU;;YAETlB,MAAMO,GAAG,CAACY,CAAAA,qBACT,MAACC;oBAECF,WAAU;;wBAETC,KAAKP,KAAK;sCACX,KAAC/B;4BACCiB,MAAK;4BACLuB,SAAQ;4BACRC,MAAK;4BACLL,cAAY9B,EAAE,qBAAqB;gCAAEiB,QAAQe,KAAKP,KAAK;4BAAC;4BACxDW,SAAS,IACPrC,SAAS;oCACP2B,UAAU5B,MAAM4B,QAAQ;oCACxBvB,QAAQL,MAAMK,MAAM,CAACc,MAAM,CAAC,CAACoB,GAAGC,KAAOA,OAAON,KAAKhB,KAAK;gCAC1D;sCAEH;;;mBAfIgB,KAAKhB,KAAK;0BAoBnB,KAACtB;gBACCiB,MAAK;gBACLuB,SAAQ;gBACRC,MAAK;gBACLC,SAAS,IAAMrC,SAAS;wBAAE2B,UAAU5B,MAAM4B,QAAQ;wBAAEvB,QAAQ,EAAE;oBAAC;0BAE9DH,EAAE;;;;AAIX"}
@@ -0,0 +1,2 @@
1
+ export * from './filter-chips.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/filter-chips/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './filter-chips.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/ui/molecules/filter-chips/index.ts"],"sourcesContent":["export * from './filter-chips.js';\n"],"names":[],"mappings":"AAAA,cAAc,oBAAoB"}
@@ -1,4 +1,4 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
1
+ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
2
  export interface MenuProps {
3
3
  children: ReactNode;
4
4
  className?: string;
@@ -17,6 +17,12 @@ export declare namespace Menu {
17
17
  }) => import("react").JSX.Element;
18
18
  var Item: ({ className, tone, ...props }: MenuItemProps) => import("react").JSX.Element;
19
19
  var Link: ({ className, ...props }: ComponentPropsWithoutRef<"a">) => import("react").JSX.Element;
20
+ var Separator: () => import("react").JSX.Element;
21
+ var Group: ({ label, children }: {
22
+ label: string;
23
+ children: ReactNode;
24
+ }) => import("react").JSX.Element;
25
+ var Radio: ({ checked, className, children, ...props }: MenuRadioProps) => import("react").JSX.Element;
20
26
  }
21
27
  /** How an entry reads: an ordinary action, or one that destroys something. */
22
28
  export type MenuItemTone = 'neutral' | 'destructive';
@@ -34,4 +40,8 @@ export interface MenuItemProps extends ComponentPropsWithoutRef<'button'> {
34
40
  */
35
41
  tone?: MenuItemTone;
36
42
  }
43
+ export interface MenuRadioProps extends Omit<ComponentPropsWithoutRef<'button'>, 'role'> {
44
+ /** Whether this is the option in effect. */
45
+ checked: boolean;
46
+ }
37
47
  //# sourceMappingURL=menu.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/menu/menu.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIjE,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,SAAS,+BAStD;yBATe,IAAI;2CAWsB,wBAAwB,CAAC,QAAQ,CAAC;0CAiBzE;QACD,QAAQ,EAAE,SAAS,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;8CAwCwD,aAAa;wCA2B/B,wBAAwB,CAAC,GAAG,CAAC;;AAlDpE,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,aAAc,SAAQ,wBAAwB,CAAC,QAAQ,CAAC;IACvE;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB"}
1
+ {"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/menu/menu.tsx"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,wBAAwB,EAAE,KAAK,SAAS,EAAS,MAAM,OAAO,CAAC;AAI7E,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,SAAS,+BAStD;yBATe,IAAI;2CAWsB,wBAAwB,CAAC,QAAQ,CAAC;0CAiBzE;QACD,QAAQ,EAAE,SAAS,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;8CAwCwD,aAAa;wCA2B/B,wBAAwB,CAAC,GAAG,CAAC;;qCA0BhC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,SAAS,CAAA;KAAE;4DA2Bf,cAAc;;AAvGzE,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,aAAa,CAAC;AAErD,MAAM,WAAW,aAAc,SAAQ,wBAAwB,CAAC,QAAQ,CAAC;IACvE;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AA2ED,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,wBAAwB,CAAC,QAAQ,CAAC,EAClC,MAAM,CACP;IACC,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;CAClB"}
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
  import { _ as _extends } from "@swc/helpers/_/_extends";
3
3
  import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
4
- import { jsx as _jsx } from "react/jsx-runtime";
5
- import { Menu as HuiMenu, MenuButton, MenuItem as HuiMenuItem, MenuItems } from '@headlessui/react';
4
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
+ import { Menu as HuiMenu, MenuButton, MenuItem as HuiMenuItem, MenuItems, MenuSeparator } from '@headlessui/react';
6
+ import { useId } from 'react';
6
7
  import { cn } from '../../utils/cn.js';
7
8
  /**
8
9
  * A dropdown menu — the user context menu in the top bar, and the tab-overflow
@@ -65,9 +66,68 @@ function Item(_0) {
65
66
  className: cn('flex min-h-(--target-min) w-full items-center gap-2xs rounded-md px-2xs py-3xs text-step-sm text-content no-underline', 'focus-ring data-focus:bg-surface data-focus:text-content data-focus:-outline-offset-2', className)
66
67
  }, props));
67
68
  }
69
+ /**
70
+ * A rule between groups of entries — before the destructive ones, so "do" and
71
+ * "destroy" are not one undifferentiated list.
72
+ */ function Separator() {
73
+ return /*#__PURE__*/ _jsx(MenuSeparator, {
74
+ className: "my-3xs h-px bg-border"
75
+ });
76
+ }
77
+ /**
78
+ * A named set of entries — "Tema", "Densidad" — so a choice among them is
79
+ * announced with what it chooses. The heading is shown, and names the group.
80
+ */ function Group({ label, children }) {
81
+ const id = useId();
82
+ return /*#__PURE__*/ _jsxs("div", {
83
+ role: "group",
84
+ "aria-labelledby": id,
85
+ className: "py-3xs",
86
+ children: [
87
+ /*#__PURE__*/ _jsx("p", {
88
+ id: id,
89
+ className: "m-0 px-2xs py-3xs text-step-xs font-semibold text-content-muted",
90
+ children: label
91
+ }),
92
+ children
93
+ ]
94
+ });
95
+ }
96
+ /**
97
+ * One option of a {@link Group} where exactly one holds: a `menuitemradio`,
98
+ * its state in `aria-checked` and in a visible mark rather than colour alone.
99
+ */ function Radio(_0) {
100
+ let { checked, className, children } = _0, props = _object_without_properties_loose(_0, [
101
+ "checked",
102
+ "className",
103
+ "children"
104
+ ]);
105
+ return /*#__PURE__*/ _jsxs(HuiMenuItem, _extends({
106
+ as: "button",
107
+ // Headless UI names every item `menuitem` and lets no prop change it;
108
+ // the role is set on the element once it mounts, which keeps the
109
+ // menu's own keyboard handling, and React never writes it back because
110
+ // the prop it renders never changes.
111
+ ref: (node)=>node == null ? void 0 : node.setAttribute('role', 'menuitemradio'),
112
+ "aria-checked": checked,
113
+ className: cn('flex min-h-(--target-min) w-full items-center gap-2xs rounded-md px-2xs py-3xs text-step-sm text-content', 'focus-ring data-focus:bg-surface data-focus:-outline-offset-2', className)
114
+ }, props, {
115
+ children: [
116
+ /*#__PURE__*/ _jsx("span", {
117
+ "aria-hidden": "true",
118
+ className: "shrink-0",
119
+ children: checked ? '●' : '○'
120
+ }),
121
+ children
122
+ ]
123
+ }));
124
+ }
68
125
  Menu.Trigger = Trigger;
69
126
  Menu.Items = Items;
70
127
  Menu.Item = Item;
71
128
  Menu.Link = Link;
129
+ Menu.Separator = Separator;
130
+ Menu.Group = Group;
131
+ Menu.Radio = Radio;
72
132
 
73
133
  //# sourceMappingURL=menu.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/menu/menu.tsx"],"sourcesContent":["'use client';\n\nimport {\n Menu as HuiMenu,\n MenuButton,\n MenuItem as HuiMenuItem,\n MenuItems,\n} from '@headlessui/react';\nimport type { ComponentPropsWithoutRef, ReactNode } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\nexport interface MenuProps {\n children: ReactNode;\n className?: string;\n}\n\n/**\n * A dropdown menu — the user context menu in the top bar, and the tab-overflow\n * list. A thin styled wrapper over Headless UI so focus management, keyboard\n * nav, and outside-click come for free; the tokens keep it on-brand.\n */\nexport function Menu({ children, className }: MenuProps) {\n return (\n <HuiMenu\n as=\"div\"\n className={cn('relative inline-block text-left', className)}\n >\n {children}\n </HuiMenu>\n );\n}\n\nfunction Trigger({ className, ...props }: ComponentPropsWithoutRef<'button'>) {\n return (\n <MenuButton\n className={cn(\n 'inline-flex min-h-(--target-min) min-w-(--target-min) items-center gap-2xs rounded-lg px-2xs py-3xs text-step-sm text-content',\n 'transition duration-200 ease-smooth hover:bg-surface',\n 'focus-ring',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction Items({\n children,\n className,\n}: {\n children: ReactNode;\n className?: string;\n}) {\n return (\n <MenuItems\n className={cn(\n 'right-0 min-w-48 absolute z-(--z-dropdown) mt-2xs rounded-lg border border-border bg-surface-elevated p-3xs shadow-overlay',\n // The panel itself is a focus-managed CONTAINER, not a target: the ring\n // belongs on the item inside it. Stripping the outline here is\n // correct, and is not a missing `focus-ring`.\n 'focus:outline-none',\n className,\n )}\n >\n {children}\n </MenuItems>\n );\n}\n\n/** How an entry reads: an ordinary action, or one that destroys something. */\nexport type MenuItemTone = 'neutral' | 'destructive';\n\nexport interface MenuItemProps extends ComponentPropsWithoutRef<'button'> {\n /**\n * `destructive` colours the entry as a deletion.\n *\n * It exists because an overflow menu is where a destructive verb ends up once\n * the visible actions are full, and a `Button` in that position would carry\n * `variant=\"destructive\"` — a menu that cannot say the same thing makes the\n * appearance of a verb depend on how many siblings it happens to have.\n *\n * Colour is not the affordance, and must not be the only warning: a verb\n * whose descriptor carries `confirm` still asks before it fires.\n */\n tone?: MenuItemTone;\n}\n\nconst ITEM_TONE_CLASS: Record<MenuItemTone, string> = {\n neutral: 'text-content data-focus:text-content',\n destructive: 'text-danger data-focus:text-danger',\n};\n\nfunction Item({ className, tone = 'neutral', ...props }: MenuItemProps) {\n return (\n <HuiMenuItem\n as=\"button\"\n className={cn(\n 'flex min-h-(--target-min) w-full items-center gap-2xs rounded-md px-2xs py-3xs text-step-sm',\n // The item under the keyboard wears the focus ring, inset so the\n // panel's edge never clips it; a background swap alone is ~1.05:1.\n 'focus-ring data-focus:bg-surface data-focus:-outline-offset-2',\n ITEM_TONE_CLASS[tone],\n 'disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n />\n );\n}\n\n/**\n * A navigating menu entry.\n *\n * A plain anchor rather than a router link: the entries that need this are the\n * account menu's, and those point at a different app on a different origin,\n * where a client-side transition is not an option anyway. Callers wanting an\n * in-app transition should pass their router's link through `as`-style\n * composition at the call site instead.\n */\nfunction Link({ className, ...props }: ComponentPropsWithoutRef<'a'>) {\n return (\n <HuiMenuItem\n as=\"a\"\n className={cn(\n 'flex min-h-(--target-min) w-full items-center gap-2xs rounded-md px-2xs py-3xs text-step-sm text-content no-underline',\n 'focus-ring data-focus:bg-surface data-focus:text-content data-focus:-outline-offset-2',\n className,\n )}\n {...props}\n />\n );\n}\n\nMenu.Trigger = Trigger;\nMenu.Items = Items;\nMenu.Item = Item;\nMenu.Link = Link;\n"],"names":["Menu","HuiMenu","MenuButton","MenuItem","HuiMenuItem","MenuItems","cn","children","className","as","Trigger","props","Items","ITEM_TONE_CLASS","neutral","destructive","Item","tone","Link"],"mappings":"AAAA;;;;AAEA,SACEA,QAAQC,OAAO,EACfC,UAAU,EACVC,YAAYC,WAAW,EACvBC,SAAS,QACJ,oBAAoB;AAG3B,SAASC,EAAE,QAAQ,oBAAoB;AAOvC;;;;CAIC,GACD,OAAO,SAASN,KAAK,EAAEO,QAAQ,EAAEC,SAAS,EAAa;IACrD,qBACE,KAACP;QACCQ,IAAG;QACHD,WAAWF,GAAG,mCAAmCE;kBAEhDD;;AAGP;AAEA,SAASG,QAAQ,EAA2D;QAA3D,EAAEF,SAAS,EAAgD,OAA3CG;;;IAC/B,qBACE,KAACT;QACCM,WAAWF,GACT,iIACA,wDACA,cACAE;OAEEG;AAGV;AAEA,SAASC,MAAM,EACbL,QAAQ,EACRC,SAAS,EAIV;IACC,qBACE,KAACH;QACCG,WAAWF,GACT,8HACA,wEAAwE;QACxE,+DAA+D;QAC/D,8CAA8C;QAC9C,sBACAE;kBAGDD;;AAGP;AAoBA,MAAMM,kBAAgD;IACpDC,SAAS;IACTC,aAAa;AACf;AAEA,SAASC,KAAK,EAAwD;QAAxD,EAAER,SAAS,EAAES,OAAO,SAAS,EAA2B,OAAtBN;;;;IAC9C,qBACE,KAACP;QACCK,IAAG;QACHD,WAAWF,GACT,+FACA,iEAAiE;QACjE,mEAAmE;QACnE,iEACAO,eAAe,CAACI,KAAK,EACrB,mDACAT;OAEEG;AAGV;AAEA;;;;;;;;CAQC,GACD,SAASO,KAAK,EAAsD;QAAtD,EAAEV,SAAS,EAA2C,OAAtCG;;;IAC5B,qBACE,KAACP;QACCK,IAAG;QACHD,WAAWF,GACT,yHACA,yFACAE;OAEEG;AAGV;AAEAX,KAAKU,OAAO,GAAGA;AACfV,KAAKY,KAAK,GAAGA;AACbZ,KAAKgB,IAAI,GAAGA;AACZhB,KAAKkB,IAAI,GAAGA"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/menu/menu.tsx"],"sourcesContent":["'use client';\n\nimport {\n Menu as HuiMenu,\n MenuButton,\n MenuItem as HuiMenuItem,\n MenuItems,\n MenuSeparator,\n} from '@headlessui/react';\nimport { type ComponentPropsWithoutRef, type ReactNode, useId } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\nexport interface MenuProps {\n children: ReactNode;\n className?: string;\n}\n\n/**\n * A dropdown menu — the user context menu in the top bar, and the tab-overflow\n * list. A thin styled wrapper over Headless UI so focus management, keyboard\n * nav, and outside-click come for free; the tokens keep it on-brand.\n */\nexport function Menu({ children, className }: MenuProps) {\n return (\n <HuiMenu\n as=\"div\"\n className={cn('relative inline-block text-left', className)}\n >\n {children}\n </HuiMenu>\n );\n}\n\nfunction Trigger({ className, ...props }: ComponentPropsWithoutRef<'button'>) {\n return (\n <MenuButton\n className={cn(\n 'inline-flex min-h-(--target-min) min-w-(--target-min) items-center gap-2xs rounded-lg px-2xs py-3xs text-step-sm text-content',\n 'transition duration-200 ease-smooth hover:bg-surface',\n 'focus-ring',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction Items({\n children,\n className,\n}: {\n children: ReactNode;\n className?: string;\n}) {\n return (\n <MenuItems\n className={cn(\n 'right-0 min-w-48 absolute z-(--z-dropdown) mt-2xs rounded-lg border border-border bg-surface-elevated p-3xs shadow-overlay',\n // The panel itself is a focus-managed CONTAINER, not a target: the ring\n // belongs on the item inside it. Stripping the outline here is\n // correct, and is not a missing `focus-ring`.\n 'focus:outline-none',\n className,\n )}\n >\n {children}\n </MenuItems>\n );\n}\n\n/** How an entry reads: an ordinary action, or one that destroys something. */\nexport type MenuItemTone = 'neutral' | 'destructive';\n\nexport interface MenuItemProps extends ComponentPropsWithoutRef<'button'> {\n /**\n * `destructive` colours the entry as a deletion.\n *\n * It exists because an overflow menu is where a destructive verb ends up once\n * the visible actions are full, and a `Button` in that position would carry\n * `variant=\"destructive\"` — a menu that cannot say the same thing makes the\n * appearance of a verb depend on how many siblings it happens to have.\n *\n * Colour is not the affordance, and must not be the only warning: a verb\n * whose descriptor carries `confirm` still asks before it fires.\n */\n tone?: MenuItemTone;\n}\n\nconst ITEM_TONE_CLASS: Record<MenuItemTone, string> = {\n neutral: 'text-content data-focus:text-content',\n destructive: 'text-danger data-focus:text-danger',\n};\n\nfunction Item({ className, tone = 'neutral', ...props }: MenuItemProps) {\n return (\n <HuiMenuItem\n as=\"button\"\n className={cn(\n 'flex min-h-(--target-min) w-full items-center gap-2xs rounded-md px-2xs py-3xs text-step-sm',\n // The item under the keyboard wears the focus ring, inset so the\n // panel's edge never clips it; a background swap alone is ~1.05:1.\n 'focus-ring data-focus:bg-surface data-focus:-outline-offset-2',\n ITEM_TONE_CLASS[tone],\n 'disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n {...props}\n />\n );\n}\n\n/**\n * A navigating menu entry.\n *\n * A plain anchor rather than a router link: the entries that need this are the\n * account menu's, and those point at a different app on a different origin,\n * where a client-side transition is not an option anyway. Callers wanting an\n * in-app transition should pass their router's link through `as`-style\n * composition at the call site instead.\n */\nfunction Link({ className, ...props }: ComponentPropsWithoutRef<'a'>) {\n return (\n <HuiMenuItem\n as=\"a\"\n className={cn(\n 'flex min-h-(--target-min) w-full items-center gap-2xs rounded-md px-2xs py-3xs text-step-sm text-content no-underline',\n 'focus-ring data-focus:bg-surface data-focus:text-content data-focus:-outline-offset-2',\n className,\n )}\n {...props}\n />\n );\n}\n\n/**\n * A rule between groups of entries — before the destructive ones, so \"do\" and\n * \"destroy\" are not one undifferentiated list.\n */\nfunction Separator() {\n return <MenuSeparator className=\"my-3xs h-px bg-border\" />;\n}\n\n/**\n * A named set of entries — \"Tema\", \"Densidad\" — so a choice among them is\n * announced with what it chooses. The heading is shown, and names the group.\n */\nfunction Group({ label, children }: { label: string; children: ReactNode }) {\n const id = useId();\n return (\n <div role=\"group\" aria-labelledby={id} className=\"py-3xs\">\n <p\n id={id}\n className=\"m-0 px-2xs py-3xs text-step-xs font-semibold text-content-muted\"\n >\n {label}\n </p>\n {children}\n </div>\n );\n}\n\nexport interface MenuRadioProps extends Omit<\n ComponentPropsWithoutRef<'button'>,\n 'role'\n> {\n /** Whether this is the option in effect. */\n checked: boolean;\n}\n\n/**\n * One option of a {@link Group} where exactly one holds: a `menuitemradio`,\n * its state in `aria-checked` and in a visible mark rather than colour alone.\n */\nfunction Radio({ checked, className, children, ...props }: MenuRadioProps) {\n return (\n <HuiMenuItem\n as=\"button\"\n // Headless UI names every item `menuitem` and lets no prop change it;\n // the role is set on the element once it mounts, which keeps the\n // menu's own keyboard handling, and React never writes it back because\n // the prop it renders never changes.\n ref={(node: HTMLButtonElement | null) =>\n node?.setAttribute('role', 'menuitemradio')\n }\n aria-checked={checked}\n className={cn(\n 'flex min-h-(--target-min) w-full items-center gap-2xs rounded-md px-2xs py-3xs text-step-sm text-content',\n 'focus-ring data-focus:bg-surface data-focus:-outline-offset-2',\n className,\n )}\n {...props}\n >\n <span aria-hidden=\"true\" className=\"shrink-0\">\n {checked ? '●' : '○'}\n </span>\n {children}\n </HuiMenuItem>\n );\n}\n\nMenu.Trigger = Trigger;\nMenu.Items = Items;\nMenu.Item = Item;\nMenu.Link = Link;\nMenu.Separator = Separator;\nMenu.Group = Group;\nMenu.Radio = Radio;\n"],"names":["Menu","HuiMenu","MenuButton","MenuItem","HuiMenuItem","MenuItems","MenuSeparator","useId","cn","children","className","as","Trigger","props","Items","ITEM_TONE_CLASS","neutral","destructive","Item","tone","Link","Separator","Group","label","id","div","role","aria-labelledby","p","Radio","checked","ref","node","setAttribute","aria-checked","span","aria-hidden"],"mappings":"AAAA;;;;AAEA,SACEA,QAAQC,OAAO,EACfC,UAAU,EACVC,YAAYC,WAAW,EACvBC,SAAS,EACTC,aAAa,QACR,oBAAoB;AAC3B,SAAwDC,KAAK,QAAQ,QAAQ;AAE7E,SAASC,EAAE,QAAQ,oBAAoB;AAOvC;;;;CAIC,GACD,OAAO,SAASR,KAAK,EAAES,QAAQ,EAAEC,SAAS,EAAa;IACrD,qBACE,KAACT;QACCU,IAAG;QACHD,WAAWF,GAAG,mCAAmCE;kBAEhDD;;AAGP;AAEA,SAASG,QAAQ,EAA2D;QAA3D,EAAEF,SAAS,EAAgD,OAA3CG;;;IAC/B,qBACE,KAACX;QACCQ,WAAWF,GACT,iIACA,wDACA,cACAE;OAEEG;AAGV;AAEA,SAASC,MAAM,EACbL,QAAQ,EACRC,SAAS,EAIV;IACC,qBACE,KAACL;QACCK,WAAWF,GACT,8HACA,wEAAwE;QACxE,+DAA+D;QAC/D,8CAA8C;QAC9C,sBACAE;kBAGDD;;AAGP;AAoBA,MAAMM,kBAAgD;IACpDC,SAAS;IACTC,aAAa;AACf;AAEA,SAASC,KAAK,EAAwD;QAAxD,EAAER,SAAS,EAAES,OAAO,SAAS,EAA2B,OAAtBN;;;;IAC9C,qBACE,KAACT;QACCO,IAAG;QACHD,WAAWF,GACT,+FACA,iEAAiE;QACjE,mEAAmE;QACnE,iEACAO,eAAe,CAACI,KAAK,EACrB,mDACAT;OAEEG;AAGV;AAEA;;;;;;;;CAQC,GACD,SAASO,KAAK,EAAsD;QAAtD,EAAEV,SAAS,EAA2C,OAAtCG;;;IAC5B,qBACE,KAACT;QACCO,IAAG;QACHD,WAAWF,GACT,yHACA,yFACAE;OAEEG;AAGV;AAEA;;;CAGC,GACD,SAASQ;IACP,qBAAO,KAACf;QAAcI,WAAU;;AAClC;AAEA;;;CAGC,GACD,SAASY,MAAM,EAAEC,KAAK,EAAEd,QAAQ,EAA0C;IACxE,MAAMe,KAAKjB;IACX,qBACE,MAACkB;QAAIC,MAAK;QAAQC,mBAAiBH;QAAId,WAAU;;0BAC/C,KAACkB;gBACCJ,IAAIA;gBACJd,WAAU;0BAETa;;YAEFd;;;AAGP;AAUA;;;CAGC,GACD,SAASoB,MAAM,EAA0D;QAA1D,EAAEC,OAAO,EAAEpB,SAAS,EAAED,QAAQ,EAA4B,OAAvBI;;;;;IAChD,qBACE,MAACT;QACCO,IAAG;QACH,sEAAsE;QACtE,iEAAiE;QACjE,uEAAuE;QACvE,qCAAqC;QACrCoB,KAAK,CAACC,OACJA,wBAAAA,KAAMC,YAAY,CAAC,QAAQ;QAE7BC,gBAAcJ;QACdpB,WAAWF,GACT,4GACA,iEACAE;OAEEG;;0BAEJ,KAACsB;gBAAKC,eAAY;gBAAO1B,WAAU;0BAChCoB,UAAU,MAAM;;YAElBrB;;;AAGP;AAEAT,KAAKY,OAAO,GAAGA;AACfZ,KAAKc,KAAK,GAAGA;AACbd,KAAKkB,IAAI,GAAGA;AACZlB,KAAKoB,IAAI,GAAGA;AACZpB,KAAKqB,SAAS,GAAGA;AACjBrB,KAAKsB,KAAK,GAAGA;AACbtB,KAAK6B,KAAK,GAAGA"}
@@ -11,6 +11,11 @@ export interface PaginationProps {
11
11
  /**
12
12
  * Page stepper plus an optional page-size selector, driven by callbacks.
13
13
  *
14
+ * A `nav` with First, Previous, Next and Last. An end that goes nowhere stays
15
+ * focusable and says so with `aria-disabled`, rather than dropping out of the
16
+ * tab order under the reader's hand. A page change is announced — "Page 2 of
17
+ * 5" — and the first render is not.
18
+ *
14
19
  * For a listing whose pages are addresses (server-rendered, crawlable, working
15
20
  * without JavaScript) use {@link PaginationLinks} instead.
16
21
  */
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/pagination/pagination.tsx"],"names":[],"mappings":"AASA,eAAO,MAAM,yBAAyB,4BAA6B,CAAC;AAEpE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,WAAW,EACX,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,eAA2C,EAC3C,SAAS,GACV,EAAE,eAAe,+BAwDjB"}
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/pagination/pagination.tsx"],"names":[],"mappings":"AAWA,eAAO,MAAM,yBAAyB,4BAA6B,CAAC;AAEpE,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,EACzB,WAAW,EACX,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,eAA2C,EAC3C,SAAS,GACV,EAAE,eAAe,+BAkEjB"}
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
3
4
  import { useT } from '../../../i18n/index.js';
4
5
  import { Button } from '../../atoms/button/index.js';
5
6
  import { Select } from '../../atoms/field/index.js';
@@ -15,12 +16,32 @@ export const DEFAULT_PAGE_SIZE_OPTIONS = [
15
16
  /**
16
17
  * Page stepper plus an optional page-size selector, driven by callbacks.
17
18
  *
19
+ * A `nav` with First, Previous, Next and Last. An end that goes nowhere stays
20
+ * focusable and says so with `aria-disabled`, rather than dropping out of the
21
+ * tab order under the reader's hand. A page change is announced — "Page 2 of
22
+ * 5" — and the first render is not.
23
+ *
18
24
  * For a listing whose pages are addresses (server-rendered, crawlable, working
19
25
  * without JavaScript) use {@link PaginationLinks} instead.
20
26
  */ export function Pagination({ currentPage, pageSize, totalItems, onPageChange, onPageSizeChange, pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS, className }) {
21
27
  const t = useT();
22
28
  const totalPages = pageCount(totalItems, pageSize);
23
- return /*#__PURE__*/ _jsxs("div", {
29
+ const [announced, setAnnounced] = useState(false);
30
+ const step = (label, page, enabled)=>/*#__PURE__*/ _jsx(Button, {
31
+ type: "button",
32
+ variant: "secondary",
33
+ size: "sm",
34
+ // Headless UI runs no handler on an `aria-disabled` button, so an end
35
+ // that goes nowhere is inert while it keeps its place in the tab order.
36
+ "aria-disabled": enabled ? undefined : true,
37
+ onClick: ()=>{
38
+ setAnnounced(true);
39
+ onPageChange(page);
40
+ },
41
+ children: label
42
+ });
43
+ return /*#__PURE__*/ _jsxs("nav", {
44
+ "aria-label": t('pagination.label'),
24
45
  className: cn('flex flex-wrap items-center justify-between gap-2xs', className),
25
46
  children: [
26
47
  /*#__PURE__*/ _jsx(Text, {
@@ -32,8 +53,16 @@ export const DEFAULT_PAGE_SIZE_OPTIONS = [
32
53
  items: totalItems
33
54
  })
34
55
  }),
56
+ /*#__PURE__*/ _jsx("span", {
57
+ role: "status",
58
+ className: "sr-only",
59
+ children: announced ? t('pagination.page', {
60
+ current: currentPage,
61
+ total: totalPages
62
+ }) : ''
63
+ }),
35
64
  /*#__PURE__*/ _jsxs("div", {
36
- className: "flex items-center gap-2xs",
65
+ className: "flex flex-wrap items-center gap-2xs",
37
66
  children: [
38
67
  onPageSizeChange && /*#__PURE__*/ _jsxs("label", {
39
68
  className: "flex items-center gap-3xs text-step-sm text-content-muted",
@@ -49,22 +78,10 @@ export const DEFAULT_PAGE_SIZE_OPTIONS = [
49
78
  })
50
79
  ]
51
80
  }),
52
- /*#__PURE__*/ _jsx(Button, {
53
- type: "button",
54
- variant: "secondary",
55
- size: "sm",
56
- disabled: currentPage <= 1,
57
- onClick: ()=>onPageChange(currentPage - 1),
58
- children: t('pagination.previous')
59
- }),
60
- /*#__PURE__*/ _jsx(Button, {
61
- type: "button",
62
- variant: "secondary",
63
- size: "sm",
64
- disabled: currentPage >= totalPages,
65
- onClick: ()=>onPageChange(currentPage + 1),
66
- children: t('pagination.next')
67
- })
81
+ step(t('pagination.first'), 1, currentPage > 1),
82
+ step(t('pagination.previous'), currentPage - 1, currentPage > 1),
83
+ step(t('pagination.next'), currentPage + 1, currentPage < totalPages),
84
+ step(t('pagination.last'), totalPages, currentPage < totalPages)
68
85
  ]
69
86
  })
70
87
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/pagination/pagination.tsx"],"sourcesContent":["'use client';\n\nimport { useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Select } from '../../atoms/field/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport { cn } from '../../utils/cn.js';\nimport { pageCount } from './page-math.js';\n\nexport const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100] as const;\n\nexport interface PaginationProps {\n currentPage: number;\n pageSize: number;\n totalItems: number;\n onPageChange: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n pageSizeOptions?: readonly number[];\n className?: string;\n}\n\n/**\n * Page stepper plus an optional page-size selector, driven by callbacks.\n *\n * For a listing whose pages are addresses (server-rendered, crawlable, working\n * without JavaScript) use {@link PaginationLinks} instead.\n */\nexport function Pagination({\n currentPage,\n pageSize,\n totalItems,\n onPageChange,\n onPageSizeChange,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n className,\n}: PaginationProps) {\n const t = useT();\n const totalPages = pageCount(totalItems, pageSize);\n\n return (\n <div\n className={cn(\n 'flex flex-wrap items-center justify-between gap-2xs',\n className,\n )}\n >\n <Text step={-1} tone=\"muted\">\n {t('pagination.summary', {\n current: currentPage,\n total: totalPages,\n items: totalItems,\n })}\n </Text>\n\n <div className=\"flex items-center gap-2xs\">\n {onPageSizeChange && (\n <label className=\"flex items-center gap-3xs text-step-sm text-content-muted\">\n {t('pagination.rows')}\n <Select\n value={pageSize}\n onChange={event => onPageSizeChange(Number(event.target.value))}\n >\n {pageSizeOptions.map(option => (\n <option key={option} value={option}>\n {option}\n </option>\n ))}\n </Select>\n </label>\n )}\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={currentPage <= 1}\n onClick={() => onPageChange(currentPage - 1)}\n >\n {t('pagination.previous')}\n </Button>\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={currentPage >= totalPages}\n onClick={() => onPageChange(currentPage + 1)}\n >\n {t('pagination.next')}\n </Button>\n </div>\n </div>\n );\n}\n"],"names":["useT","Button","Select","Text","cn","pageCount","DEFAULT_PAGE_SIZE_OPTIONS","Pagination","currentPage","pageSize","totalItems","onPageChange","onPageSizeChange","pageSizeOptions","className","t","totalPages","div","step","tone","current","total","items","label","value","onChange","event","Number","target","map","option","type","variant","size","disabled","onClick"],"mappings":"AAAA;;AAEA,SAASA,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,MAAM,QAAQ,6BAA6B;AACpD,SAASC,IAAI,QAAQ,4BAA4B;AACjD,SAASC,EAAE,QAAQ,oBAAoB;AACvC,SAASC,SAAS,QAAQ,iBAAiB;AAE3C,OAAO,MAAMC,4BAA4B;IAAC;IAAI;IAAI;IAAI;CAAI,CAAU;AAYpE;;;;;CAKC,GACD,OAAO,SAASC,WAAW,EACzBC,WAAW,EACXC,QAAQ,EACRC,UAAU,EACVC,YAAY,EACZC,gBAAgB,EAChBC,kBAAkBP,yBAAyB,EAC3CQ,SAAS,EACO;IAChB,MAAMC,IAAIf;IACV,MAAMgB,aAAaX,UAAUK,YAAYD;IAEzC,qBACE,MAACQ;QACCH,WAAWV,GACT,uDACAU;;0BAGF,KAACX;gBAAKe,MAAM,CAAC;gBAAGC,MAAK;0BAClBJ,EAAE,sBAAsB;oBACvBK,SAASZ;oBACTa,OAAOL;oBACPM,OAAOZ;gBACT;;0BAGF,MAACO;gBAAIH,WAAU;;oBACZF,kCACC,MAACW;wBAAMT,WAAU;;4BACdC,EAAE;0CACH,KAACb;gCACCsB,OAAOf;gCACPgB,UAAUC,CAAAA,QAASd,iBAAiBe,OAAOD,MAAME,MAAM,CAACJ,KAAK;0CAE5DX,gBAAgBgB,GAAG,CAACC,CAAAA,uBACnB,KAACA;wCAAoBN,OAAOM;kDACzBA;uCADUA;;;;kCAOrB,KAAC7B;wBACC8B,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAU1B,eAAe;wBACzB2B,SAAS,IAAMxB,aAAaH,cAAc;kCAEzCO,EAAE;;kCAEL,KAACd;wBACC8B,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAU1B,eAAeQ;wBACzBmB,SAAS,IAAMxB,aAAaH,cAAc;kCAEzCO,EAAE;;;;;;AAKb"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/pagination/pagination.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Select } from '../../atoms/field/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport { cn } from '../../utils/cn.js';\nimport { pageCount } from './page-math.js';\n\nexport const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100] as const;\n\nexport interface PaginationProps {\n currentPage: number;\n pageSize: number;\n totalItems: number;\n onPageChange: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n pageSizeOptions?: readonly number[];\n className?: string;\n}\n\n/**\n * Page stepper plus an optional page-size selector, driven by callbacks.\n *\n * A `nav` with First, Previous, Next and Last. An end that goes nowhere stays\n * focusable and says so with `aria-disabled`, rather than dropping out of the\n * tab order under the reader's hand. A page change is announced — \"Page 2 of\n * 5\" — and the first render is not.\n *\n * For a listing whose pages are addresses (server-rendered, crawlable, working\n * without JavaScript) use {@link PaginationLinks} instead.\n */\nexport function Pagination({\n currentPage,\n pageSize,\n totalItems,\n onPageChange,\n onPageSizeChange,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n className,\n}: PaginationProps) {\n const t = useT();\n const totalPages = pageCount(totalItems, pageSize);\n const [announced, setAnnounced] = useState(false);\n\n const step = (label: string, page: number, enabled: boolean) => (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n // Headless UI runs no handler on an `aria-disabled` button, so an end\n // that goes nowhere is inert while it keeps its place in the tab order.\n aria-disabled={enabled ? undefined : true}\n onClick={() => {\n setAnnounced(true);\n onPageChange(page);\n }}\n >\n {label}\n </Button>\n );\n\n return (\n <nav\n aria-label={t('pagination.label')}\n className={cn(\n 'flex flex-wrap items-center justify-between gap-2xs',\n className,\n )}\n >\n <Text step={-1} tone=\"muted\">\n {t('pagination.summary', {\n current: currentPage,\n total: totalPages,\n items: totalItems,\n })}\n </Text>\n <span role=\"status\" className=\"sr-only\">\n {announced\n ? t('pagination.page', { current: currentPage, total: totalPages })\n : ''}\n </span>\n\n <div className=\"flex flex-wrap items-center gap-2xs\">\n {onPageSizeChange && (\n <label className=\"flex items-center gap-3xs text-step-sm text-content-muted\">\n {t('pagination.rows')}\n <Select\n value={pageSize}\n onChange={event => onPageSizeChange(Number(event.target.value))}\n >\n {pageSizeOptions.map(option => (\n <option key={option} value={option}>\n {option}\n </option>\n ))}\n </Select>\n </label>\n )}\n {step(t('pagination.first'), 1, currentPage > 1)}\n {step(t('pagination.previous'), currentPage - 1, currentPage > 1)}\n {step(t('pagination.next'), currentPage + 1, currentPage < totalPages)}\n {step(t('pagination.last'), totalPages, currentPage < totalPages)}\n </div>\n </nav>\n );\n}\n"],"names":["useState","useT","Button","Select","Text","cn","pageCount","DEFAULT_PAGE_SIZE_OPTIONS","Pagination","currentPage","pageSize","totalItems","onPageChange","onPageSizeChange","pageSizeOptions","className","t","totalPages","announced","setAnnounced","step","label","page","enabled","type","variant","size","aria-disabled","undefined","onClick","nav","aria-label","tone","current","total","items","span","role","div","value","onChange","event","Number","target","map","option"],"mappings":"AAAA;;AAEA,SAASA,QAAQ,QAAQ,QAAQ;AAEjC,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,MAAM,QAAQ,6BAA6B;AACpD,SAASC,IAAI,QAAQ,4BAA4B;AACjD,SAASC,EAAE,QAAQ,oBAAoB;AACvC,SAASC,SAAS,QAAQ,iBAAiB;AAE3C,OAAO,MAAMC,4BAA4B;IAAC;IAAI;IAAI;IAAI;CAAI,CAAU;AAYpE;;;;;;;;;;CAUC,GACD,OAAO,SAASC,WAAW,EACzBC,WAAW,EACXC,QAAQ,EACRC,UAAU,EACVC,YAAY,EACZC,gBAAgB,EAChBC,kBAAkBP,yBAAyB,EAC3CQ,SAAS,EACO;IAChB,MAAMC,IAAIf;IACV,MAAMgB,aAAaX,UAAUK,YAAYD;IACzC,MAAM,CAACQ,WAAWC,aAAa,GAAGnB,SAAS;IAE3C,MAAMoB,OAAO,CAACC,OAAeC,MAAcC,wBACzC,KAACrB;YACCsB,MAAK;YACLC,SAAQ;YACRC,MAAK;YACL,sEAAsE;YACtE,wEAAwE;YACxEC,iBAAeJ,UAAUK,YAAY;YACrCC,SAAS;gBACPV,aAAa;gBACbP,aAAaU;YACf;sBAECD;;IAIL,qBACE,MAACS;QACCC,cAAYf,EAAE;QACdD,WAAWV,GACT,uDACAU;;0BAGF,KAACX;gBAAKgB,MAAM,CAAC;gBAAGY,MAAK;0BAClBhB,EAAE,sBAAsB;oBACvBiB,SAASxB;oBACTyB,OAAOjB;oBACPkB,OAAOxB;gBACT;;0BAEF,KAACyB;gBAAKC,MAAK;gBAAStB,WAAU;0BAC3BG,YACGF,EAAE,mBAAmB;oBAAEiB,SAASxB;oBAAayB,OAAOjB;gBAAW,KAC/D;;0BAGN,MAACqB;gBAAIvB,WAAU;;oBACZF,kCACC,MAACQ;wBAAMN,WAAU;;4BACdC,EAAE;0CACH,KAACb;gCACCoC,OAAO7B;gCACP8B,UAAUC,CAAAA,QAAS5B,iBAAiB6B,OAAOD,MAAME,MAAM,CAACJ,KAAK;0CAE5DzB,gBAAgB8B,GAAG,CAACC,CAAAA,uBACnB,KAACA;wCAAoBN,OAAOM;kDACzBA;uCADUA;;;;oBAOpBzB,KAAKJ,EAAE,qBAAqB,GAAGP,cAAc;oBAC7CW,KAAKJ,EAAE,wBAAwBP,cAAc,GAAGA,cAAc;oBAC9DW,KAAKJ,EAAE,oBAAoBP,cAAc,GAAGA,cAAcQ;oBAC1DG,KAAKJ,EAAE,oBAAoBC,YAAYR,cAAcQ;;;;;AAI9D"}
@@ -0,0 +1,2 @@
1
+ export * from './region-state.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/region-state/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './region-state.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/ui/molecules/region-state/index.ts"],"sourcesContent":["export * from './region-state.js';\n"],"names":[],"mappings":"AAAA,cAAc,oBAAoB"}
@@ -0,0 +1,42 @@
1
+ import type { ReactNode } from 'react';
2
+ /** A failure as the organisms hold it: a typed failure, or anything thrown. */
3
+ export interface RegionStateError {
4
+ message: string;
5
+ code?: unknown;
6
+ details?: Record<string, unknown>;
7
+ }
8
+ export interface RegionStateProps {
9
+ /** What the region holds, plural — "invoices" — for the state's sentence. */
10
+ plural: string;
11
+ /** The first load: the organism's skeleton speaks for it, so nothing here. */
12
+ isLoading?: boolean;
13
+ /** A load that failed. */
14
+ error?: RegionStateError;
15
+ /** Loads the region again. Without it, an error offers no Retry. */
16
+ onRetry?: () => void;
17
+ /** The load answered, with nothing in it. */
18
+ isEmpty?: boolean;
19
+ /** A filter is in effect, so an empty answer is the filter's, not the data's. */
20
+ isFiltered?: boolean;
21
+ /** Clears every filter — what an empty filtered region offers. */
22
+ onClearFilters?: () => void;
23
+ /** What an empty, unfiltered region offers: creating the first record. */
24
+ emptyAction?: ReactNode;
25
+ /** Data from before the failure is still on screen. */
26
+ hasStaleData?: boolean;
27
+ }
28
+ /**
29
+ * What a region says when it is not simply showing its content
30
+ * ([ADR 0028](../../../../../../../docs/adr/0028-states-an-organism-must-render.md)):
31
+ *
32
+ * - a **load error** is an `Alert` with Retry — `forbidden` says so in its own
33
+ * words — and data kept from before it is marked possibly out of date;
34
+ * - an **empty result under a filter** says so and offers Clear filters;
35
+ * - an **empty region** says what appears here, and offers its first record.
36
+ *
37
+ * It renders nothing while loading, and nothing when there is content: the
38
+ * organism's own skeleton and rows speak then. An empty result and a dead
39
+ * backend never look alike.
40
+ */
41
+ export declare function RegionState({ plural, isLoading, error, onRetry, isEmpty, isFiltered, onClearFilters, emptyAction, hasStaleData, }: RegionStateProps): import("react").JSX.Element | null;
42
+ //# sourceMappingURL=region-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region-state.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/region-state/region-state.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAQvC,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iFAAiF;IACjF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,0EAA0E;IAC1E,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,uDAAuD;IACvD,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,SAAiB,EACjB,KAAK,EACL,OAAO,EACP,OAAe,EACf,UAAkB,EAClB,cAAc,EACd,WAAW,EACX,YAAoB,GACrB,EAAE,gBAAgB,sCA8ElB"}