@open-mercato/ui 0.7.1-develop.7191.1.3c9346566b → 0.7.1-develop.7194.1.ab4fc81f82

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 (58) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/backend/AppShell.js +66 -7
  3. package/dist/backend/AppShell.js.map +2 -2
  4. package/dist/backend/CrudForm.js +14 -8
  5. package/dist/backend/CrudForm.js.map +2 -2
  6. package/dist/backend/DataTable.js +167 -24
  7. package/dist/backend/DataTable.js.map +2 -2
  8. package/dist/backend/PerspectiveSidebar.js +60 -21
  9. package/dist/backend/PerspectiveSidebar.js.map +2 -2
  10. package/dist/backend/forms/ActionsDropdown.js +35 -5
  11. package/dist/backend/forms/ActionsDropdown.js.map +2 -2
  12. package/dist/backend/forms/index.js.map +2 -2
  13. package/dist/backend/icons/lucideRegistry.generated.js +6 -0
  14. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  15. package/dist/backend/injection/spotIds.js +2 -0
  16. package/dist/backend/injection/spotIds.js.map +2 -2
  17. package/dist/backend/inputs/DurationInput.js +182 -0
  18. package/dist/backend/inputs/DurationInput.js.map +7 -0
  19. package/dist/backend/inputs/EventSelect.js.map +2 -2
  20. package/dist/backend/inputs/index.js +1 -0
  21. package/dist/backend/inputs/index.js.map +2 -2
  22. package/dist/backend/nav/navBadges.js +38 -0
  23. package/dist/backend/nav/navBadges.js.map +7 -0
  24. package/dist/backend/views/NewViewForm.js +46 -38
  25. package/dist/backend/views/NewViewForm.js.map +2 -2
  26. package/dist/primitives/segmented-control.js +4 -4
  27. package/dist/primitives/segmented-control.js.map +1 -1
  28. package/dist/primitives/sheet.js +21 -11
  29. package/dist/primitives/sheet.js.map +2 -2
  30. package/dist/primitives/textarea.js +54 -4
  31. package/dist/primitives/textarea.js.map +2 -2
  32. package/package.json +3 -3
  33. package/src/backend/AppShell.tsx +106 -2
  34. package/src/backend/CrudForm.tsx +28 -9
  35. package/src/backend/DataTable.tsx +293 -36
  36. package/src/backend/PerspectiveSidebar.tsx +80 -21
  37. package/src/backend/__tests__/CrudForm.density.test.tsx +113 -0
  38. package/src/backend/__tests__/DataTable.exportSectionTitles.test.tsx +134 -0
  39. package/src/backend/__tests__/DataTable.perspectiveReconcile.test.tsx +312 -0
  40. package/src/backend/__tests__/DataTable.propBulkActions.test.tsx +200 -0
  41. package/src/backend/__tests__/DataTable.render.test.tsx +76 -0
  42. package/src/backend/__tests__/PerspectiveSidebar.roleAutosave.test.tsx +252 -0
  43. package/src/backend/__tests__/navBadges.test.ts +57 -0
  44. package/src/backend/forms/ActionsDropdown.tsx +73 -6
  45. package/src/backend/forms/index.ts +1 -1
  46. package/src/backend/icons/lucideRegistry.generated.tsx +6 -0
  47. package/src/backend/injection/spotIds.ts +8 -0
  48. package/src/backend/inputs/DurationInput.tsx +207 -0
  49. package/src/backend/inputs/EventSelect.tsx +2 -0
  50. package/src/backend/inputs/__tests__/DurationInput.test.tsx +220 -0
  51. package/src/backend/inputs/index.ts +1 -0
  52. package/src/backend/nav/navBadges.ts +61 -0
  53. package/src/backend/views/NewViewForm.tsx +46 -32
  54. package/src/primitives/__tests__/segmented-control.test.tsx +2 -2
  55. package/src/primitives/__tests__/textarea.test.tsx +50 -0
  56. package/src/primitives/segmented-control.tsx +7 -7
  57. package/src/primitives/sheet.tsx +7 -1
  58. package/src/primitives/textarea.tsx +77 -4
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/injection/spotIds.ts"],
4
- "sourcesContent": ["import type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\nimport {\n crudFormExtensionSpotId,\n dataTableExtensionSpotId,\n} from '@open-mercato/shared/modules/widgets/extension-points'\n\nexport const BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID: InjectionSpotId = 'backend:record:current'\nexport const BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:top'\nexport const BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:footer'\nexport const BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:top'\nexport const BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:footer'\n\n// Standardized backend chrome spot ids\nexport const BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID: InjectionSpotId = 'backend:topbar:profile-menu'\nexport const BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID: InjectionSpotId = 'backend:topbar:actions'\nexport const BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:nav'\nexport const BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:nav:footer'\n\n// Standardized global status spot ids\nexport const GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID: InjectionSpotId = 'global:sidebar:status-badges'\nexport const GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID: InjectionSpotId = 'global:header:status-indicators'\n\n// Standardized pattern helpers\nexport const CrudFormInjectionSpots = {\n base: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId),\n beforeFields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'before-fields'),\n afterFields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'after-fields'),\n header: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'header'),\n fields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'fields'),\n footer: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'footer'),\n sidebar: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'sidebar'),\n group: (entityId: string, groupId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `group:${groupId}`),\n fieldBefore: (entityId: string, fieldId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `field:${fieldId}:before`),\n fieldAfter: (entityId: string, fieldId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `field:${fieldId}:after`),\n} as const\n\nexport const DataTableInjectionSpots = {\n header: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'header'),\n footer: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'footer'),\n toolbar: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'toolbar'),\n // Slot rendered immediately after the search input on the same row as the\n // FilterBar \u2014 intended for compact, icon-sized triggers (AI assistants,\n // saved view shortcuts, etc.). Hosts pass the resolved spot ID through to\n // FilterBar's `searchTrailing` prop. Stays empty when the table has no\n // search input.\n searchTrailing: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'search-trailing'),\n emptyState: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'empty-state'),\n columns: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'columns'),\n rowActions: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'row-actions'),\n bulkActions: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'bulk-actions'),\n filters: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'filters'),\n} as const\n\n// Portal chrome spot IDs (FROZEN once shipped)\nexport const PORTAL_SIDEBAR_MAIN_INJECTION_SPOT_ID: InjectionSpotId = 'menu:portal:sidebar:main'\nexport const PORTAL_SIDEBAR_ACCOUNT_INJECTION_SPOT_ID: InjectionSpotId = 'menu:portal:sidebar:account'\nexport const PORTAL_HEADER_ACTIONS_INJECTION_SPOT_ID: InjectionSpotId = 'menu:portal:header:actions'\nexport const PORTAL_USER_DROPDOWN_INJECTION_SPOT_ID: InjectionSpotId = 'menu:portal:user-dropdown'\n\n// Portal page injection spots (FROZEN once shipped)\nexport const PortalInjectionSpots = {\n dashboardSections: 'portal:dashboard:sections' as InjectionSpotId,\n dashboardProfile: 'portal:dashboard:profile' as InjectionSpotId,\n dashboardSidebar: 'portal:dashboard:sidebar' as InjectionSpotId,\n pageBefore: (pageId: string): InjectionSpotId => `portal:${pageId}:before` as InjectionSpotId,\n pageAfter: (pageId: string): InjectionSpotId => `portal:${pageId}:after` as InjectionSpotId,\n} as const\n\nexport const DetailInjectionSpots = {\n header: (entityId: string): InjectionSpotId => `detail:${entityId}:header`,\n tabs: (entityId: string): InjectionSpotId => `detail:${entityId}:tabs`,\n sidebar: (entityId: string): InjectionSpotId => `detail:${entityId}:sidebar`,\n footer: (entityId: string): InjectionSpotId => `detail:${entityId}:footer`,\n statusBadges: (entityId: string): InjectionSpotId => `detail:${entityId}:status-badges`,\n} as const\n"],
5
- "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEA,MAAM,2CAA4D;AAClE,MAAM,uCAAwD;AAC9D,MAAM,0CAA2D;AACjE,MAAM,wCAAyD;AAC/D,MAAM,2CAA4D;AAGlE,MAAM,gDAAiE;AACvE,MAAM,2CAA4D;AAClE,MAAM,wCAAyD;AAC/D,MAAM,+CAAgE;AAGtE,MAAM,iDAAkE;AACxE,MAAM,oDAAqE;AAG3E,MAAM,yBAAyB;AAAA,EACpC,MAAM,CAAC,aAAsC,wBAAwB,QAAQ;AAAA,EAC7E,cAAc,CAAC,aAAsC,wBAAwB,UAAU,eAAe;AAAA,EACtG,aAAa,CAAC,aAAsC,wBAAwB,UAAU,cAAc;AAAA,EACpG,QAAQ,CAAC,aAAsC,wBAAwB,UAAU,QAAQ;AAAA,EACzF,QAAQ,CAAC,aAAsC,wBAAwB,UAAU,QAAQ;AAAA,EACzF,QAAQ,CAAC,aAAsC,wBAAwB,UAAU,QAAQ;AAAA,EACzF,SAAS,CAAC,aAAsC,wBAAwB,UAAU,SAAS;AAAA,EAC3F,OAAO,CAAC,UAAkB,YAAqC,wBAAwB,UAAU,SAAS,OAAO,EAAE;AAAA,EACnH,aAAa,CAAC,UAAkB,YAAqC,wBAAwB,UAAU,SAAS,OAAO,SAAS;AAAA,EAChI,YAAY,CAAC,UAAkB,YAAqC,wBAAwB,UAAU,SAAS,OAAO,QAAQ;AAChI;AAEO,MAAM,0BAA0B;AAAA,EACrC,QAAQ,CAAC,YAAqC,yBAAyB,SAAS,QAAQ;AAAA,EACxF,QAAQ,CAAC,YAAqC,yBAAyB,SAAS,QAAQ;AAAA,EACxF,SAAS,CAAC,YAAqC,yBAAyB,SAAS,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM1F,gBAAgB,CAAC,YAAqC,yBAAyB,SAAS,iBAAiB;AAAA,EACzG,YAAY,CAAC,YAAqC,yBAAyB,SAAS,aAAa;AAAA,EACjG,SAAS,CAAC,YAAqC,yBAAyB,SAAS,SAAS;AAAA,EAC1F,YAAY,CAAC,YAAqC,yBAAyB,SAAS,aAAa;AAAA,EACjG,aAAa,CAAC,YAAqC,yBAAyB,SAAS,cAAc;AAAA,EACnG,SAAS,CAAC,YAAqC,yBAAyB,SAAS,SAAS;AAC5F;AAGO,MAAM,wCAAyD;AAC/D,MAAM,2CAA4D;AAClE,MAAM,0CAA2D;AACjE,MAAM,yCAA0D;AAGhE,MAAM,uBAAuB;AAAA,EAClC,mBAAmB;AAAA,EACnB,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB,YAAY,CAAC,WAAoC,UAAU,MAAM;AAAA,EACjE,WAAW,CAAC,WAAoC,UAAU,MAAM;AAClE;AAEO,MAAM,uBAAuB;AAAA,EAClC,QAAQ,CAAC,aAAsC,UAAU,QAAQ;AAAA,EACjE,MAAM,CAAC,aAAsC,UAAU,QAAQ;AAAA,EAC/D,SAAS,CAAC,aAAsC,UAAU,QAAQ;AAAA,EAClE,QAAQ,CAAC,aAAsC,UAAU,QAAQ;AAAA,EACjE,cAAc,CAAC,aAAsC,UAAU,QAAQ;AACzE;",
4
+ "sourcesContent": ["import type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\nimport {\n crudFormExtensionSpotId,\n dataTableExtensionSpotId,\n} from '@open-mercato/shared/modules/widgets/extension-points'\n\nexport const BACKEND_RECORD_CURRENT_INJECTION_SPOT_ID: InjectionSpotId = 'backend:record:current'\nexport const BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:top'\nexport const BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:layout:footer'\nexport const BACKEND_SIDEBAR_TOP_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:top'\nexport const BACKEND_SIDEBAR_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:footer'\n\n// Standardized backend chrome spot ids\nexport const BACKEND_TOPBAR_PROFILE_MENU_INJECTION_SPOT_ID: InjectionSpotId = 'backend:topbar:profile-menu'\nexport const BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID: InjectionSpotId = 'backend:topbar:actions'\nexport const BACKEND_SIDEBAR_NAV_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:nav'\nexport const BACKEND_SIDEBAR_NAV_FOOTER_INJECTION_SPOT_ID: InjectionSpotId = 'backend:sidebar:nav:footer'\n\n/**\n * Headless mount point for modules that publish live counts onto sidebar nav\n * items through `setNavBadge`. Rendered once per shell and always \u2014 a feeder\n * must keep counting while the sidebar is collapsed or off-screen on mobile \u2014\n * and its widgets are expected to render nothing.\n */\nexport const BACKEND_NAV_BADGES_INJECTION_SPOT_ID: InjectionSpotId = 'backend:nav:badges'\n\n// Standardized global status spot ids\nexport const GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID: InjectionSpotId = 'global:sidebar:status-badges'\nexport const GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID: InjectionSpotId = 'global:header:status-indicators'\n\n// Standardized pattern helpers\nexport const CrudFormInjectionSpots = {\n base: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId),\n beforeFields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'before-fields'),\n afterFields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'after-fields'),\n header: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'header'),\n fields: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'fields'),\n footer: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'footer'),\n sidebar: (entityId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, 'sidebar'),\n group: (entityId: string, groupId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `group:${groupId}`),\n fieldBefore: (entityId: string, fieldId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `field:${fieldId}:before`),\n fieldAfter: (entityId: string, fieldId: string): InjectionSpotId => crudFormExtensionSpotId(entityId, `field:${fieldId}:after`),\n} as const\n\nexport const DataTableInjectionSpots = {\n header: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'header'),\n footer: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'footer'),\n toolbar: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'toolbar'),\n // Slot rendered immediately after the search input on the same row as the\n // FilterBar \u2014 intended for compact, icon-sized triggers (AI assistants,\n // saved view shortcuts, etc.). Hosts pass the resolved spot ID through to\n // FilterBar's `searchTrailing` prop. Stays empty when the table has no\n // search input.\n searchTrailing: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'search-trailing'),\n emptyState: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'empty-state'),\n columns: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'columns'),\n rowActions: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'row-actions'),\n bulkActions: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'bulk-actions'),\n filters: (tableId: string): InjectionSpotId => dataTableExtensionSpotId(tableId, 'filters'),\n} as const\n\n// Portal chrome spot IDs (FROZEN once shipped)\nexport const PORTAL_SIDEBAR_MAIN_INJECTION_SPOT_ID: InjectionSpotId = 'menu:portal:sidebar:main'\nexport const PORTAL_SIDEBAR_ACCOUNT_INJECTION_SPOT_ID: InjectionSpotId = 'menu:portal:sidebar:account'\nexport const PORTAL_HEADER_ACTIONS_INJECTION_SPOT_ID: InjectionSpotId = 'menu:portal:header:actions'\nexport const PORTAL_USER_DROPDOWN_INJECTION_SPOT_ID: InjectionSpotId = 'menu:portal:user-dropdown'\n\n// Portal page injection spots (FROZEN once shipped)\nexport const PortalInjectionSpots = {\n dashboardSections: 'portal:dashboard:sections' as InjectionSpotId,\n dashboardProfile: 'portal:dashboard:profile' as InjectionSpotId,\n dashboardSidebar: 'portal:dashboard:sidebar' as InjectionSpotId,\n pageBefore: (pageId: string): InjectionSpotId => `portal:${pageId}:before` as InjectionSpotId,\n pageAfter: (pageId: string): InjectionSpotId => `portal:${pageId}:after` as InjectionSpotId,\n} as const\n\nexport const DetailInjectionSpots = {\n header: (entityId: string): InjectionSpotId => `detail:${entityId}:header`,\n tabs: (entityId: string): InjectionSpotId => `detail:${entityId}:tabs`,\n sidebar: (entityId: string): InjectionSpotId => `detail:${entityId}:sidebar`,\n footer: (entityId: string): InjectionSpotId => `detail:${entityId}:footer`,\n statusBadges: (entityId: string): InjectionSpotId => `detail:${entityId}:status-badges`,\n} as const\n"],
5
+ "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEA,MAAM,2CAA4D;AAClE,MAAM,uCAAwD;AAC9D,MAAM,0CAA2D;AACjE,MAAM,wCAAyD;AAC/D,MAAM,2CAA4D;AAGlE,MAAM,gDAAiE;AACvE,MAAM,2CAA4D;AAClE,MAAM,wCAAyD;AAC/D,MAAM,+CAAgE;AAQtE,MAAM,uCAAwD;AAG9D,MAAM,iDAAkE;AACxE,MAAM,oDAAqE;AAG3E,MAAM,yBAAyB;AAAA,EACpC,MAAM,CAAC,aAAsC,wBAAwB,QAAQ;AAAA,EAC7E,cAAc,CAAC,aAAsC,wBAAwB,UAAU,eAAe;AAAA,EACtG,aAAa,CAAC,aAAsC,wBAAwB,UAAU,cAAc;AAAA,EACpG,QAAQ,CAAC,aAAsC,wBAAwB,UAAU,QAAQ;AAAA,EACzF,QAAQ,CAAC,aAAsC,wBAAwB,UAAU,QAAQ;AAAA,EACzF,QAAQ,CAAC,aAAsC,wBAAwB,UAAU,QAAQ;AAAA,EACzF,SAAS,CAAC,aAAsC,wBAAwB,UAAU,SAAS;AAAA,EAC3F,OAAO,CAAC,UAAkB,YAAqC,wBAAwB,UAAU,SAAS,OAAO,EAAE;AAAA,EACnH,aAAa,CAAC,UAAkB,YAAqC,wBAAwB,UAAU,SAAS,OAAO,SAAS;AAAA,EAChI,YAAY,CAAC,UAAkB,YAAqC,wBAAwB,UAAU,SAAS,OAAO,QAAQ;AAChI;AAEO,MAAM,0BAA0B;AAAA,EACrC,QAAQ,CAAC,YAAqC,yBAAyB,SAAS,QAAQ;AAAA,EACxF,QAAQ,CAAC,YAAqC,yBAAyB,SAAS,QAAQ;AAAA,EACxF,SAAS,CAAC,YAAqC,yBAAyB,SAAS,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM1F,gBAAgB,CAAC,YAAqC,yBAAyB,SAAS,iBAAiB;AAAA,EACzG,YAAY,CAAC,YAAqC,yBAAyB,SAAS,aAAa;AAAA,EACjG,SAAS,CAAC,YAAqC,yBAAyB,SAAS,SAAS;AAAA,EAC1F,YAAY,CAAC,YAAqC,yBAAyB,SAAS,aAAa;AAAA,EACjG,aAAa,CAAC,YAAqC,yBAAyB,SAAS,cAAc;AAAA,EACnG,SAAS,CAAC,YAAqC,yBAAyB,SAAS,SAAS;AAC5F;AAGO,MAAM,wCAAyD;AAC/D,MAAM,2CAA4D;AAClE,MAAM,0CAA2D;AACjE,MAAM,yCAA0D;AAGhE,MAAM,uBAAuB;AAAA,EAClC,mBAAmB;AAAA,EACnB,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB,YAAY,CAAC,WAAoC,UAAU,MAAM;AAAA,EACjE,WAAW,CAAC,WAAoC,UAAU,MAAM;AAClE;AAEO,MAAM,uBAAuB;AAAA,EAClC,QAAQ,CAAC,aAAsC,UAAU,QAAQ;AAAA,EACjE,MAAM,CAAC,aAAsC,UAAU,QAAQ;AAAA,EAC/D,SAAS,CAAC,aAAsC,UAAU,QAAQ;AAAA,EAClE,QAAQ,CAAC,aAAsC,UAAU,QAAQ;AAAA,EACjE,cAAc,CAAC,aAAsC,UAAU,QAAQ;AACzE;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,182 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "@open-mercato/shared/lib/utils";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { Input } from "../../primitives/input.js";
7
+ import { Button } from "../../primitives/button.js";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue
14
+ } from "../../primitives/select.js";
15
+ const singleUnitPatterns = [
16
+ { regex: /^PT(\d+)S$/, unit: "seconds" },
17
+ { regex: /^PT(\d+)M$/, unit: "minutes" },
18
+ { regex: /^PT(\d+)H$/, unit: "hours" },
19
+ { regex: /^P(\d+)D$/, unit: "days" }
20
+ ];
21
+ function parseSingleUnitDuration(value) {
22
+ for (const { regex, unit } of singleUnitPatterns) {
23
+ const match = value.match(regex);
24
+ if (match) {
25
+ const amount = parseInt(match[1], 10);
26
+ if (!isNaN(amount)) return { amount, unit };
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+ function formatSingleUnitDuration(amount, unit) {
32
+ switch (unit) {
33
+ case "seconds":
34
+ return `PT${amount}S`;
35
+ case "minutes":
36
+ return `PT${amount}M`;
37
+ case "hours":
38
+ return `PT${amount}H`;
39
+ case "days":
40
+ return `P${amount}D`;
41
+ }
42
+ }
43
+ function isCompositeCompatibleDuration(value) {
44
+ if (!value) return true;
45
+ if (value.includes("{{")) return false;
46
+ return parseSingleUnitDuration(value) !== null;
47
+ }
48
+ const durationUnits = ["seconds", "minutes", "hours", "days"];
49
+ function DurationInput({
50
+ value,
51
+ onChange,
52
+ disabled = false,
53
+ id,
54
+ "aria-label": ariaLabel,
55
+ className
56
+ }) {
57
+ const t = useT();
58
+ const currentValue = value ?? "";
59
+ const parsed = React.useMemo(() => parseSingleUnitDuration(currentValue), [currentValue]);
60
+ const compositeCompatible = isCompositeCompatibleDuration(currentValue);
61
+ const [mode, setMode] = React.useState(
62
+ () => compositeCompatible ? "composite" : "raw"
63
+ );
64
+ const [unit, setUnit] = React.useState(parsed?.unit ?? "minutes");
65
+ React.useEffect(() => {
66
+ if (!isCompositeCompatibleDuration(currentValue)) setMode("raw");
67
+ }, [currentValue]);
68
+ React.useEffect(() => {
69
+ if (parsed && parsed.unit !== unit) setUnit(parsed.unit);
70
+ }, [parsed, unit]);
71
+ const amountLabel = ariaLabel ?? t("ui.durationInput.amountLabel", "Duration amount");
72
+ const unitLabel = t("ui.durationInput.unitLabel", "Duration unit");
73
+ const rawLabel = ariaLabel ?? t("ui.durationInput.rawLabel", "Duration expression");
74
+ const unitOptionLabels = {
75
+ seconds: t("ui.durationInput.units.seconds", "Seconds"),
76
+ minutes: t("ui.durationInput.units.minutes", "Minutes"),
77
+ hours: t("ui.durationInput.units.hours", "Hours"),
78
+ days: t("ui.durationInput.units.days", "Days")
79
+ };
80
+ const handleAmountChange = React.useCallback(
81
+ (event) => {
82
+ if (disabled) return;
83
+ const raw = event.target.value;
84
+ if (raw === "") {
85
+ onChange("");
86
+ return;
87
+ }
88
+ const amount = parseInt(raw, 10);
89
+ if (isNaN(amount) || amount < 0) return;
90
+ onChange(formatSingleUnitDuration(amount, unit));
91
+ },
92
+ [disabled, onChange, unit]
93
+ );
94
+ const handleUnitChange = React.useCallback(
95
+ (nextUnit) => {
96
+ if (disabled) return;
97
+ const resolved = durationUnits.find((candidate) => candidate === nextUnit);
98
+ if (!resolved) return;
99
+ setUnit(resolved);
100
+ if (parsed) onChange(formatSingleUnitDuration(parsed.amount, resolved));
101
+ },
102
+ [disabled, onChange, parsed]
103
+ );
104
+ const handleRawChange = React.useCallback(
105
+ (event) => {
106
+ if (disabled) return;
107
+ onChange(event.target.value);
108
+ },
109
+ [disabled, onChange]
110
+ );
111
+ const switchToRaw = React.useCallback(() => {
112
+ setMode("raw");
113
+ }, []);
114
+ const switchToComposite = React.useCallback(() => {
115
+ if (!isCompositeCompatibleDuration(currentValue)) return;
116
+ setMode("composite");
117
+ }, [currentValue]);
118
+ if (mode === "raw") {
119
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", className), children: [
120
+ /* @__PURE__ */ jsx(
121
+ Input,
122
+ {
123
+ id,
124
+ type: "text",
125
+ value: currentValue,
126
+ onChange: handleRawChange,
127
+ disabled,
128
+ "aria-label": rawLabel,
129
+ className: "flex-1"
130
+ }
131
+ ),
132
+ /* @__PURE__ */ jsx(
133
+ Button,
134
+ {
135
+ type: "button",
136
+ variant: "ghost",
137
+ size: "sm",
138
+ onClick: switchToComposite,
139
+ disabled: disabled || !compositeCompatible,
140
+ children: t("ui.durationInput.switchToComposite", "Use picker")
141
+ }
142
+ )
143
+ ] });
144
+ }
145
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", className), children: [
146
+ /* @__PURE__ */ jsx(
147
+ Input,
148
+ {
149
+ id,
150
+ type: "number",
151
+ min: 0,
152
+ value: parsed ? String(parsed.amount) : "",
153
+ onChange: handleAmountChange,
154
+ disabled,
155
+ "aria-label": amountLabel,
156
+ className: "w-24"
157
+ }
158
+ ),
159
+ /* @__PURE__ */ jsxs(Select, { value: unit, onValueChange: handleUnitChange, disabled, children: [
160
+ /* @__PURE__ */ jsx(SelectTrigger, { "aria-label": unitLabel, className: "w-32", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
161
+ /* @__PURE__ */ jsx(SelectContent, { children: durationUnits.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option, children: unitOptionLabels[option] }, option)) })
162
+ ] }),
163
+ /* @__PURE__ */ jsx(
164
+ Button,
165
+ {
166
+ type: "button",
167
+ variant: "ghost",
168
+ size: "sm",
169
+ onClick: switchToRaw,
170
+ disabled,
171
+ children: t("ui.durationInput.switchToRaw", "Edit as text")
172
+ }
173
+ )
174
+ ] });
175
+ }
176
+ export {
177
+ DurationInput,
178
+ formatSingleUnitDuration,
179
+ isCompositeCompatibleDuration,
180
+ parseSingleUnitDuration
181
+ };
182
+ //# sourceMappingURL=DurationInput.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/inputs/DurationInput.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Input } from '../../primitives/input'\nimport { Button } from '../../primitives/button'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\n\nexport type DurationUnit = 'seconds' | 'minutes' | 'hours' | 'days'\n\nexport type ParsedSingleUnitDuration = { amount: number; unit: DurationUnit }\n\nconst singleUnitPatterns: Array<{ regex: RegExp; unit: DurationUnit }> = [\n { regex: /^PT(\\d+)S$/, unit: 'seconds' },\n { regex: /^PT(\\d+)M$/, unit: 'minutes' },\n { regex: /^PT(\\d+)H$/, unit: 'hours' },\n { regex: /^P(\\d+)D$/, unit: 'days' },\n]\n\nexport function parseSingleUnitDuration(value: string): ParsedSingleUnitDuration | null {\n for (const { regex, unit } of singleUnitPatterns) {\n const match = value.match(regex)\n if (match) {\n const amount = parseInt(match[1], 10)\n if (!isNaN(amount)) return { amount, unit }\n }\n }\n return null\n}\n\nexport function formatSingleUnitDuration(amount: number, unit: DurationUnit): string {\n switch (unit) {\n case 'seconds':\n return `PT${amount}S`\n case 'minutes':\n return `PT${amount}M`\n case 'hours':\n return `PT${amount}H`\n case 'days':\n return `P${amount}D`\n }\n}\n\nexport function isCompositeCompatibleDuration(value: string | null | undefined): boolean {\n if (!value) return true\n if (value.includes('{{')) return false\n return parseSingleUnitDuration(value) !== null\n}\n\nexport type DurationInputProps = {\n value?: string\n onChange: (value: string) => void\n disabled?: boolean\n id?: string\n 'aria-label'?: string\n className?: string\n}\n\nconst durationUnits: DurationUnit[] = ['seconds', 'minutes', 'hours', 'days']\n\nexport function DurationInput({\n value,\n onChange,\n disabled = false,\n id,\n 'aria-label': ariaLabel,\n className,\n}: DurationInputProps) {\n const t = useT()\n const currentValue = value ?? ''\n const parsed = React.useMemo(() => parseSingleUnitDuration(currentValue), [currentValue])\n const compositeCompatible = isCompositeCompatibleDuration(currentValue)\n\n const [mode, setMode] = React.useState<'composite' | 'raw'>(() =>\n compositeCompatible ? 'composite' : 'raw'\n )\n const [unit, setUnit] = React.useState<DurationUnit>(parsed?.unit ?? 'minutes')\n\n React.useEffect(() => {\n if (!isCompositeCompatibleDuration(currentValue)) setMode('raw')\n }, [currentValue])\n\n React.useEffect(() => {\n if (parsed && parsed.unit !== unit) setUnit(parsed.unit)\n }, [parsed, unit])\n\n const amountLabel = ariaLabel ?? t('ui.durationInput.amountLabel', 'Duration amount')\n const unitLabel = t('ui.durationInput.unitLabel', 'Duration unit')\n const rawLabel = ariaLabel ?? t('ui.durationInput.rawLabel', 'Duration expression')\n const unitOptionLabels: Record<DurationUnit, string> = {\n seconds: t('ui.durationInput.units.seconds', 'Seconds'),\n minutes: t('ui.durationInput.units.minutes', 'Minutes'),\n hours: t('ui.durationInput.units.hours', 'Hours'),\n days: t('ui.durationInput.units.days', 'Days'),\n }\n\n const handleAmountChange = React.useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return\n const raw = event.target.value\n if (raw === '') {\n onChange('')\n return\n }\n const amount = parseInt(raw, 10)\n if (isNaN(amount) || amount < 0) return\n onChange(formatSingleUnitDuration(amount, unit))\n },\n [disabled, onChange, unit]\n )\n\n const handleUnitChange = React.useCallback(\n (nextUnit: string) => {\n if (disabled) return\n const resolved = durationUnits.find((candidate) => candidate === nextUnit)\n if (!resolved) return\n setUnit(resolved)\n if (parsed) onChange(formatSingleUnitDuration(parsed.amount, resolved))\n },\n [disabled, onChange, parsed]\n )\n\n const handleRawChange = React.useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return\n onChange(event.target.value)\n },\n [disabled, onChange]\n )\n\n const switchToRaw = React.useCallback(() => {\n setMode('raw')\n }, [])\n\n const switchToComposite = React.useCallback(() => {\n if (!isCompositeCompatibleDuration(currentValue)) return\n setMode('composite')\n }, [currentValue])\n\n if (mode === 'raw') {\n return (\n <div className={cn('flex items-center gap-2', className)}>\n <Input\n id={id}\n type=\"text\"\n value={currentValue}\n onChange={handleRawChange}\n disabled={disabled}\n aria-label={rawLabel}\n className=\"flex-1\"\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={switchToComposite}\n disabled={disabled || !compositeCompatible}\n >\n {t('ui.durationInput.switchToComposite', 'Use picker')}\n </Button>\n </div>\n )\n }\n\n return (\n <div className={cn('flex items-center gap-2', className)}>\n <Input\n id={id}\n type=\"number\"\n min={0}\n value={parsed ? String(parsed.amount) : ''}\n onChange={handleAmountChange}\n disabled={disabled}\n aria-label={amountLabel}\n className=\"w-24\"\n />\n <Select value={unit} onValueChange={handleUnitChange} disabled={disabled}>\n <SelectTrigger aria-label={unitLabel} className=\"w-32\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {durationUnits.map((option) => (\n <SelectItem key={option} value={option}>\n {unitOptionLabels[option]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={switchToRaw}\n disabled={disabled}\n >\n {t('ui.durationInput.switchToRaw', 'Edit as text')}\n </Button>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/inputs/EventSelect.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useMemo } from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '../utils/apiCall'\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\n\n/**\n * Event definition returned by the API\n */\nexport interface EventDefinition {\n id: string\n label: string\n description?: string\n category?: 'crud' | 'lifecycle' | 'system' | 'custom'\n module?: string\n entity?: string\n excludeFromTriggers?: boolean\n}\n\nexport interface EventSelectProps {\n /** Current selected event ID */\n value: string\n /** Called when event is selected */\n onChange: (eventId: string) => void\n /** Placeholder text when no event selected. Defaults to a translated string. */\n placeholder?: string\n /** Additional CSS classes */\n className?: string\n /** Whether the select is disabled */\n disabled?: boolean\n /** Filter events by category */\n categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>\n /** Filter events by module */\n modules?: string[]\n /** Whether to exclude events marked as excludeFromTriggers (default: true) */\n excludeTriggerExcluded?: boolean\n /** Trigger size \u2014 defaults to `'default'` (DS row-height contract). */\n size?: 'sm' | 'default' | 'lg'\n}\n\n/**\n * EventSelect - A reusable select component for choosing declared events\n *\n * Fetches available events from the API and groups them by module.\n */\nexport function EventSelect({\n value,\n onChange,\n placeholder,\n className,\n disabled,\n categories,\n modules,\n excludeTriggerExcluded = true,\n size = 'default',\n}: EventSelectProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.eventSelect.placeholder', 'Select an event...')\n const loadingPlaceholder = t('ui.inputs.eventSelect.loading', 'Loading...')\n const emptyPlaceholder = t('ui.inputs.eventSelect.empty', 'No events available')\n\n // Fetch events from the API\n const { data: allEvents = [], isLoading } = useQuery({\n queryKey: ['declared-events', excludeTriggerExcluded],\n queryFn: async () => {\n const result = await apiCall<{ data: EventDefinition[]; total: number }>(\n `/api/events?excludeTriggerExcluded=${excludeTriggerExcluded}`\n )\n if (!result.ok) return []\n return result.result?.data || []\n },\n staleTime: 5 * 60 * 1000, // Cache for 5 minutes\n })\n\n // Filter events based on props\n const filteredEvents = useMemo(() => {\n let events = allEvents\n\n if (categories?.length) {\n events = events.filter(e => e.category && categories.includes(e.category))\n }\n if (modules?.length) {\n events = events.filter(e => e.module && modules.includes(e.module))\n }\n\n return events\n }, [allEvents, categories, modules])\n\n // Group events by module for better UX\n const eventsByModule = useMemo(() => {\n const grouped: Record<string, EventDefinition[]> = {}\n for (const event of filteredEvents) {\n const module = event.module || 'other'\n if (!grouped[module]) grouped[module] = []\n grouped[module].push(event)\n }\n // Sort modules alphabetically\n return Object.fromEntries(\n Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b))\n )\n }, [filteredEvents])\n\n // Format module name for display\n const formatModuleName = (module: string): string => {\n return module.charAt(0).toUpperCase() + module.slice(1).replace(/_/g, ' ')\n }\n\n const isEmpty = !isLoading && filteredEvents.length === 0\n\n return (\n <Select\n value={value || undefined}\n onValueChange={(next) => onChange(next ?? '')}\n disabled={disabled || isLoading}\n >\n <SelectTrigger size={size} className={className}>\n <SelectValue\n placeholder={isLoading ? loadingPlaceholder : isEmpty ? emptyPlaceholder : resolvedPlaceholder}\n />\n </SelectTrigger>\n <SelectContent>\n {Object.entries(eventsByModule).map(([module, moduleEvents]) => (\n <SelectGroup key={module}>\n <SelectLabel>{formatModuleName(module)}</SelectLabel>\n {moduleEvents.map(event => (\n <SelectItem key={event.id} value={event.id}>\n {event.label}\n </SelectItem>\n ))}\n </SelectGroup>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\n/**\n * Hook for getting available events\n */\nexport function useAvailableEvents(options?: {\n categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>\n modules?: string[]\n excludeTriggerExcluded?: boolean\n}) {\n const excludeTriggerExcluded = options?.excludeTriggerExcluded !== false\n\n const { data: allEvents = [], isLoading, error, refetch } = useQuery({\n queryKey: ['declared-events', excludeTriggerExcluded],\n queryFn: async () => {\n const result = await apiCall<{ data: EventDefinition[]; total: number }>(\n `/api/events?excludeTriggerExcluded=${excludeTriggerExcluded}`\n )\n if (!result.ok) return []\n return result.result?.data || []\n },\n staleTime: 5 * 60 * 1000,\n })\n\n const filteredEvents = useMemo(() => {\n let events = allEvents\n\n if (options?.categories?.length) {\n events = events.filter(e => e.category && options.categories!.includes(e.category))\n }\n if (options?.modules?.length) {\n events = events.filter(e => e.module && options.modules!.includes(e.module))\n }\n\n return events\n }, [allEvents, options])\n\n // Group by module\n const eventsByModule = useMemo(() => {\n const grouped: Record<string, EventDefinition[]> = {}\n for (const event of filteredEvents) {\n const module = event.module || 'other'\n if (!grouped[module]) grouped[module] = []\n grouped[module].push(event)\n }\n return Object.fromEntries(\n Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b))\n )\n }, [filteredEvents])\n\n return {\n events: filteredEvents,\n eventsByModule,\n isLoading,\n error,\n refetch,\n }\n}\n\nexport default EventSelect\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useMemo } from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { EventPayloadSchema } from '@open-mercato/shared/modules/events'\nimport { apiCall } from '../utils/apiCall'\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\n\n/**\n * Event definition returned by the API\n */\nexport interface EventDefinition {\n id: string\n label: string\n description?: string\n category?: 'crud' | 'lifecycle' | 'system' | 'custom'\n module?: string\n entity?: string\n excludeFromTriggers?: boolean\n payloadSchema?: EventPayloadSchema\n}\n\nexport interface EventSelectProps {\n /** Current selected event ID */\n value: string\n /** Called when event is selected */\n onChange: (eventId: string) => void\n /** Placeholder text when no event selected. Defaults to a translated string. */\n placeholder?: string\n /** Additional CSS classes */\n className?: string\n /** Whether the select is disabled */\n disabled?: boolean\n /** Filter events by category */\n categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>\n /** Filter events by module */\n modules?: string[]\n /** Whether to exclude events marked as excludeFromTriggers (default: true) */\n excludeTriggerExcluded?: boolean\n /** Trigger size \u2014 defaults to `'default'` (DS row-height contract). */\n size?: 'sm' | 'default' | 'lg'\n}\n\n/**\n * EventSelect - A reusable select component for choosing declared events\n *\n * Fetches available events from the API and groups them by module.\n */\nexport function EventSelect({\n value,\n onChange,\n placeholder,\n className,\n disabled,\n categories,\n modules,\n excludeTriggerExcluded = true,\n size = 'default',\n}: EventSelectProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.eventSelect.placeholder', 'Select an event...')\n const loadingPlaceholder = t('ui.inputs.eventSelect.loading', 'Loading...')\n const emptyPlaceholder = t('ui.inputs.eventSelect.empty', 'No events available')\n\n // Fetch events from the API\n const { data: allEvents = [], isLoading } = useQuery({\n queryKey: ['declared-events', excludeTriggerExcluded],\n queryFn: async () => {\n const result = await apiCall<{ data: EventDefinition[]; total: number }>(\n `/api/events?excludeTriggerExcluded=${excludeTriggerExcluded}`\n )\n if (!result.ok) return []\n return result.result?.data || []\n },\n staleTime: 5 * 60 * 1000, // Cache for 5 minutes\n })\n\n // Filter events based on props\n const filteredEvents = useMemo(() => {\n let events = allEvents\n\n if (categories?.length) {\n events = events.filter(e => e.category && categories.includes(e.category))\n }\n if (modules?.length) {\n events = events.filter(e => e.module && modules.includes(e.module))\n }\n\n return events\n }, [allEvents, categories, modules])\n\n // Group events by module for better UX\n const eventsByModule = useMemo(() => {\n const grouped: Record<string, EventDefinition[]> = {}\n for (const event of filteredEvents) {\n const module = event.module || 'other'\n if (!grouped[module]) grouped[module] = []\n grouped[module].push(event)\n }\n // Sort modules alphabetically\n return Object.fromEntries(\n Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b))\n )\n }, [filteredEvents])\n\n // Format module name for display\n const formatModuleName = (module: string): string => {\n return module.charAt(0).toUpperCase() + module.slice(1).replace(/_/g, ' ')\n }\n\n const isEmpty = !isLoading && filteredEvents.length === 0\n\n return (\n <Select\n value={value || undefined}\n onValueChange={(next) => onChange(next ?? '')}\n disabled={disabled || isLoading}\n >\n <SelectTrigger size={size} className={className}>\n <SelectValue\n placeholder={isLoading ? loadingPlaceholder : isEmpty ? emptyPlaceholder : resolvedPlaceholder}\n />\n </SelectTrigger>\n <SelectContent>\n {Object.entries(eventsByModule).map(([module, moduleEvents]) => (\n <SelectGroup key={module}>\n <SelectLabel>{formatModuleName(module)}</SelectLabel>\n {moduleEvents.map(event => (\n <SelectItem key={event.id} value={event.id}>\n {event.label}\n </SelectItem>\n ))}\n </SelectGroup>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\n/**\n * Hook for getting available events\n */\nexport function useAvailableEvents(options?: {\n categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>\n modules?: string[]\n excludeTriggerExcluded?: boolean\n}) {\n const excludeTriggerExcluded = options?.excludeTriggerExcluded !== false\n\n const { data: allEvents = [], isLoading, error, refetch } = useQuery({\n queryKey: ['declared-events', excludeTriggerExcluded],\n queryFn: async () => {\n const result = await apiCall<{ data: EventDefinition[]; total: number }>(\n `/api/events?excludeTriggerExcluded=${excludeTriggerExcluded}`\n )\n if (!result.ok) return []\n return result.result?.data || []\n },\n staleTime: 5 * 60 * 1000,\n })\n\n const filteredEvents = useMemo(() => {\n let events = allEvents\n\n if (options?.categories?.length) {\n events = events.filter(e => e.category && options.categories!.includes(e.category))\n }\n if (options?.modules?.length) {\n events = events.filter(e => e.module && options.modules!.includes(e.module))\n }\n\n return events\n }, [allEvents, options])\n\n // Group by module\n const eventsByModule = useMemo(() => {\n const grouped: Record<string, EventDefinition[]> = {}\n for (const event of filteredEvents) {\n const module = event.module || 'other'\n if (!grouped[module]) grouped[module] = []\n grouped[module].push(event)\n }\n return Object.fromEntries(\n Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b))\n )\n }, [filteredEvents])\n\n return {\n events: filteredEvents,\n eventsByModule,\n isLoading,\n error,\n refetch,\n }\n}\n\nexport default EventSelect\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -9,4 +9,5 @@ export * from "./TimeInput.js";
9
9
  export * from "./TimePicker.js";
10
10
  export * from "./DateTimePicker.js";
11
11
  export * from "./DatePicker.js";
12
+ export * from "./DurationInput.js";
12
13
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/inputs/index.ts"],
4
- "sourcesContent": ["export * from './PhoneNumberField'\nexport * from './TagsInput'\nexport * from './LookupSelect'\nexport * from './ComboboxInput'\nexport * from './SwitchableMarkdownInput'\nexport * from './EventSelect'\nexport * from './EventPatternInput'\nexport * from './TimeInput'\nexport * from './TimePicker'\nexport * from './DateTimePicker'\nexport * from './DatePicker'\n"],
5
- "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;",
4
+ "sourcesContent": ["export * from './PhoneNumberField'\nexport * from './TagsInput'\nexport * from './LookupSelect'\nexport * from './ComboboxInput'\nexport * from './SwitchableMarkdownInput'\nexport * from './EventSelect'\nexport * from './EventPatternInput'\nexport * from './TimeInput'\nexport * from './TimePicker'\nexport * from './DateTimePicker'\nexport * from './DatePicker'\nexport * from './DurationInput'\n"],
5
+ "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ const badges = /* @__PURE__ */ new Map();
4
+ const listeners = /* @__PURE__ */ new Set();
5
+ function emit() {
6
+ for (const listener of listeners) listener();
7
+ }
8
+ function setNavBadge(href, badge) {
9
+ if (!href) return;
10
+ if (!badge || !Number.isFinite(badge.count) || badge.count <= 0) {
11
+ if (badges.delete(href)) emit();
12
+ return;
13
+ }
14
+ const current = badges.get(href);
15
+ if (current && current.count === badge.count && current.tone === badge.tone && current.label === badge.label) return;
16
+ badges.set(href, badge);
17
+ emit();
18
+ }
19
+ function clearNavBadge(href) {
20
+ setNavBadge(href, null);
21
+ }
22
+ function subscribe(listener) {
23
+ listeners.add(listener);
24
+ return () => {
25
+ listeners.delete(listener);
26
+ };
27
+ }
28
+ function useNavBadge(href) {
29
+ const getSnapshot = React.useCallback(() => badges.get(href) ?? null, [href]);
30
+ const getServerSnapshot = React.useCallback(() => null, []);
31
+ return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
32
+ }
33
+ export {
34
+ clearNavBadge,
35
+ setNavBadge,
36
+ useNavBadge
37
+ };
38
+ //# sourceMappingURL=navBadges.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/nav/navBadges.ts"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\n\n/**\n * Live counts rendered on sidebar nav items (\"3 waiting for you\").\n *\n * Why this is a client-side store rather than a field on the chrome payload:\n * `/api/auth/admin/nav` is cached for 30 minutes behind a module-surface\n * fingerprint, so a per-user count served through it would be both stale and\n * cache-poisoning. Core owns the capability and the rendering; whichever module\n * owns the number mounts a headless widget into `shell:sidebar:badges` and\n * publishes here.\n *\n * Counts are advisory: a nav item renders exactly what it is given, and an\n * absent or zero count renders nothing at all \u2014 never a \"0\" chip.\n */\nexport type NavBadgeTone = 'default' | 'attention'\n\nexport type NavBadge = {\n count: number\n tone?: NavBadgeTone\n /** Accessible label; falls back to the raw count when omitted. */\n label?: string\n}\n\nconst badges = new Map<string, NavBadge>()\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\nexport function setNavBadge(href: string, badge: NavBadge | null): void {\n if (!href) return\n if (!badge || !Number.isFinite(badge.count) || badge.count <= 0) {\n if (badges.delete(href)) emit()\n return\n }\n const current = badges.get(href)\n if (current && current.count === badge.count && current.tone === badge.tone && current.label === badge.label) return\n badges.set(href, badge)\n emit()\n}\n\nexport function clearNavBadge(href: string): void {\n setNavBadge(href, null)\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\nexport function useNavBadge(href: string): NavBadge | null {\n const getSnapshot = React.useCallback(() => badges.get(href) ?? null, [href])\n const getServerSnapshot = React.useCallback(() => null, [])\n return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AAwBvB,MAAM,SAAS,oBAAI,IAAsB;AACzC,MAAM,YAAY,oBAAI,IAAgB;AAEtC,SAAS,OAAa;AACpB,aAAW,YAAY,UAAW,UAAS;AAC7C;AAEO,SAAS,YAAY,MAAc,OAA8B;AACtE,MAAI,CAAC,KAAM;AACX,MAAI,CAAC,SAAS,CAAC,OAAO,SAAS,MAAM,KAAK,KAAK,MAAM,SAAS,GAAG;AAC/D,QAAI,OAAO,OAAO,IAAI,EAAG,MAAK;AAC9B;AAAA,EACF;AACA,QAAM,UAAU,OAAO,IAAI,IAAI;AAC/B,MAAI,WAAW,QAAQ,UAAU,MAAM,SAAS,QAAQ,SAAS,MAAM,QAAQ,QAAQ,UAAU,MAAM,MAAO;AAC9G,SAAO,IAAI,MAAM,KAAK;AACtB,OAAK;AACP;AAEO,SAAS,cAAc,MAAoB;AAChD,cAAY,MAAM,IAAI;AACxB;AAEA,SAAS,UAAU,UAAkC;AACnD,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAEO,SAAS,YAAY,MAA+B;AACzD,QAAM,cAAc,MAAM,YAAY,MAAM,OAAO,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC;AAC5E,QAAM,oBAAoB,MAAM,YAAY,MAAM,MAAM,CAAC,CAAC;AAC1D,SAAO,MAAM,qBAAqB,WAAW,aAAa,iBAAiB;AAC7E;",
6
+ "names": []
7
+ }
@@ -1,53 +1,61 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
3
4
  import { Check, X } from "lucide-react";
4
5
  import { IconButton } from "../../primitives/icon-button.js";
5
6
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
7
  function NewViewForm({ name, onNameChange, onSubmit, onCancel, saving }) {
7
8
  const t = useT();
8
9
  const trimmed = name.trim();
9
- return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
10
- /* @__PURE__ */ jsx(
11
- "input",
12
- {
13
- value: name,
14
- onChange: (e) => onNameChange(e.target.value),
15
- placeholder: t("ui.perspectives.form.namePlaceholder", "View name..."),
16
- autoFocus: true,
17
- className: "w-full h-9 rounded border border-primary pl-2 pr-16 text-sm",
18
- onKeyDown: (e) => {
19
- if (e.key === "Enter" && trimmed) onSubmit();
20
- if (e.key === "Escape") onCancel();
21
- }
22
- }
23
- ),
24
- /* @__PURE__ */ jsxs("div", { className: "absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5", children: [
10
+ const hintId = React.useId();
11
+ const nameRequiredHint = t("ui.perspectives.form.nameRequired", "Enter a name to create the view");
12
+ return /* @__PURE__ */ jsxs("div", { children: [
13
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
25
14
  /* @__PURE__ */ jsx(
26
- IconButton,
15
+ "input",
27
16
  {
28
- type: "button",
29
- variant: "ghost",
30
- size: "xs",
31
- onClick: () => {
32
- if (trimmed) onSubmit();
33
- },
34
- disabled: !trimmed || saving,
35
- "aria-label": t("ui.perspectives.form.confirmCreate", "Create view"),
36
- children: /* @__PURE__ */ jsx(Check, { className: `size-4 ${trimmed ? "text-brand-violet" : "text-muted-foreground opacity-50"}` })
17
+ value: name,
18
+ onChange: (e) => onNameChange(e.target.value),
19
+ placeholder: t("ui.perspectives.form.namePlaceholder", "View name..."),
20
+ autoFocus: true,
21
+ "aria-describedby": trimmed ? void 0 : hintId,
22
+ className: "w-full h-9 rounded border border-primary pl-2 pr-16 text-sm",
23
+ onKeyDown: (e) => {
24
+ if (e.key === "Enter" && trimmed) onSubmit();
25
+ if (e.key === "Escape") onCancel();
26
+ }
37
27
  }
38
28
  ),
39
- /* @__PURE__ */ jsx(
40
- IconButton,
41
- {
42
- type: "button",
43
- variant: "ghost",
44
- size: "xs",
45
- onClick: onCancel,
46
- "aria-label": t("ui.perspectives.form.cancelCreate", "Cancel"),
47
- children: /* @__PURE__ */ jsx(X, { className: "size-4 text-muted-foreground hover:text-destructive" })
48
- }
49
- )
50
- ] })
29
+ /* @__PURE__ */ jsxs("div", { className: "absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5", children: [
30
+ /* @__PURE__ */ jsx(
31
+ IconButton,
32
+ {
33
+ type: "button",
34
+ variant: "ghost",
35
+ size: "xs",
36
+ onClick: () => {
37
+ if (trimmed) onSubmit();
38
+ },
39
+ disabled: !trimmed || saving,
40
+ title: trimmed ? void 0 : nameRequiredHint,
41
+ "aria-label": t("ui.perspectives.form.confirmCreate", "Create view"),
42
+ children: /* @__PURE__ */ jsx(Check, { className: `size-4 ${trimmed ? "text-brand-violet" : "text-muted-foreground opacity-50"}` })
43
+ }
44
+ ),
45
+ /* @__PURE__ */ jsx(
46
+ IconButton,
47
+ {
48
+ type: "button",
49
+ variant: "ghost",
50
+ size: "xs",
51
+ onClick: onCancel,
52
+ "aria-label": t("ui.perspectives.form.cancelCreate", "Cancel"),
53
+ children: /* @__PURE__ */ jsx(X, { className: "size-4 text-muted-foreground hover:text-destructive" })
54
+ }
55
+ )
56
+ ] })
57
+ ] }),
58
+ /* @__PURE__ */ jsx("p", { id: hintId, className: `mt-1 text-xs text-muted-foreground ${trimmed ? "invisible" : ""}`, children: nameRequiredHint })
51
59
  ] });
52
60
  }
53
61
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/views/NewViewForm.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Check, X } from 'lucide-react'\nimport { IconButton } from '../../primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype Props = {\n name: string\n onNameChange: (name: string) => void\n onSubmit: () => void\n onCancel: () => void\n saving: boolean\n}\n\nexport function NewViewForm({ name, onNameChange, onSubmit, onCancel, saving }: Props) {\n const t = useT()\n const trimmed = name.trim()\n return (\n <div className=\"relative\">\n <input\n value={name}\n onChange={(e) => onNameChange(e.target.value)}\n placeholder={t('ui.perspectives.form.namePlaceholder', 'View name...')}\n autoFocus\n className=\"w-full h-9 rounded border border-primary pl-2 pr-16 text-sm\"\n onKeyDown={(e) => {\n if (e.key === 'Enter' && trimmed) onSubmit()\n if (e.key === 'Escape') onCancel()\n }}\n />\n <div className=\"absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5\">\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n onClick={() => { if (trimmed) onSubmit() }}\n disabled={!trimmed || saving}\n aria-label={t('ui.perspectives.form.confirmCreate', 'Create view')}\n >\n <Check className={`size-4 ${trimmed ? 'text-brand-violet' : 'text-muted-foreground opacity-50'}`} />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n onClick={onCancel}\n aria-label={t('ui.perspectives.form.cancelCreate', 'Cancel')}\n >\n <X className=\"size-4 text-muted-foreground hover:text-destructive\" />\n </IconButton>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": ";AAmBM,cAWA,YAXA;AAjBN,SAAS,OAAO,SAAS;AACzB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AAUd,SAAS,YAAY,EAAE,MAAM,cAAc,UAAU,UAAU,OAAO,GAAU;AACrF,QAAM,IAAI,KAAK;AACf,QAAM,UAAU,KAAK,KAAK;AAC1B,SACE,qBAAC,SAAI,WAAU,YACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,KAAK;AAAA,QAC5C,aAAa,EAAE,wCAAwC,cAAc;AAAA,QACrE,WAAS;AAAA,QACT,WAAU;AAAA,QACV,WAAW,CAAC,MAAM;AAChB,cAAI,EAAE,QAAQ,WAAW,QAAS,UAAS;AAC3C,cAAI,EAAE,QAAQ,SAAU,UAAS;AAAA,QACnC;AAAA;AAAA,IACF;AAAA,IACA,qBAAC,SAAI,WAAU,uEACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS,MAAM;AAAE,gBAAI,QAAS,UAAS;AAAA,UAAE;AAAA,UACzC,UAAU,CAAC,WAAW;AAAA,UACtB,cAAY,EAAE,sCAAsC,aAAa;AAAA,UAEjE,8BAAC,SAAM,WAAW,UAAU,UAAU,sBAAsB,kCAAkC,IAAI;AAAA;AAAA,MACpG;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS;AAAA,UACT,cAAY,EAAE,qCAAqC,QAAQ;AAAA,UAE3D,8BAAC,KAAE,WAAU,uDAAsD;AAAA;AAAA,MACrE;AAAA,OACF;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Check, X } from 'lucide-react'\nimport { IconButton } from '../../primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype Props = {\n name: string\n onNameChange: (name: string) => void\n onSubmit: () => void\n onCancel: () => void\n saving: boolean\n}\n\nexport function NewViewForm({ name, onNameChange, onSubmit, onCancel, saving }: Props) {\n const t = useT()\n const trimmed = name.trim()\n const hintId = React.useId()\n // A greyed-out check that does nothing when clicked is the dead end reported\n // in #5113 \u2014 always render the reason the control is inert.\n const nameRequiredHint = t('ui.perspectives.form.nameRequired', 'Enter a name to create the view')\n return (\n <div>\n {/* The overlay buttons are centred against this wrapper, so it must contain\n nothing but the input \u2014 a sibling hint here would drag them off-centre\n and make them jump on the keystroke that hides it. */}\n <div className=\"relative\">\n <input\n value={name}\n onChange={(e) => onNameChange(e.target.value)}\n placeholder={t('ui.perspectives.form.namePlaceholder', 'View name...')}\n autoFocus\n aria-describedby={trimmed ? undefined : hintId}\n className=\"w-full h-9 rounded border border-primary pl-2 pr-16 text-sm\"\n onKeyDown={(e) => {\n if (e.key === 'Enter' && trimmed) onSubmit()\n if (e.key === 'Escape') onCancel()\n }}\n />\n <div className=\"absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5\">\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n onClick={() => { if (trimmed) onSubmit() }}\n disabled={!trimmed || saving}\n title={trimmed ? undefined : nameRequiredHint}\n aria-label={t('ui.perspectives.form.confirmCreate', 'Create view')}\n >\n <Check className={`size-4 ${trimmed ? 'text-brand-violet' : 'text-muted-foreground opacity-50'}`} />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n onClick={onCancel}\n aria-label={t('ui.perspectives.form.cancelCreate', 'Cancel')}\n >\n <X className=\"size-4 text-muted-foreground hover:text-destructive\" />\n </IconButton>\n </div>\n </div>\n {/* Kept mounted and merely hidden so the first keystroke does not reflow the\n sidebar, and so the aria-describedby target never disappears mid-edit. */}\n <p id={hintId} className={`mt-1 text-xs text-muted-foreground ${trimmed ? 'invisible' : ''}`}>{nameRequiredHint}</p>\n </div>\n )\n}\n"],
5
+ "mappings": ";AA2BQ,cAYA,YAZA;AA1BR,YAAY,WAAW;AACvB,SAAS,OAAO,SAAS;AACzB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AAUd,SAAS,YAAY,EAAE,MAAM,cAAc,UAAU,UAAU,OAAO,GAAU;AACrF,QAAM,IAAI,KAAK;AACf,QAAM,UAAU,KAAK,KAAK;AAC1B,QAAM,SAAS,MAAM,MAAM;AAG3B,QAAM,mBAAmB,EAAE,qCAAqC,iCAAiC;AACjG,SACE,qBAAC,SAIC;AAAA,yBAAC,SAAI,WAAU,YACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,KAAK;AAAA,UAC5C,aAAa,EAAE,wCAAwC,cAAc;AAAA,UACrE,WAAS;AAAA,UACT,oBAAkB,UAAU,SAAY;AAAA,UACxC,WAAU;AAAA,UACV,WAAW,CAAC,MAAM;AAChB,gBAAI,EAAE,QAAQ,WAAW,QAAS,UAAS;AAC3C,gBAAI,EAAE,QAAQ,SAAU,UAAS;AAAA,UACnC;AAAA;AAAA,MACF;AAAA,MACA,qBAAC,SAAI,WAAU,uEACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,SAAS,MAAM;AAAE,kBAAI,QAAS,UAAS;AAAA,YAAE;AAAA,YACzC,UAAU,CAAC,WAAW;AAAA,YACtB,OAAO,UAAU,SAAY;AAAA,YAC7B,cAAY,EAAE,sCAAsC,aAAa;AAAA,YAEjE,8BAAC,SAAM,WAAW,UAAU,UAAU,sBAAsB,kCAAkC,IAAI;AAAA;AAAA,QACpG;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,SAAS;AAAA,YACT,cAAY,EAAE,qCAAqC,QAAQ;AAAA,YAE3D,8BAAC,KAAE,WAAU,uDAAsD;AAAA;AAAA,QACrE;AAAA,SACF;AAAA,OACF;AAAA,IAGA,oBAAC,OAAE,IAAI,QAAQ,WAAW,sCAAsC,UAAU,cAAc,EAAE,IAAK,4BAAiB;AAAA,KAClH;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -9,8 +9,8 @@ const SegmentedControlContext = React.createContext({
9
9
  disabled: false
10
10
  });
11
11
  const trackVariants = cva(
12
- // Pill-shaped track with subtle inner padding so selected items render
13
- // a smaller inner pill (the iOS slide-thumb effect). Track is muted —
12
+ // Rounded-rectangle track (DS Open Mercato node 2604:114) with subtle inner
13
+ // padding so the selected item renders as a smaller inner card. Track is muted —
14
14
  // selected item raises with bg-background + shadow-sm. We use full
15
15
  // `bg-muted` (not /40) so the contrast between track and a selected
16
16
  // bg-background item stays visible in the light theme; in dark mode
@@ -21,7 +21,7 @@ const trackVariants = cva(
21
21
  // sm → track h-7 (28px) − 2px border − 2px padding (p-px ×2) = 24px → matches item h-6
22
22
  // Using `p-0.5` (2px each side = 4px total) instead breaks both sizes by
23
23
  // 2px so the selected pill clips top and bottom against the track border.
24
- "inline-flex w-fit gap-0 rounded-full border border-input bg-muted p-px transition-colors",
24
+ "inline-flex w-fit gap-0 rounded-lg border border-input bg-muted p-px transition-colors",
25
25
  {
26
26
  variants: {
27
27
  size: {
@@ -46,7 +46,7 @@ const itemVariants = cva(
46
46
  // shadow, important when the surrounding theme is high-key and the
47
47
  // shadow alone is hard to read). Unselected text is muted; hover
48
48
  // only nudges color (no bg change — keeps the track flat).
49
- "inline-flex items-center justify-center rounded-full font-medium transition-all outline-none focus-visible:shadow-focus disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-background data-[state=checked]:text-foreground data-[state=checked]:font-semibold data-[state=checked]:shadow-sm data-[state=unchecked]:bg-transparent data-[state=unchecked]:text-muted-foreground data-[state=unchecked]:hover:text-foreground",
49
+ "inline-flex items-center justify-center rounded-md font-medium transition-all outline-none focus-visible:shadow-focus disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-background data-[state=checked]:text-foreground data-[state=checked]:font-semibold data-[state=checked]:shadow-sm data-[state=unchecked]:bg-transparent data-[state=unchecked]:text-muted-foreground data-[state=unchecked]:hover:text-foreground",
50
50
  {
51
51
  variants: {
52
52
  size: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/segmented-control.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\n/**\n * iOS-style segmented control per Figma `Switch / Chart / Cryptocurrency`\n * (component set id `199963:1442` in DS Open Mercato). Renders a single\n * track with N items where exactly one is selected at a time. Selecting\n * a new item fires `onValueChange`.\n *\n * Use for **mutually-exclusive view state** \u2014 list page filters like\n * \"All / Active / Archived\", chart period selectors, layout toggles\n * (List / Grid). For *related actions* (each does something different),\n * reach for `ButtonGroup` instead.\n *\n * Built on Radix `RadioGroup` so we inherit the radio-group ARIA contract\n * (`role=\"radiogroup\"`, `role=\"radio\"` on items, arrow-key navigation,\n * roving tabindex) for free. No new dependency \u2014 Radix RadioGroup is\n * already installed via the `Radio` primitive.\n *\n * ```tsx\n * const [view, setView] = React.useState('all')\n * <SegmentedControl value={view} onValueChange={setView} aria-label=\"View filter\">\n * <SegmentedControlItem value=\"all\">All</SegmentedControlItem>\n * <SegmentedControlItem value=\"active\">Active</SegmentedControlItem>\n * <SegmentedControlItem value=\"archived\">Archived</SegmentedControlItem>\n * </SegmentedControl>\n * ```\n *\n * Sizes:\n * - `default` (h-8 / 32px) \u2014 standard toolbar density.\n * - `sm` (h-7 / 28px) \u2014 tighter; pair with `text-xs`.\n */\n\ntype SegmentedControlContextValue = {\n size: 'sm' | 'default'\n disabled?: boolean\n}\n\nconst SegmentedControlContext = React.createContext<SegmentedControlContextValue>({\n size: 'default',\n disabled: false,\n})\n\nconst trackVariants = cva(\n // Pill-shaped track with subtle inner padding so selected items render\n // a smaller inner pill (the iOS slide-thumb effect). Track is muted \u2014\n // selected item raises with bg-background + shadow-sm. We use full\n // `bg-muted` (not /40) so the contrast between track and a selected\n // bg-background item stays visible in the light theme; in dark mode\n // the token already darkens further so the contrast holds.\n //\n // Height math (box-border on every element):\n // default \u2192 track h-8 (32px) \u2212 2px border \u2212 2px padding (p-px \u00D72) = 28px \u2192 matches item h-7\n // sm \u2192 track h-7 (28px) \u2212 2px border \u2212 2px padding (p-px \u00D72) = 24px \u2192 matches item h-6\n // Using `p-0.5` (2px each side = 4px total) instead breaks both sizes by\n // 2px so the selected pill clips top and bottom against the track border.\n 'inline-flex w-fit gap-0 rounded-full border border-input bg-muted p-px transition-colors',\n {\n variants: {\n size: {\n sm: 'h-7',\n default: 'h-8',\n },\n disabled: {\n true: 'cursor-not-allowed opacity-60',\n false: '',\n },\n },\n defaultVariants: {\n size: 'default',\n disabled: false,\n },\n },\n)\n\nconst itemVariants = cva(\n // Items are pills that fill the track minus its 2px inner padding.\n // Selected state lifts via bg-background + shadow-sm + font-semibold\n // (the font-weight bump gives a secondary visual cue beyond color +\n // shadow, important when the surrounding theme is high-key and the\n // shadow alone is hard to read). Unselected text is muted; hover\n // only nudges color (no bg change \u2014 keeps the track flat).\n 'inline-flex items-center justify-center rounded-full font-medium ' +\n 'transition-all outline-none focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-50 ' +\n 'data-[state=checked]:bg-background data-[state=checked]:text-foreground data-[state=checked]:font-semibold data-[state=checked]:shadow-sm ' +\n 'data-[state=unchecked]:bg-transparent data-[state=unchecked]:text-muted-foreground data-[state=unchecked]:hover:text-foreground',\n {\n variants: {\n size: {\n sm: 'h-6 px-2.5 text-xs',\n default: 'h-7 px-3 text-sm',\n },\n },\n defaultVariants: {\n size: 'default',\n },\n },\n)\n\nexport type SegmentedControlProps = Omit<\n React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,\n 'orientation'\n> &\n VariantProps<typeof trackVariants> & {\n /** Optional screen-reader label for the radio group. */\n 'aria-label'?: string\n }\n\nexport const SegmentedControl = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n SegmentedControlProps\n>(({ className, size, disabled, children, ...props }, ref) => {\n const ctx = React.useMemo<SegmentedControlContextValue>(\n () => ({ size: size ?? 'default', disabled: disabled ?? false }),\n [size, disabled],\n )\n return (\n <SegmentedControlContext.Provider value={ctx}>\n <RadioGroupPrimitive.Root\n ref={ref}\n orientation=\"horizontal\"\n disabled={disabled ?? undefined}\n data-slot=\"segmented-control\"\n className={cn(trackVariants({ size, disabled }), className)}\n {...props}\n >\n {children}\n </RadioGroupPrimitive.Root>\n </SegmentedControlContext.Provider>\n )\n})\nSegmentedControl.displayName = 'SegmentedControl'\n\nexport type SegmentedControlItemProps = React.ComponentPropsWithoutRef<\n typeof RadioGroupPrimitive.Item\n>\n\nexport const SegmentedControlItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n SegmentedControlItemProps\n>(({ className, children, ...props }, ref) => {\n const { size } = React.useContext(SegmentedControlContext)\n return (\n <RadioGroupPrimitive.Item\n ref={ref}\n data-slot=\"segmented-control-item\"\n className={cn(itemVariants({ size }), className)}\n {...props}\n >\n {children}\n </RadioGroupPrimitive.Item>\n )\n})\nSegmentedControlItem.displayName = 'SegmentedControlItem'\n\nexport { trackVariants as segmentedControlTrackVariants, itemVariants as segmentedControlItemVariants }\n"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\n/**\n * Segmented control per Figma DS Open Mercato (node `2604:114`): a muted\n * rounded-rectangle track with a raised white card for the selected segment.\n * Renders a single track with N items where exactly one is selected at a time. Selecting\n * a new item fires `onValueChange`.\n *\n * Use for **mutually-exclusive view state** \u2014 list page filters like\n * \"All / Active / Archived\", chart period selectors, layout toggles\n * (List / Grid). For *related actions* (each does something different),\n * reach for `ButtonGroup` instead.\n *\n * Built on Radix `RadioGroup` so we inherit the radio-group ARIA contract\n * (`role=\"radiogroup\"`, `role=\"radio\"` on items, arrow-key navigation,\n * roving tabindex) for free. No new dependency \u2014 Radix RadioGroup is\n * already installed via the `Radio` primitive.\n *\n * ```tsx\n * const [view, setView] = React.useState('all')\n * <SegmentedControl value={view} onValueChange={setView} aria-label=\"View filter\">\n * <SegmentedControlItem value=\"all\">All</SegmentedControlItem>\n * <SegmentedControlItem value=\"active\">Active</SegmentedControlItem>\n * <SegmentedControlItem value=\"archived\">Archived</SegmentedControlItem>\n * </SegmentedControl>\n * ```\n *\n * Sizes:\n * - `default` (h-8 / 32px) \u2014 standard toolbar density.\n * - `sm` (h-7 / 28px) \u2014 tighter; pair with `text-xs`.\n */\n\ntype SegmentedControlContextValue = {\n size: 'sm' | 'default'\n disabled?: boolean\n}\n\nconst SegmentedControlContext = React.createContext<SegmentedControlContextValue>({\n size: 'default',\n disabled: false,\n})\n\nconst trackVariants = cva(\n // Rounded-rectangle track (DS Open Mercato node 2604:114) with subtle inner\n // padding so the selected item renders as a smaller inner card. Track is muted \u2014\n // selected item raises with bg-background + shadow-sm. We use full\n // `bg-muted` (not /40) so the contrast between track and a selected\n // bg-background item stays visible in the light theme; in dark mode\n // the token already darkens further so the contrast holds.\n //\n // Height math (box-border on every element):\n // default \u2192 track h-8 (32px) \u2212 2px border \u2212 2px padding (p-px \u00D72) = 28px \u2192 matches item h-7\n // sm \u2192 track h-7 (28px) \u2212 2px border \u2212 2px padding (p-px \u00D72) = 24px \u2192 matches item h-6\n // Using `p-0.5` (2px each side = 4px total) instead breaks both sizes by\n // 2px so the selected pill clips top and bottom against the track border.\n 'inline-flex w-fit gap-0 rounded-lg border border-input bg-muted p-px transition-colors',\n {\n variants: {\n size: {\n sm: 'h-7',\n default: 'h-8',\n },\n disabled: {\n true: 'cursor-not-allowed opacity-60',\n false: '',\n },\n },\n defaultVariants: {\n size: 'default',\n disabled: false,\n },\n },\n)\n\nconst itemVariants = cva(\n // Items are pills that fill the track minus its 2px inner padding.\n // Selected state lifts via bg-background + shadow-sm + font-semibold\n // (the font-weight bump gives a secondary visual cue beyond color +\n // shadow, important when the surrounding theme is high-key and the\n // shadow alone is hard to read). Unselected text is muted; hover\n // only nudges color (no bg change \u2014 keeps the track flat).\n 'inline-flex items-center justify-center rounded-md font-medium ' +\n 'transition-all outline-none focus-visible:shadow-focus ' +\n 'disabled:cursor-not-allowed disabled:opacity-50 ' +\n 'data-[state=checked]:bg-background data-[state=checked]:text-foreground data-[state=checked]:font-semibold data-[state=checked]:shadow-sm ' +\n 'data-[state=unchecked]:bg-transparent data-[state=unchecked]:text-muted-foreground data-[state=unchecked]:hover:text-foreground',\n {\n variants: {\n size: {\n sm: 'h-6 px-2.5 text-xs',\n default: 'h-7 px-3 text-sm',\n },\n },\n defaultVariants: {\n size: 'default',\n },\n },\n)\n\nexport type SegmentedControlProps = Omit<\n React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,\n 'orientation'\n> &\n VariantProps<typeof trackVariants> & {\n /** Optional screen-reader label for the radio group. */\n 'aria-label'?: string\n }\n\nexport const SegmentedControl = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n SegmentedControlProps\n>(({ className, size, disabled, children, ...props }, ref) => {\n const ctx = React.useMemo<SegmentedControlContextValue>(\n () => ({ size: size ?? 'default', disabled: disabled ?? false }),\n [size, disabled],\n )\n return (\n <SegmentedControlContext.Provider value={ctx}>\n <RadioGroupPrimitive.Root\n ref={ref}\n orientation=\"horizontal\"\n disabled={disabled ?? undefined}\n data-slot=\"segmented-control\"\n className={cn(trackVariants({ size, disabled }), className)}\n {...props}\n >\n {children}\n </RadioGroupPrimitive.Root>\n </SegmentedControlContext.Provider>\n )\n})\nSegmentedControl.displayName = 'SegmentedControl'\n\nexport type SegmentedControlItemProps = React.ComponentPropsWithoutRef<\n typeof RadioGroupPrimitive.Item\n>\n\nexport const SegmentedControlItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n SegmentedControlItemProps\n>(({ className, children, ...props }, ref) => {\n const { size } = React.useContext(SegmentedControlContext)\n return (\n <RadioGroupPrimitive.Item\n ref={ref}\n data-slot=\"segmented-control-item\"\n className={cn(itemVariants({ size }), className)}\n {...props}\n >\n {children}\n </RadioGroupPrimitive.Item>\n )\n})\nSegmentedControlItem.displayName = 'SegmentedControlItem'\n\nexport { trackVariants as segmentedControlTrackVariants, itemVariants as segmentedControlItemVariants }\n"],
5
5
  "mappings": ";AA4HM;AA1HN,YAAY,WAAW;AACvB,YAAY,yBAAyB;AACrC,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAqCnB,MAAM,0BAA0B,MAAM,cAA4C;AAAA,EAChF,MAAM;AAAA,EACN,UAAU;AACZ,CAAC;AAED,MAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAapB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,SAAS;AAAA,MACX;AAAA,MACA,UAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,MACN,UAAU;AAAA,IACZ;AAAA,EACF;AACF;AAEA,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOnB;AAAA,EAKA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,SAAS;AAAA,MACX;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAWO,MAAM,mBAAmB,MAAM,WAGpC,CAAC,EAAE,WAAW,MAAM,UAAU,UAAU,GAAG,MAAM,GAAG,QAAQ;AAC5D,QAAM,MAAM,MAAM;AAAA,IAChB,OAAO,EAAE,MAAM,QAAQ,WAAW,UAAU,YAAY,MAAM;AAAA,IAC9D,CAAC,MAAM,QAAQ;AAAA,EACjB;AACA,SACE,oBAAC,wBAAwB,UAAxB,EAAiC,OAAO,KACvC;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC;AAAA,MACA,aAAY;AAAA,MACZ,UAAU,YAAY;AAAA,MACtB,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,EAAE,MAAM,SAAS,CAAC,GAAG,SAAS;AAAA,MACzD,GAAG;AAAA,MAEH;AAAA;AAAA,EACH,GACF;AAEJ,CAAC;AACD,iBAAiB,cAAc;AAMxB,MAAM,uBAAuB,MAAM,WAGxC,CAAC,EAAE,WAAW,UAAU,GAAG,MAAM,GAAG,QAAQ;AAC5C,QAAM,EAAE,KAAK,IAAI,MAAM,WAAW,uBAAuB;AACzD,SACE;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC;AAAA,MACA,aAAU;AAAA,MACV,WAAW,GAAG,aAAa,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MAC9C,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ,CAAC;AACD,qBAAqB,cAAc;",
6
6
  "names": []
7
7
  }