@open-mercato/ui 0.4.2-canary-c02407ff85

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 (319) hide show
  1. package/build.mjs +62 -0
  2. package/dist/backend/AppShell.js +902 -0
  3. package/dist/backend/AppShell.js.map +7 -0
  4. package/dist/backend/ConfirmDialog.js +17 -0
  5. package/dist/backend/ConfirmDialog.js.map +7 -0
  6. package/dist/backend/ContextHelp.js +31 -0
  7. package/dist/backend/ContextHelp.js.map +7 -0
  8. package/dist/backend/CrudForm.js +2028 -0
  9. package/dist/backend/CrudForm.js.map +7 -0
  10. package/dist/backend/DataTable.js +1363 -0
  11. package/dist/backend/DataTable.js.map +7 -0
  12. package/dist/backend/EmptyState.js +52 -0
  13. package/dist/backend/EmptyState.js.map +7 -0
  14. package/dist/backend/FilterBar.js +140 -0
  15. package/dist/backend/FilterBar.js.map +7 -0
  16. package/dist/backend/FilterOverlay.js +279 -0
  17. package/dist/backend/FilterOverlay.js.map +7 -0
  18. package/dist/backend/FlashMessages.js +66 -0
  19. package/dist/backend/FlashMessages.js.map +7 -0
  20. package/dist/backend/JsonBuilder.js +322 -0
  21. package/dist/backend/JsonBuilder.js.map +7 -0
  22. package/dist/backend/JsonDisplay.js +203 -0
  23. package/dist/backend/JsonDisplay.js.map +7 -0
  24. package/dist/backend/Page.js +27 -0
  25. package/dist/backend/Page.js.map +7 -0
  26. package/dist/backend/PerspectiveSidebar.js +282 -0
  27. package/dist/backend/PerspectiveSidebar.js.map +7 -0
  28. package/dist/backend/RowActions.js +148 -0
  29. package/dist/backend/RowActions.js.map +7 -0
  30. package/dist/backend/TruncatedCell.js +92 -0
  31. package/dist/backend/TruncatedCell.js.map +7 -0
  32. package/dist/backend/UserMenu.js +107 -0
  33. package/dist/backend/UserMenu.js.map +7 -0
  34. package/dist/backend/ValueIcons.js +34 -0
  35. package/dist/backend/ValueIcons.js.map +7 -0
  36. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
  37. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
  38. package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
  39. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
  40. package/dist/backend/dashboard/DashboardScreen.js +578 -0
  41. package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
  42. package/dist/backend/dashboard/index.js +5 -0
  43. package/dist/backend/dashboard/index.js.map +7 -0
  44. package/dist/backend/dashboard/widgetRegistry.js +55 -0
  45. package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
  46. package/dist/backend/detail/ActivitiesSection.js +962 -0
  47. package/dist/backend/detail/ActivitiesSection.js.map +7 -0
  48. package/dist/backend/detail/AddressEditor.js +413 -0
  49. package/dist/backend/detail/AddressEditor.js.map +7 -0
  50. package/dist/backend/detail/AddressTiles.js +437 -0
  51. package/dist/backend/detail/AddressTiles.js.map +7 -0
  52. package/dist/backend/detail/AddressesSection.js +264 -0
  53. package/dist/backend/detail/AddressesSection.js.map +7 -0
  54. package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
  55. package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
  56. package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
  57. package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
  58. package/dist/backend/detail/AttachmentsSection.js +367 -0
  59. package/dist/backend/detail/AttachmentsSection.js.map +7 -0
  60. package/dist/backend/detail/CustomDataSection.js +433 -0
  61. package/dist/backend/detail/CustomDataSection.js.map +7 -0
  62. package/dist/backend/detail/DetailFieldsSection.js +75 -0
  63. package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
  64. package/dist/backend/detail/ErrorMessage.js +28 -0
  65. package/dist/backend/detail/ErrorMessage.js.map +7 -0
  66. package/dist/backend/detail/InlineEditors.js +681 -0
  67. package/dist/backend/detail/InlineEditors.js.map +7 -0
  68. package/dist/backend/detail/LoadingMessage.js +14 -0
  69. package/dist/backend/detail/LoadingMessage.js.map +7 -0
  70. package/dist/backend/detail/NotesSection.js +1032 -0
  71. package/dist/backend/detail/NotesSection.js.map +7 -0
  72. package/dist/backend/detail/TabEmptyState.js +25 -0
  73. package/dist/backend/detail/TabEmptyState.js.map +7 -0
  74. package/dist/backend/detail/TagsSection.js +254 -0
  75. package/dist/backend/detail/TagsSection.js.map +7 -0
  76. package/dist/backend/detail/addressFormat.js +77 -0
  77. package/dist/backend/detail/addressFormat.js.map +7 -0
  78. package/dist/backend/detail/index.js +34 -0
  79. package/dist/backend/detail/index.js.map +7 -0
  80. package/dist/backend/fields/registry.generated.js +8 -0
  81. package/dist/backend/fields/registry.generated.js.map +7 -0
  82. package/dist/backend/fields/registry.js +29 -0
  83. package/dist/backend/fields/registry.js.map +7 -0
  84. package/dist/backend/indexes/PartialIndexBanner.js +58 -0
  85. package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
  86. package/dist/backend/indexes/store.js +62 -0
  87. package/dist/backend/indexes/store.js.map +7 -0
  88. package/dist/backend/injection/InjectionSpot.js +179 -0
  89. package/dist/backend/injection/InjectionSpot.js.map +7 -0
  90. package/dist/backend/injection/PageInjectionBoundary.js +26 -0
  91. package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
  92. package/dist/backend/injection/helpers.js +26 -0
  93. package/dist/backend/injection/helpers.js.map +7 -0
  94. package/dist/backend/injection/widgetRegistry.js +55 -0
  95. package/dist/backend/injection/widgetRegistry.js.map +7 -0
  96. package/dist/backend/inputs/ComboboxInput.js +225 -0
  97. package/dist/backend/inputs/ComboboxInput.js.map +7 -0
  98. package/dist/backend/inputs/LookupSelect.js +191 -0
  99. package/dist/backend/inputs/LookupSelect.js.map +7 -0
  100. package/dist/backend/inputs/PhoneNumberField.js +100 -0
  101. package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
  102. package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
  103. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
  104. package/dist/backend/inputs/TagsInput.js +222 -0
  105. package/dist/backend/inputs/TagsInput.js.map +7 -0
  106. package/dist/backend/inputs/index.js +6 -0
  107. package/dist/backend/inputs/index.js.map +7 -0
  108. package/dist/backend/operations/LastOperationBanner.js +80 -0
  109. package/dist/backend/operations/LastOperationBanner.js.map +7 -0
  110. package/dist/backend/operations/store.js +183 -0
  111. package/dist/backend/operations/store.js.map +7 -0
  112. package/dist/backend/schedule/ScheduleAgenda.js +107 -0
  113. package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
  114. package/dist/backend/schedule/ScheduleGrid.js +107 -0
  115. package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
  116. package/dist/backend/schedule/ScheduleToolbar.js +166 -0
  117. package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
  118. package/dist/backend/schedule/ScheduleView.js +165 -0
  119. package/dist/backend/schedule/ScheduleView.js.map +7 -0
  120. package/dist/backend/schedule/index.js +6 -0
  121. package/dist/backend/schedule/index.js.map +7 -0
  122. package/dist/backend/schedule/recurrence.js +83 -0
  123. package/dist/backend/schedule/recurrence.js.map +7 -0
  124. package/dist/backend/schedule/types.js +1 -0
  125. package/dist/backend/schedule/types.js.map +7 -0
  126. package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
  127. package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
  128. package/dist/backend/utils/api.js +127 -0
  129. package/dist/backend/utils/api.js.map +7 -0
  130. package/dist/backend/utils/apiCall.js +48 -0
  131. package/dist/backend/utils/apiCall.js.map +7 -0
  132. package/dist/backend/utils/crud.js +126 -0
  133. package/dist/backend/utils/crud.js.map +7 -0
  134. package/dist/backend/utils/customFieldColumns.js +56 -0
  135. package/dist/backend/utils/customFieldColumns.js.map +7 -0
  136. package/dist/backend/utils/customFieldDefs.js +143 -0
  137. package/dist/backend/utils/customFieldDefs.js.map +7 -0
  138. package/dist/backend/utils/customFieldFilters.js +126 -0
  139. package/dist/backend/utils/customFieldFilters.js.map +7 -0
  140. package/dist/backend/utils/customFieldForms.js +162 -0
  141. package/dist/backend/utils/customFieldForms.js.map +7 -0
  142. package/dist/backend/utils/customFieldValues.js +26 -0
  143. package/dist/backend/utils/customFieldValues.js.map +7 -0
  144. package/dist/backend/utils/flash.js +16 -0
  145. package/dist/backend/utils/flash.js.map +7 -0
  146. package/dist/backend/utils/nav.js +185 -0
  147. package/dist/backend/utils/nav.js.map +7 -0
  148. package/dist/backend/utils/serverErrors.js +230 -0
  149. package/dist/backend/utils/serverErrors.js.map +7 -0
  150. package/dist/frontend/AuthFooter.js +23 -0
  151. package/dist/frontend/AuthFooter.js.map +7 -0
  152. package/dist/frontend/LanguageSwitcher.js +57 -0
  153. package/dist/frontend/LanguageSwitcher.js.map +7 -0
  154. package/dist/frontend/Layout.js +14 -0
  155. package/dist/frontend/Layout.js.map +7 -0
  156. package/dist/index.js +32 -0
  157. package/dist/index.js.map +7 -0
  158. package/dist/primitives/DataLoader.js +67 -0
  159. package/dist/primitives/DataLoader.js.map +7 -0
  160. package/dist/primitives/ErrorNotice.js +20 -0
  161. package/dist/primitives/ErrorNotice.js.map +7 -0
  162. package/dist/primitives/alert.js +38 -0
  163. package/dist/primitives/alert.js.map +7 -0
  164. package/dist/primitives/badge.js +28 -0
  165. package/dist/primitives/badge.js.map +7 -0
  166. package/dist/primitives/button.js +44 -0
  167. package/dist/primitives/button.js.map +7 -0
  168. package/dist/primitives/card.js +91 -0
  169. package/dist/primitives/card.js.map +7 -0
  170. package/dist/primitives/checkbox.js +28 -0
  171. package/dist/primitives/checkbox.js.map +7 -0
  172. package/dist/primitives/dialog.js +90 -0
  173. package/dist/primitives/dialog.js.map +7 -0
  174. package/dist/primitives/input.js +22 -0
  175. package/dist/primitives/input.js.map +7 -0
  176. package/dist/primitives/label.js +21 -0
  177. package/dist/primitives/label.js.map +7 -0
  178. package/dist/primitives/separator.js +9 -0
  179. package/dist/primitives/separator.js.map +7 -0
  180. package/dist/primitives/spinner.js +24 -0
  181. package/dist/primitives/spinner.js.map +7 -0
  182. package/dist/primitives/switch.js +80 -0
  183. package/dist/primitives/switch.js.map +7 -0
  184. package/dist/primitives/table.js +29 -0
  185. package/dist/primitives/table.js.map +7 -0
  186. package/dist/primitives/tabs.js +87 -0
  187. package/dist/primitives/tabs.js.map +7 -0
  188. package/dist/primitives/textarea.js +21 -0
  189. package/dist/primitives/textarea.js.map +7 -0
  190. package/dist/primitives/tooltip.js +60 -0
  191. package/dist/primitives/tooltip.js.map +7 -0
  192. package/dist/theme/QueryProvider.js +44 -0
  193. package/dist/theme/QueryProvider.js.map +7 -0
  194. package/dist/theme/ThemeProvider.js +95 -0
  195. package/dist/theme/ThemeProvider.js.map +7 -0
  196. package/dist/theme/ThemeToggle.js +88 -0
  197. package/dist/theme/ThemeToggle.js.map +7 -0
  198. package/dist/theme/index.js +10 -0
  199. package/dist/theme/index.js.map +7 -0
  200. package/dist/types/react-big-calendar.d.js +1 -0
  201. package/dist/types/react-big-calendar.d.js.map +7 -0
  202. package/jest.config.cjs +23 -0
  203. package/jest.setup.ts +55 -0
  204. package/package.json +105 -0
  205. package/src/backend/AppShell.tsx +1096 -0
  206. package/src/backend/ConfirmDialog.tsx +19 -0
  207. package/src/backend/ContextHelp.tsx +38 -0
  208. package/src/backend/CrudForm.tsx +2503 -0
  209. package/src/backend/DataTable.tsx +1730 -0
  210. package/src/backend/EmptyState.tsx +65 -0
  211. package/src/backend/FilterBar.tsx +161 -0
  212. package/src/backend/FilterOverlay.tsx +328 -0
  213. package/src/backend/FlashMessages.tsx +82 -0
  214. package/src/backend/JsonBuilder.tsx +362 -0
  215. package/src/backend/JsonDisplay.tsx +254 -0
  216. package/src/backend/Page.tsx +30 -0
  217. package/src/backend/PerspectiveSidebar.tsx +337 -0
  218. package/src/backend/RowActions.tsx +151 -0
  219. package/src/backend/TruncatedCell.tsx +133 -0
  220. package/src/backend/UserMenu.tsx +118 -0
  221. package/src/backend/ValueIcons.tsx +48 -0
  222. package/src/backend/__tests__/AppShell.test.tsx +115 -0
  223. package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
  224. package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
  225. package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
  226. package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
  227. package/src/backend/__tests__/serverErrors.test.ts +83 -0
  228. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
  229. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
  230. package/src/backend/dashboard/DashboardScreen.tsx +684 -0
  231. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
  232. package/src/backend/dashboard/index.ts +1 -0
  233. package/src/backend/dashboard/widgetRegistry.ts +68 -0
  234. package/src/backend/detail/ActivitiesSection.tsx +1284 -0
  235. package/src/backend/detail/AddressEditor.tsx +472 -0
  236. package/src/backend/detail/AddressTiles.tsx +587 -0
  237. package/src/backend/detail/AddressesSection.tsx +346 -0
  238. package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
  239. package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
  240. package/src/backend/detail/AttachmentsSection.tsx +414 -0
  241. package/src/backend/detail/CustomDataSection.tsx +530 -0
  242. package/src/backend/detail/DetailFieldsSection.tsx +147 -0
  243. package/src/backend/detail/ErrorMessage.tsx +32 -0
  244. package/src/backend/detail/InlineEditors.tsx +877 -0
  245. package/src/backend/detail/LoadingMessage.tsx +14 -0
  246. package/src/backend/detail/NotesSection.tsx +1275 -0
  247. package/src/backend/detail/TabEmptyState.tsx +48 -0
  248. package/src/backend/detail/TagsSection.tsx +314 -0
  249. package/src/backend/detail/addressFormat.tsx +121 -0
  250. package/src/backend/detail/index.ts +44 -0
  251. package/src/backend/fields/registry.generated.ts +8 -0
  252. package/src/backend/fields/registry.ts +38 -0
  253. package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
  254. package/src/backend/indexes/store.ts +88 -0
  255. package/src/backend/injection/InjectionSpot.tsx +236 -0
  256. package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
  257. package/src/backend/injection/helpers.ts +35 -0
  258. package/src/backend/injection/widgetRegistry.ts +68 -0
  259. package/src/backend/inputs/ComboboxInput.tsx +269 -0
  260. package/src/backend/inputs/LookupSelect.tsx +247 -0
  261. package/src/backend/inputs/PhoneNumberField.tsx +129 -0
  262. package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
  263. package/src/backend/inputs/TagsInput.tsx +259 -0
  264. package/src/backend/inputs/index.ts +5 -0
  265. package/src/backend/operations/LastOperationBanner.tsx +85 -0
  266. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
  267. package/src/backend/operations/store.ts +230 -0
  268. package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
  269. package/src/backend/schedule/ScheduleGrid.tsx +136 -0
  270. package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
  271. package/src/backend/schedule/ScheduleView.tsx +198 -0
  272. package/src/backend/schedule/index.ts +5 -0
  273. package/src/backend/schedule/recurrence.ts +99 -0
  274. package/src/backend/schedule/types.ts +26 -0
  275. package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
  276. package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
  277. package/src/backend/utils/__tests__/crud.test.ts +87 -0
  278. package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
  279. package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
  280. package/src/backend/utils/api.ts +149 -0
  281. package/src/backend/utils/apiCall.ts +96 -0
  282. package/src/backend/utils/crud.ts +174 -0
  283. package/src/backend/utils/customFieldColumns.ts +71 -0
  284. package/src/backend/utils/customFieldDefs.ts +245 -0
  285. package/src/backend/utils/customFieldFilters.ts +145 -0
  286. package/src/backend/utils/customFieldForms.ts +196 -0
  287. package/src/backend/utils/customFieldValues.ts +41 -0
  288. package/src/backend/utils/flash.ts +17 -0
  289. package/src/backend/utils/nav.ts +238 -0
  290. package/src/backend/utils/serverErrors.ts +302 -0
  291. package/src/frontend/AuthFooter.tsx +29 -0
  292. package/src/frontend/LanguageSwitcher.tsx +66 -0
  293. package/src/frontend/Layout.tsx +13 -0
  294. package/src/index.ts +32 -0
  295. package/src/primitives/DataLoader.tsx +92 -0
  296. package/src/primitives/ErrorNotice.tsx +26 -0
  297. package/src/primitives/alert.tsx +52 -0
  298. package/src/primitives/badge.tsx +31 -0
  299. package/src/primitives/button.tsx +47 -0
  300. package/src/primitives/card.tsx +92 -0
  301. package/src/primitives/checkbox.tsx +28 -0
  302. package/src/primitives/dialog.tsx +110 -0
  303. package/src/primitives/input.tsx +20 -0
  304. package/src/primitives/label.tsx +18 -0
  305. package/src/primitives/separator.tsx +7 -0
  306. package/src/primitives/spinner.tsx +27 -0
  307. package/src/primitives/switch.tsx +86 -0
  308. package/src/primitives/table.tsx +27 -0
  309. package/src/primitives/tabs.tsx +128 -0
  310. package/src/primitives/textarea.tsx +20 -0
  311. package/src/primitives/tooltip.tsx +85 -0
  312. package/src/theme/QueryProvider.tsx +46 -0
  313. package/src/theme/ThemeProvider.tsx +120 -0
  314. package/src/theme/ThemeToggle.tsx +88 -0
  315. package/src/theme/index.ts +3 -0
  316. package/src/types/react-big-calendar.d.ts +16 -0
  317. package/tsconfig.build.json +11 -0
  318. package/tsconfig.json +9 -0
  319. package/watch.mjs +6 -0
@@ -0,0 +1,282 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Button } from "../primitives/button.js";
5
+ import { Spinner } from "../primitives/spinner.js";
6
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
7
+ const emptyArray = [];
8
+ function PerspectiveSidebar({
9
+ open,
10
+ onOpenChange,
11
+ loading,
12
+ perspectives,
13
+ rolePerspectives,
14
+ roles,
15
+ activePerspectiveId,
16
+ onActivatePerspective,
17
+ onDeletePerspective,
18
+ onClearRole,
19
+ onSave,
20
+ canApplyToRoles,
21
+ columnOptions,
22
+ onToggleColumn,
23
+ onMoveColumn,
24
+ saving,
25
+ deletingIds,
26
+ roleClearingIds,
27
+ apiWarning
28
+ }) {
29
+ const t = useT();
30
+ function perspectiveLabel(p) {
31
+ return p.name.trim().length ? p.name : t("ui.perspectives.untitled", "Untitled perspective");
32
+ }
33
+ const [name, setName] = React.useState("");
34
+ const [isDefault, setIsDefault] = React.useState(false);
35
+ const [applyToRoles, setApplyToRoles] = React.useState([]);
36
+ const [setRoleDefault, setSetRoleDefault] = React.useState(false);
37
+ const [error, setError] = React.useState(null);
38
+ React.useEffect(() => {
39
+ if (!open) {
40
+ setError(null);
41
+ }
42
+ }, [open]);
43
+ React.useEffect(() => {
44
+ if (!open) return;
45
+ const active = perspectives.find((p) => p.id === activePerspectiveId) ?? rolePerspectives.find((p) => p.id === activePerspectiveId);
46
+ if (active) {
47
+ setName(active.name);
48
+ setIsDefault(active.isDefault);
49
+ } else if (!name) {
50
+ setName("");
51
+ setIsDefault(false);
52
+ }
53
+ }, [open, activePerspectiveId]);
54
+ const groupedRolePerspectives = React.useMemo(() => {
55
+ const map = /* @__PURE__ */ new Map();
56
+ for (const rp of rolePerspectives) {
57
+ if (!map.has(rp.roleId)) map.set(rp.roleId, []);
58
+ map.get(rp.roleId).push(rp);
59
+ }
60
+ return map;
61
+ }, [rolePerspectives]);
62
+ const toggleRoleSelection = (roleId) => {
63
+ setApplyToRoles((prev) => {
64
+ const next = new Set(prev);
65
+ if (next.has(roleId)) next.delete(roleId);
66
+ else next.add(roleId);
67
+ return Array.from(next);
68
+ });
69
+ };
70
+ const handleSave = async () => {
71
+ setError(null);
72
+ try {
73
+ await onSave({ name: name.trim(), isDefault, applyToRoles, setRoleDefault });
74
+ if (!isDefault) setIsDefault(false);
75
+ setApplyToRoles([]);
76
+ setSetRoleDefault(false);
77
+ } catch (err) {
78
+ setError(err?.message ?? "Failed to save perspective");
79
+ }
80
+ };
81
+ if (!open) return null;
82
+ return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50", children: [
83
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/30", onClick: () => onOpenChange(false) }),
84
+ /* @__PURE__ */ jsxs("div", { className: "absolute left-0 top-0 h-full w-full sm:w-[420px] bg-background shadow-xl border-r flex flex-col", children: [
85
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-4 border-b", children: [
86
+ /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold", children: t("ui.perspectives.title", "Perspectives") }),
87
+ /* @__PURE__ */ jsx("button", { className: "text-sm text-muted-foreground", onClick: () => onOpenChange(false), children: t("ui.perspectives.close", "Close") })
88
+ ] }),
89
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-auto divide-y", children: [
90
+ /* @__PURE__ */ jsxs("section", { className: "p-4 space-y-3", children: [
91
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
92
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold uppercase text-muted-foreground", children: t("ui.perspectives.myPerspectives.title", "My perspectives") }),
93
+ loading ? /* @__PURE__ */ jsx(Spinner, { size: "sm" }) : null
94
+ ] }),
95
+ (perspectives ?? emptyArray).length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("ui.perspectives.myPerspectives.empty", "No saved perspectives yet. Adjust columns or filters and save your first perspective.") }) : /* @__PURE__ */ jsx("div", { className: "space-y-2", children: perspectives.map((p) => {
96
+ const isActive = activePerspectiveId === p.id;
97
+ const deleting = deletingIds.includes(p.id);
98
+ return /* @__PURE__ */ jsxs("div", { className: `rounded border px-3 py-2 flex items-start justify-between gap-3 ${isActive ? "border-primary/80 bg-primary/5" : "border-border bg-card"}`, children: [
99
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
100
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: perspectiveLabel(p) }),
101
+ /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-2", children: [
102
+ p.isDefault ? /* @__PURE__ */ jsx("span", { className: "inline-flex items-center gap-1 rounded bg-primary/10 px-2 py-0.5 text-primary text-[11px] uppercase tracking-wide", children: t("ui.perspectives.badge.default", "Default") }) : null,
103
+ /* @__PURE__ */ jsx("span", { children: t("ui.perspectives.updated", "Updated {date}", { date: new Date(p.updatedAt ?? p.createdAt).toLocaleString() }) })
104
+ ] })
105
+ ] }),
106
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
107
+ /* @__PURE__ */ jsx(
108
+ Button,
109
+ {
110
+ size: "sm",
111
+ variant: isActive ? "secondary" : "outline",
112
+ onClick: () => onActivatePerspective(p, "personal"),
113
+ disabled: isActive || deleting,
114
+ children: isActive ? t("ui.perspectives.actions.active", "Active") : t("ui.perspectives.actions.use", "Use")
115
+ }
116
+ ),
117
+ /* @__PURE__ */ jsx(
118
+ Button,
119
+ {
120
+ size: "sm",
121
+ variant: "ghost",
122
+ onClick: () => void onDeletePerspective(p.id),
123
+ disabled: deleting,
124
+ children: deleting ? t("ui.perspectives.actions.removing", "Removing\u2026") : t("common.delete", "Delete")
125
+ }
126
+ )
127
+ ] })
128
+ ] }, p.id);
129
+ }) })
130
+ ] }),
131
+ /* @__PURE__ */ jsxs("section", { className: "p-4 space-y-3", children: [
132
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
133
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold uppercase text-muted-foreground", children: t("ui.perspectives.rolePerspectives.title", "Role perspectives") }),
134
+ rolePerspectives.length === 0 ? null : /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: rolePerspectives.length })
135
+ ] }),
136
+ rolePerspectives.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("ui.perspectives.rolePerspectives.empty", "No shared role perspectives available.") }) : /* @__PURE__ */ jsx("div", { className: "space-y-3", children: Array.from(groupedRolePerspectives.entries()).map(([roleId, items]) => {
137
+ const role = roles.find((r) => r.id === roleId);
138
+ const clearing = roleClearingIds.includes(roleId);
139
+ return /* @__PURE__ */ jsxs("div", { className: "rounded border px-3 py-2 space-y-2 bg-muted/40", children: [
140
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
141
+ /* @__PURE__ */ jsxs("div", { children: [
142
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: role?.name ?? t("ui.perspectives.role.fallback", "Role") }),
143
+ role?.hasDefault ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: t("ui.perspectives.role.defaultConfigured", "Default perspective configured") }) : null
144
+ ] }),
145
+ /* @__PURE__ */ jsx(
146
+ Button,
147
+ {
148
+ size: "sm",
149
+ variant: "ghost",
150
+ onClick: () => void onClearRole(roleId),
151
+ disabled: clearing,
152
+ children: clearing ? t("ui.perspectives.role.clearing", "Clearing\u2026") : t("ui.perspectives.role.clear", "Clear role")
153
+ }
154
+ )
155
+ ] }),
156
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: items.map((item) => {
157
+ const isActive = activePerspectiveId === item.id;
158
+ return /* @__PURE__ */ jsxs("div", { className: `rounded border px-3 py-2 flex items-start justify-between gap-3 ${isActive ? "border-primary/80 bg-primary/5" : "border-border bg-background"}`, children: [
159
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
160
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: perspectiveLabel(item) }),
161
+ /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-2", children: [
162
+ item.isDefault ? /* @__PURE__ */ jsx("span", { className: "inline-flex items-center gap-1 rounded bg-primary/10 px-2 py-0.5 text-primary text-[11px] uppercase tracking-wide", children: t("ui.perspectives.badge.roleDefault", "Role default") }) : null,
163
+ /* @__PURE__ */ jsx("span", { children: t("ui.perspectives.updated", "Updated {date}", { date: new Date(item.updatedAt ?? item.createdAt).toLocaleString() }) })
164
+ ] })
165
+ ] }),
166
+ /* @__PURE__ */ jsx(
167
+ Button,
168
+ {
169
+ size: "sm",
170
+ variant: isActive ? "secondary" : "outline",
171
+ onClick: () => onActivatePerspective(item, "role"),
172
+ disabled: isActive,
173
+ children: isActive ? t("ui.perspectives.actions.active", "Active") : t("ui.perspectives.actions.use", "Use")
174
+ }
175
+ )
176
+ ] }, item.id);
177
+ }) })
178
+ ] }, roleId);
179
+ }) })
180
+ ] }),
181
+ /* @__PURE__ */ jsxs("section", { className: "p-4 space-y-3", children: [
182
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold uppercase text-muted-foreground", children: t("ui.perspectives.saveCurrentView.title", "Save current view") }),
183
+ apiWarning ? /* @__PURE__ */ jsx("div", { className: "rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700", children: apiWarning }) : null,
184
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
185
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground uppercase", children: t("ui.perspectives.form.nameLabel", "Name") }),
186
+ /* @__PURE__ */ jsx(
187
+ "input",
188
+ {
189
+ value: name,
190
+ onChange: (e) => setName(e.target.value),
191
+ placeholder: t("ui.perspectives.form.namePlaceholder", "e.g. My condensed view"),
192
+ className: "w-full h-9 rounded border px-2 text-sm"
193
+ }
194
+ )
195
+ ] }),
196
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: /* @__PURE__ */ jsxs("label", { className: "inline-flex items-center gap-2 text-sm", children: [
197
+ /* @__PURE__ */ jsx(
198
+ "input",
199
+ {
200
+ type: "checkbox",
201
+ checked: isDefault,
202
+ onChange: (e) => setIsDefault(e.target.checked)
203
+ }
204
+ ),
205
+ t("ui.perspectives.form.makeDefault", "Make this my default perspective")
206
+ ] }) }),
207
+ canApplyToRoles ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
208
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-muted-foreground uppercase", children: t("ui.perspectives.form.shareWithRoles", "Share with roles") }),
209
+ /* @__PURE__ */ jsx("div", { className: "max-h-32 overflow-auto border rounded p-2 space-y-1", children: roles.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: t("ui.perspectives.form.noRolesAvailable", "No roles available.") }) : roles.map((role) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
210
+ /* @__PURE__ */ jsx(
211
+ "input",
212
+ {
213
+ type: "checkbox",
214
+ checked: applyToRoles.includes(role.id),
215
+ onChange: () => toggleRoleSelection(role.id)
216
+ }
217
+ ),
218
+ /* @__PURE__ */ jsx("span", { children: role.name })
219
+ ] }, role.id)) }),
220
+ /* @__PURE__ */ jsxs("label", { className: "inline-flex items-center gap-2 text-sm", children: [
221
+ /* @__PURE__ */ jsx(
222
+ "input",
223
+ {
224
+ type: "checkbox",
225
+ checked: setRoleDefault,
226
+ onChange: (e) => setSetRoleDefault(e.target.checked),
227
+ disabled: applyToRoles.length === 0
228
+ }
229
+ ),
230
+ t("ui.perspectives.form.setRoleDefault", "Set as default for selected roles")
231
+ ] })
232
+ ] }) : null,
233
+ error ? /* @__PURE__ */ jsx("div", { className: "text-sm text-red-600", children: error }) : null,
234
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => void handleSave(), disabled: saving || !name.trim() || Boolean(apiWarning), children: saving ? t("ui.perspectives.form.saving", "Saving\u2026") : t("ui.perspectives.form.save", "Save perspective") })
235
+ ] }),
236
+ /* @__PURE__ */ jsxs("section", { className: "p-4 space-y-3", children: [
237
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold uppercase text-muted-foreground", children: t("ui.perspectives.form.columns", "Columns") }),
238
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: columnOptions.map((col, index) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 rounded border px-3 py-2 bg-card", children: [
239
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
240
+ /* @__PURE__ */ jsx(
241
+ "input",
242
+ {
243
+ type: "checkbox",
244
+ checked: col.visible,
245
+ onChange: (e) => onToggleColumn(col.id, e.target.checked),
246
+ disabled: !col.canHide
247
+ }
248
+ ),
249
+ /* @__PURE__ */ jsx("span", { children: col.label })
250
+ ] }),
251
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
252
+ /* @__PURE__ */ jsx(
253
+ Button,
254
+ {
255
+ size: "sm",
256
+ variant: "ghost",
257
+ onClick: () => onMoveColumn(col.id, "up"),
258
+ disabled: index === 0,
259
+ children: "\u2191"
260
+ }
261
+ ),
262
+ /* @__PURE__ */ jsx(
263
+ Button,
264
+ {
265
+ size: "sm",
266
+ variant: "ghost",
267
+ onClick: () => onMoveColumn(col.id, "down"),
268
+ disabled: index === columnOptions.length - 1,
269
+ children: "\u2193"
270
+ }
271
+ )
272
+ ] })
273
+ ] }, col.id)) })
274
+ ] })
275
+ ] })
276
+ ] })
277
+ ] });
278
+ }
279
+ export {
280
+ PerspectiveSidebar
281
+ };
282
+ //# sourceMappingURL=PerspectiveSidebar.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/backend/PerspectiveSidebar.tsx"],
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Button } from '../primitives/button'\nimport { Spinner } from '../primitives/spinner'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type {\n PerspectiveDto,\n RolePerspectiveDto,\n} from '@open-mercato/shared/modules/perspectives/types'\n\ntype ColumnOption = {\n id: string\n label: string\n visible: boolean\n canHide: boolean\n}\n\nexport type PerspectiveSidebarProps = {\n open: boolean\n onOpenChange: (open: boolean) => void\n loading: boolean\n perspectives: PerspectiveDto[]\n rolePerspectives: RolePerspectiveDto[]\n roles: Array<{ id: string; name: string; hasPerspective: boolean; hasDefault: boolean }>\n activePerspectiveId: string | null\n onActivatePerspective: (perspective: PerspectiveDto | RolePerspectiveDto, source: 'personal' | 'role') => void\n onDeletePerspective: (perspectiveId: string) => Promise<void>\n onClearRole: (roleId: string) => Promise<void>\n onSave: (input: { name: string; isDefault: boolean; applyToRoles: string[]; setRoleDefault: boolean }) => Promise<void>\n canApplyToRoles: boolean\n columnOptions: ColumnOption[]\n onToggleColumn: (id: string, visible: boolean) => void\n onMoveColumn: (id: string, direction: 'up' | 'down') => void\n saving: boolean\n deletingIds: string[]\n roleClearingIds: string[]\n apiWarning?: string | null\n}\n\nconst emptyArray: any[] = []\n\nexport function PerspectiveSidebar({\n open,\n onOpenChange,\n loading,\n perspectives,\n rolePerspectives,\n roles,\n activePerspectiveId,\n onActivatePerspective,\n onDeletePerspective,\n onClearRole,\n onSave,\n canApplyToRoles,\n columnOptions,\n onToggleColumn,\n onMoveColumn,\n saving,\n deletingIds,\n roleClearingIds,\n apiWarning,\n}: PerspectiveSidebarProps) {\n const t = useT()\n \n function perspectiveLabel(p: PerspectiveDto | RolePerspectiveDto) {\n return p.name.trim().length ? p.name : t('ui.perspectives.untitled', 'Untitled perspective')\n }\n const [name, setName] = React.useState('')\n const [isDefault, setIsDefault] = React.useState(false)\n const [applyToRoles, setApplyToRoles] = React.useState<string[]>([])\n const [setRoleDefault, setSetRoleDefault] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (!open) {\n setError(null)\n }\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n const active = perspectives.find((p) => p.id === activePerspectiveId)\n ?? rolePerspectives.find((p) => p.id === activePerspectiveId)\n if (active) {\n setName(active.name)\n setIsDefault(active.isDefault)\n } else if (!name) {\n setName('')\n setIsDefault(false)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, activePerspectiveId])\n\n const groupedRolePerspectives = React.useMemo(() => {\n const map = new Map<string, RolePerspectiveDto[]>()\n for (const rp of rolePerspectives) {\n if (!map.has(rp.roleId)) map.set(rp.roleId, [])\n map.get(rp.roleId)!.push(rp)\n }\n return map\n }, [rolePerspectives])\n\n const toggleRoleSelection = (roleId: string) => {\n setApplyToRoles((prev) => {\n const next = new Set(prev)\n if (next.has(roleId)) next.delete(roleId)\n else next.add(roleId)\n return Array.from(next)\n })\n }\n\n const handleSave = async () => {\n setError(null)\n try {\n await onSave({ name: name.trim(), isDefault, applyToRoles, setRoleDefault })\n if (!isDefault) setIsDefault(false)\n setApplyToRoles([])\n setSetRoleDefault(false)\n } catch (err: any) {\n setError(err?.message ?? 'Failed to save perspective')\n }\n }\n\n if (!open) return null\n\n return (\n <div className=\"fixed inset-0 z-50\">\n <div className=\"absolute inset-0 bg-black/30\" onClick={() => onOpenChange(false)} />\n <div className=\"absolute left-0 top-0 h-full w-full sm:w-[420px] bg-background shadow-xl border-r flex flex-col\">\n <div className=\"flex items-center justify-between p-4 border-b\">\n <h2 className=\"text-base font-semibold\">{t('ui.perspectives.title', 'Perspectives')}</h2>\n <button className=\"text-sm text-muted-foreground\" onClick={() => onOpenChange(false)}>{t('ui.perspectives.close', 'Close')}</button>\n </div>\n <div className=\"flex-1 overflow-auto divide-y\">\n <section className=\"p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold uppercase text-muted-foreground\">{t('ui.perspectives.myPerspectives.title', 'My perspectives')}</h3>\n {loading ? <Spinner size=\"sm\" /> : null}\n </div>\n {(perspectives ?? emptyArray).length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">{t('ui.perspectives.myPerspectives.empty', 'No saved perspectives yet. Adjust columns or filters and save your first perspective.')}</p>\n ) : (\n <div className=\"space-y-2\">\n {perspectives.map((p) => {\n const isActive = activePerspectiveId === p.id\n const deleting = deletingIds.includes(p.id)\n return (\n <div key={p.id} className={`rounded border px-3 py-2 flex items-start justify-between gap-3 ${isActive ? 'border-primary/80 bg-primary/5' : 'border-border bg-card'}`}>\n <div className=\"space-y-1\">\n <div className=\"text-sm font-medium\">{perspectiveLabel(p)}</div>\n <div className=\"text-xs text-muted-foreground flex items-center gap-2\">\n {p.isDefault ? <span className=\"inline-flex items-center gap-1 rounded bg-primary/10 px-2 py-0.5 text-primary text-[11px] uppercase tracking-wide\">{t('ui.perspectives.badge.default', 'Default')}</span> : null}\n <span>{t('ui.perspectives.updated', 'Updated {date}', { date: new Date(p.updatedAt ?? p.createdAt).toLocaleString() })}</span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1\">\n <Button\n size=\"sm\"\n variant={isActive ? 'secondary' : 'outline'}\n onClick={() => onActivatePerspective(p, 'personal')}\n disabled={isActive || deleting}\n >\n {isActive ? t('ui.perspectives.actions.active', 'Active') : t('ui.perspectives.actions.use', 'Use')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => void onDeletePerspective(p.id)}\n disabled={deleting}\n >\n {deleting ? t('ui.perspectives.actions.removing', 'Removing\u2026') : t('common.delete', 'Delete')}\n </Button>\n </div>\n </div>\n )\n })}\n </div>\n )}\n </section>\n <section className=\"p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold uppercase text-muted-foreground\">{t('ui.perspectives.rolePerspectives.title', 'Role perspectives')}</h3>\n {rolePerspectives.length === 0 ? null : <span className=\"text-xs text-muted-foreground\">{rolePerspectives.length}</span>}\n </div>\n {rolePerspectives.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">{t('ui.perspectives.rolePerspectives.empty', 'No shared role perspectives available.')}</p>\n ) : (\n <div className=\"space-y-3\">\n {Array.from(groupedRolePerspectives.entries()).map(([roleId, items]) => {\n const role = roles.find((r) => r.id === roleId)\n const clearing = roleClearingIds.includes(roleId)\n return (\n <div key={roleId} className=\"rounded border px-3 py-2 space-y-2 bg-muted/40\">\n <div className=\"flex items-center justify-between gap-2\">\n <div>\n <div className=\"text-sm font-semibold\">{role?.name ?? t('ui.perspectives.role.fallback', 'Role')}</div>\n {role?.hasDefault ? <div className=\"text-xs text-muted-foreground\">{t('ui.perspectives.role.defaultConfigured', 'Default perspective configured')}</div> : null}\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => void onClearRole(roleId)}\n disabled={clearing}\n >\n {clearing ? t('ui.perspectives.role.clearing', 'Clearing\u2026') : t('ui.perspectives.role.clear', 'Clear role')}\n </Button>\n </div>\n <div className=\"space-y-2\">\n {items.map((item) => {\n const isActive = activePerspectiveId === item.id\n return (\n <div key={item.id} className={`rounded border px-3 py-2 flex items-start justify-between gap-3 ${isActive ? 'border-primary/80 bg-primary/5' : 'border-border bg-background'}`}>\n <div className=\"space-y-1\">\n <div className=\"text-sm font-medium\">{perspectiveLabel(item)}</div>\n <div className=\"text-xs text-muted-foreground flex items-center gap-2\">\n {item.isDefault ? <span className=\"inline-flex items-center gap-1 rounded bg-primary/10 px-2 py-0.5 text-primary text-[11px] uppercase tracking-wide\">{t('ui.perspectives.badge.roleDefault', 'Role default')}</span> : null}\n <span>{t('ui.perspectives.updated', 'Updated {date}', { date: new Date(item.updatedAt ?? item.createdAt).toLocaleString() })}</span>\n </div>\n </div>\n <Button\n size=\"sm\"\n variant={isActive ? 'secondary' : 'outline'}\n onClick={() => onActivatePerspective(item, 'role')}\n disabled={isActive}\n >\n {isActive ? t('ui.perspectives.actions.active', 'Active') : t('ui.perspectives.actions.use', 'Use')}\n </Button>\n </div>\n )\n })}\n </div>\n </div>\n )\n })}\n </div>\n )}\n </section>\n <section className=\"p-4 space-y-3\">\n <h3 className=\"text-sm font-semibold uppercase text-muted-foreground\">{t('ui.perspectives.saveCurrentView.title', 'Save current view')}</h3>\n {apiWarning ? (\n <div className=\"rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700\">\n {apiWarning}\n </div>\n ) : null}\n <div className=\"space-y-2\">\n <label className=\"text-xs font-medium text-muted-foreground uppercase\">{t('ui.perspectives.form.nameLabel', 'Name')}</label>\n <input\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder={t('ui.perspectives.form.namePlaceholder', 'e.g. My condensed view')}\n className=\"w-full h-9 rounded border px-2 text-sm\"\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"inline-flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={isDefault}\n onChange={(e) => setIsDefault(e.target.checked)}\n />\n {t('ui.perspectives.form.makeDefault', 'Make this my default perspective')}\n </label>\n </div>\n {canApplyToRoles ? (\n <div className=\"space-y-2\">\n <div className=\"text-xs font-medium text-muted-foreground uppercase\">{t('ui.perspectives.form.shareWithRoles', 'Share with roles')}</div>\n <div className=\"max-h-32 overflow-auto border rounded p-2 space-y-1\">\n {roles.length === 0 ? (\n <div className=\"text-xs text-muted-foreground\">{t('ui.perspectives.form.noRolesAvailable', 'No roles available.')}</div>\n ) : roles.map((role) => (\n <label key={role.id} className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={applyToRoles.includes(role.id)}\n onChange={() => toggleRoleSelection(role.id)}\n />\n <span>{role.name}</span>\n </label>\n ))}\n </div>\n <label className=\"inline-flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={setRoleDefault}\n onChange={(e) => setSetRoleDefault(e.target.checked)}\n disabled={applyToRoles.length === 0}\n />\n {t('ui.perspectives.form.setRoleDefault', 'Set as default for selected roles')}\n </label>\n </div>\n ) : null}\n {error ? <div className=\"text-sm text-red-600\">{error}</div> : null}\n <Button size=\"sm\" onClick={() => void handleSave()} disabled={saving || !name.trim() || Boolean(apiWarning)}>\n {saving ? t('ui.perspectives.form.saving', 'Saving\u2026') : t('ui.perspectives.form.save', 'Save perspective')}\n </Button>\n </section>\n <section className=\"p-4 space-y-3\">\n <h3 className=\"text-sm font-semibold uppercase text-muted-foreground\">{t('ui.perspectives.form.columns', 'Columns')}</h3>\n <div className=\"space-y-2\">\n {columnOptions.map((col, index) => (\n <div key={col.id} className=\"flex items-center justify-between gap-2 rounded border px-3 py-2 bg-card\">\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={col.visible}\n onChange={(e) => onToggleColumn(col.id, e.target.checked)}\n disabled={!col.canHide}\n />\n <span>{col.label}</span>\n </label>\n <div className=\"flex items-center gap-1\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => onMoveColumn(col.id, 'up')}\n disabled={index === 0}\n >\n \u2191\n </Button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => onMoveColumn(col.id, 'down')}\n disabled={index === columnOptions.length - 1}\n >\n \u2193\n </Button>\n </div>\n </div>\n ))}\n </div>\n </section>\n </div>\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,148 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ function RowActions({ items = [] }) {
7
+ if (items.length === 0) return null;
8
+ const t = useT();
9
+ const [open, setOpen] = React.useState(false);
10
+ const btnRef = React.useRef(null);
11
+ const menuRef = React.useRef(null);
12
+ const hoverTimeoutRef = React.useRef(null);
13
+ const [anchorRect, setAnchorRect] = React.useState(null);
14
+ const [direction, setDirection] = React.useState("down");
15
+ const updatePosition = React.useCallback(() => {
16
+ if (!btnRef.current) return;
17
+ const rect = btnRef.current.getBoundingClientRect();
18
+ setAnchorRect(rect);
19
+ const spaceBelow = window.innerHeight - rect.bottom;
20
+ const spaceAbove = rect.top;
21
+ setDirection(spaceBelow < 180 && spaceAbove > spaceBelow ? "up" : "down");
22
+ }, []);
23
+ React.useEffect(() => {
24
+ if (!open) return;
25
+ updatePosition();
26
+ function onDocClick(e) {
27
+ const t2 = e.target;
28
+ if (menuRef.current && !menuRef.current.contains(t2) && btnRef.current && !btnRef.current.contains(t2)) {
29
+ setOpen(false);
30
+ }
31
+ }
32
+ function onKey(e) {
33
+ if (e.key === "Escape") {
34
+ setOpen(false);
35
+ btnRef.current?.focus();
36
+ }
37
+ }
38
+ function onScrollOrResize() {
39
+ updatePosition();
40
+ }
41
+ document.addEventListener("mousedown", onDocClick);
42
+ document.addEventListener("keydown", onKey);
43
+ window.addEventListener("scroll", onScrollOrResize, true);
44
+ window.addEventListener("resize", onScrollOrResize);
45
+ return () => {
46
+ document.removeEventListener("mousedown", onDocClick);
47
+ document.removeEventListener("keydown", onKey);
48
+ window.removeEventListener("scroll", onScrollOrResize, true);
49
+ window.removeEventListener("resize", onScrollOrResize);
50
+ };
51
+ }, [open, updatePosition]);
52
+ React.useEffect(() => {
53
+ return () => {
54
+ if (hoverTimeoutRef.current) {
55
+ clearTimeout(hoverTimeoutRef.current);
56
+ }
57
+ };
58
+ }, []);
59
+ const handleMouseEnter = () => {
60
+ if (hoverTimeoutRef.current) {
61
+ clearTimeout(hoverTimeoutRef.current);
62
+ }
63
+ setOpen(true);
64
+ };
65
+ const handleMouseLeave = () => {
66
+ hoverTimeoutRef.current = setTimeout(() => {
67
+ setOpen(false);
68
+ }, 150);
69
+ };
70
+ return /* @__PURE__ */ jsxs(
71
+ "div",
72
+ {
73
+ className: "relative inline-block text-left",
74
+ onMouseEnter: handleMouseEnter,
75
+ onMouseLeave: handleMouseLeave,
76
+ children: [
77
+ /* @__PURE__ */ jsxs(
78
+ "button",
79
+ {
80
+ ref: btnRef,
81
+ type: "button",
82
+ className: "h-8 w-8 inline-flex items-center justify-center rounded hover:bg-accent",
83
+ "aria-haspopup": "menu",
84
+ "aria-expanded": open,
85
+ onClick: () => {
86
+ setOpen((v) => !v);
87
+ requestAnimationFrame(updatePosition);
88
+ },
89
+ children: [
90
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u22EF" }),
91
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("ui.rowActions.openActions", "Open actions") })
92
+ ]
93
+ }
94
+ ),
95
+ open && anchorRect && createPortal(
96
+ /* @__PURE__ */ jsx(
97
+ "div",
98
+ {
99
+ ref: menuRef,
100
+ role: "menu",
101
+ className: "fixed w-44 rounded-md border bg-background p-1 shadow focus:outline-none z-[1000]",
102
+ style: {
103
+ top: direction === "down" ? anchorRect.bottom + 8 : anchorRect.top - 8,
104
+ left: anchorRect.right,
105
+ transform: `translate(-100%, ${direction === "down" ? "0" : "-100%"})`
106
+ },
107
+ onMouseEnter: handleMouseEnter,
108
+ onMouseLeave: handleMouseLeave,
109
+ children: items.map((it, idx) => it.href ? /* @__PURE__ */ jsx(
110
+ "a",
111
+ {
112
+ href: it.href,
113
+ className: `block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? "text-red-600" : ""}`,
114
+ role: "menuitem",
115
+ onClick: (event) => {
116
+ event.stopPropagation();
117
+ setOpen(false);
118
+ },
119
+ children: it.label
120
+ },
121
+ idx
122
+ ) : /* @__PURE__ */ jsx(
123
+ "button",
124
+ {
125
+ type: "button",
126
+ className: `block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? "text-red-600" : ""}`,
127
+ role: "menuitem",
128
+ onClick: (event) => {
129
+ event.stopPropagation();
130
+ setOpen(false);
131
+ it.onSelect?.();
132
+ },
133
+ children: it.label
134
+ },
135
+ idx
136
+ ))
137
+ }
138
+ ),
139
+ document.body
140
+ )
141
+ ]
142
+ }
143
+ );
144
+ }
145
+ export {
146
+ RowActions
147
+ };
148
+ //# sourceMappingURL=RowActions.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/backend/RowActions.tsx"],
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { createPortal } from 'react-dom'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type RowActionItem = {\n id?: string\n label: string\n onSelect?: () => void\n href?: string\n destructive?: boolean\n}\n\nexport function RowActions({ items = [] }: { items?: RowActionItem[] }) {\n if (items.length === 0) return null\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const btnRef = React.useRef<HTMLButtonElement>(null)\n const menuRef = React.useRef<HTMLDivElement>(null)\n const hoverTimeoutRef = React.useRef<NodeJS.Timeout | null>(null)\n const [anchorRect, setAnchorRect] = React.useState<DOMRect | null>(null)\n const [direction, setDirection] = React.useState<'down' | 'up'>('down')\n\n const updatePosition = React.useCallback(() => {\n if (!btnRef.current) return\n const rect = btnRef.current.getBoundingClientRect()\n setAnchorRect(rect)\n // Decide whether to open up or down based on available viewport space\n const spaceBelow = window.innerHeight - rect.bottom\n const spaceAbove = rect.top\n setDirection(spaceBelow < 180 && spaceAbove > spaceBelow ? 'up' : 'down')\n }, [])\n\n React.useEffect(() => {\n if (!open) return\n updatePosition()\n function onDocClick(e: MouseEvent) {\n const t = e.target as Node\n if (menuRef.current && !menuRef.current.contains(t) && btnRef.current && !btnRef.current.contains(t)) {\n setOpen(false)\n }\n }\n function onKey(e: KeyboardEvent) {\n if (e.key === 'Escape') {\n setOpen(false)\n btnRef.current?.focus()\n }\n }\n function onScrollOrResize() {\n updatePosition()\n }\n document.addEventListener('mousedown', onDocClick)\n document.addEventListener('keydown', onKey)\n window.addEventListener('scroll', onScrollOrResize, true)\n window.addEventListener('resize', onScrollOrResize)\n return () => {\n document.removeEventListener('mousedown', onDocClick)\n document.removeEventListener('keydown', onKey)\n window.removeEventListener('scroll', onScrollOrResize, true)\n window.removeEventListener('resize', onScrollOrResize)\n }\n }, [open, updatePosition])\n\n // Cleanup timeout on unmount\n React.useEffect(() => {\n return () => {\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current)\n }\n }\n }, [])\n\n const handleMouseEnter = () => {\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current)\n }\n setOpen(true)\n }\n\n const handleMouseLeave = () => {\n hoverTimeoutRef.current = setTimeout(() => {\n setOpen(false)\n }, 150) // Small delay to prevent flickering when moving to menu\n }\n\n return (\n <div\n className=\"relative inline-block text-left\"\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n >\n <button\n ref={btnRef}\n type=\"button\"\n className=\"h-8 w-8 inline-flex items-center justify-center rounded hover:bg-accent\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n onClick={() => { setOpen((v) => !v); requestAnimationFrame(updatePosition) }}\n >\n <span aria-hidden=\"true\">\u22EF</span>\n <span className=\"sr-only\">{t('ui.rowActions.openActions', 'Open actions')}</span>\n </button>\n {open && anchorRect && createPortal(\n <div\n ref={menuRef}\n role=\"menu\"\n className=\"fixed w-44 rounded-md border bg-background p-1 shadow focus:outline-none z-[1000]\"\n style={{\n top: direction === 'down' ? anchorRect.bottom + 8 : anchorRect.top - 8,\n left: anchorRect.right,\n transform: `translate(-100%, ${direction === 'down' ? '0' : '-100%'})`,\n }}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n >\n {items.map((it, idx) => (\n it.href ? (\n <a\n key={idx}\n href={it.href}\n className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? 'text-red-600' : ''}`}\n role=\"menuitem\"\n onClick={(event) => {\n event.stopPropagation()\n setOpen(false)\n }}\n >\n {it.label}\n </a>\n ) : (\n <button\n key={idx}\n type=\"button\"\n className={`block w-full text-left px-2 py-1 text-sm rounded hover:bg-accent ${it.destructive ? 'text-red-600' : ''}`}\n role=\"menuitem\"\n onClick={(event) => {\n event.stopPropagation()\n setOpen(false)\n it.onSelect?.()\n }}\n >\n {it.label}\n </button>\n )\n ))}\n </div>,\n document.body\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["t"]
7
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+ import { Fragment, jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { SimpleTooltip } from "../primitives/tooltip.js";
5
+ import { cn } from "@open-mercato/shared/lib/utils";
6
+ function extractTextContent(node) {
7
+ if (node == null) return "";
8
+ if (typeof node === "string") return node;
9
+ if (typeof node === "number") return String(node);
10
+ if (typeof node === "boolean") return "";
11
+ if (Array.isArray(node)) {
12
+ return node.map(extractTextContent).join("");
13
+ }
14
+ if (React.isValidElement(node)) {
15
+ const props = node.props;
16
+ if (props) {
17
+ if (props.children != null) {
18
+ const childText = extractTextContent(props.children);
19
+ if (childText) return childText;
20
+ }
21
+ if (typeof props.value === "string") return props.value;
22
+ if (typeof props.label === "string") return props.label;
23
+ if (typeof props.title === "string") return props.title;
24
+ }
25
+ }
26
+ if (node && typeof node === "object" && "toString" in node) {
27
+ const str = String(node);
28
+ if (str !== "[object Object]") return str;
29
+ }
30
+ return "";
31
+ }
32
+ function TruncatedCell({
33
+ children,
34
+ maxWidth = "max-w-[150px]",
35
+ className,
36
+ tooltipContent,
37
+ disabled = false
38
+ }) {
39
+ const contentRef = React.useRef(null);
40
+ const [isTruncated, setIsTruncated] = React.useState(false);
41
+ const resolvedTooltipContent = tooltipContent ?? extractTextContent(children);
42
+ React.useEffect(() => {
43
+ const checkTruncation = () => {
44
+ const el2 = contentRef.current;
45
+ if (el2) {
46
+ setIsTruncated(el2.scrollWidth > el2.clientWidth);
47
+ }
48
+ };
49
+ checkTruncation();
50
+ const el = contentRef.current;
51
+ if (el) {
52
+ const observer = new ResizeObserver(checkTruncation);
53
+ observer.observe(el);
54
+ return () => observer.disconnect();
55
+ }
56
+ }, [children, maxWidth]);
57
+ if (disabled) {
58
+ return /* @__PURE__ */ jsx(Fragment, { children });
59
+ }
60
+ const isTailwindClass = maxWidth.startsWith("max-w-");
61
+ const styleMaxWidth = isTailwindClass ? void 0 : maxWidth;
62
+ const classMaxWidth = isTailwindClass ? maxWidth : "";
63
+ const content = /* @__PURE__ */ jsx(
64
+ "div",
65
+ {
66
+ ref: contentRef,
67
+ className: cn(
68
+ "overflow-hidden text-ellipsis whitespace-nowrap",
69
+ classMaxWidth,
70
+ className
71
+ ),
72
+ style: styleMaxWidth ? { maxWidth: styleMaxWidth } : void 0,
73
+ children
74
+ }
75
+ );
76
+ if (!resolvedTooltipContent || !isTruncated) {
77
+ return content;
78
+ }
79
+ return /* @__PURE__ */ jsx(
80
+ SimpleTooltip,
81
+ {
82
+ content: resolvedTooltipContent,
83
+ side: "top",
84
+ delayDuration: 300,
85
+ children: content
86
+ }
87
+ );
88
+ }
89
+ export {
90
+ TruncatedCell
91
+ };
92
+ //# sourceMappingURL=TruncatedCell.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/backend/TruncatedCell.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { SimpleTooltip } from '../primitives/tooltip'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type TruncatedCellProps = {\n children: React.ReactNode\n /** Maximum width for the cell content. Can be a Tailwind class (e.g., 'max-w-[200px]') or CSS value */\n maxWidth?: string\n /** Custom class name for the wrapper */\n className?: string\n /** Tooltip content - if not provided, will try to extract text from children */\n tooltipContent?: React.ReactNode\n /** Disable truncation and tooltip */\n disabled?: boolean\n}\n\n/**\n * Extracts text content from React nodes for tooltip display\n */\nfunction extractTextContent(node: React.ReactNode): string {\n if (node == null) return ''\n if (typeof node === 'string') return node\n if (typeof node === 'number') return String(node)\n if (typeof node === 'boolean') return ''\n if (Array.isArray(node)) {\n return node.map(extractTextContent).join('')\n }\n if (React.isValidElement(node)) {\n // Handle React elements - extract text from props.children\n const props = node.props as Record<string, unknown>\n if (props) {\n // First try children\n if (props.children != null) {\n const childText = extractTextContent(props.children as React.ReactNode)\n if (childText) return childText\n }\n // Try common text props\n if (typeof props.value === 'string') return props.value\n if (typeof props.label === 'string') return props.label\n if (typeof props.title === 'string') return props.title\n }\n }\n // Try to convert to string as last resort\n if (node && typeof node === 'object' && 'toString' in node) {\n const str = String(node)\n if (str !== '[object Object]') return str\n }\n return ''\n}\n\n/**\n * A cell wrapper that truncates content and shows a tooltip on hover\n * only when the content is wider than the available space.\n *\n * @example\n * <TruncatedCell maxWidth=\"max-w-[200px]\">\n * <span>This is a very long text that will be truncated</span>\n * </TruncatedCell>\n */\nexport function TruncatedCell({\n children,\n maxWidth = 'max-w-[150px]',\n className,\n tooltipContent,\n disabled = false,\n}: TruncatedCellProps) {\n const contentRef = React.useRef<HTMLDivElement>(null)\n const [isTruncated, setIsTruncated] = React.useState(false)\n\n // Get tooltip content - prefer explicit tooltipContent, fall back to extracting from children\n const resolvedTooltipContent = tooltipContent ?? extractTextContent(children)\n\n // Check if content is truncated after render and on resize\n React.useEffect(() => {\n const checkTruncation = () => {\n const el = contentRef.current\n if (el) {\n setIsTruncated(el.scrollWidth > el.clientWidth)\n }\n }\n\n // Check on mount\n checkTruncation()\n\n // Use ResizeObserver to detect size changes\n const el = contentRef.current\n if (el) {\n const observer = new ResizeObserver(checkTruncation)\n observer.observe(el)\n return () => observer.disconnect()\n }\n }, [children, maxWidth])\n\n if (disabled) {\n return <>{children}</>\n }\n\n // Determine if maxWidth is a Tailwind class or a CSS value\n const isTailwindClass = maxWidth.startsWith('max-w-')\n const styleMaxWidth = isTailwindClass ? undefined : maxWidth\n const classMaxWidth = isTailwindClass ? maxWidth : ''\n\n const content = (\n <div\n ref={contentRef}\n className={cn(\n 'overflow-hidden text-ellipsis whitespace-nowrap',\n classMaxWidth,\n className\n )}\n style={styleMaxWidth ? { maxWidth: styleMaxWidth } : undefined}\n >\n {children}\n </div>\n )\n\n // Only show tooltip when content is actually truncated\n if (!resolvedTooltipContent || !isTruncated) {\n return content\n }\n\n return (\n <SimpleTooltip\n content={resolvedTooltipContent}\n side=\"top\"\n delayDuration={300}\n >\n {content}\n </SimpleTooltip>\n )\n}\n"],
5
+ "mappings": ";AAgGW;AA9FX,YAAY,WAAW;AACvB,SAAS,qBAAqB;AAC9B,SAAS,UAAU;AAiBnB,SAAS,mBAAmB,MAA+B;AACzD,MAAI,QAAQ,KAAM,QAAO;AACzB,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,OAAO,SAAS,SAAU,QAAO,OAAO,IAAI;AAChD,MAAI,OAAO,SAAS,UAAW,QAAO;AACtC,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,WAAO,KAAK,IAAI,kBAAkB,EAAE,KAAK,EAAE;AAAA,EAC7C;AACA,MAAI,MAAM,eAAe,IAAI,GAAG;AAE9B,UAAM,QAAQ,KAAK;AACnB,QAAI,OAAO;AAET,UAAI,MAAM,YAAY,MAAM;AAC1B,cAAM,YAAY,mBAAmB,MAAM,QAA2B;AACtE,YAAI,UAAW,QAAO;AAAA,MACxB;AAEA,UAAI,OAAO,MAAM,UAAU,SAAU,QAAO,MAAM;AAClD,UAAI,OAAO,MAAM,UAAU,SAAU,QAAO,MAAM;AAClD,UAAI,OAAO,MAAM,UAAU,SAAU,QAAO,MAAM;AAAA,IACpD;AAAA,EACF;AAEA,MAAI,QAAQ,OAAO,SAAS,YAAY,cAAc,MAAM;AAC1D,UAAM,MAAM,OAAO,IAAI;AACvB,QAAI,QAAQ,kBAAmB,QAAO;AAAA,EACxC;AACA,SAAO;AACT;AAWO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAAuB;AACrB,QAAM,aAAa,MAAM,OAAuB,IAAI;AACpD,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,KAAK;AAG1D,QAAM,yBAAyB,kBAAkB,mBAAmB,QAAQ;AAG5E,QAAM,UAAU,MAAM;AACpB,UAAM,kBAAkB,MAAM;AAC5B,YAAMA,MAAK,WAAW;AACtB,UAAIA,KAAI;AACN,uBAAeA,IAAG,cAAcA,IAAG,WAAW;AAAA,MAChD;AAAA,IACF;AAGA,oBAAgB;AAGhB,UAAM,KAAK,WAAW;AACtB,QAAI,IAAI;AACN,YAAM,WAAW,IAAI,eAAe,eAAe;AACnD,eAAS,QAAQ,EAAE;AACnB,aAAO,MAAM,SAAS,WAAW;AAAA,IACnC;AAAA,EACF,GAAG,CAAC,UAAU,QAAQ,CAAC;AAEvB,MAAI,UAAU;AACZ,WAAO,gCAAG,UAAS;AAAA,EACrB;AAGA,QAAM,kBAAkB,SAAS,WAAW,QAAQ;AACpD,QAAM,gBAAgB,kBAAkB,SAAY;AACpD,QAAM,gBAAgB,kBAAkB,WAAW;AAEnD,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA,OAAO,gBAAgB,EAAE,UAAU,cAAc,IAAI;AAAA,MAEpD;AAAA;AAAA,EACH;AAIF,MAAI,CAAC,0BAA0B,CAAC,aAAa;AAC3C,WAAO;AAAA,EACT;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAS;AAAA,MACT,MAAK;AAAA,MACL,eAAe;AAAA,MAEd;AAAA;AAAA,EACH;AAEJ;",
6
+ "names": ["el"]
7
+ }