@open-mercato/ui 0.6.7 → 0.6.8-develop.6874.1.982d6097d8

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 (214) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +4 -2
  3. package/agentic/standalone-guide.md +1 -1
  4. package/build.mjs +2 -54
  5. package/dist/ai/AiChat.js +26 -7
  6. package/dist/ai/AiChat.js.map +2 -2
  7. package/dist/ai/LoopDisabledBanner.js +1 -1
  8. package/dist/ai/LoopDisabledBanner.js.map +2 -2
  9. package/dist/ai/parts/ConfirmationCard.js +4 -4
  10. package/dist/ai/parts/ConfirmationCard.js.map +2 -2
  11. package/dist/ai/parts/FieldDiffCard.js +2 -2
  12. package/dist/ai/parts/FieldDiffCard.js.map +2 -2
  13. package/dist/ai/parts/MutationResultCard.js +3 -3
  14. package/dist/ai/parts/MutationResultCard.js.map +2 -2
  15. package/dist/ai/ui-parts/pending-phase3-placeholder.js +1 -1
  16. package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
  17. package/dist/backend/AppShell.js +43 -4
  18. package/dist/backend/AppShell.js.map +2 -2
  19. package/dist/backend/AuthSessionGuard.js +14 -0
  20. package/dist/backend/AuthSessionGuard.js.map +2 -2
  21. package/dist/backend/BackendChromeProvider.js +6 -1
  22. package/dist/backend/BackendChromeProvider.js.map +2 -2
  23. package/dist/backend/CrudForm.js +6 -4
  24. package/dist/backend/CrudForm.js.map +2 -2
  25. package/dist/backend/DataTable.js +20 -18
  26. package/dist/backend/DataTable.js.map +2 -2
  27. package/dist/backend/JsonBuilder.js +3 -3
  28. package/dist/backend/JsonBuilder.js.map +2 -2
  29. package/dist/backend/NextStepCallout.js +4 -4
  30. package/dist/backend/NextStepCallout.js.map +1 -1
  31. package/dist/backend/PerspectiveSidebar.js +1 -1
  32. package/dist/backend/PerspectiveSidebar.js.map +2 -2
  33. package/dist/backend/ProfileDropdown.js +2 -1
  34. package/dist/backend/ProfileDropdown.js.map +2 -2
  35. package/dist/backend/RowActions.js +2 -2
  36. package/dist/backend/RowActions.js.map +2 -2
  37. package/dist/backend/confirm-dialog/ConfirmDialog.js +13 -1
  38. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  39. package/dist/backend/crud/useGroupOrder.js +14 -21
  40. package/dist/backend/crud/useGroupOrder.js.map +2 -2
  41. package/dist/backend/custom-fields/FieldDefinitionsManager.js +1 -1
  42. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +2 -2
  43. package/dist/backend/dashboard/DashboardScreen.js +3 -3
  44. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  45. package/dist/backend/detail/AddressTiles.js +4 -5
  46. package/dist/backend/detail/AddressTiles.js.map +2 -2
  47. package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
  48. package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
  49. package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
  50. package/dist/backend/detail/AttachmentsSection.js +1 -1
  51. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  52. package/dist/backend/detail/NotesSection.js +1 -1
  53. package/dist/backend/detail/NotesSection.js.map +2 -2
  54. package/dist/backend/detail/TagsSection.js +1 -1
  55. package/dist/backend/detail/TagsSection.js.map +2 -2
  56. package/dist/backend/filters/AdvancedFilterPanel.js +1 -1
  57. package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
  58. package/dist/backend/forms/FormHeader.js +1 -2
  59. package/dist/backend/forms/FormHeader.js.map +2 -2
  60. package/dist/backend/icons/lucideRegistry.generated.js +1 -25
  61. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  62. package/dist/backend/icons/lucideRegistry.js +3 -3
  63. package/dist/backend/icons/lucideRegistry.js.map +2 -2
  64. package/dist/backend/icons/lucideRegistryRuntime.js +28 -0
  65. package/dist/backend/icons/lucideRegistryRuntime.js.map +7 -0
  66. package/dist/backend/injection/spotIds.js +23 -18
  67. package/dist/backend/injection/spotIds.js.map +2 -2
  68. package/dist/backend/inputs/ComboboxInput.js.map +1 -1
  69. package/dist/backend/messages/EmailThreadsPanel.js +1 -1
  70. package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
  71. package/dist/backend/perspectiveState.js +34 -0
  72. package/dist/backend/perspectiveState.js.map +7 -0
  73. package/dist/backend/schedule/ScheduleAgenda.js +3 -3
  74. package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
  75. package/dist/backend/schedule/ScheduleGrid.js +3 -3
  76. package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
  77. package/dist/backend/schedule/ScheduleToolbar.js +87 -70
  78. package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
  79. package/dist/backend/sidebar/SidebarCustomizationEditor.js +30 -2
  80. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
  81. package/dist/backend/sidebar/customization-helpers.js +30 -0
  82. package/dist/backend/sidebar/customization-helpers.js.map +2 -2
  83. package/dist/backend/utils/apiCall.js +21 -1
  84. package/dist/backend/utils/apiCall.js.map +2 -2
  85. package/dist/backend/utils/customFieldDefs.js.map +2 -2
  86. package/dist/backend/utils/customFieldForms.js +10 -9
  87. package/dist/backend/utils/customFieldForms.js.map +2 -2
  88. package/dist/backend/utils/fetchAllPages.js +57 -0
  89. package/dist/backend/utils/fetchAllPages.js.map +7 -0
  90. package/dist/backend/utils/nav.js +5 -2
  91. package/dist/backend/utils/nav.js.map +2 -2
  92. package/dist/backend/version-history/VersionHistoryPanel.js +1 -1
  93. package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
  94. package/dist/frontend/LanguageSwitcher.js +2 -1
  95. package/dist/frontend/LanguageSwitcher.js.map +2 -2
  96. package/dist/portal/PortalContext.js +1 -1
  97. package/dist/portal/PortalContext.js.map +2 -2
  98. package/dist/portal/hooks/portalBridgeStatus.js +25 -0
  99. package/dist/portal/hooks/portalBridgeStatus.js.map +7 -0
  100. package/dist/portal/hooks/usePortalEventBridge.js +6 -0
  101. package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
  102. package/dist/portal/hooks/usePortalNotifications.js +42 -4
  103. package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
  104. package/dist/portal/utils/nav.js +2 -3
  105. package/dist/portal/utils/nav.js.map +2 -2
  106. package/dist/primitives/ErrorNotice.js +1 -1
  107. package/dist/primitives/ErrorNotice.js.map +2 -2
  108. package/dist/primitives/Notice.js +1 -1
  109. package/dist/primitives/Notice.js.map +1 -1
  110. package/dist/primitives/button.js +9 -4
  111. package/dist/primitives/button.js.map +2 -2
  112. package/dist/theme/ThemeProvider.js +1 -1
  113. package/dist/theme/ThemeProvider.js.map +2 -2
  114. package/dist/theme/index.js +3 -0
  115. package/dist/theme/index.js.map +2 -2
  116. package/dist/theme/theme-init-script.js +17 -0
  117. package/dist/theme/theme-init-script.js.map +7 -0
  118. package/figma/alert.figma.tsx +35 -0
  119. package/figma/badge.figma.tsx +33 -0
  120. package/figma/button.figma.tsx +33 -0
  121. package/figma/checkbox.figma.tsx +28 -0
  122. package/figma/drawer.figma.tsx +40 -0
  123. package/figma/input-variants.figma.tsx +58 -0
  124. package/figma/radio.figma.tsx +16 -0
  125. package/figma/select.figma.tsx +24 -0
  126. package/figma/status-badge.figma.tsx +24 -0
  127. package/figma/switch.figma.tsx +28 -0
  128. package/figma/tabs.figma.tsx +22 -0
  129. package/figma/tag.figma.tsx +27 -0
  130. package/figma.config.json +6 -0
  131. package/jest.config.cjs +2 -1
  132. package/package.json +21 -18
  133. package/scripts/lucideRegistrySource.cjs +133 -0
  134. package/src/ai/AiChat.tsx +36 -7
  135. package/src/ai/LoopDisabledBanner.tsx +1 -1
  136. package/src/ai/__tests__/AiChat.test.tsx +31 -0
  137. package/src/ai/parts/ConfirmationCard.tsx +4 -4
  138. package/src/ai/parts/FieldDiffCard.tsx +2 -2
  139. package/src/ai/parts/MutationResultCard.tsx +3 -3
  140. package/src/ai/ui-parts/pending-phase3-placeholder.tsx +2 -2
  141. package/src/backend/AppShell.tsx +55 -4
  142. package/src/backend/AuthSessionGuard.tsx +29 -0
  143. package/src/backend/BackendChromeProvider.tsx +19 -1
  144. package/src/backend/CrudForm.tsx +18 -4
  145. package/src/backend/DataTable.tsx +36 -19
  146. package/src/backend/JsonBuilder.tsx +3 -3
  147. package/src/backend/NextStepCallout.tsx +4 -4
  148. package/src/backend/PerspectiveSidebar.tsx +1 -1
  149. package/src/backend/ProfileDropdown.tsx +1 -0
  150. package/src/backend/RowActions.tsx +2 -2
  151. package/src/backend/__tests__/AppShell.test.tsx +65 -7
  152. package/src/backend/__tests__/AuthSessionGuard.test.tsx +52 -0
  153. package/src/backend/__tests__/BackendChromeProvider.test.tsx +98 -2
  154. package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +158 -0
  155. package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +98 -0
  156. package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +105 -0
  157. package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +62 -1
  158. package/src/backend/__tests__/nav-utils.test.ts +139 -0
  159. package/src/backend/confirm-dialog/ConfirmDialog.tsx +16 -1
  160. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +68 -1
  161. package/src/backend/crud/__tests__/useGroupOrder.test.ts +39 -1
  162. package/src/backend/crud/useGroupOrder.ts +30 -25
  163. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +1 -1
  164. package/src/backend/dashboard/DashboardScreen.tsx +3 -3
  165. package/src/backend/detail/AddressTiles.tsx +3 -6
  166. package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
  167. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  168. package/src/backend/detail/AttachmentsSection.tsx +1 -1
  169. package/src/backend/detail/NotesSection.tsx +1 -1
  170. package/src/backend/detail/TagsSection.tsx +1 -1
  171. package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +21 -0
  172. package/src/backend/filters/AdvancedFilterPanel.tsx +1 -1
  173. package/src/backend/forms/FormHeader.tsx +1 -2
  174. package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +177 -0
  175. package/src/backend/icons/lucideRegistry.generated.tsx +3 -37
  176. package/src/backend/icons/lucideRegistry.ts +2 -2
  177. package/src/backend/icons/lucideRegistryRuntime.tsx +36 -0
  178. package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +38 -0
  179. package/src/backend/injection/spotIds.ts +23 -18
  180. package/src/backend/inputs/ComboboxInput.tsx +1 -1
  181. package/src/backend/messages/EmailThreadsPanel.tsx +1 -1
  182. package/src/backend/perspectiveState.ts +44 -0
  183. package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
  184. package/src/backend/schedule/ScheduleGrid.tsx +3 -3
  185. package/src/backend/schedule/ScheduleToolbar.tsx +89 -68
  186. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +42 -3
  187. package/src/backend/sidebar/__tests__/group-visibility.test.ts +131 -0
  188. package/src/backend/sidebar/customization-helpers.ts +47 -0
  189. package/src/backend/utils/__tests__/apiCall.test.ts +61 -0
  190. package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +95 -0
  191. package/src/backend/utils/__tests__/fetchAllPages.test.ts +103 -0
  192. package/src/backend/utils/apiCall.ts +29 -1
  193. package/src/backend/utils/customFieldDefs.ts +1 -0
  194. package/src/backend/utils/customFieldForms.ts +15 -11
  195. package/src/backend/utils/fetchAllPages.ts +93 -0
  196. package/src/backend/utils/nav.ts +22 -2
  197. package/src/backend/version-history/VersionHistoryPanel.tsx +1 -1
  198. package/src/frontend/LanguageSwitcher.tsx +1 -0
  199. package/src/portal/PortalContext.tsx +1 -1
  200. package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +66 -0
  201. package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +155 -0
  202. package/src/portal/hooks/portalBridgeStatus.ts +29 -0
  203. package/src/portal/hooks/usePortalEventBridge.ts +6 -0
  204. package/src/portal/hooks/usePortalNotifications.ts +55 -12
  205. package/src/portal/utils/__tests__/nav.test.ts +2 -2
  206. package/src/portal/utils/nav.ts +3 -4
  207. package/src/primitives/ErrorNotice.tsx +1 -1
  208. package/src/primitives/Notice.tsx +5 -5
  209. package/src/primitives/__tests__/button.test.tsx +70 -0
  210. package/src/primitives/button.tsx +10 -4
  211. package/src/theme/ThemeProvider.tsx +1 -2
  212. package/src/theme/__tests__/theme-init-script.test.ts +70 -0
  213. package/src/theme/index.ts +1 -0
  214. package/src/theme/theme-init-script.ts +23 -0
@@ -357,7 +357,7 @@ export function DashboardScreen() {
357
357
 
358
358
  if (error && layout.length === 0) {
359
359
  return (
360
- <Alert variant="destructive">
360
+ <Alert status="error">
361
361
  <AlertTitle>{t('dashboard.unavailable')}</AlertTitle>
362
362
  <AlertDescription>{error}</AlertDescription>
363
363
  <div className="mt-2"><Button variant="outline" onClick={handleRefresh}>{t('dashboard.retry')}</Button></div>
@@ -367,7 +367,7 @@ export function DashboardScreen() {
367
367
 
368
368
  if (!hasRegisteredWidgets && layout.length === 0) {
369
369
  return (
370
- <Alert variant="info">
370
+ <Alert status="information">
371
371
  <AlertTitle>{t('dashboard.empty.noWidgets.title', 'No dashboard widgets yet')}</AlertTitle>
372
372
  <AlertDescription>
373
373
  {t(
@@ -403,7 +403,7 @@ export function DashboardScreen() {
403
403
  </div>
404
404
 
405
405
  {error && layout.length > 0 && (
406
- <Alert variant="destructive">
406
+ <Alert status="error">
407
407
  <AlertTitle>{t('dashboard.error.partial')}</AlertTitle>
408
408
  <AlertDescription>{error}</AlertDescription>
409
409
  <div className="mt-2"><Button variant="ghost" onClick={handleRefresh}>{t('dashboard.error.reload')}</Button></div>
@@ -7,7 +7,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
7
  import { TabEmptyState } from '@open-mercato/ui/backend/detail'
8
8
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
9
9
  import { normalizeCoordinateInput, validateCoordinateInput } from '@open-mercato/shared/lib/location/coordinates'
10
- import { AddressView, formatAddressString, type AddressFormatStrategy } from './addressFormat'
10
+ import { AddressView, type AddressFormatStrategy } from './addressFormat'
11
11
  import AddressEditor, { type AddressTypesAdapter } from './AddressEditor'
12
12
  import {
13
13
  Dialog,
@@ -176,7 +176,7 @@ export function AddressTiles<C = unknown>({
176
176
  t,
177
177
  emptyLabel,
178
178
  isSubmitting = false,
179
- gridClassName = 'grid grid-cols-1 gap-2 sm:gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4',
179
+ gridClassName = 'grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2 sm:gap-4',
180
180
  hideAddButton = false,
181
181
  onAddActionChange,
182
182
  emptyStateTitle,
@@ -480,7 +480,7 @@ export function AddressTiles<C = unknown>({
480
480
  addressTypesAdapter={addressTypesAdapter}
481
481
  addressTypesContext={addressTypesContext}
482
482
  />
483
- {generalError ? <p className="text-xs text-red-600">{generalError}</p> : null}
483
+ {generalError ? <p className="text-xs text-status-error-text">{generalError}</p> : null}
484
484
  <div className="flex flex-wrap justify-end gap-2">
485
485
  <Button type="button" variant="outline" onClick={handleCancel} disabled={disableActions}>
486
486
  {label('cancel', 'Cancel')}
@@ -569,9 +569,6 @@ export function AddressTiles<C = unknown>({
569
569
  </p>
570
570
  ) : null}
571
571
  <AddressView address={address} format={format} className="text-sm text-foreground" />
572
- <p className="text-xs text-muted-foreground">
573
- {formatAddressString(address, format)}
574
- </p>
575
572
  </div>
576
573
  <div className="flex items-center gap-1 opacity-100 md:opacity-0 transition-opacity md:group-hover:opacity-100 focus-within:opacity-100">
577
574
  <Button
@@ -38,7 +38,7 @@ export function AttachmentDeleteDialog({ open, onOpenChange, fileName, onConfirm
38
38
  <Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isDeleting}>
39
39
  {t('attachments.library.metadata.cancel', 'Cancel')}
40
40
  </Button>
41
- <Button type="button" variant="destructive" onClick={onConfirm} disabled={isDeleting}>
41
+ <Button type="button" variant="destructive-solid" onClick={onConfirm} disabled={isDeleting}>
42
42
  {t('attachments.library.actions.delete', 'Delete')}
43
43
  </Button>
44
44
  </DialogFooter>
@@ -77,7 +77,7 @@ type AttachmentMetadataFormValues = {
77
77
  assignments?: AssignmentDraft[]
78
78
  } & Record<string, unknown>
79
79
 
80
- type AssignmentEditorLabels = {
80
+ export type AssignmentEditorLabels = {
81
81
  title: string
82
82
  description: string
83
83
  type: string
@@ -256,7 +256,7 @@ function AttachmentsSectionImpl({
256
256
  </div>
257
257
  )}
258
258
 
259
- {error ? <p className="text-xs font-medium text-red-600">{error}</p> : null}
259
+ {error ? <p className="text-xs font-medium text-status-error-text">{error}</p> : null}
260
260
 
261
261
  {loading ? (
262
262
  <div className="text-sm text-muted-foreground">{t('attachments.library.loading', 'Loading attachments…')}</div>
@@ -1256,7 +1256,7 @@ function NotesSectionImpl<C = unknown>({
1256
1256
  editorWrapperClassName="w-full rounded-md border border-muted-foreground/20 bg-background p-2"
1257
1257
  remarkPlugins={markdownPlugins}
1258
1258
  />
1259
- {contentError ? <p className="text-xs text-red-600">{contentError}</p> : null}
1259
+ {contentError ? <p className="text-xs text-status-error-text">{contentError}</p> : null}
1260
1260
  <div className="flex flex-wrap items-center gap-2">
1261
1261
  <Button type="button" size="sm" onClick={handleContentSave} disabled={contentSavingId === note.id}>
1262
1262
  {contentSavingId === note.id ? (
@@ -387,7 +387,7 @@ function TagsSectionImpl({
387
387
  loadSuggestions={loadSuggestions}
388
388
  autoFocus={!autoSave}
389
389
  />
390
- {error ? <p className="text-xs text-red-600">{error}</p> : null}
390
+ {error ? <p className="text-xs text-status-error-text">{error}</p> : null}
391
391
  {autoSave ? null : (
392
392
  <div className="flex items-center gap-2 mt-3 mb-2">
393
393
  <Button type="button" size="sm" onClick={handleSave} disabled={saving || isSubmitting}>
@@ -75,4 +75,25 @@ describe('AttachmentAssignmentsEditor (metadata dialog)', () => {
75
75
  fireEvent.click(screen.getByRole('button', { name: 'Remove assignment' }))
76
76
  expect(screen.queryByPlaceholderText('catalog.product')).toBeNull()
77
77
  })
78
+
79
+ it('keeps the surviving assignment values after removing a middle row', () => {
80
+ render(
81
+ <EditorHarness
82
+ initial={[
83
+ { type: 'catalog.product', id: 'p-1', href: '', label: '' },
84
+ { type: 'sales.order', id: 'o-2', href: '', label: '' },
85
+ { type: 'customers.person', id: 'c-3', href: '', label: '' },
86
+ ]}
87
+ />,
88
+ )
89
+
90
+ fireEvent.click(screen.getAllByRole('button', { name: labels.remove })[1])
91
+
92
+ const typeInputs = screen.getAllByPlaceholderText('catalog.product') as HTMLInputElement[]
93
+ const idInputs = screen.getAllByPlaceholderText('Record ID') as HTMLInputElement[]
94
+ expect(typeInputs[0]).toHaveValue('catalog.product')
95
+ expect(idInputs[0]).toHaveValue('p-1')
96
+ expect(typeInputs[1]).toHaveValue('customers.person')
97
+ expect(idInputs[1]).toHaveValue('c-3')
98
+ })
78
99
  })
@@ -444,7 +444,7 @@ export function AdvancedFilterPanel(props: AdvancedFilterPanelProps) {
444
444
  )}
445
445
  {props.pendingErrors.length > 0 ? (
446
446
  <div className="p-3 border-t border-border" data-testid="filter-validation-banner">
447
- <Alert variant="destructive">
447
+ <Alert status="error">
448
448
  <AlertDescription>
449
449
  {props.pendingErrors.length === 1
450
450
  ? t('ui.advancedFilter.banner.incomplete', '{count} filter is incomplete — filters won\'t apply until all values are picked.', { count: 1 })
@@ -181,11 +181,10 @@ function DetailHeader({
181
181
  {onDelete ? (
182
182
  <Button
183
183
  type="button"
184
- variant="outline"
184
+ variant="destructive"
185
185
  size="sm"
186
186
  onClick={onDelete}
187
187
  disabled={isDeleting}
188
- className="text-red-600 border-red-200 hover:bg-red-50 rounded"
189
188
  >
190
189
  {isDeleting ? (
191
190
  <Loader2 className="size-4 mr-2 animate-spin" />
@@ -0,0 +1,177 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { Project, ScriptKind, SyntaxKind, type ObjectLiteralExpression, type SourceFile } from 'ts-morph'
5
+ import { buildLucideRegistrySource } from '../../../../scripts/lucideRegistrySource.cjs'
6
+
7
+ const packageDir = join(__dirname, '..', '..', '..', '..')
8
+ const iconsDir = join(packageDir, 'src', 'backend', 'icons')
9
+
10
+ type ResolvedIcon = { kebab: string; exportName: string }
11
+
12
+ const populatedFixture: ResolvedIcon[] = [
13
+ { kebab: 'bell', exportName: 'Bell' },
14
+ { kebab: 'alert-circle', exportName: 'AlertCircle' },
15
+ { kebab: 'bar-chart-2', exportName: 'BarChart2' },
16
+ ]
17
+
18
+ const duplicateExportFixture: ResolvedIcon[] = [
19
+ { kebab: 'trash', exportName: 'Trash' },
20
+ { kebab: 'trash-can', exportName: 'Trash' },
21
+ ]
22
+
23
+ function parse(source: string): SourceFile {
24
+ const project = new Project({ useInMemoryFileSystem: true })
25
+ return project.createSourceFile('lucideRegistry.generated.tsx', source, {
26
+ overwrite: true,
27
+ scriptKind: ScriptKind.TSX,
28
+ })
29
+ }
30
+
31
+ function syntacticDiagnostics(source: string): string[] {
32
+ const sourceFile = parse(source)
33
+ return sourceFile
34
+ .getProject()
35
+ .getProgram()
36
+ .getSyntacticDiagnostics(sourceFile)
37
+ .map((diagnostic) => `${diagnostic.getLineNumber() ?? 0}: ${diagnostic.getMessageText()}`)
38
+ }
39
+
40
+ function registryObject(source: string): ObjectLiteralExpression {
41
+ const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
42
+ return declaration.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression)
43
+ }
44
+
45
+ function registryEntries(source: string): Record<string, string> {
46
+ const entries: Record<string, string> = {}
47
+ for (const property of registryObject(source).getProperties()) {
48
+ const assignment = property.asKindOrThrow(SyntaxKind.PropertyAssignment)
49
+ entries[assignment.getSymbolOrThrow().getName()] = assignment.getInitializerOrThrow().getText()
50
+ }
51
+ return entries
52
+ }
53
+
54
+ function valueImportNames(source: string, moduleSpecifier: string): string[] {
55
+ return parse(source)
56
+ .getImportDeclarations()
57
+ .filter((declaration) => declaration.getModuleSpecifierValue() === moduleSpecifier)
58
+ .filter((declaration) => !declaration.isTypeOnly())
59
+ .flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
60
+ }
61
+
62
+ describe('buildLucideRegistrySource', () => {
63
+ it('maps every discovered kebab name to its lucide export', () => {
64
+ expect(registryEntries(buildLucideRegistrySource(populatedFixture))).toEqual({
65
+ 'alert-circle': 'AlertCircle',
66
+ 'bar-chart-2': 'BarChart2',
67
+ bell: 'Bell',
68
+ })
69
+ })
70
+
71
+ it('emits the registry keys sorted by kebab name', () => {
72
+ const keys = registryObject(buildLucideRegistrySource(populatedFixture))
73
+ .getProperties()
74
+ .map((property) => property.asKindOrThrow(SyntaxKind.PropertyAssignment).getSymbolOrThrow().getName())
75
+ expect(keys).toEqual(['alert-circle', 'bar-chart-2', 'bell'])
76
+ })
77
+
78
+ it('imports each lucide export exactly once', () => {
79
+ expect(valueImportNames(buildLucideRegistrySource(populatedFixture), 'lucide-react')).toEqual([
80
+ 'AlertCircle',
81
+ 'BarChart2',
82
+ 'Bell',
83
+ ])
84
+ })
85
+
86
+ it('quotes keys that are not valid identifiers', () => {
87
+ const property = registryObject(buildLucideRegistrySource(populatedFixture))
88
+ .getPropertyOrThrow("'bar-chart-2'")
89
+ .asKindOrThrow(SyntaxKind.PropertyAssignment)
90
+ expect(property.getNameNode().getKind()).toBe(SyntaxKind.StringLiteral)
91
+ })
92
+
93
+ it('de-duplicates the value import when two names share one export', () => {
94
+ const source = buildLucideRegistrySource(duplicateExportFixture)
95
+ expect(valueImportNames(source, 'lucide-react')).toEqual(['Trash'])
96
+ expect(registryEntries(source)).toEqual({ trash: 'Trash', 'trash-can': 'Trash' })
97
+ })
98
+
99
+ it('emits a valid module with an empty registry when no icons are discovered', () => {
100
+ const source = buildLucideRegistrySource([])
101
+ expect(registryObject(source).getProperties()).toHaveLength(0)
102
+ expect(valueImportNames(source, 'lucide-react')).toEqual([])
103
+ expect(source).not.toContain("import {} from 'lucide-react'")
104
+ })
105
+
106
+ it('keeps the registry a plain mutable object literal', () => {
107
+ const source = buildLucideRegistrySource(populatedFixture)
108
+ const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
109
+ expect(declaration.getTypeNodeOrThrow().getText()).toBe('Record<string, LucideIcon>')
110
+ expect(declaration.getVariableStatementOrThrow().isExported()).toBe(true)
111
+ expect(source).not.toContain('as const')
112
+ expect(source).not.toContain('Object.freeze')
113
+ })
114
+
115
+ it('keeps the LucideIcon import type-only', () => {
116
+ const typeOnly = parse(buildLucideRegistrySource(populatedFixture))
117
+ .getImportDeclarations()
118
+ .filter((declaration) => declaration.isTypeOnly())
119
+ .flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
120
+ expect(typeOnly).toEqual(['LucideIcon'])
121
+ })
122
+
123
+ it.each([
124
+ ['populated', populatedFixture],
125
+ ['empty', [] as ResolvedIcon[]],
126
+ ['duplicate exports', duplicateExportFixture],
127
+ ])('emits %s output with zero syntactic diagnostics', (_label, fixture) => {
128
+ expect(syntacticDiagnostics(buildLucideRegistrySource(fixture))).toEqual([])
129
+ })
130
+
131
+ it('fails loudly when an export name would produce an unparseable module', () => {
132
+ expect(() => buildLucideRegistrySource([{ kebab: 'broken', exportName: 'not a valid export' }])).toThrow(
133
+ /not syntactically valid/
134
+ )
135
+ })
136
+ })
137
+
138
+ describe('lucideRegistry barrel', () => {
139
+ it('keeps its public export list unchanged', () => {
140
+ const project = new Project({ useInMemoryFileSystem: true })
141
+ const barrel = project.createSourceFile(
142
+ 'lucideRegistry.ts',
143
+ readFileSync(join(iconsDir, 'lucideRegistry.ts'), 'utf-8'),
144
+ { overwrite: true }
145
+ )
146
+ expect([...barrel.getExportedDeclarations().keys()].sort()).toEqual([
147
+ 'LUCIDE_ICON_REGISTRY',
148
+ 'registerAdditionalIcons',
149
+ 'resolveRegisteredLucideIcon',
150
+ 'resolveRegisteredLucideIconNode',
151
+ ])
152
+ })
153
+ })
154
+
155
+ describe('lucideRegistry.generated importers', () => {
156
+ it('is imported only from within src/backend/icons', () => {
157
+ const repoRoot = join(packageDir, '..', '..')
158
+ let matches: string[] = []
159
+ try {
160
+ matches = execFileSync(
161
+ 'git',
162
+ ['grep', '-l', 'lucideRegistry.generated', '--', '*.ts', '*.tsx'],
163
+ { cwd: repoRoot, encoding: 'utf-8' }
164
+ )
165
+ .split('\n')
166
+ .filter(Boolean)
167
+ } catch (error) {
168
+ const status = (error as { status?: number }).status
169
+ if (status !== 1) throw error
170
+ }
171
+
172
+ const outsideIconsFolder = matches.filter(
173
+ (path) => !path.startsWith('packages/ui/src/backend/icons/')
174
+ )
175
+ expect(outsideIconsFolder).toEqual([])
176
+ })
177
+ })
@@ -2,8 +2,7 @@
2
2
  // AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.
3
3
  // Generated from discovered icon string usages across the repo.
4
4
 
5
- import type * as React from 'react'
6
- import type { LucideIcon } from 'lucide-react'
5
+ import type { LucideIcon } from 'lucide-react';
7
6
  import {
8
7
  Activity,
9
8
  AlertCircle,
@@ -150,7 +149,7 @@ import {
150
149
  Wrench,
151
150
  X,
152
151
  XCircle,
153
- } from 'lucide-react'
152
+ } from 'lucide-react';
154
153
 
155
154
  export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
156
155
  'activity': Activity,
@@ -298,37 +297,4 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
298
297
  'wrench': Wrench,
299
298
  'x': X,
300
299
  'x-circle': XCircle,
301
- }
302
-
303
- function normalizeKebabIconName(input: string): string {
304
- const trimmed = input.trim()
305
- if (!trimmed) return ''
306
- const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
307
- if (!withoutPrefix) return ''
308
- if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
309
- return withoutPrefix
310
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
311
- .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
312
- .toLowerCase()
313
- }
314
- return withoutPrefix
315
- .replace(/[_\s]+/g, '-')
316
- .replace(/-+/g, '-')
317
- .toLowerCase()
318
- }
319
-
320
- export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
321
- if (!name) return null
322
- const normalized = normalizeKebabIconName(name)
323
- if (!normalized) return null
324
- return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
325
- }
326
-
327
- export function resolveRegisteredLucideIconNode(
328
- name: string | undefined,
329
- className: string
330
- ): React.ReactNode | null {
331
- const Icon = resolveRegisteredLucideIcon(name)
332
- if (!Icon) return null
333
- return <Icon className={className} />
334
- }
300
+ };
@@ -1,8 +1,8 @@
1
1
  export {
2
2
  resolveRegisteredLucideIcon,
3
3
  resolveRegisteredLucideIconNode,
4
- LUCIDE_ICON_REGISTRY,
5
- } from './lucideRegistry.generated'
4
+ } from './lucideRegistryRuntime'
5
+ export { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
6
6
 
7
7
  import type { LucideIcon } from 'lucide-react'
8
8
  import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
@@ -0,0 +1,36 @@
1
+ import type * as React from 'react'
2
+ import type { LucideIcon } from 'lucide-react'
3
+ import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
4
+
5
+ function normalizeKebabIconName(input: string): string {
6
+ const trimmed = input.trim()
7
+ if (!trimmed) return ''
8
+ const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
9
+ if (!withoutPrefix) return ''
10
+ if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
11
+ return withoutPrefix
12
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
13
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
14
+ .toLowerCase()
15
+ }
16
+ return withoutPrefix
17
+ .replace(/[_\s]+/g, '-')
18
+ .replace(/-+/g, '-')
19
+ .toLowerCase()
20
+ }
21
+
22
+ export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
23
+ if (!name) return null
24
+ const normalized = normalizeKebabIconName(name)
25
+ if (!normalized) return null
26
+ return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
27
+ }
28
+
29
+ export function resolveRegisteredLucideIconNode(
30
+ name: string | undefined,
31
+ className: string
32
+ ): React.ReactNode | null {
33
+ const Icon = resolveRegisteredLucideIcon(name)
34
+ if (!Icon) return null
35
+ return <Icon className={className} />
36
+ }
@@ -0,0 +1,38 @@
1
+ import {
2
+ CRUD_FORM_EXTENSION_SURFACES,
3
+ DATA_TABLE_EXTENSION_SURFACES,
4
+ } from '@open-mercato/shared/modules/widgets/extension-points'
5
+ import { CrudFormInjectionSpots, DataTableInjectionSpots } from '../spotIds'
6
+
7
+ describe('standard extension point spot IDs', () => {
8
+ it('keeps every bound DataTable suffix aligned with the runtime helper', () => {
9
+ const tableId = 'catalog.products.list'
10
+ const boundSuffixes = DATA_TABLE_EXTENSION_SURFACES
11
+ .filter((surface) => surface.bound && surface.suffix)
12
+ .map((surface) => surface.suffix)
13
+
14
+ expect(boundSuffixes).toEqual([
15
+ 'header',
16
+ 'footer',
17
+ 'toolbar',
18
+ 'search-trailing',
19
+ 'columns',
20
+ 'row-actions',
21
+ 'bulk-actions',
22
+ 'filters',
23
+ ])
24
+ expect(DataTableInjectionSpots.emptyState(tableId)).toBe('data-table:catalog.products.list:empty-state')
25
+ })
26
+
27
+ it('keeps bound CrudForm spots distinct from helper-only spots', () => {
28
+ const entityId = 'catalog.product'
29
+ const boundKeys = CRUD_FORM_EXTENSION_SURFACES
30
+ .filter((surface) => surface.bound)
31
+ .map((surface) => surface.key)
32
+
33
+ expect(boundKeys).toEqual(['base', 'header', 'fields', 'replacement'])
34
+ expect(CrudFormInjectionSpots.base(entityId)).toBe('crud-form:catalog.product')
35
+ expect(CrudFormInjectionSpots.header(entityId)).toBe('crud-form:catalog.product:header')
36
+ expect(CrudFormInjectionSpots.fields(entityId)).toBe('crud-form:catalog.product:fields')
37
+ })
38
+ })
@@ -1,4 +1,8 @@
1
1
  import type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'
2
+ import {
3
+ crudFormExtensionSpotId,
4
+ dataTableExtensionSpotId,
5
+ } from '@open-mercato/shared/modules/widgets/extension-points'
2
6
 
3
7
  export const BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID: InjectionSpotId = 'backend:record:current'
4
8
  export const BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:top'
@@ -18,32 +22,33 @@ export const GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID: InjectionSpotId
18
22
 
19
23
  // Standardized pattern helpers
20
24
  export const CrudFormInjectionSpots = {
21
- base: (entityId: string): InjectionSpotId => `crud-form:${entityId}`,
22
- beforeFields: (entityId: string): InjectionSpotId => `crud-form:${entityId}:before-fields`,
23
- afterFields: (entityId: string): InjectionSpotId => `crud-form:${entityId}:after-fields`,
24
- header: (entityId: string): InjectionSpotId => `crud-form:${entityId}:header`,
25
- footer: (entityId: string): InjectionSpotId => `crud-form:${entityId}:footer`,
26
- sidebar: (entityId: string): InjectionSpotId => `crud-form:${entityId}:sidebar`,
27
- group: (entityId: string, groupId: string): InjectionSpotId => `crud-form:${entityId}:group:${groupId}`,
28
- fieldBefore: (entityId: string, fieldId: string): InjectionSpotId => `crud-form:${entityId}:field:${fieldId}:before`,
29
- fieldAfter: (entityId: string, fieldId: string): InjectionSpotId => `crud-form:${entityId}:field:${fieldId}:after`,
25
+ base: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId),
26
+ beforeFields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'before-fields'),
27
+ afterFields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'after-fields'),
28
+ header: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'header'),
29
+ fields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'fields'),
30
+ footer: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'footer'),
31
+ sidebar: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'sidebar'),
32
+ group: (entityId: string, groupId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `group:${groupId}`),
33
+ fieldBefore: (entityId: string, fieldId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `field:${fieldId}:before`),
34
+ fieldAfter: (entityId: string, fieldId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `field:${fieldId}:after`),
30
35
  } as const
31
36
 
32
37
  export const DataTableInjectionSpots = {
33
- header: (tableId: string): InjectionSpotId => `data-table:${tableId}:header`,
34
- footer: (tableId: string): InjectionSpotId => `data-table:${tableId}:footer`,
35
- toolbar: (tableId: string): InjectionSpotId => `data-table:${tableId}:toolbar`,
38
+ header: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'header'),
39
+ footer: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'footer'),
40
+ toolbar: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'toolbar'),
36
41
  // Slot rendered immediately after the search input on the same row as the
37
42
  // FilterBar — intended for compact, icon-sized triggers (AI assistants,
38
43
  // saved view shortcuts, etc.). Hosts pass the resolved spot ID through to
39
44
  // FilterBar's `searchTrailing` prop. Stays empty when the table has no
40
45
  // search input.
41
- searchTrailing: (tableId: string): InjectionSpotId => `data-table:${tableId}:search-trailing`,
42
- emptyState: (tableId: string): InjectionSpotId => `data-table:${tableId}:empty-state`,
43
- columns: (tableId: string): InjectionSpotId => `data-table:${tableId}:columns`,
44
- rowActions: (tableId: string): InjectionSpotId => `data-table:${tableId}:row-actions`,
45
- bulkActions: (tableId: string): InjectionSpotId => `data-table:${tableId}:bulk-actions`,
46
- filters: (tableId: string): InjectionSpotId => `data-table:${tableId}:filters`,
46
+ searchTrailing: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'search-trailing'),
47
+ emptyState: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'empty-state'),
48
+ columns: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'columns'),
49
+ rowActions: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'row-actions'),
50
+ bulkActions: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'bulk-actions'),
51
+ filters: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'filters'),
47
52
  } as const
48
53
 
49
54
  // Portal chrome spot IDs (FROZEN once shipped)
@@ -241,7 +241,7 @@ export function ComboboxInput({
241
241
  .finally(() => { if (!cancelled) setLoading(false) })
242
242
  }
243
243
  return () => { cancelled = true }
244
- // eslint-disable-next-line react-hooks/exhaustive-deps resolveDescription intentionally excluded:
244
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- resolveDescription intentionally excluded:
245
245
  // including it would re-run the effect on every render when the prop is an inline function
246
246
  }, [value, disabled, knownLabelValues, coveredOptionValues, eagerResolveLabel, loadSuggestions])
247
247
 
@@ -141,7 +141,7 @@ export function EmailThreadsPanel({
141
141
  {!canCompose && composeDisabledHint ? <div>{composeDisabledHint}</div> : null}
142
142
 
143
143
  {error ? (
144
- <Alert variant="destructive">
144
+ <Alert status="error">
145
145
  <AlertDescription>{error}</AlertDescription>
146
146
  </Alert>
147
147
  ) : null}
@@ -0,0 +1,44 @@
1
+ export const PERSPECTIVE_COOKIE_PREFIX = 'om_table_perspective'
2
+ export const PERSPECTIVE_STORAGE_PREFIX = 'om_table_perspective_snapshot'
3
+
4
+ /**
5
+ * Purge every browser-local DataTable perspective snapshot and active-view cookie
6
+ * across all tables in this origin. Snapshots persist the unsaved/live table state
7
+ * (column widths, order, visibility) keyed only by `tableId`, so they are shared by
8
+ * every account that signs in on the same browser profile. Call this at the auth
9
+ * identity boundary so one user's unsaved layout never carries over to a different
10
+ * user — including a different tenant — reusing the same browser tab (#4185).
11
+ *
12
+ * Lives here rather than in DataTable so the backend shell can call it without
13
+ * pulling the whole table implementation into the bundle of every backend page.
14
+ * `@open-mercato/ui/backend/DataTable` re-exports it, which is the published
15
+ * import path and must stay stable.
16
+ */
17
+ export function clearAllPerspectiveState(): void {
18
+ if (typeof window !== 'undefined') {
19
+ try {
20
+ const storage = window.localStorage
21
+ const staleKeys: string[] = []
22
+ for (let index = 0; index < storage.length; index += 1) {
23
+ const key = storage.key(index)
24
+ if (key && key.startsWith(`${PERSPECTIVE_STORAGE_PREFIX}:`)) staleKeys.push(key)
25
+ }
26
+ for (const key of staleKeys) storage.removeItem(key)
27
+ } catch {
28
+ // private mode / quota errors are non-fatal
29
+ }
30
+ }
31
+ if (typeof document !== 'undefined') {
32
+ try {
33
+ const cookies = document.cookie ? document.cookie.split(';') : []
34
+ for (const cookie of cookies) {
35
+ const name = cookie.split('=')[0]?.trim()
36
+ if (name && name.startsWith(`${PERSPECTIVE_COOKIE_PREFIX}:`)) {
37
+ document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`
38
+ }
39
+ }
40
+ } catch {
41
+ // ignore — cookie access can throw in sandboxed contexts
42
+ }
43
+ }
44
+ }
@@ -57,9 +57,9 @@ function getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallbac
57
57
  }
58
58
 
59
59
  function getKindStyles(kind: ScheduleItem['kind']): string {
60
- if (kind === 'event') return 'border-blue-500/40 bg-blue-500/10 text-blue-950'
61
- if (kind === 'exception') return 'border-amber-500/40 bg-amber-500/10 text-amber-950'
62
- return 'border-emerald-500/40 bg-emerald-500/10 text-emerald-950'
60
+ if (kind === 'event') return 'border-status-info-border bg-status-info-bg text-status-info-text'
61
+ if (kind === 'exception') return 'border-status-warning-border bg-status-warning-bg text-status-warning-text'
62
+ return 'border-status-success-border bg-status-success-bg text-status-success-text'
63
63
  }
64
64
 
65
65
  export type ScheduleAgendaProps = {
@@ -57,9 +57,9 @@ function getStatusLabel(status: ScheduleItem['status'], t: (key: string, fallbac
57
57
  }
58
58
 
59
59
  function getKindStyles(kind: ScheduleItem['kind']): string {
60
- if (kind === 'event') return 'border-blue-500/40 bg-blue-500/10 text-blue-950'
61
- if (kind === 'exception') return 'border-amber-500/40 bg-amber-500/10 text-amber-950'
62
- return 'border-emerald-500/40 bg-emerald-500/10 text-emerald-950'
60
+ if (kind === 'event') return 'border-status-info-border bg-status-info-bg text-status-info-text'
61
+ if (kind === 'exception') return 'border-status-warning-border bg-status-warning-bg text-status-warning-text'
62
+ return 'border-status-success-border bg-status-success-bg text-status-success-text'
63
63
  }
64
64
 
65
65
  export type ScheduleGridProps = {