@open-mercato/ui 0.5.1-develop.2856.35de414092 → 0.5.1-develop.2874.77704bccbd

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 (246) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +204 -121
  3. package/dist/backend/AppShell.js +25 -28
  4. package/dist/backend/AppShell.js.map +2 -2
  5. package/dist/backend/ContextHelp.js +1 -1
  6. package/dist/backend/ContextHelp.js.map +1 -1
  7. package/dist/backend/CrudForm.js +12 -15
  8. package/dist/backend/CrudForm.js.map +2 -2
  9. package/dist/backend/DataTable.js +9 -10
  10. package/dist/backend/DataTable.js.map +2 -2
  11. package/dist/backend/FilterBar.js +6 -8
  12. package/dist/backend/FilterBar.js.map +2 -2
  13. package/dist/backend/FilterOverlay.js +10 -10
  14. package/dist/backend/FilterOverlay.js.map +2 -2
  15. package/dist/backend/FlashMessages.js +1 -1
  16. package/dist/backend/FlashMessages.js.map +2 -2
  17. package/dist/backend/JsonBuilder.js +6 -6
  18. package/dist/backend/JsonBuilder.js.map +1 -1
  19. package/dist/backend/NextStepCallout.js +1 -1
  20. package/dist/backend/NextStepCallout.js.map +1 -1
  21. package/dist/backend/PerspectiveSidebar.js +2 -2
  22. package/dist/backend/PerspectiveSidebar.js.map +2 -2
  23. package/dist/backend/ProfileDropdown.js +1 -1
  24. package/dist/backend/ProfileDropdown.js.map +1 -1
  25. package/dist/backend/RowActions.js +1 -1
  26. package/dist/backend/RowActions.js.map +1 -1
  27. package/dist/backend/UserMenu.js +2 -2
  28. package/dist/backend/UserMenu.js.map +1 -1
  29. package/dist/backend/WebhookSetupGuide.js +11 -11
  30. package/dist/backend/WebhookSetupGuide.js.map +2 -2
  31. package/dist/backend/charts/KpiCard.js +3 -3
  32. package/dist/backend/charts/KpiCard.js.map +1 -1
  33. package/dist/backend/columns/ColumnChooserPanel.js +1 -1
  34. package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
  35. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +3 -3
  36. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
  37. package/dist/backend/dashboard/DashboardScreen.js +1 -1
  38. package/dist/backend/dashboard/DashboardScreen.js.map +1 -1
  39. package/dist/backend/date-range/DateRangeSelect.js +1 -1
  40. package/dist/backend/date-range/DateRangeSelect.js.map +1 -1
  41. package/dist/backend/date-range/InlineDateRangeSelect.js +1 -1
  42. package/dist/backend/date-range/InlineDateRangeSelect.js.map +1 -1
  43. package/dist/backend/detail/AccessDeniedMessage.js +1 -1
  44. package/dist/backend/detail/AccessDeniedMessage.js.map +1 -1
  45. package/dist/backend/detail/ActivitiesSection.js +5 -5
  46. package/dist/backend/detail/ActivitiesSection.js.map +1 -1
  47. package/dist/backend/detail/AddressEditor.js +3 -3
  48. package/dist/backend/detail/AddressEditor.js.map +2 -2
  49. package/dist/backend/detail/AddressTiles.js +3 -3
  50. package/dist/backend/detail/AddressTiles.js.map +2 -2
  51. package/dist/backend/detail/AttachmentMetadataDialog.js +1 -1
  52. package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
  53. package/dist/backend/detail/CustomDataSection.js +1 -1
  54. package/dist/backend/detail/CustomDataSection.js.map +1 -1
  55. package/dist/backend/detail/InlineEditors.js +5 -5
  56. package/dist/backend/detail/InlineEditors.js.map +1 -1
  57. package/dist/backend/detail/NotesSection.js +6 -6
  58. package/dist/backend/detail/NotesSection.js.map +1 -1
  59. package/dist/backend/detail/TagsSection.js +1 -1
  60. package/dist/backend/detail/TagsSection.js.map +1 -1
  61. package/dist/backend/devtools/UmesDevToolsPanel.js +6 -6
  62. package/dist/backend/devtools/UmesDevToolsPanel.js.map +2 -2
  63. package/dist/backend/devtools/components/ConflictWarnings.js +3 -3
  64. package/dist/backend/devtools/components/ConflictWarnings.js.map +2 -2
  65. package/dist/backend/devtools/components/EnricherTiming.js +2 -2
  66. package/dist/backend/devtools/components/EnricherTiming.js.map +2 -2
  67. package/dist/backend/devtools/components/EventFlow.js +5 -5
  68. package/dist/backend/devtools/components/EventFlow.js.map +2 -2
  69. package/dist/backend/devtools/components/ExtensionPointList.js +3 -3
  70. package/dist/backend/devtools/components/ExtensionPointList.js.map +2 -2
  71. package/dist/backend/devtools/components/InterceptorActivity.js +6 -6
  72. package/dist/backend/devtools/components/InterceptorActivity.js.map +2 -2
  73. package/dist/backend/forms/ActionsDropdown.js +1 -1
  74. package/dist/backend/forms/ActionsDropdown.js.map +1 -1
  75. package/dist/backend/forms/FormActionButtons.js +2 -3
  76. package/dist/backend/forms/FormActionButtons.js.map +2 -2
  77. package/dist/backend/indexes/PartialIndexBanner.js +8 -8
  78. package/dist/backend/indexes/PartialIndexBanner.js.map +2 -2
  79. package/dist/backend/inputs/ComboboxInput.js +1 -1
  80. package/dist/backend/inputs/ComboboxInput.js.map +2 -2
  81. package/dist/backend/inputs/DatePicker.js +3 -3
  82. package/dist/backend/inputs/DatePicker.js.map +1 -1
  83. package/dist/backend/inputs/DateTimePicker.js +3 -3
  84. package/dist/backend/inputs/DateTimePicker.js.map +1 -1
  85. package/dist/backend/inputs/EventSelect.js +1 -1
  86. package/dist/backend/inputs/EventSelect.js.map +2 -2
  87. package/dist/backend/inputs/LookupSelect.js +1 -1
  88. package/dist/backend/inputs/LookupSelect.js.map +1 -1
  89. package/dist/backend/inputs/SwitchableMarkdownInput.js +1 -1
  90. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +1 -1
  91. package/dist/backend/inputs/TagsInput.js +2 -2
  92. package/dist/backend/inputs/TagsInput.js.map +2 -2
  93. package/dist/backend/inputs/TimeInput.js +1 -1
  94. package/dist/backend/inputs/TimeInput.js.map +1 -1
  95. package/dist/backend/inputs/TimePicker.js +3 -3
  96. package/dist/backend/inputs/TimePicker.js.map +1 -1
  97. package/dist/backend/messages/MessageObjectDetail.js +1 -1
  98. package/dist/backend/messages/MessageObjectDetail.js.map +1 -1
  99. package/dist/backend/messages/MessageObjectPreview.js +1 -1
  100. package/dist/backend/messages/MessageObjectPreview.js.map +1 -1
  101. package/dist/backend/messages/message-compose-form-groups.js +3 -3
  102. package/dist/backend/messages/message-compose-form-groups.js.map +1 -1
  103. package/dist/backend/notifications/NotificationCountBadge.js +1 -1
  104. package/dist/backend/notifications/NotificationCountBadge.js.map +2 -2
  105. package/dist/backend/notifications/NotificationPanel.js +3 -3
  106. package/dist/backend/notifications/NotificationPanel.js.map +1 -1
  107. package/dist/backend/progress/ProgressTopBar.js +4 -4
  108. package/dist/backend/progress/ProgressTopBar.js.map +2 -2
  109. package/dist/backend/schedule/ScheduleAgenda.js +1 -1
  110. package/dist/backend/schedule/ScheduleAgenda.js.map +2 -2
  111. package/dist/backend/schedule/ScheduleCalendar.js +1 -1
  112. package/dist/backend/schedule/ScheduleCalendar.js.map +1 -1
  113. package/dist/backend/schedule/ScheduleGrid.js +1 -1
  114. package/dist/backend/schedule/ScheduleGrid.js.map +2 -2
  115. package/dist/backend/version-history/VersionHistoryPanel.js +4 -4
  116. package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
  117. package/dist/frontend/AuthFooter.js +1 -1
  118. package/dist/frontend/AuthFooter.js.map +1 -1
  119. package/dist/frontend/LanguageSwitcher.js +1 -1
  120. package/dist/frontend/LanguageSwitcher.js.map +1 -1
  121. package/dist/frontend/Layout.js +2 -2
  122. package/dist/frontend/Layout.js.map +1 -1
  123. package/dist/index.js +5 -0
  124. package/dist/index.js.map +2 -2
  125. package/dist/portal/PortalShell.js +15 -15
  126. package/dist/portal/PortalShell.js.map +2 -2
  127. package/dist/portal/components/PortalCard.js +2 -2
  128. package/dist/portal/components/PortalCard.js.map +2 -2
  129. package/dist/portal/components/PortalNotificationPanel.js +18 -18
  130. package/dist/portal/components/PortalNotificationPanel.js.map +2 -2
  131. package/dist/portal/components/PortalPageHeader.js +1 -1
  132. package/dist/portal/components/PortalPageHeader.js.map +2 -2
  133. package/dist/primitives/avatar.js +11 -1
  134. package/dist/primitives/avatar.js.map +2 -2
  135. package/dist/primitives/badge.js +1 -1
  136. package/dist/primitives/badge.js.map +1 -1
  137. package/dist/primitives/button.js +9 -5
  138. package/dist/primitives/button.js.map +2 -2
  139. package/dist/primitives/calendar.js +1 -1
  140. package/dist/primitives/calendar.js.map +1 -1
  141. package/dist/primitives/checkbox-field.js +63 -0
  142. package/dist/primitives/checkbox-field.js.map +7 -0
  143. package/dist/primitives/checkbox.js +31 -17
  144. package/dist/primitives/checkbox.js.map +2 -2
  145. package/dist/primitives/dialog.js +4 -4
  146. package/dist/primitives/dialog.js.map +1 -1
  147. package/dist/primitives/fancy-button.js +72 -0
  148. package/dist/primitives/fancy-button.js.map +7 -0
  149. package/dist/primitives/icon-button.js +20 -4
  150. package/dist/primitives/icon-button.js.map +2 -2
  151. package/dist/primitives/kbd.js +27 -0
  152. package/dist/primitives/kbd.js.map +7 -0
  153. package/dist/primitives/link-button.js +56 -0
  154. package/dist/primitives/link-button.js.map +7 -0
  155. package/dist/primitives/popover.js +1 -1
  156. package/dist/primitives/popover.js.map +1 -1
  157. package/dist/primitives/social-button.js +61 -0
  158. package/dist/primitives/social-button.js.map +7 -0
  159. package/dist/primitives/tabs.js +1 -1
  160. package/dist/primitives/tabs.js.map +1 -1
  161. package/dist/primitives/tag.js +45 -0
  162. package/dist/primitives/tag.js.map +7 -0
  163. package/dist/primitives/tooltip.js +1 -1
  164. package/dist/primitives/tooltip.js.map +1 -1
  165. package/package.json +3 -3
  166. package/src/backend/AppShell.tsx +25 -28
  167. package/src/backend/ContextHelp.tsx +1 -1
  168. package/src/backend/CrudForm.tsx +12 -15
  169. package/src/backend/DataTable.tsx +9 -10
  170. package/src/backend/FilterBar.tsx +6 -5
  171. package/src/backend/FilterOverlay.tsx +10 -10
  172. package/src/backend/FlashMessages.tsx +1 -1
  173. package/src/backend/JsonBuilder.tsx +6 -6
  174. package/src/backend/NextStepCallout.tsx +1 -1
  175. package/src/backend/PerspectiveSidebar.tsx +2 -2
  176. package/src/backend/ProfileDropdown.tsx +1 -1
  177. package/src/backend/RowActions.tsx +1 -1
  178. package/src/backend/UserMenu.tsx +2 -2
  179. package/src/backend/WebhookSetupGuide.tsx +11 -11
  180. package/src/backend/charts/KpiCard.tsx +3 -3
  181. package/src/backend/columns/ColumnChooserPanel.tsx +1 -1
  182. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +3 -3
  183. package/src/backend/dashboard/DashboardScreen.tsx +1 -1
  184. package/src/backend/date-range/DateRangeSelect.tsx +1 -1
  185. package/src/backend/date-range/InlineDateRangeSelect.tsx +1 -1
  186. package/src/backend/detail/AccessDeniedMessage.tsx +1 -1
  187. package/src/backend/detail/ActivitiesSection.tsx +5 -5
  188. package/src/backend/detail/AddressEditor.tsx +3 -3
  189. package/src/backend/detail/AddressTiles.tsx +3 -3
  190. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  191. package/src/backend/detail/CustomDataSection.tsx +1 -1
  192. package/src/backend/detail/InlineEditors.tsx +5 -5
  193. package/src/backend/detail/NotesSection.tsx +6 -6
  194. package/src/backend/detail/TagsSection.tsx +1 -1
  195. package/src/backend/devtools/UmesDevToolsPanel.tsx +6 -6
  196. package/src/backend/devtools/components/ConflictWarnings.tsx +4 -4
  197. package/src/backend/devtools/components/EnricherTiming.tsx +2 -2
  198. package/src/backend/devtools/components/EventFlow.tsx +5 -5
  199. package/src/backend/devtools/components/ExtensionPointList.tsx +3 -3
  200. package/src/backend/devtools/components/InterceptorActivity.tsx +6 -6
  201. package/src/backend/forms/ActionsDropdown.tsx +1 -1
  202. package/src/backend/forms/FormActionButtons.tsx +4 -5
  203. package/src/backend/indexes/PartialIndexBanner.tsx +8 -8
  204. package/src/backend/inputs/ComboboxInput.tsx +1 -1
  205. package/src/backend/inputs/DatePicker.tsx +3 -3
  206. package/src/backend/inputs/DateTimePicker.tsx +3 -3
  207. package/src/backend/inputs/EventSelect.tsx +1 -1
  208. package/src/backend/inputs/LookupSelect.tsx +1 -1
  209. package/src/backend/inputs/SwitchableMarkdownInput.tsx +1 -1
  210. package/src/backend/inputs/TagsInput.tsx +2 -2
  211. package/src/backend/inputs/TimeInput.tsx +1 -1
  212. package/src/backend/inputs/TimePicker.tsx +3 -3
  213. package/src/backend/messages/MessageObjectDetail.tsx +1 -1
  214. package/src/backend/messages/MessageObjectPreview.tsx +1 -1
  215. package/src/backend/messages/message-compose-form-groups.tsx +3 -3
  216. package/src/backend/notifications/NotificationCountBadge.tsx +1 -1
  217. package/src/backend/notifications/NotificationPanel.tsx +3 -3
  218. package/src/backend/progress/ProgressTopBar.tsx +4 -4
  219. package/src/backend/schedule/ScheduleAgenda.tsx +1 -1
  220. package/src/backend/schedule/ScheduleCalendar.tsx +1 -1
  221. package/src/backend/schedule/ScheduleGrid.tsx +1 -1
  222. package/src/backend/version-history/VersionHistoryPanel.tsx +4 -4
  223. package/src/frontend/AuthFooter.tsx +1 -1
  224. package/src/frontend/LanguageSwitcher.tsx +1 -1
  225. package/src/frontend/Layout.tsx +2 -2
  226. package/src/index.ts +6 -1
  227. package/src/portal/PortalShell.tsx +15 -15
  228. package/src/portal/components/PortalCard.tsx +2 -2
  229. package/src/portal/components/PortalNotificationPanel.tsx +18 -18
  230. package/src/portal/components/PortalPageHeader.tsx +1 -1
  231. package/src/primitives/avatar.tsx +22 -0
  232. package/src/primitives/badge.tsx +1 -1
  233. package/src/primitives/button.tsx +12 -5
  234. package/src/primitives/calendar.tsx +1 -1
  235. package/src/primitives/checkbox-field.tsx +85 -0
  236. package/src/primitives/checkbox.tsx +44 -18
  237. package/src/primitives/dialog.tsx +4 -4
  238. package/src/primitives/fancy-button.tsx +89 -0
  239. package/src/primitives/icon-button.tsx +19 -2
  240. package/src/primitives/kbd.tsx +38 -0
  241. package/src/primitives/link-button.tsx +55 -0
  242. package/src/primitives/popover.tsx +1 -1
  243. package/src/primitives/social-button.tsx +80 -0
  244. package/src/primitives/tabs.tsx +1 -1
  245. package/src/primitives/tag.tsx +66 -0
  246. package/src/primitives/tooltip.tsx +1 -1
@@ -1,3 +1,3 @@
1
1
  Generated lucide registry with 134 icons -> /home/runner/work/open-mercato/open-mercato/packages/ui/src/backend/icons/lucideRegistry.generated.tsx
2
- [build:ui] found 264 entry points
2
+ [build:ui] found 270 entry points
3
3
  [build:ui] built successfully
package/AGENTS.md CHANGED
@@ -1,125 +1,213 @@
1
1
  # UI Package - Agent Guidelines
2
2
 
3
- This document captures UI usage patterns based on current implementations in the customers, sales, and staff (auth users/roles) modules. Use these as the default conventions when building new UI in `packages/ui` or when consuming UI components from other modules.
3
+ UI usage patterns based on customers, sales, and staff modules. Use these defaults when building new UI in `packages/ui` or consuming from other modules.
4
+
5
+ > **DS reference:** [`.ai/ds-rules.md`](../../.ai/ds-rules.md) — color tokens, typography, spacing, decision trees. **Component reference (variants/sizes/props/examples/MUST rules):** [`.ai/ui-components.md`](../../.ai/ui-components.md).
4
6
 
5
7
  ## Reference Modules
6
8
 
7
- - Customers: `packages/core/src/modules/customers/backend/customers/people/create/page.tsx`, `packages/core/src/modules/customers/backend/customers/people/page.tsx`, `packages/core/src/modules/customers/components/detail/TaskForm.tsx`
8
- - Sales: `packages/core/src/modules/sales/components/documents/SalesDocumentsTable.tsx`, `packages/core/src/modules/sales/components/documents/PaymentsSection.tsx`, `packages/core/src/modules/sales/components/documents/SalesDocumentForm.tsx`
9
- - Staff (auth users/roles): `packages/core/src/modules/auth/backend/users/page.tsx`, `packages/core/src/modules/auth/backend/users/create/page.tsx`, `packages/core/src/modules/auth/backend/roles/create/page.tsx`
9
+ - Customers: `packages/core/src/modules/customers/backend/customers/people/create/page.tsx`, `…/people/page.tsx`, `…/components/detail/TaskForm.tsx`
10
+ - Sales: `packages/core/src/modules/sales/components/documents/SalesDocumentsTable.tsx`, `…/PaymentsSection.tsx`, `…/SalesDocumentForm.tsx`
11
+ - Staff: `packages/core/src/modules/auth/backend/users/page.tsx`, `…/users/create/page.tsx`, `…/roles/create/page.tsx`
12
+
13
+ ## Component quick reference
14
+
15
+ When you need… use this. Details (variants, sizes, props, MUST rules) live in [`.ai/ui-components.md`](../../.ai/ui-components.md).
16
+
17
+ | Need | Component | Import |
18
+ |---|---|---|
19
+ | Button with text label (with or without icon) | `Button` | `@open-mercato/ui/primitives/button` |
20
+ | Icon-only button | `IconButton` | `@open-mercato/ui/primitives/icon-button` |
21
+ | Inline link styled as button | `LinkButton` | `@open-mercato/ui/primitives/link-button` |
22
+ | OAuth/sign-in button (brand-styled) | `SocialButton` | `@open-mercato/ui/primitives/social-button` |
23
+ | Marketing CTA with brand gradient | `FancyButton` | `@open-mercato/ui/primitives/fancy-button` |
24
+ | Checkbox primitive (with indeterminate) | `Checkbox` | `@open-mercato/ui/primitives/checkbox` |
25
+ | Checkbox with label + description | `CheckboxField` | `@open-mercato/ui/primitives/checkbox-field` |
26
+ | User / entity avatar | `Avatar`, `AvatarStack` | `@open-mercato/ui/primitives/avatar` |
27
+ | Keyboard shortcut keys | `Kbd`, `KbdShortcut` | `@open-mercato/ui/primitives/kbd` |
28
+ | Entity tag pill | `Tag` (with `TagMap`) | `@open-mercato/ui/primitives/tag` |
29
+ | Wrap a `<Link>` as button | `Button asChild` / `IconButton asChild` | — |
30
+
31
+ ## Critical MUST rules (top of mind)
32
+
33
+ 1. **NEVER use raw `<button>` or `<input type="checkbox">`** — always use the primitives. Native checkboxes get `accent-color: var(--accent-indigo)` as a safety net for legacy code, but new code MUST use `Checkbox`.
34
+ 2. **Always pass `type="button"` explicitly** on non-submit `Button`/`IconButton` — HTML defaults to `submit`.
35
+ 3. **Same-row buttons MUST share `size`.** Mixing `sm` (h-8) + `default`/`icon` (h-9) is a regression. Standardized rows: DataTable toolbar = `default`/`icon` h-9, FormActionButtons = `default` h-9.
36
+ 4. **NEVER raw `<Link>` styled as a button** — wrap with `<Button asChild>` to inherit size + radius.
37
+ 5. **`<Button className="h-9">` is an anti-pattern** — redundant with default size, hides contract from grep.
38
+ 6. **`Checkbox` checked color is `--accent-indigo` (NOT `--primary`)** — matches Figma and distinguishes selection from primary actions.
10
39
 
11
- ## Button and IconButton Usage
40
+ ## CrudForm Guidelines
12
41
 
13
- **MUST use `Button` or `IconButton` from `@open-mercato/ui` for every interactive button.** Never use raw `<button>` elements.
42
+ - Use `CrudForm` as the default for create/edit flows and dialog forms.
43
+ - If a backend page cannot use `CrudForm`, use `useGuardedMutation` from `@open-mercato/ui/backend/injection/useGuardedMutation` for every write (`POST`/`PUT`/`PATCH`/`DELETE`).
44
+ - Always call writes through `runMutation({ operation, context, mutationPayload })` so global injection modules (e.g. record-lock conflict handling) can run `onBeforeSave`/`onAfterSave`, apply scoped headers, and receive errors consistently.
45
+ - Use manual `useInjectionSpotEvents(GLOBAL_MUTATION_INJECTION_SPOT_ID)` only when `useGuardedMutation` is insufficient.
46
+ - Keep `CrudForm` reusable — extract shared field/group builders and submit handlers into module-level helpers.
47
+ - Drive validation with Zod and surface field errors via `createCrudFormError`.
48
+ - With `CrudForm` + Zod, validation messages may be i18n keys (`CrudForm` translates them).
49
+ - If you validate outside `CrudForm` or manually map `safeParse(...).error.issues`, you MUST translate `issue.message` before passing to `createCrudFormError`.
50
+ - Keep `fields` and `groups` in memoized helpers.
51
+ - Pass `entityIds` when custom fields are involved.
52
+ - Use `createCrud`/`updateCrud`/`deleteCrud` for submit actions and call `flash()` for success/failure messaging.
14
53
 
15
- ### When to Use Which
54
+ ## UI Interaction
55
+ - Every new dialog must support `Cmd/Ctrl + Enter` as a primary action shortcut and `Escape` to cancel, mirroring the shared UX patterns used across modules.
56
+ - Default to `CrudForm` for new forms and `DataTable` for tables displaying information unless a different component is explicitly required.
57
+ - Use the `EventSelect` component from `@open-mercato/ui/backend/inputs/EventSelect` for event selection. It fetches declared events via the `/api/events` endpoint.
58
+ - Never use `window.confirm` — use the shared `ConfirmDialog` and `useConfirmDialog` from `@open-mercato/ui/backend/confirm-dialog` for confirmation flows.
59
+ - New CRUD forms should use `CrudForm` wired to CRUD factory/commands APIs and be shared between create/edit flows.
60
+ - Prefer reusing components from the shared `packages/ui` package before introducing new UI primitives.
61
+ - For new `DataTable` columns, set `meta.truncate` and `meta.maxWidth` in the column config when you need specific truncation behavior; only rely on defaults when those are not set.
62
+ - When you create new UI check reusable components before creating UI from scratch (see [`.ai/specs/implemented/SPEC-001-2026-01-21-ui-reusable-components.md`](.ai/specs/implemented/SPEC-001-2026-01-21-ui-reusable-components.md))
63
+ - For form/detail page headers and footers, use `FormHeader` and `FormFooter` from `@open-mercato/ui/backend/forms`. `FormHeader` supports two modes: `edit` (compact, used automatically by CrudForm) and `detail` (large title with entity type label, status badge, Actions dropdown). Delete/Cancel/Save are always standalone buttons; additional context actions (Convert, Send, etc.) go into the `menuActions` array rendered as an "Actions" dropdown. See [SPEC-016](.ai/specs/implemented/SPEC-016-2026-02-03-form-headers-footers.md) for full API.
16
64
 
17
- | Use case | Component | Example |
18
- |----------|-----------|---------|
19
- | Button with text label (with or without icon) | `Button` | Save, Cancel, Apply filters |
20
- | Icon-only button (no visible text) | `IconButton` | Close ✕, Settings ⚙, Trash 🗑 |
21
- | Button wrapping a `<Link>` | `IconButton asChild` or `Button asChild` | `<IconButton asChild><Link href="...">...</Link></IconButton>` |
65
+ ## Avatar
22
66
 
23
- ### Imports
67
+ `Avatar` displays a user or entity with a photo or auto-generated initials. `AvatarStack` overlaps multiple avatars with an overflow indicator.
68
+
69
+ ### Import
24
70
 
25
71
  ```typescript
26
- import { Button } from '@open-mercato/ui/primitives/button'
27
- import { IconButton } from '@open-mercato/ui/primitives/icon-button'
72
+ import { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'
28
73
  ```
29
74
 
30
- ### MUST Rules
75
+ ### Sizes
31
76
 
32
- 1. **MUST always pass `type="button"` explicitly** on non-submit buttons. Neither `Button` nor `IconButton` sets a default type — HTML defaults to `type="submit"`, which causes accidental form submissions.
33
- 2. **MUST NOT use raw `<button>` elements** anywhere in the codebase. Use `Button` or `IconButton` instead.
34
- 3. **MUST use `IconButton`** (not `Button size="icon"`) for icon-only buttons. `IconButton` has fixed square dimensions optimized for icon-only content.
35
- 4. **MUST add `hover:bg-transparent`** when using `variant="ghost"` for tab-style buttons with underline indicators, to suppress the default hover background.
36
- 5. **MUST add `h-auto`** when using Button/IconButton in compact inline contexts (tag chips, toolbars, inline lists) where the fixed height from size variants would overflow the container.
77
+ | Size | px | Use case |
78
+ |---|---|---|
79
+ | `sm` | 24px | Table rows, AvatarStack, inline lists |
80
+ | `default` | 32px | Default sidebar, comments, activity feed |
81
+ | `md` | 40px | Section headers, assignee cards |
82
+ | `lg` | 80px | Profile / detail page header |
37
83
 
38
- ### Variant Reference
84
+ ### Usage
39
85
 
40
- **Button variants**: `default` (primary CTA), `destructive` (danger), `outline` (bordered), `secondary` (subdued), `ghost` (no border/bg), `muted` (dimmed text, ghost-like), `link` (underlined text).
86
+ ```tsx
87
+ // Photo
88
+ <Avatar src="/avatars/jan.jpg" name="Jan Kowalski" size="md" />
89
+
90
+ // Initials (auto-generated from name)
91
+ <Avatar name="Jan Kowalski" /> // → "JK"
92
+ <Avatar name="Copperleaf Design" /> // → "CD"
93
+
94
+ // Stack with overflow
95
+ <AvatarStack max={3}>
96
+ <Avatar name="Jan Kowalski" size="sm" />
97
+ <Avatar name="Oliwia Z." size="sm" />
98
+ <Avatar name="Anna Nowak" size="sm" />
99
+ <Avatar name="Sarah Mitchell" size="sm" />
100
+ </AvatarStack>
101
+ // renders: JK · OZ · AN · +1
102
+ ```
41
103
 
42
- **Button sizes**: `default` (h-9 px-4), `sm` (h-8 px-3), `lg` (h-10 px-6), `icon` (size-9, square).
104
+ ### MUST rules
43
105
 
44
- **IconButton variants**: `outline` (bordered, default), `ghost` (no border/bg).
106
+ - NEVER render `<div className="rounded-full bg-muted ...">` for avatars — use `Avatar`
107
+ - `size="sm"` uses `text-[9px]` — DS exception for tiny initials (same as notification badge count)
108
+ - `ring-2 ring-background` is built-in — provides the border needed for `AvatarStack` overlap
109
+ - For unknown users or empty states: render `<Avatar />` (shows blank muted circle)
45
110
 
46
- **IconButton sizes**: `xs` (size-6 / 24px), `sm` (size-7 / 28px), `default` (size-8 / 32px), `lg` (size-9 / 36px).
111
+ ---
47
112
 
48
- ### Common Patterns
113
+ ## Kbd
114
+
115
+ `Kbd` renders a keyboard key. `KbdShortcut` renders a full shortcut sequence (`⌘ + Enter`).
116
+
117
+ Use in dialog footers, tooltips, and empty states to communicate keyboard affordances required by our UX rules (every dialog MUST support `Cmd/Ctrl+Enter` submit and `Escape` cancel).
118
+
119
+ ### Import
120
+
121
+ ```typescript
122
+ import { Kbd, KbdShortcut } from '@open-mercato/ui/primitives/kbd'
123
+ ```
124
+
125
+ ### Usage
49
126
 
50
127
  ```tsx
51
- // Sidebar / nav toggle
52
- <IconButton variant="outline" size="sm" type="button" onClick={toggle} aria-label="Toggle sidebar">
53
- <PanelLeft className="size-4" />
54
- </IconButton>
55
-
56
- // Close / dismiss button
57
- <IconButton variant="ghost" size="sm" type="button" onClick={onClose} aria-label="Close">
58
- <X className="size-4" />
59
- </IconButton>
60
-
61
- // Tab navigation (underline style)
62
- <Button
63
- type="button"
64
- variant="ghost"
65
- size="sm"
66
- className={cn(
67
- 'h-auto rounded-none border-b-2 px-0 py-1 hover:bg-transparent',
68
- isActive ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground'
69
- )}
70
- >
71
- {label}
72
- </Button>
73
-
74
- // Dropdown menu item
75
- <Button variant="ghost" size="sm" type="button" className="w-full justify-start" role="menuitem">
76
- <Icon className="size-4" /> {label}
77
- </Button>
78
-
79
- // Compact toolbar button (rich text editor)
80
- <Button variant="ghost" size="sm" type="button" className="h-auto px-2 py-0.5 text-xs">
81
- Bold
82
- </Button>
83
-
84
- // Collapsible section header
85
- <Button variant="muted" type="button" className="w-full justify-between" onClick={toggle}>
86
- <span>{sectionLabel}</span>
87
- <ChevronDown className={cn('size-4 transition-transform', open && 'rotate-180')} />
88
- </Button>
89
-
90
- // Link-styled icon button (wrapping Next.js Link)
91
- <IconButton asChild variant="ghost" size="sm">
92
- <Link href="/backend/settings">
93
- <Settings className="size-4" />
94
- </Link>
95
- </IconButton>
128
+ // Single key
129
+ <Kbd>Esc</Kbd>
130
+ <Kbd>⌘</Kbd>
131
+
132
+ // Shortcut sequence
133
+ <KbdShortcut keys={['⌘', 'Enter']} /> // renders: + Enter
134
+ <KbdShortcut keys={['Ctrl', 'S']} />
135
+
136
+ // In a dialog footer hint
137
+ <span className="text-xs text-muted-foreground">
138
+ Press <KbdShortcut keys={['⌘', 'Enter']} /> to save or <Kbd>Esc</Kbd> to cancel
139
+ </span>
96
140
  ```
97
141
 
98
- ## CrudForm Guidelines
142
+ ### MUST rules
99
143
 
100
- - Use `CrudForm` as the default for create/edit flows and for dialog forms.
101
- - If a backend page cannot use `CrudForm`, use `useGuardedMutation` from `@open-mercato/ui/backend/injection/useGuardedMutation` for every write operation (`POST`/`PUT`/`PATCH`/`DELETE`).
102
- - Always call writes through `runMutation({ operation, context, mutationPayload })` so global injection modules (for example record-lock conflict handling) can run `onBeforeSave`/`onAfterSave`, apply scoped request headers, and receive mutation errors consistently.
103
- - Use manual `useInjectionSpotEvents(GLOBAL_MUTATION_INJECTION_SPOT_ID)` wiring only when you need behavior that `useGuardedMutation` does not support.
104
- - Keep `CrudForm` implementations reusable: extract shared field/group builders and submit handlers into module-level helpers when multiple pages or dialogs need the same shape.
105
- - Drive validation with a Zod schema and surface field errors via `createCrudFormError`.
106
- - When using `CrudForm` with Zod, validation messages may be i18n keys because `CrudForm` translates them before display.
107
- - If you validate outside `CrudForm` or manually map `safeParse(...).error.issues`, you MUST translate `issue.message` before passing it to `createCrudFormError` or rendering it in the UI.
108
- - Keep `fields` and `groups` in memoized helpers (see customers person form config).
109
- - Pass `entityIds` when custom fields are involved so form helpers load correct custom-field sets.
110
- - Use `createCrud`/`updateCrud`/`deleteCrud` for submit actions and call `flash()` for success or failure messaging.
111
- - For multi-step submit flows, keep the form submit handler focused and move secondary operations (like extra address writes) into isolated helpers with per-item error handling.
144
+ - NEVER use raw `<span>` or `<code>` to display keyboard keys use `Kbd`
145
+ - Platform-specific keys (`⌘` vs `Ctrl`): detect with `navigator.platform` or use `Ctrl/⌘` text when cross-platform
112
146
 
113
- ## UI Interaction
114
- - Every new dialog must support `Cmd/Ctrl + Enter` as a primary action shortcut and `Escape` to cancel, mirroring the shared UX patterns used across modules.
115
- - Default to `CrudForm` for new forms and `DataTable` for tables displaying information unless a different component is explicitly required.
116
- - Use the `EventSelect` component from `@open-mercato/ui/backend/inputs/EventSelect` for event selection. It fetches declared events via the `/api/events` endpoint.
117
- - Never use `window.confirm`use the shared `ConfirmDialog` and `useConfirmDialog` from `@open-mercato/ui/backend/confirm-dialog` for confirmation flows.
118
- - New CRUD forms should use `CrudForm` wired to CRUD factory/commands APIs and be shared between create/edit flows.
119
- - Prefer reusing components from the shared `packages/ui` package before introducing new UI primitives.
120
- - For new `DataTable` columns, set `meta.truncate` and `meta.maxWidth` in the column config when you need specific truncation behavior; only rely on defaults when those are not set.
121
- - When you create new UI check reusable components before creating UI from scratch (see [`.ai/specs/implemented/SPEC-001-2026-01-21-ui-reusable-components.md`](.ai/specs/implemented/SPEC-001-2026-01-21-ui-reusable-components.md))
122
- - For form/detail page headers and footers, use `FormHeader` and `FormFooter` from `@open-mercato/ui/backend/forms`. `FormHeader` supports two modes: `edit` (compact, used automatically by CrudForm) and `detail` (large title with entity type label, status badge, Actions dropdown). Delete/Cancel/Save are always standalone buttons; additional context actions (Convert, Send, etc.) go into the `menuActions` array rendered as an "Actions" dropdown. See [SPEC-016](.ai/specs/implemented/SPEC-016-2026-02-03-form-headers-footers.md) for full API.
147
+ ---
148
+
149
+ ## Tag
150
+
151
+ `Tag` is a static pill element representing a user-applied label on an entity (e.g. "Customer", "Hot", "Renewal"). Use it for entity tags NOT for system status display (use `StatusBadge` for that).
152
+
153
+ ### Tag vs StatusBadge
154
+
155
+ | | `Tag` | `StatusBadge` |
156
+ |---|---|---|
157
+ | Purpose | User-applied label / category | System status (active, pending, failed…) |
158
+ | Shape | `rounded-full` pill | `rounded-full` pill |
159
+ | Dot | optional (`dot` prop) | optional (`dot` prop) |
160
+ | `brand` variant | ✅ (violet — for custom views/renewal tags) | ❌ |
161
+
162
+ ### Import
163
+
164
+ ```typescript
165
+ import { Tag } from '@open-mercato/ui/primitives/tag'
166
+ import type { TagMap } from '@open-mercato/ui/primitives/tag'
167
+ ```
168
+
169
+ ### Variants
170
+
171
+ | Variant | Token | Example use |
172
+ |---|---|---|
173
+ | `default` | `border-border bg-background text-muted-foreground` | Generic / inactive tag |
174
+ | `success` | `status-success-*` | Customer, Shipped, Active |
175
+ | `warning` | `status-warning-*` | Renewal, At risk |
176
+ | `error` | `status-error-*` | Hot, Overdue, Blocked |
177
+ | `info` | `status-info-*` | Pending, In review |
178
+ | `neutral` | `status-neutral-*` | Archived, Draft |
179
+ | `brand` | `brand-violet/10` bg, `brand-violet/30` border, `text-brand-violet` | Custom views, Perspectives |
180
+
181
+ ### Usage
182
+
183
+ ```tsx
184
+ <Tag variant="success" dot>Customer</Tag>
185
+ <Tag variant="error" dot>Hot</Tag>
186
+ <Tag variant="brand" dot>Renewal Q1 2026</Tag>
187
+ <Tag variant="neutral">Inactive</Tag>
188
+ ```
189
+
190
+ ### TagMap helper
191
+
192
+ ```typescript
193
+ import type { TagMap } from '@open-mercato/ui/primitives/tag'
194
+
195
+ const leadTagMap: TagMap<'customer' | 'hot' | 'inactive' | 'renewal'> = {
196
+ customer: 'success',
197
+ hot: 'error',
198
+ inactive: 'neutral',
199
+ renewal: 'brand',
200
+ }
201
+
202
+ <Tag variant={leadTagMap[tag.type]} dot>{tag.label}</Tag>
203
+ ```
204
+
205
+ ### MUST rules
206
+
207
+ - NEVER hardcode colors on `Tag` — use variants only
208
+ - Use `dot` for tags that represent a status-like category (Customer, Hot); omit for purely descriptive labels
209
+ - For "Manage tags" / add-tag affordances: use a `Button variant="ghost"` or dashed outline — NOT `Tag`
210
+ - `brand` variant is for user-saved views and renewal/custom category tags only (see brand color rules in root AGENTS.md)
123
211
 
124
212
  ## DataTable Guidelines
125
213
 
@@ -129,10 +217,10 @@ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
129
217
  - Populate `columns` with explicit renderers and set `meta.truncate`/`meta.maxWidth` where truncation is needed.
130
218
  - For filters, use `FilterBar`/`FilterOverlay` with async option loaders; keep `pageSize` at or below 100.
131
219
  - Support exports using `buildCrudExportUrl` and pass `exportOptions` to `DataTable`.
132
- - Use `RowActions` for per-row actions and include navigation via `onRowClick` or action links.
220
+ - Use `RowActions` for per-row actions; navigate via `onRowClick` or action links.
133
221
  - Keep table state (paging, sorting, filters, search) in component state and reload on scope changes.
134
- - Keep `extensionTableId` stable and deterministic; host pages should not derive it from transient UI state.
135
- - Render injected row actions and bulk actions through `RowActions`/bulk action handlers so injected actions follow the same guard and i18n behavior as built-ins.
222
+ - Keep `extensionTableId` stable and deterministic.
223
+ - Render injected row actions and bulk actions through `RowActions`/bulk handlers so they follow the same guard and i18n behavior as built-ins.
136
224
 
137
225
  ## CrudForm Field Injection (UMES Phase G)
138
226
 
@@ -142,37 +230,37 @@ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
142
230
 
143
231
  ## Menu Injection (UMES Phase A/B)
144
232
 
145
- - Use `useInjectedMenuItems(surfaceId)` to load declarative menu widgets for chrome surfaces (`menu:sidebar:*`, `menu:topbar:*`).
233
+ - Use `useInjectedMenuItems(surfaceId)` for chrome surfaces (`menu:sidebar:*`, `menu:topbar:*`).
146
234
  - Merge built-in and injected items with `mergeMenuItems(builtIn, injected)` to preserve deterministic placement.
147
- - For relative positioning, rely on `InjectionPosition` + `relativeTo` IDs; if `relativeTo` is missing, insertion falls back to append.
148
- - Treat injected labels as i18n-first: prefer `labelKey` (with human fallback `label`) and `groupLabelKey` (with optional `groupLabel`) so keys never leak to UI.
149
- - Add stable attributes (`data-menu-item-id="<id>"`) when rendering merged items so integration tests can assert injected entries reliably.
150
- - When filtering menu items by `item.features` or route `requireFeatures`, MUST use the shared wildcard-aware matcher from `@open-mercato/shared/lib/auth/featureMatch`; do not rely on `Set.has(...)` or exact `includes(...)` checks because role grants may be `module.*`.
235
+ - For relative positioning, use `InjectionPosition` + `relativeTo` IDs; if `relativeTo` is missing, insertion falls back to append.
236
+ - Treat injected labels as i18n-first: prefer `labelKey` (with human fallback `label`) and `groupLabelKey`.
237
+ - Add stable attributes (`data-menu-item-id="<id>"`) when rendering merged items so integration tests can assert injected entries.
238
+ - When filtering menu items by `item.features` or route `requireFeatures`, MUST use the shared wildcard-aware matcher from `@open-mercato/shared/lib/auth/featureMatch` `Set.has(...)`/`includes(...)` miss `module.*` grants.
151
239
 
152
240
  ## Loading, Empty, and Error States
153
241
 
154
242
  - For list/detail data loading, use `LoadingMessage` and `ErrorMessage` from `@open-mercato/ui/backend/detail`.
155
243
  - For record-backed backend detail/edit pages, treat `notFound` as a dedicated page state, separate from generic `error`.
156
- - When a record is missing, return early with a page-level state built on `ErrorMessage` and a clear recovery action such as "Back to list"; do not render `CrudForm`, detail sections, tabs, or record actions in that branch.
157
- - Do not use ad hoc centered `<div>` error markup for missing-record pages when the shared backend detail primitives can express the state.
158
- - Use `TabEmptyState` when a section is empty but otherwise healthy (see sales document sub-sections).
159
- - Keep loading flags local to the section and reset errors before each load.
244
+ - When a record is missing, return early with a page-level `ErrorMessage` and a clear recovery action ("Back to list"); do not render `CrudForm`, detail sections, tabs, or record actions.
245
+ - Don't use ad hoc centered `<div>` error markup when shared backend detail primitives can express the state.
246
+ - Use `TabEmptyState` when a section is empty but otherwise healthy.
247
+ - Keep loading flags local to the section; reset errors before each load.
160
248
 
161
249
  ## Flash Messages
162
250
 
163
251
  - Use `flash(message, 'success' | 'error')` from `@open-mercato/ui/backend/FlashMessages` for user feedback after CRUD operations.
164
- - Prefer specific translation keys and keep the message copy in module locale files.
165
- - For non-blocking errors in side effects (for example, creating secondary records), show a flash error and allow the main flow to complete.
252
+ - Prefer specific translation keys; keep message copy in module locale files.
253
+ - For non-blocking errors in side effects (e.g. creating secondary records), show a flash error and let the main flow complete.
166
254
 
167
255
  ## Notifications
168
256
 
169
257
  - Define notification types in `src/modules/<module>/notifications.ts` and client renderers in `notifications.client.ts`.
170
258
  - Define reactive notification handlers in `src/modules/<module>/notifications.handlers.ts` when notifications should trigger automatic side-effects.
171
259
  - Renderers live in `widgets/notifications/` and should use `useT()` for copy.
172
- - Use the shared action labels where possible (for example, `notifications.actions.dismiss`).
173
- - Prefer notification creation in commands or subscribers and keep UI renderers lightweight.
260
+ - Use shared action labels where possible (e.g. `notifications.actions.dismiss`).
261
+ - Prefer notification creation in commands or subscribers; keep UI renderers lightweight.
174
262
  - For component-scoped reactions, use `useNotificationEffect(notificationType, effect)` instead of module-specific polling loops.
175
- - When gating notification handlers or other UI runtime registries by `features`, MUST use the shared wildcard-aware matcher; `module.*` grants must enable matching handlers, sections, and actions.
263
+ - When gating notification handlers by `features`, MUST use the shared wildcard-aware matcher.
176
264
 
177
265
  ## Component Reuse
178
266
 
@@ -184,7 +272,7 @@ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
184
272
 
185
273
  ## Component Replacement (UMES Phase H)
186
274
 
187
- - When a host surface is replacement-aware, resolve implementations via `useRegisteredComponent(handle, Fallback)` instead of hardcoded component references.
275
+ - When a host surface is replacement-aware, resolve implementations via `useRegisteredComponent(handle, Fallback)` instead of hardcoded references.
188
276
  - Prefer additive override modes (`wrapper`, `props`) before full `replace`; reserve `replace` for cases where compatibility is preserved.
189
277
  - Keep handle IDs stable and document them when introducing new replacement-aware surfaces.
190
278
 
@@ -204,7 +292,7 @@ The portal extensibility system lets app modules build customer-facing pages tha
204
292
 
205
293
  ### Portal Shell (`packages/ui/src/portal/PortalShell.tsx`)
206
294
 
207
- Shared layout with header, nav (built-in + injected), main content, and footer. Supports event bridge and component replacement handles.
295
+ Shared layout with header, nav (built-in + injected), main, footer. Supports event bridge and component replacement handles.
208
296
 
209
297
  ```tsx
210
298
  import { PortalShell } from '@open-mercato/ui/portal/PortalShell'
@@ -224,8 +312,8 @@ function MyPage({ orgSlug }) {
224
312
 
225
313
  | Spot ID | Purpose |
226
314
  |---------|---------|
227
- | `menu:portal:sidebar:main` | Main portal navigation items |
228
- | `menu:portal:sidebar:account` | Account/settings navigation items |
315
+ | `menu:portal:sidebar:main` | Main portal navigation |
316
+ | `menu:portal:sidebar:account` | Account/settings navigation |
229
317
  | `menu:portal:header:actions` | Header action buttons |
230
318
  | `menu:portal:user-dropdown` | User dropdown menu items |
231
319
 
@@ -288,10 +376,7 @@ Reference: see `packages/core/src/modules/portal/frontend/[orgSlug]/portal/{dash
288
376
 
289
377
  ### Declarative Customer Role Features in setup.ts
290
378
 
291
- Modules can declare features to be merged into customer role ACLs:
292
-
293
379
  ```typescript
294
- // setup.ts
295
380
  export const setup: ModuleSetupConfig = {
296
381
  defaultCustomerRoleFeatures: {
297
382
  buyer: ['portal.orders.view', 'portal.orders.create'],
@@ -305,12 +390,10 @@ export const setup: ModuleSetupConfig = {
305
390
  Events with `portalBroadcast: true` are streamed to authenticated portal users via `/api/customer_accounts/portal/events/stream`.
306
391
 
307
392
  ```typescript
308
- // events.ts
309
393
  const events = [
310
394
  { id: 'sales.order.status_changed', label: 'Order Status Changed', portalBroadcast: true },
311
395
  ] as const
312
396
 
313
- // In portal component
314
397
  import { usePortalAppEvent } from '@open-mercato/ui/portal/hooks/usePortalAppEvent'
315
398
  usePortalAppEvent('sales.order.status_changed', (event) => { refetch() })
316
399
  ```
@@ -7,6 +7,7 @@ import Image from "next/image";
7
7
  import { ChevronUp, ChevronDown } from "lucide-react";
8
8
  import { Button } from "../primitives/button.js";
9
9
  import { IconButton } from "../primitives/icon-button.js";
10
+ import { Checkbox } from "../primitives/checkbox.js";
10
11
  import { FlashMessages } from "./FlashMessages.js";
11
12
  import { QueryProvider } from "../theme/QueryProvider.js";
12
13
  import { usePathname, useSearchParams } from "next/navigation";
@@ -564,7 +565,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
564
565
  setSelectedRoleIds((prev) => prev.includes(roleId) ? prev.filter((id) => id !== roleId) : [...prev, roleId]);
565
566
  }, []);
566
567
  const asideWidth = effectiveCollapsed ? "72px" : expandedSidebarWidth;
567
- const asideClassesBase = `border-r bg-background/60 py-4 min-h-svh`;
568
+ const asideClassesBase = `border-r bg-background/80 py-4 min-h-svh`;
568
569
  React.useEffect(() => {
569
570
  try {
570
571
  localStorage.setItem("om:sidebarCollapsed", collapsed ? "1" : "0");
@@ -703,18 +704,18 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
703
704
  ] }) }) : null,
704
705
  /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-3 pr-1", children: [
705
706
  /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
706
- /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/60" }),
707
+ /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/50" }),
707
708
  /* @__PURE__ */ jsxs("div", { className: "space-y-2 pl-1", children: [
708
709
  /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/50" }),
709
- /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/40" }),
710
- /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/40" })
710
+ /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/50" }),
711
+ /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/50" })
711
712
  ] })
712
713
  ] }),
713
714
  /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
714
- /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/60" }),
715
+ /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/50" }),
715
716
  /* @__PURE__ */ jsxs("div", { className: "space-y-2 pl-1", children: [
716
- /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/40" }),
717
- /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/40" })
717
+ /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/50" }),
718
+ /* @__PURE__ */ jsx("div", { className: "h-8 rounded bg-muted/50" })
718
719
  ] })
719
720
  ] })
720
721
  ] })
@@ -778,12 +779,10 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
778
779
  /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: placeholder }),
779
780
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
780
781
  /* @__PURE__ */ jsx(
781
- "input",
782
+ Checkbox,
782
783
  {
783
- type: "checkbox",
784
- className: "h-4 w-4 accent-foreground",
785
784
  checked: !hidden,
786
- onChange: (event) => setItemHidden(itemKey, !event.target.checked),
785
+ onCheckedChange: (next) => setItemHidden(itemKey, next !== true),
787
786
  disabled: savingPreferences,
788
787
  "aria-label": t("appShell.sidebarCustomizationShowItem"),
789
788
  title: t("appShell.sidebarCustomizationShowItem")
@@ -796,7 +795,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
796
795
  onChange: (event) => setItemLabel(itemKey, event.target.value),
797
796
  placeholder,
798
797
  disabled: savingPreferences,
799
- className: "h-8 flex-1 rounded border bg-background px-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-60"
798
+ className: "h-8 flex-1 rounded border bg-background px-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
800
799
  }
801
800
  )
802
801
  ] }),
@@ -807,7 +806,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
807
806
  );
808
807
  });
809
808
  };
810
- const customizationEditor = customizing ? customDraft ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded border border-dashed bg-muted/20 p-3", children: [
809
+ const customizationEditor = customizing ? customDraft ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded border border-dashed bg-muted/30 p-3", children: [
811
810
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
812
811
  /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: t("appShell.sidebarCustomizationHeading") }),
813
812
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
@@ -844,7 +843,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
844
843
  ] })
845
844
  ] }),
846
845
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("appShell.sidebarCustomizationHint", { locale: localeLabel }) }),
847
- canApplyToRoles ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded border bg-background/70 p-3 shadow-sm", children: [
846
+ canApplyToRoles ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded border bg-background/80 p-3 shadow-sm", children: [
848
847
  /* @__PURE__ */ jsxs("div", { children: [
849
848
  /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: t("appShell.sidebarApplyToRolesTitle") }),
850
849
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("appShell.sidebarApplyToRolesDescription") })
@@ -852,14 +851,12 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
852
851
  availableRoleTargets.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: availableRoleTargets.map((role) => {
853
852
  const checked = selectedRoleIds.includes(role.id);
854
853
  const willClear = role.hasPreference && !checked;
855
- return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 rounded border bg-background px-2 py-1 text-sm shadow-sm", children: [
854
+ return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 rounded-md border bg-background px-2 py-1 text-sm shadow-sm cursor-pointer", children: [
856
855
  /* @__PURE__ */ jsx(
857
- "input",
856
+ Checkbox,
858
857
  {
859
- type: "checkbox",
860
- className: "h-4 w-4 accent-foreground",
861
858
  checked,
862
- onChange: () => toggleRoleSelection(role.id),
859
+ onCheckedChange: () => toggleRoleSelection(role.id),
863
860
  disabled: savingPreferences
864
861
  }
865
862
  ),
@@ -886,7 +883,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
886
883
  onChange: (event) => setGroupLabel(groupId, event.target.value),
887
884
  placeholder,
888
885
  disabled: savingPreferences,
889
- className: "mt-1 h-8 w-full rounded border bg-background px-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-60"
886
+ className: "mt-1 h-8 w-full rounded border bg-background px-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
890
887
  }
891
888
  )
892
889
  ] }),
@@ -920,7 +917,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
920
917
  /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: renderEditableItems(baseGroup.items, currentGroup.items) })
921
918
  ] }, groupId);
922
919
  }) })
923
- ] }) : /* @__PURE__ */ jsx("div", { className: "rounded border border-dashed bg-muted/20 p-3 text-sm text-muted-foreground", children: t("appShell.sidebarCustomizationLoading") }) : null;
920
+ ] }) : /* @__PURE__ */ jsx("div", { className: "rounded border border-dashed bg-muted/30 p-3 text-sm text-muted-foreground", children: t("appShell.sidebarCustomizationLoading") }) : null;
924
921
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-h-full gap-3", children: [
925
922
  !hideHeader && /* @__PURE__ */ jsx("div", { className: `flex items-center ${compact ? "justify-center" : "justify-between"} mb-2`, children: /* @__PURE__ */ jsxs(Link, { href: "/backend", className: "flex items-center gap-2", "aria-label": t("appShell.goToDashboard"), children: [
926
923
  /* @__PURE__ */ jsx(Image, { src: logo?.src ?? "/open-mercato.svg", alt: logo?.alt ?? resolvedProductName, width: 32, height: 32, className: "rounded m-4" }),
@@ -1041,7 +1038,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1041
1038
  })
1042
1039
  ] }) });
1043
1040
  })() }),
1044
- /* @__PURE__ */ jsxs("div", { className: "sticky bottom-0 pt-4 border-t bg-background/60 backdrop-blur-sm pb-1", children: [
1041
+ /* @__PURE__ */ jsxs("div", { className: "sticky bottom-0 pt-4 border-t bg-background/80 backdrop-blur-sm pb-1", children: [
1045
1042
  shouldRenderSidebarInjectionSpots ? /* @__PURE__ */ jsx(
1046
1043
  InjectionSpot,
1047
1044
  {
@@ -1149,7 +1146,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1149
1146
  /* @__PURE__ */ jsxs("div", { className: `min-h-svh lg:grid ${gridColsClass}`, children: [
1150
1147
  /* @__PURE__ */ jsx("aside", { className: `${asideClassesBase} ${effectiveCollapsed ? "px-2" : "px-3"} hidden lg:block`, style: { width: asideWidth }, children: renderSidebar(effectiveCollapsed) }),
1151
1148
  /* @__PURE__ */ jsxs("div", { className: "flex min-h-svh flex-col min-w-0", children: [
1152
- /* @__PURE__ */ jsxs("header", { className: "border-b bg-background/60 px-3 lg:px-4 py-2 lg:py-3 flex items-center justify-between gap-2", children: [
1149
+ /* @__PURE__ */ jsxs("header", { className: "border-b bg-background/80 px-3 lg:px-4 py-2 lg:py-3 flex items-center justify-between gap-2", children: [
1153
1150
  /* @__PURE__ */ jsx(
1154
1151
  "div",
1155
1152
  {
@@ -1217,8 +1214,8 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1217
1214
  rightHeaderSlot ? rightHeaderSlot : /* @__PURE__ */ jsx("span", { className: "opacity-80", children: email || t("appShell.userFallback") })
1218
1215
  ] })
1219
1216
  ] }),
1220
- /* @__PURE__ */ jsx(ProgressTopBar, { t, className: "sticky top-0 z-10" }),
1221
- /* @__PURE__ */ jsxs("main", { className: "flex-1 p-4 lg:p-6", children: [
1217
+ /* @__PURE__ */ jsx(ProgressTopBar, { t, className: "sticky top-0 z-sticky" }),
1218
+ /* @__PURE__ */ jsxs("main", { className: "flex-1 p-4 lg:p-6 mx-auto w-full max-w-screen-2xl", children: [
1222
1219
  /* @__PURE__ */ jsx(InjectionSpot, { spotId: BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID, context: injectionContext }),
1223
1220
  /* @__PURE__ */ jsx(FlashMessages, {}),
1224
1221
  /* @__PURE__ */ jsx(PartialIndexBanner, {}),
@@ -1236,7 +1233,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1236
1233
  children,
1237
1234
  /* @__PURE__ */ jsx(InjectionSpot, { spotId: BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID, context: injectionContext })
1238
1235
  ] }),
1239
- /* @__PURE__ */ jsxs("footer", { className: "border-t bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/50 px-4 py-3 flex flex-wrap items-center justify-end gap-4", children: [
1236
+ /* @__PURE__ */ jsxs("footer", { className: "border-t bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/80 px-4 py-3 flex flex-wrap items-center justify-end gap-4", children: [
1240
1237
  version ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("appShell.version", { version }) }) : null,
1241
1238
  /* @__PURE__ */ jsxs("nav", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
1242
1239
  /* @__PURE__ */ jsx(Link, { href: "/terms", className: "transition hover:text-foreground", children: t("common.terms") }),
@@ -1244,8 +1241,8 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1244
1241
  ] })
1245
1242
  ] })
1246
1243
  ] }),
1247
- mobileOpen && /* @__PURE__ */ jsxs("div", { className: "lg:hidden fixed inset-0 z-50", children: [
1248
- /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/40", onClick: () => setMobileOpen(false), "aria-hidden": "true" }),
1244
+ mobileOpen && /* @__PURE__ */ jsxs("div", { className: "lg:hidden fixed inset-0 z-modal", children: [
1245
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/20", onClick: () => setMobileOpen(false), "aria-hidden": "true" }),
1249
1246
  /* @__PURE__ */ jsxs("aside", { className: "absolute left-0 top-0 flex h-full w-[260px] flex-col bg-background border-r overflow-hidden", children: [
1250
1247
  /* @__PURE__ */ jsxs("div", { className: "shrink-0 p-3 pb-2 flex items-center justify-between border-b", children: [
1251
1248
  /* @__PURE__ */ jsxs(Link, { href: "/backend", className: "flex items-center gap-2 text-sm font-semibold", onClick: () => setMobileOpen(false), "aria-label": t("appShell.goToDashboard"), children: [