@open-mercato/ui 0.5.1-develop.2663.2c29774b5b → 0.5.1-develop.2681.c559bb2bc3

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 (53) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/backend/CrudForm.js +187 -39
  3. package/dist/backend/CrudForm.js.map +2 -2
  4. package/dist/backend/Page.js +12 -4
  5. package/dist/backend/Page.js.map +2 -2
  6. package/dist/backend/confirm-dialog/ConfirmDialog.js +7 -4
  7. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  8. package/dist/backend/crud/CollapsibleGroup.js +88 -0
  9. package/dist/backend/crud/CollapsibleGroup.js.map +7 -0
  10. package/dist/backend/crud/CollapsibleZoneLayout.js +178 -0
  11. package/dist/backend/crud/CollapsibleZoneLayout.js.map +7 -0
  12. package/dist/backend/crud/useGroupCollapse.js +24 -0
  13. package/dist/backend/crud/useGroupCollapse.js.map +7 -0
  14. package/dist/backend/crud/useGroupOrder.js +61 -0
  15. package/dist/backend/crud/useGroupOrder.js.map +7 -0
  16. package/dist/backend/crud/usePersistedBooleanFlag.js +29 -0
  17. package/dist/backend/crud/usePersistedBooleanFlag.js.map +7 -0
  18. package/dist/backend/crud/useZoneCollapse.js +24 -0
  19. package/dist/backend/crud/useZoneCollapse.js.map +7 -0
  20. package/dist/backend/detail/AttachmentsSection.js +77 -33
  21. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  22. package/dist/backend/detail/NotesSection.js +82 -6
  23. package/dist/backend/detail/NotesSection.js.map +2 -2
  24. package/dist/backend/icons/lucideRegistry.generated.js +16 -2
  25. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  26. package/dist/backend/inputs/SwitchableMarkdownInput.js +3 -1
  27. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
  28. package/dist/primitives/avatar.js +59 -0
  29. package/dist/primitives/avatar.js.map +7 -0
  30. package/package.json +3 -3
  31. package/src/backend/CrudForm.tsx +230 -21
  32. package/src/backend/Page.tsx +20 -4
  33. package/src/backend/__tests__/AttachmentsSection.test.tsx +82 -0
  34. package/src/backend/__tests__/CollapsibleZoneLayout.test.tsx +171 -0
  35. package/src/backend/__tests__/CrudForm.validation.test.tsx +4 -4
  36. package/src/backend/__tests__/NotesSection.test.tsx +63 -0
  37. package/src/backend/confirm-dialog/ConfirmDialog.tsx +9 -4
  38. package/src/backend/crud/CollapsibleGroup.tsx +111 -0
  39. package/src/backend/crud/CollapsibleZoneLayout.tsx +234 -0
  40. package/src/backend/crud/__tests__/useGroupCollapse.test.ts +38 -0
  41. package/src/backend/crud/__tests__/useGroupOrder.test.ts +63 -0
  42. package/src/backend/crud/__tests__/usePersistedBooleanFlag.test.ts +49 -0
  43. package/src/backend/crud/__tests__/useZoneCollapse.test.ts +31 -0
  44. package/src/backend/crud/useGroupCollapse.ts +22 -0
  45. package/src/backend/crud/useGroupOrder.ts +74 -0
  46. package/src/backend/crud/usePersistedBooleanFlag.ts +35 -0
  47. package/src/backend/crud/useZoneCollapse.ts +22 -0
  48. package/src/backend/detail/AttachmentsSection.tsx +81 -38
  49. package/src/backend/detail/NotesSection.tsx +99 -6
  50. package/src/backend/icons/lucideRegistry.generated.tsx +16 -2
  51. package/src/backend/inputs/SwitchableMarkdownInput.tsx +3 -1
  52. package/src/primitives/__tests__/avatar.test.tsx +64 -0
  53. package/src/primitives/avatar.tsx +75 -0
@@ -1,7 +1,11 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "@open-mercato/shared/lib/utils";
3
- function Page({ children, className }) {
4
- return /* @__PURE__ */ jsx("div", { className: cn("space-y-6", className), children });
3
+ function Page({
4
+ children,
5
+ className,
6
+ ...props
7
+ }) {
8
+ return /* @__PURE__ */ jsx("div", { className: cn("space-y-6", className), ...props, children });
5
9
  }
6
10
  function PageHeader({
7
11
  title,
@@ -16,8 +20,12 @@ function PageHeader({
16
20
  actions ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: actions }) : null
17
21
  ] });
18
22
  }
19
- function PageBody({ children, className }) {
20
- return /* @__PURE__ */ jsx("div", { className: cn("space-y-4", className), children });
23
+ function PageBody({
24
+ children,
25
+ className,
26
+ ...props
27
+ }) {
28
+ return /* @__PURE__ */ jsx("div", { className: cn("space-y-4", className), ...props, children });
21
29
  }
22
30
  export {
23
31
  Page,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/backend/Page.tsx"],
4
- "sourcesContent": ["import * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport function Page({ children, className }: { children: React.ReactNode; className?: string }) {\n return <div className={cn('space-y-6', className)}>{children}</div>\n}\n\nexport function PageHeader({\n title,\n description,\n actions,\n}: {\n title: string\n description?: string\n actions?: React.ReactNode\n}) {\n return (\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\">\n <div className=\"min-w-0\">\n <h1 className=\"text-xl sm:text-2xl font-semibold leading-tight\">{title}</h1>\n {description ? <p className=\"text-sm text-muted-foreground mt-1\">{description}</p> : null}\n </div>\n {actions ? <div className=\"flex flex-wrap items-center gap-2\">{actions}</div> : null}\n </div>\n )\n}\n\nexport function PageBody({ children, className }: { children: React.ReactNode; className?: string }) {\n return <div className={cn('space-y-4', className)}>{children}</div>\n}\n"],
5
- "mappings": "AAIS,cAcH,YAdG;AAHT,SAAS,UAAU;AAEZ,SAAS,KAAK,EAAE,UAAU,UAAU,GAAsD;AAC/F,SAAO,oBAAC,SAAI,WAAW,GAAG,aAAa,SAAS,GAAI,UAAS;AAC/D;AAEO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,qBAAC,SAAI,WAAU,8EACb;AAAA,yBAAC,SAAI,WAAU,WACb;AAAA,0BAAC,QAAG,WAAU,mDAAmD,iBAAM;AAAA,MACtE,cAAc,oBAAC,OAAE,WAAU,sCAAsC,uBAAY,IAAO;AAAA,OACvF;AAAA,IACC,UAAU,oBAAC,SAAI,WAAU,qCAAqC,mBAAQ,IAAS;AAAA,KAClF;AAEJ;AAEO,SAAS,SAAS,EAAE,UAAU,UAAU,GAAsD;AACnG,SAAO,oBAAC,SAAI,WAAW,GAAG,aAAa,SAAS,GAAI,UAAS;AAC/D;",
4
+ "sourcesContent": ["import * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport function Page({\n children,\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div className={cn('space-y-6', className)} {...props}>\n {children}\n </div>\n )\n}\n\nexport function PageHeader({\n title,\n description,\n actions,\n}: {\n title: string\n description?: string\n actions?: React.ReactNode\n}) {\n return (\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4\">\n <div className=\"min-w-0\">\n <h1 className=\"text-xl sm:text-2xl font-semibold leading-tight\">{title}</h1>\n {description ? <p className=\"text-sm text-muted-foreground mt-1\">{description}</p> : null}\n </div>\n {actions ? <div className=\"flex flex-wrap items-center gap-2\">{actions}</div> : null}\n </div>\n )\n}\n\nexport function PageBody({\n children,\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <div className={cn('space-y-4', className)} {...props}>\n {children}\n </div>\n )\n}\n"],
5
+ "mappings": "AASI,cAiBE,YAjBF;AARJ,SAAS,UAAU;AAEZ,SAAS,KAAK;AAAA,EACnB;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyC;AACvC,SACE,oBAAC,SAAI,WAAW,GAAG,aAAa,SAAS,GAAI,GAAG,OAC7C,UACH;AAEJ;AAEO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,qBAAC,SAAI,WAAU,8EACb;AAAA,yBAAC,SAAI,WAAU,WACb;AAAA,0BAAC,QAAG,WAAU,mDAAmD,iBAAM;AAAA,MACtE,cAAc,oBAAC,OAAE,WAAU,sCAAsC,uBAAY,IAAO;AAAA,OACvF;AAAA,IACC,UAAU,oBAAC,SAAI,WAAU,qCAAqC,mBAAQ,IAAS;AAAA,KAClF;AAEJ;AAEO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyC;AACvC,SACE,oBAAC,SAAI,WAAW,GAAG,aAAa,SAAS,GAAI,GAAG,OAC7C,UACH;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -24,6 +24,9 @@ function ConfirmDialog({
24
24
  const [internalOpen, setInternalOpen] = React.useState(false);
25
25
  const cancelButtonRef = React.useRef(null);
26
26
  const confirmButtonRef = React.useRef(null);
27
+ const reactId = React.useId();
28
+ const titleId = `confirm-dialog-title-${reactId}`;
29
+ const descriptionId = `confirm-dialog-description-${reactId}`;
27
30
  const isControlled = controlledOpen !== void 0;
28
31
  const open = isControlled ? controlledOpen : internalOpen;
29
32
  const setOpen = isControlled ? onOpenChange || (() => {
@@ -111,8 +114,8 @@ function ConfirmDialog({
111
114
  {
112
115
  ref: dialogRef,
113
116
  role: "alertdialog",
114
- "aria-labelledby": "confirm-dialog-title",
115
- "aria-describedby": text ? "confirm-dialog-description" : void 0,
117
+ "aria-labelledby": titleId,
118
+ "aria-describedby": text ? descriptionId : void 0,
116
119
  onClick: handleBackdropClick,
117
120
  className: cn(
118
121
  // Reset dialog defaults
@@ -157,7 +160,7 @@ function ConfirmDialog({
157
160
  /* @__PURE__ */ jsx(
158
161
  "h2",
159
162
  {
160
- id: "confirm-dialog-title",
163
+ id: titleId,
161
164
  className: cn(
162
165
  "text-sm font-medium leading-snug tracking-tight pr-6",
163
166
  // Mobile: centered, Desktop: left-aligned
@@ -169,7 +172,7 @@ function ConfirmDialog({
169
172
  text && /* @__PURE__ */ jsx(
170
173
  "p",
171
174
  {
172
- id: "confirm-dialog-description",
175
+ id: descriptionId,
173
176
  className: "text-sm font-medium leading-snug text-muted-foreground",
174
177
  children: text
175
178
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/confirm-dialog/ConfirmDialog.tsx"],
4
- "sourcesContent": ["\"use client\";\nimport * as React from \"react\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { IconButton } from \"../../primitives/icon-button\";\nimport { cn } from \"@open-mercato/shared/lib/utils\";\nimport { Loader2, X } from \"lucide-react\";\n\nexport type ConfirmDialogProps = {\n /** Whether the dialog is open (controlled mode \u2014 used by useConfirmDialog) */\n open?: boolean;\n /** Callback when open state changes (controlled mode) */\n onOpenChange?: (open: boolean) => void;\n /** Callback when user confirms */\n onConfirm: () => void | Promise<void>;\n /** Callback when user cancels (optional, defaults to closing) */\n onCancel?: () => void;\n /** Dialog title. Defaults to i18n key \"ui.dialogs.confirm.defaultTitle\" (\"Are you sure?\") */\n title?: string;\n /** Dialog body text / description */\n text?: string;\n /** Confirm button label. Defaults to i18n \"ui.dialogs.confirm.confirmText\" (\"Confirm\").\n * Pass `false` to hide the confirm button entirely. */\n confirmText?: string | false;\n /** Cancel button label. Defaults to i18n \"ui.dialogs.confirm.cancelText\" (\"Cancel\").\n * Pass `false` to hide the cancel button entirely. */\n cancelText?: string | false;\n /** Visual variant \u2014 \"destructive\" renders the confirm button in red */\n variant?: \"default\" | \"destructive\";\n /** Whether the confirm button shows a loading spinner.\n * Useful for async onConfirm handlers (e.g., waiting for API response before closing). */\n loading?: boolean;\n /** Trigger element \u2014 when provided, component manages its own open state (declarative mode).\n * Clicking the trigger opens the dialog. */\n trigger?: React.ReactNode;\n};\n\nexport function ConfirmDialog({\n open: controlledOpen,\n onOpenChange,\n onConfirm,\n onCancel,\n title,\n text,\n confirmText,\n cancelText,\n variant = \"default\",\n loading = false,\n trigger,\n}: ConfirmDialogProps) {\n const t = useT();\n const dialogRef = React.useRef<HTMLDialogElement>(null);\n const [internalOpen, setInternalOpen] = React.useState(false);\n const cancelButtonRef = React.useRef<HTMLButtonElement>(null);\n const confirmButtonRef = React.useRef<HTMLButtonElement>(null);\n\n // Determine if we're in controlled mode (open prop provided) or declarative mode (trigger provided)\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n const setOpen = isControlled\n ? onOpenChange || (() => {})\n : setInternalOpen;\n const handleCancelCallback = React.useCallback(() => {\n if (!isControlled) {\n onCancel?.();\n }\n }, [isControlled, onCancel]);\n\n // Default text values from i18n\n const resolvedTitle =\n title ?? t(\"ui.dialogs.confirm.defaultTitle\", \"Are you sure?\");\n const resolvedConfirmText =\n confirmText === false\n ? false\n : confirmText ?? t(\"ui.dialogs.confirm.confirmText\", \"Confirm\");\n const resolvedCancelText =\n cancelText === false\n ? false\n : cancelText ?? t(\"ui.dialogs.confirm.cancelText\", \"Cancel\");\n const closeAriaLabel = t(\"ui.dialog.close.ariaLabel\", \"Close\");\n const requestClose = React.useCallback(() => {\n setOpen(false);\n handleCancelCallback();\n }, [setOpen, handleCancelCallback]);\n\n // Handle dialog open/close with native showModal/close\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (open) {\n if (!dialog.open) {\n dialog.showModal();\n // Focus cancel button (safe default) or confirm if no cancel\n setTimeout(() => {\n if (resolvedCancelText !== false && cancelButtonRef.current) {\n cancelButtonRef.current.focus();\n } else if (confirmButtonRef.current) {\n confirmButtonRef.current.focus();\n }\n }, 0);\n }\n } else {\n if (dialog.open) {\n dialog.close();\n }\n }\n }, [open, resolvedCancelText]);\n\n // Handle native cancel event (Escape key)\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n const handleCancel = (e: Event) => {\n // Prevent close if loading\n if (loading) {\n e.preventDefault();\n return;\n }\n requestClose();\n };\n\n dialog.addEventListener(\"cancel\", handleCancel);\n return () => dialog.removeEventListener(\"cancel\", handleCancel);\n }, [loading, requestClose]);\n\n // Handle backdrop click\n const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {\n // Only close if clicking directly on the dialog (backdrop), not its children\n if (e.target === dialogRef.current && !loading) {\n requestClose();\n }\n };\n\n // Handle keyboard shortcuts\n React.useEffect(() => {\n if (!open) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Cmd/Ctrl+Enter confirms\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\" && !loading) {\n e.preventDefault();\n handleConfirm();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [open, loading]);\n\n const handleConfirm = async () => {\n await onConfirm();\n // Don't auto-close if loading \u2014 let the parent control when to close\n if (!loading) {\n setOpen(false);\n }\n };\n\n const handleCancel = () => {\n requestClose();\n };\n\n const handleTriggerClick = () => {\n if (trigger) {\n setOpen(true);\n }\n };\n\n return (\n <>\n {trigger && (\n <div onClick={handleTriggerClick} className=\"inline-block\" role=\"presentation\">\n {trigger}\n </div>\n )}\n\n <dialog\n ref={dialogRef}\n role=\"alertdialog\"\n aria-labelledby=\"confirm-dialog-title\"\n aria-describedby={text ? \"confirm-dialog-description\" : undefined}\n onClick={handleBackdropClick}\n className={cn(\n // Reset dialog defaults\n \"m-0 p-0 max-w-none bg-transparent border-none\",\n // Backdrop styling\n \"backdrop:bg-black/50 backdrop:backdrop-blur-sm backdrop:transition-opacity\",\n // Mobile: bottom sheet\n \"fixed inset-x-0 bottom-0 top-auto w-full\",\n // Desktop: centered\n \"sm:inset-auto sm:mx-auto sm:my-auto sm:max-w-md sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2\",\n // Animation with reduced motion support\n \"motion-safe:open:animate-in motion-safe:open:fade-in-0 motion-safe:open:slide-in-from-bottom-4\",\n \"sm:motion-safe:open:slide-in-from-bottom-0 sm:motion-safe:open:zoom-in-95\",\n // Duration\n \"motion-safe:open:duration-300\"\n )}\n >\n <div\n role=\"document\"\n className={cn(\n // Panel container\n \"flex flex-col gap-4 rounded-t-2xl border-t bg-card p-6 text-foreground shadow-lg\",\n \"sm:rounded-xl sm:border\",\n // Relative positioning for close button\n \"relative\"\n )}\n >\n {/* Close button */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleCancel}\n disabled={loading}\n aria-label={closeAriaLabel}\n className=\"absolute right-4 top-4 opacity-70 hover:opacity-100\"\n >\n <X className=\"h-4 w-4\" />\n </IconButton>\n\n {/* Title */}\n <h2\n id=\"confirm-dialog-title\"\n className={cn(\n \"text-sm font-medium leading-snug tracking-tight pr-6\",\n // Mobile: centered, Desktop: left-aligned\n \"text-center sm:text-left\"\n )}\n >\n {resolvedTitle}\n </h2>\n\n {/* Description (optional) */}\n {text && (\n <p\n id=\"confirm-dialog-description\"\n className=\"text-sm font-medium leading-snug text-muted-foreground\"\n >\n {text}\n </p>\n )}\n\n {/* Actions */}\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n {resolvedCancelText !== false && (\n <Button\n ref={cancelButtonRef}\n variant=\"outline\"\n onClick={handleCancel}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {resolvedCancelText}\n </Button>\n )}\n {resolvedConfirmText !== false && (\n <Button\n ref={confirmButtonRef}\n variant={variant === \"destructive\" ? \"destructive\" : \"default\"}\n onClick={handleConfirm}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {loading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {resolvedConfirmText}\n </Button>\n )}\n </div>\n </div>\n </dialog>\n </>\n );\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\";\nimport * as React from \"react\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { IconButton } from \"../../primitives/icon-button\";\nimport { cn } from \"@open-mercato/shared/lib/utils\";\nimport { Loader2, X } from \"lucide-react\";\n\nexport type ConfirmDialogProps = {\n /** Whether the dialog is open (controlled mode \u2014 used by useConfirmDialog) */\n open?: boolean;\n /** Callback when open state changes (controlled mode) */\n onOpenChange?: (open: boolean) => void;\n /** Callback when user confirms */\n onConfirm: () => void | Promise<void>;\n /** Callback when user cancels (optional, defaults to closing) */\n onCancel?: () => void;\n /** Dialog title. Defaults to i18n key \"ui.dialogs.confirm.defaultTitle\" (\"Are you sure?\") */\n title?: string;\n /** Dialog body text / description */\n text?: string;\n /** Confirm button label. Defaults to i18n \"ui.dialogs.confirm.confirmText\" (\"Confirm\").\n * Pass `false` to hide the confirm button entirely. */\n confirmText?: string | false;\n /** Cancel button label. Defaults to i18n \"ui.dialogs.confirm.cancelText\" (\"Cancel\").\n * Pass `false` to hide the cancel button entirely. */\n cancelText?: string | false;\n /** Visual variant \u2014 \"destructive\" renders the confirm button in red */\n variant?: \"default\" | \"destructive\";\n /** Whether the confirm button shows a loading spinner.\n * Useful for async onConfirm handlers (e.g., waiting for API response before closing). */\n loading?: boolean;\n /** Trigger element \u2014 when provided, component manages its own open state (declarative mode).\n * Clicking the trigger opens the dialog. */\n trigger?: React.ReactNode;\n};\n\nexport function ConfirmDialog({\n open: controlledOpen,\n onOpenChange,\n onConfirm,\n onCancel,\n title,\n text,\n confirmText,\n cancelText,\n variant = \"default\",\n loading = false,\n trigger,\n}: ConfirmDialogProps) {\n const t = useT();\n const dialogRef = React.useRef<HTMLDialogElement>(null);\n const [internalOpen, setInternalOpen] = React.useState(false);\n const cancelButtonRef = React.useRef<HTMLButtonElement>(null);\n const confirmButtonRef = React.useRef<HTMLButtonElement>(null);\n // Unique IDs so multiple ConfirmDialog instances on the same page don't\n // collide and make `aria-labelledby` resolve to the wrong dialog's title.\n const reactId = React.useId();\n const titleId = `confirm-dialog-title-${reactId}`;\n const descriptionId = `confirm-dialog-description-${reactId}`;\n\n // Determine if we're in controlled mode (open prop provided) or declarative mode (trigger provided)\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n const setOpen = isControlled\n ? onOpenChange || (() => {})\n : setInternalOpen;\n const handleCancelCallback = React.useCallback(() => {\n if (!isControlled) {\n onCancel?.();\n }\n }, [isControlled, onCancel]);\n\n // Default text values from i18n\n const resolvedTitle =\n title ?? t(\"ui.dialogs.confirm.defaultTitle\", \"Are you sure?\");\n const resolvedConfirmText =\n confirmText === false\n ? false\n : confirmText ?? t(\"ui.dialogs.confirm.confirmText\", \"Confirm\");\n const resolvedCancelText =\n cancelText === false\n ? false\n : cancelText ?? t(\"ui.dialogs.confirm.cancelText\", \"Cancel\");\n const closeAriaLabel = t(\"ui.dialog.close.ariaLabel\", \"Close\");\n const requestClose = React.useCallback(() => {\n setOpen(false);\n handleCancelCallback();\n }, [setOpen, handleCancelCallback]);\n\n // Handle dialog open/close with native showModal/close\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (open) {\n if (!dialog.open) {\n dialog.showModal();\n // Focus cancel button (safe default) or confirm if no cancel\n setTimeout(() => {\n if (resolvedCancelText !== false && cancelButtonRef.current) {\n cancelButtonRef.current.focus();\n } else if (confirmButtonRef.current) {\n confirmButtonRef.current.focus();\n }\n }, 0);\n }\n } else {\n if (dialog.open) {\n dialog.close();\n }\n }\n }, [open, resolvedCancelText]);\n\n // Handle native cancel event (Escape key)\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n const handleCancel = (e: Event) => {\n // Prevent close if loading\n if (loading) {\n e.preventDefault();\n return;\n }\n requestClose();\n };\n\n dialog.addEventListener(\"cancel\", handleCancel);\n return () => dialog.removeEventListener(\"cancel\", handleCancel);\n }, [loading, requestClose]);\n\n // Handle backdrop click\n const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {\n // Only close if clicking directly on the dialog (backdrop), not its children\n if (e.target === dialogRef.current && !loading) {\n requestClose();\n }\n };\n\n // Handle keyboard shortcuts\n React.useEffect(() => {\n if (!open) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Cmd/Ctrl+Enter confirms\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\" && !loading) {\n e.preventDefault();\n handleConfirm();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [open, loading]);\n\n const handleConfirm = async () => {\n await onConfirm();\n // Don't auto-close if loading \u2014 let the parent control when to close\n if (!loading) {\n setOpen(false);\n }\n };\n\n const handleCancel = () => {\n requestClose();\n };\n\n const handleTriggerClick = () => {\n if (trigger) {\n setOpen(true);\n }\n };\n\n return (\n <>\n {trigger && (\n <div onClick={handleTriggerClick} className=\"inline-block\" role=\"presentation\">\n {trigger}\n </div>\n )}\n\n <dialog\n ref={dialogRef}\n role=\"alertdialog\"\n aria-labelledby={titleId}\n aria-describedby={text ? descriptionId : undefined}\n onClick={handleBackdropClick}\n className={cn(\n // Reset dialog defaults\n \"m-0 p-0 max-w-none bg-transparent border-none\",\n // Backdrop styling\n \"backdrop:bg-black/50 backdrop:backdrop-blur-sm backdrop:transition-opacity\",\n // Mobile: bottom sheet\n \"fixed inset-x-0 bottom-0 top-auto w-full\",\n // Desktop: centered\n \"sm:inset-auto sm:mx-auto sm:my-auto sm:max-w-md sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2\",\n // Animation with reduced motion support\n \"motion-safe:open:animate-in motion-safe:open:fade-in-0 motion-safe:open:slide-in-from-bottom-4\",\n \"sm:motion-safe:open:slide-in-from-bottom-0 sm:motion-safe:open:zoom-in-95\",\n // Duration\n \"motion-safe:open:duration-300\"\n )}\n >\n <div\n role=\"document\"\n className={cn(\n // Panel container\n \"flex flex-col gap-4 rounded-t-2xl border-t bg-card p-6 text-foreground shadow-lg\",\n \"sm:rounded-xl sm:border\",\n // Relative positioning for close button\n \"relative\"\n )}\n >\n {/* Close button */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleCancel}\n disabled={loading}\n aria-label={closeAriaLabel}\n className=\"absolute right-4 top-4 opacity-70 hover:opacity-100\"\n >\n <X className=\"h-4 w-4\" />\n </IconButton>\n\n {/* Title */}\n <h2\n id={titleId}\n className={cn(\n \"text-sm font-medium leading-snug tracking-tight pr-6\",\n // Mobile: centered, Desktop: left-aligned\n \"text-center sm:text-left\"\n )}\n >\n {resolvedTitle}\n </h2>\n\n {/* Description (optional) */}\n {text && (\n <p\n id={descriptionId}\n className=\"text-sm font-medium leading-snug text-muted-foreground\"\n >\n {text}\n </p>\n )}\n\n {/* Actions */}\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n {resolvedCancelText !== false && (\n <Button\n ref={cancelButtonRef}\n variant=\"outline\"\n onClick={handleCancel}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {resolvedCancelText}\n </Button>\n )}\n {resolvedConfirmText !== false && (\n <Button\n ref={confirmButtonRef}\n variant={variant === \"destructive\" ? \"destructive\" : \"default\"}\n onClick={handleConfirm}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {loading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {resolvedConfirmText}\n </Button>\n )}\n </div>\n </div>\n </dialog>\n </>\n );\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["handleCancel"]
7
7
  }
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { ChevronDown } from "lucide-react";
5
+ import { cn } from "@open-mercato/shared/lib/utils";
6
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
7
+ import { Button } from "../../primitives/button.js";
8
+ import { useGroupCollapse } from "./useGroupCollapse.js";
9
+ const CollapsibleGroup = React.forwardRef(
10
+ function CollapsibleGroup2({ groupId, title, pageType, defaultExpanded = true, errorCount = 0, fieldCount, chevronPosition = "right", icon, children }, ref) {
11
+ const t = useT();
12
+ const { expanded, toggle, setExpanded } = useGroupCollapse(pageType, groupId, defaultExpanded);
13
+ const contentId = `collapsible-group-${groupId}`;
14
+ React.useImperativeHandle(ref, () => ({
15
+ expand: () => setExpanded(true)
16
+ }), [setExpanded]);
17
+ const chevronIcon = /* @__PURE__ */ jsx(
18
+ ChevronDown,
19
+ {
20
+ className: cn(
21
+ "size-4 shrink-0 motion-safe:transition-transform motion-safe:duration-200",
22
+ expanded && "rotate-180"
23
+ )
24
+ }
25
+ );
26
+ const fieldCountLabel = typeof fieldCount === "number" && fieldCount > 0 ? /* @__PURE__ */ jsxs("span", { className: "text-xs font-normal text-muted-foreground", children: [
27
+ "\xB7 ",
28
+ fieldCount,
29
+ " ",
30
+ fieldCount === 1 ? t("ui.collapsible.fieldSingular", "field") : t("ui.collapsible.fieldPlural", "fields")
31
+ ] }) : null;
32
+ const errorBadge = errorCount > 0 ? /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-destructive/10 px-2 py-0.5 text-xs font-medium text-destructive", children: errorCount === 1 ? t("ui.collapsible.errorSingular", "{{count}} error", { count: errorCount }) : t("ui.collapsible.errorPlural", "{{count}} errors", { count: errorCount }) }) : null;
33
+ return /* @__PURE__ */ jsxs("div", { className: cn("rounded-lg border bg-card", errorCount > 0 && "border-destructive"), children: [
34
+ title && /* @__PURE__ */ jsx(
35
+ Button,
36
+ {
37
+ type: "button",
38
+ variant: "muted",
39
+ onClick: toggle,
40
+ className: cn(
41
+ "w-full px-4 py-3 text-sm font-medium hover:bg-accent/50 rounded-lg",
42
+ chevronPosition === "left" ? "justify-start gap-2" : "justify-between"
43
+ ),
44
+ "aria-expanded": expanded,
45
+ "aria-controls": contentId,
46
+ children: chevronPosition === "left" ? /* @__PURE__ */ jsxs(Fragment, { children: [
47
+ chevronIcon,
48
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
49
+ icon && /* @__PURE__ */ jsxs("span", { className: "relative shrink-0 text-muted-foreground", children: [
50
+ icon,
51
+ !expanded && errorCount > 0 && /* @__PURE__ */ jsx("span", { className: "absolute -right-0.5 -top-0.5 size-2 rounded-full bg-destructive" })
52
+ ] }),
53
+ /* @__PURE__ */ jsx("span", { children: title }),
54
+ fieldCountLabel,
55
+ errorBadge
56
+ ] })
57
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
58
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
59
+ icon && /* @__PURE__ */ jsxs("span", { className: "relative shrink-0 text-muted-foreground", children: [
60
+ icon,
61
+ !expanded && errorCount > 0 && /* @__PURE__ */ jsx("span", { className: "absolute -right-0.5 -top-0.5 size-2 rounded-full bg-destructive" })
62
+ ] }),
63
+ /* @__PURE__ */ jsx("span", { children: title }),
64
+ fieldCountLabel,
65
+ errorBadge
66
+ ] }),
67
+ chevronIcon
68
+ ] })
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsx(
72
+ "div",
73
+ {
74
+ id: contentId,
75
+ className: cn(
76
+ "motion-safe:transition-all motion-safe:duration-200 overflow-hidden",
77
+ expanded ? "max-h-[5000px] opacity-100" : "max-h-0 opacity-0"
78
+ ),
79
+ children: /* @__PURE__ */ jsx("div", { className: "px-4 py-3", children })
80
+ }
81
+ )
82
+ ] });
83
+ }
84
+ );
85
+ export {
86
+ CollapsibleGroup
87
+ };
88
+ //# sourceMappingURL=CollapsibleGroup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/crud/CollapsibleGroup.tsx"],
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { ChevronDown } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\nimport { useGroupCollapse } from './useGroupCollapse'\n\nexport interface CollapsibleGroupProps {\n groupId: string\n title?: string\n pageType: string\n defaultExpanded?: boolean\n errorCount?: number\n fieldCount?: number\n chevronPosition?: 'left' | 'right'\n icon?: React.ReactNode\n children: React.ReactNode\n}\n\nexport interface CollapsibleGroupHandle {\n expand: () => void\n}\n\nexport const CollapsibleGroup = React.forwardRef<CollapsibleGroupHandle, CollapsibleGroupProps>(\n function CollapsibleGroup({ groupId, title, pageType, defaultExpanded = true, errorCount = 0, fieldCount, chevronPosition = 'right', icon, children }, ref) {\n const t = useT()\n const { expanded, toggle, setExpanded } = useGroupCollapse(pageType, groupId, defaultExpanded)\n const contentId = `collapsible-group-${groupId}`\n\n React.useImperativeHandle(ref, () => ({\n expand: () => setExpanded(true),\n }), [setExpanded])\n\n const chevronIcon = (\n <ChevronDown\n className={cn(\n 'size-4 shrink-0 motion-safe:transition-transform motion-safe:duration-200',\n expanded && 'rotate-180'\n )}\n />\n )\n\n const fieldCountLabel = typeof fieldCount === 'number' && fieldCount > 0 ? (\n <span className=\"text-xs font-normal text-muted-foreground\">\n \u00B7 {fieldCount} {fieldCount === 1\n ? t('ui.collapsible.fieldSingular', 'field')\n : t('ui.collapsible.fieldPlural', 'fields')}\n </span>\n ) : null\n\n const errorBadge = errorCount > 0 ? (\n <span className=\"inline-flex items-center rounded-full bg-destructive/10 px-2 py-0.5 text-xs font-medium text-destructive\">\n {errorCount === 1\n ? t('ui.collapsible.errorSingular', '{{count}} error', { count: errorCount })\n : t('ui.collapsible.errorPlural', '{{count}} errors', { count: errorCount })}\n </span>\n ) : null\n\n return (\n <div className={cn('rounded-lg border bg-card', errorCount > 0 && 'border-destructive')}>\n {title && (\n <Button\n type=\"button\"\n variant=\"muted\"\n onClick={toggle}\n className={cn(\n 'w-full px-4 py-3 text-sm font-medium hover:bg-accent/50 rounded-lg',\n chevronPosition === 'left' ? 'justify-start gap-2' : 'justify-between',\n )}\n aria-expanded={expanded}\n aria-controls={contentId}\n >\n {chevronPosition === 'left' ? (\n <>\n {chevronIcon}\n <span className=\"flex items-center gap-2\">\n {icon && <span className=\"relative shrink-0 text-muted-foreground\">{icon}{!expanded && errorCount > 0 && <span className=\"absolute -right-0.5 -top-0.5 size-2 rounded-full bg-destructive\" />}</span>}\n <span>{title}</span>\n {fieldCountLabel}\n {errorBadge}\n </span>\n </>\n ) : (\n <>\n <span className=\"flex items-center gap-2\">\n {icon && <span className=\"relative shrink-0 text-muted-foreground\">{icon}{!expanded && errorCount > 0 && <span className=\"absolute -right-0.5 -top-0.5 size-2 rounded-full bg-destructive\" />}</span>}\n <span>{title}</span>\n {fieldCountLabel}\n {errorBadge}\n </span>\n {chevronIcon}\n </>\n )}\n </Button>\n )}\n <div\n id={contentId}\n className={cn(\n 'motion-safe:transition-all motion-safe:duration-200 overflow-hidden',\n expanded ? 'max-h-[5000px] opacity-100' : 'max-h-0 opacity-0'\n )}\n >\n <div className=\"px-4 py-3\">\n {children}\n </div>\n </div>\n </div>\n )\n }\n)\n"],
5
+ "mappings": ";AAmCM,SAuCQ,UAvCR,KASA,YATA;AAlCN,YAAY,WAAW;AACvB,SAAS,mBAAmB;AAC5B,SAAS,UAAU;AACnB,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,wBAAwB;AAkB1B,MAAM,mBAAmB,MAAM;AAAA,EACpC,SAASA,kBAAiB,EAAE,SAAS,OAAO,UAAU,kBAAkB,MAAM,aAAa,GAAG,YAAY,kBAAkB,SAAS,MAAM,SAAS,GAAG,KAAK;AAC1J,UAAM,IAAI,KAAK;AACf,UAAM,EAAE,UAAU,QAAQ,YAAY,IAAI,iBAAiB,UAAU,SAAS,eAAe;AAC7F,UAAM,YAAY,qBAAqB,OAAO;AAE9C,UAAM,oBAAoB,KAAK,OAAO;AAAA,MACpC,QAAQ,MAAM,YAAY,IAAI;AAAA,IAChC,IAAI,CAAC,WAAW,CAAC;AAEjB,UAAM,cACJ;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,YAAY;AAAA,QACd;AAAA;AAAA,IACF;AAGF,UAAM,kBAAkB,OAAO,eAAe,YAAY,aAAa,IACrE,qBAAC,UAAK,WAAU,6CAA4C;AAAA;AAAA,MACvD;AAAA,MAAW;AAAA,MAAE,eAAe,IAC3B,EAAE,gCAAgC,OAAO,IACzC,EAAE,8BAA8B,QAAQ;AAAA,OAC9C,IACE;AAEJ,UAAM,aAAa,aAAa,IAC9B,oBAAC,UAAK,WAAU,4GACb,yBAAe,IACZ,EAAE,gCAAgC,mBAAmB,EAAE,OAAO,WAAW,CAAC,IAC1E,EAAE,8BAA8B,oBAAoB,EAAE,OAAO,WAAW,CAAC,GAC/E,IACE;AAEJ,WACE,qBAAC,SAAI,WAAW,GAAG,6BAA6B,aAAa,KAAK,oBAAoB,GACnF;AAAA,eACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,SAAS;AAAA,UACT,WAAW;AAAA,YACT;AAAA,YACA,oBAAoB,SAAS,wBAAwB;AAAA,UACvD;AAAA,UACA,iBAAe;AAAA,UACf,iBAAe;AAAA,UAEd,8BAAoB,SACnB,iCACG;AAAA;AAAA,YACD,qBAAC,UAAK,WAAU,2BACb;AAAA,sBAAQ,qBAAC,UAAK,WAAU,2CAA2C;AAAA;AAAA,gBAAM,CAAC,YAAY,aAAa,KAAK,oBAAC,UAAK,WAAU,mEAAkE;AAAA,iBAAG;AAAA,cAC9L,oBAAC,UAAM,iBAAM;AAAA,cACZ;AAAA,cACA;AAAA,eACH;AAAA,aACF,IAEA,iCACE;AAAA,iCAAC,UAAK,WAAU,2BACb;AAAA,sBAAQ,qBAAC,UAAK,WAAU,2CAA2C;AAAA;AAAA,gBAAM,CAAC,YAAY,aAAa,KAAK,oBAAC,UAAK,WAAU,mEAAkE;AAAA,iBAAG;AAAA,cAC9L,oBAAC,UAAM,iBAAM;AAAA,cACZ;AAAA,cACA;AAAA,eACH;AAAA,YACC;AAAA,aACH;AAAA;AAAA,MAEJ;AAAA,MAEF;AAAA,QAAC;AAAA;AAAA,UACC,IAAI;AAAA,UACJ,WAAW;AAAA,YACT;AAAA,YACA,WAAW,+BAA+B;AAAA,UAC5C;AAAA,UAEA,8BAAC,SAAI,WAAU,aACZ,UACH;AAAA;AAAA,MACF;AAAA,OACF;AAAA,EAEJ;AACF;",
6
+ "names": ["CollapsibleGroup"]
7
+ }
@@ -0,0 +1,178 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { ChevronsLeft, ChevronsRight } from "lucide-react";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { cn } from "@open-mercato/shared/lib/utils";
7
+ import { Button } from "../../primitives/button.js";
8
+ import { useZoneCollapse } from "./useZoneCollapse.js";
9
+ const SIDE_BY_SIDE_MIN_WIDTH = 1280;
10
+ function subscribeViewport(callback) {
11
+ const mediaQuery = window.matchMedia("(min-width: 1024px)");
12
+ mediaQuery.addEventListener("change", callback);
13
+ return () => mediaQuery.removeEventListener("change", callback);
14
+ }
15
+ function getViewportSnapshot() {
16
+ return window.matchMedia("(min-width: 1024px)").matches;
17
+ }
18
+ function getViewportServerSnapshot() {
19
+ return false;
20
+ }
21
+ function CollapsibleZoneLayout({
22
+ zone1,
23
+ zone2,
24
+ entityName,
25
+ pageType,
26
+ zone1DefaultWidth,
27
+ errorCount = 0,
28
+ isDirty = false,
29
+ sections
30
+ }) {
31
+ const t = useT();
32
+ const { collapsed, setCollapsed } = useZoneCollapse(pageType);
33
+ const canCollapse = React.useSyncExternalStore(
34
+ subscribeViewport,
35
+ getViewportSnapshot,
36
+ getViewportServerSnapshot
37
+ );
38
+ const layoutRef = React.useRef(null);
39
+ const expandButtonRef = React.useRef(null);
40
+ const [containerWidth, setContainerWidth] = React.useState(() => typeof window === "undefined" ? 0 : window.innerWidth);
41
+ const [expandedWhileConstrained, setExpandedWhileConstrained] = React.useState(false);
42
+ React.useEffect(() => {
43
+ const node = layoutRef.current;
44
+ if (!node) return;
45
+ const updateWidth = (nextWidth) => {
46
+ setContainerWidth((prev) => Math.abs(prev - nextWidth) < 1 ? prev : nextWidth);
47
+ };
48
+ const measure = () => {
49
+ updateWidth(node.getBoundingClientRect().width || window.innerWidth);
50
+ };
51
+ measure();
52
+ if (typeof ResizeObserver === "undefined") {
53
+ window.addEventListener("resize", measure);
54
+ return () => window.removeEventListener("resize", measure);
55
+ }
56
+ const observer = new ResizeObserver((entries) => {
57
+ const entry = entries[0];
58
+ if (!entry) return;
59
+ updateWidth(entry.contentRect.width);
60
+ });
61
+ observer.observe(node);
62
+ return () => observer.disconnect();
63
+ }, []);
64
+ const canShowSideBySide = containerWidth >= SIDE_BY_SIDE_MIN_WIDTH;
65
+ React.useEffect(() => {
66
+ if (canShowSideBySide) {
67
+ setExpandedWhileConstrained(false);
68
+ }
69
+ }, [canShowSideBySide]);
70
+ const showCollapsedRail = canCollapse && (collapsed || !canShowSideBySide && !expandedWhileConstrained);
71
+ const showStackedExpanded = !showCollapsedRail && !canShowSideBySide;
72
+ const layoutMode = showCollapsedRail ? "collapsed" : showStackedExpanded ? "stacked" : "side-by-side";
73
+ const zone1SideBySideStyle = zone1DefaultWidth ? { width: zone1DefaultWidth, flexBasis: zone1DefaultWidth } : void 0;
74
+ const handleExpand = React.useCallback(() => {
75
+ if (!canCollapse) return;
76
+ setCollapsed(false);
77
+ setExpandedWhileConstrained(!canShowSideBySide);
78
+ }, [canCollapse, canShowSideBySide, setCollapsed]);
79
+ const handleCollapse = React.useCallback(() => {
80
+ if (!canCollapse) return;
81
+ setExpandedWhileConstrained(false);
82
+ setCollapsed(true);
83
+ requestAnimationFrame(() => {
84
+ expandButtonRef.current?.focus();
85
+ });
86
+ }, [canCollapse, setCollapsed]);
87
+ return /* @__PURE__ */ jsx(
88
+ "div",
89
+ {
90
+ ref: layoutRef,
91
+ "data-zone-layout-mode": layoutMode,
92
+ className: cn(
93
+ "flex gap-4",
94
+ showStackedExpanded ? "flex-col" : "flex-col lg:flex-row"
95
+ ),
96
+ children: showCollapsedRail ? /* @__PURE__ */ jsxs(Fragment, { children: [
97
+ /* @__PURE__ */ jsxs("div", { className: "hidden lg:flex shrink-0 flex-col items-center gap-3", children: [
98
+ /* @__PURE__ */ jsx(
99
+ Button,
100
+ {
101
+ ref: expandButtonRef,
102
+ type: "button",
103
+ variant: "default",
104
+ size: "sm",
105
+ onClick: handleExpand,
106
+ className: "h-auto rounded-[10px] px-1.5 py-2 shadow-sm",
107
+ "aria-label": t("ui.zone.expand", "Expand form panel"),
108
+ children: /* @__PURE__ */ jsx(ChevronsRight, { className: "size-4" })
109
+ }
110
+ ),
111
+ sections?.length ? /* @__PURE__ */ jsx("div", { className: "flex flex-col items-center gap-2 rounded-[14px] border border-border/70 bg-card px-2 py-3", children: sections.map((section) => {
112
+ const SectionIcon = section.icon;
113
+ const hasErrors = Boolean(section.errorCount && section.errorCount > 0);
114
+ return /* @__PURE__ */ jsxs(
115
+ "div",
116
+ {
117
+ className: "relative flex size-9 items-center justify-center rounded-[10px] border border-transparent bg-muted/70 text-muted-foreground",
118
+ title: section.label,
119
+ children: [
120
+ /* @__PURE__ */ jsx(SectionIcon, { className: "size-4" }),
121
+ hasErrors ? /* @__PURE__ */ jsx("span", { className: "absolute right-1.5 top-1.5 size-1.5 rounded-full bg-destructive" }) : null
122
+ ]
123
+ },
124
+ section.id
125
+ );
126
+ }) }) : null
127
+ ] }),
128
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: zone2 })
129
+ ] }) : showStackedExpanded ? /* @__PURE__ */ jsxs(Fragment, { children: [
130
+ /* @__PURE__ */ jsxs("div", { className: "w-full space-y-2", children: [
131
+ canCollapse ? /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
132
+ Button,
133
+ {
134
+ type: "button",
135
+ variant: "outline",
136
+ size: "sm",
137
+ onClick: handleCollapse,
138
+ className: "h-auto rounded-[6px] border bg-card px-1.5 py-2",
139
+ "aria-label": t("ui.zone.collapse", "Collapse form panel"),
140
+ children: /* @__PURE__ */ jsx(ChevronsLeft, { className: "size-4" })
141
+ }
142
+ ) }) : null,
143
+ /* @__PURE__ */ jsx("div", { className: "w-full", children: zone1 })
144
+ ] }),
145
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 w-full", children: zone2 })
146
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
147
+ /* @__PURE__ */ jsx(
148
+ "div",
149
+ {
150
+ className: cn("w-full lg:shrink-0", zone1DefaultWidth ? void 0 : "lg:w-[40%]"),
151
+ style: zone1SideBySideStyle,
152
+ children: zone1
153
+ }
154
+ ),
155
+ /* @__PURE__ */ jsxs("div", { className: "hidden lg:flex relative shrink-0 w-8 items-start justify-center pt-4", children: [
156
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-border" }),
157
+ /* @__PURE__ */ jsx(
158
+ Button,
159
+ {
160
+ type: "button",
161
+ variant: "outline",
162
+ size: "sm",
163
+ onClick: handleCollapse,
164
+ className: "relative z-10 h-auto rounded-[6px] border bg-card px-1.5 py-2",
165
+ "aria-label": t("ui.zone.collapse", "Collapse form panel"),
166
+ children: /* @__PURE__ */ jsx(ChevronsLeft, { className: "size-4" })
167
+ }
168
+ )
169
+ ] }),
170
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 w-full lg:flex-1", children: zone2 })
171
+ ] })
172
+ }
173
+ );
174
+ }
175
+ export {
176
+ CollapsibleZoneLayout
177
+ };
178
+ //# sourceMappingURL=CollapsibleZoneLayout.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/crud/CollapsibleZoneLayout.tsx"],
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { ChevronsLeft, ChevronsRight } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Button } from '../../primitives/button'\nimport { useZoneCollapse } from './useZoneCollapse'\nimport type { LucideIcon } from 'lucide-react'\n\nconst SIDE_BY_SIDE_MIN_WIDTH = 1280\n\nexport interface ZoneSectionDescriptor {\n id: string\n icon: LucideIcon\n label: string\n errorCount?: number\n}\n\nexport interface CollapsibleZoneLayoutProps {\n zone1: React.ReactNode\n zone2: React.ReactNode\n entityName: string\n pageType: string\n zone1DefaultWidth?: string\n errorCount?: number\n isDirty?: boolean\n /** Section descriptors for the collapsed rail icon sidebar. When omitted the rail shows the legacy minimal view. */\n sections?: ZoneSectionDescriptor[]\n}\n\nfunction subscribeViewport(callback: () => void) {\n const mediaQuery = window.matchMedia('(min-width: 1024px)')\n mediaQuery.addEventListener('change', callback)\n return () => mediaQuery.removeEventListener('change', callback)\n}\n\nfunction getViewportSnapshot() {\n return window.matchMedia('(min-width: 1024px)').matches\n}\n\nfunction getViewportServerSnapshot() {\n return false\n}\n\nexport function CollapsibleZoneLayout({\n zone1,\n zone2,\n entityName,\n pageType,\n zone1DefaultWidth,\n errorCount = 0,\n isDirty = false,\n sections,\n}: CollapsibleZoneLayoutProps) {\n const t = useT()\n const { collapsed, setCollapsed } = useZoneCollapse(pageType)\n const canCollapse = React.useSyncExternalStore(\n subscribeViewport,\n getViewportSnapshot,\n getViewportServerSnapshot,\n )\n const layoutRef = React.useRef<HTMLDivElement>(null)\n const expandButtonRef = React.useRef<HTMLButtonElement>(null)\n const [containerWidth, setContainerWidth] = React.useState(() => (typeof window === 'undefined' ? 0 : window.innerWidth))\n const [expandedWhileConstrained, setExpandedWhileConstrained] = React.useState(false)\n\n React.useEffect(() => {\n const node = layoutRef.current\n if (!node) return\n\n const updateWidth = (nextWidth: number) => {\n setContainerWidth((prev) => (Math.abs(prev - nextWidth) < 1 ? prev : nextWidth))\n }\n\n const measure = () => {\n updateWidth(node.getBoundingClientRect().width || window.innerWidth)\n }\n\n measure()\n\n if (typeof ResizeObserver === 'undefined') {\n window.addEventListener('resize', measure)\n return () => window.removeEventListener('resize', measure)\n }\n\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0]\n if (!entry) return\n updateWidth(entry.contentRect.width)\n })\n\n observer.observe(node)\n return () => observer.disconnect()\n }, [])\n\n const canShowSideBySide = containerWidth >= SIDE_BY_SIDE_MIN_WIDTH\n\n React.useEffect(() => {\n if (canShowSideBySide) {\n setExpandedWhileConstrained(false)\n }\n }, [canShowSideBySide])\n\n const showCollapsedRail = canCollapse && (collapsed || (!canShowSideBySide && !expandedWhileConstrained))\n const showStackedExpanded = !showCollapsedRail && !canShowSideBySide\n const layoutMode = showCollapsedRail ? 'collapsed' : showStackedExpanded ? 'stacked' : 'side-by-side'\n const zone1SideBySideStyle = zone1DefaultWidth\n ? { width: zone1DefaultWidth, flexBasis: zone1DefaultWidth }\n : undefined\n\n const handleExpand = React.useCallback(() => {\n if (!canCollapse) return\n setCollapsed(false)\n setExpandedWhileConstrained(!canShowSideBySide)\n }, [canCollapse, canShowSideBySide, setCollapsed])\n\n const handleCollapse = React.useCallback(() => {\n if (!canCollapse) return\n setExpandedWhileConstrained(false)\n setCollapsed(true)\n requestAnimationFrame(() => {\n expandButtonRef.current?.focus()\n })\n }, [canCollapse, setCollapsed])\n\n return (\n <div\n ref={layoutRef}\n data-zone-layout-mode={layoutMode}\n className={cn(\n 'flex gap-4',\n showStackedExpanded ? 'flex-col' : 'flex-col lg:flex-row',\n )}\n >\n {showCollapsedRail ? (\n <>\n <div className=\"hidden lg:flex shrink-0 flex-col items-center gap-3\">\n <Button\n ref={expandButtonRef}\n type=\"button\"\n variant=\"default\"\n size=\"sm\"\n onClick={handleExpand}\n className=\"h-auto rounded-[10px] px-1.5 py-2 shadow-sm\"\n aria-label={t('ui.zone.expand', 'Expand form panel')}\n >\n <ChevronsRight className=\"size-4\" />\n </Button>\n {sections?.length ? (\n <div className=\"flex flex-col items-center gap-2 rounded-[14px] border border-border/70 bg-card px-2 py-3\">\n {sections.map((section) => {\n const SectionIcon = section.icon\n const hasErrors = Boolean(section.errorCount && section.errorCount > 0)\n return (\n <div\n key={section.id}\n className=\"relative flex size-9 items-center justify-center rounded-[10px] border border-transparent bg-muted/70 text-muted-foreground\"\n title={section.label}\n >\n <SectionIcon className=\"size-4\" />\n {hasErrors ? (\n <span className=\"absolute right-1.5 top-1.5 size-1.5 rounded-full bg-destructive\" />\n ) : null}\n </div>\n )\n })}\n </div>\n ) : null}\n </div>\n {/* Zone 2 takes full width */}\n <div className=\"min-w-0 flex-1\">\n {zone2}\n </div>\n </>\n ) : showStackedExpanded ? (\n <>\n <div className=\"w-full space-y-2\">\n {canCollapse ? (\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleCollapse}\n className=\"h-auto rounded-[6px] border bg-card px-1.5 py-2\"\n aria-label={t('ui.zone.collapse', 'Collapse form panel')}\n >\n <ChevronsLeft className=\"size-4\" />\n </Button>\n </div>\n ) : null}\n <div className=\"w-full\">\n {zone1}\n </div>\n </div>\n\n <div className=\"min-w-0 w-full\">\n {zone2}\n </div>\n </>\n ) : (\n <>\n {/* Zone 1 \u2014 CrudForm area */}\n <div\n className={cn('w-full lg:shrink-0', zone1DefaultWidth ? undefined : 'lg:w-[40%]')}\n style={zone1SideBySideStyle}\n >\n {zone1}\n </div>\n\n {/* Divider with collapse toggle */}\n <div className=\"hidden lg:flex relative shrink-0 w-8 items-start justify-center pt-4\">\n <div className=\"absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-border\" />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleCollapse}\n className=\"relative z-10 h-auto rounded-[6px] border bg-card px-1.5 py-2\"\n aria-label={t('ui.zone.collapse', 'Collapse form panel')}\n >\n <ChevronsLeft className=\"size-4\" />\n </Button>\n </div>\n\n {/* Zone 2 \u2014 Tabs / related data area */}\n <div className=\"min-w-0 w-full lg:flex-1\">\n {zone2}\n </div>\n </>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { useCallback } from "react";
3
+ import { usePersistedBooleanFlag } from "./usePersistedBooleanFlag.js";
4
+ function getStorageKey(pageType, groupId) {
5
+ return `om:collapsible:${pageType}:${groupId}`;
6
+ }
7
+ function useGroupCollapse(pageType, groupId, defaultExpanded = true) {
8
+ const { value: expanded, toggle, setValue } = usePersistedBooleanFlag(
9
+ getStorageKey(pageType, groupId),
10
+ defaultExpanded
11
+ );
12
+ const setExpanded = useCallback((next) => {
13
+ if (typeof next === "function") {
14
+ setValue((prev) => next(prev));
15
+ } else {
16
+ setValue(next);
17
+ }
18
+ }, [setValue]);
19
+ return { expanded, toggle, setExpanded };
20
+ }
21
+ export {
22
+ useGroupCollapse
23
+ };
24
+ //# sourceMappingURL=useGroupCollapse.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/crud/useGroupCollapse.ts"],
4
+ "sourcesContent": ["'use client'\nimport { useCallback } from 'react'\nimport { usePersistedBooleanFlag } from './usePersistedBooleanFlag'\n\nfunction getStorageKey(pageType: string, groupId: string) {\n return `om:collapsible:${pageType}:${groupId}`\n}\n\nexport function useGroupCollapse(pageType: string, groupId: string, defaultExpanded = true) {\n const { value: expanded, toggle, setValue } = usePersistedBooleanFlag(\n getStorageKey(pageType, groupId),\n defaultExpanded,\n )\n const setExpanded = useCallback((next: boolean | ((prev: boolean) => boolean)) => {\n if (typeof next === 'function') {\n setValue((prev) => (next as (prev: boolean) => boolean)(prev))\n } else {\n setValue(next)\n }\n }, [setValue])\n return { expanded, toggle, setExpanded }\n}\n"],
5
+ "mappings": ";AACA,SAAS,mBAAmB;AAC5B,SAAS,+BAA+B;AAExC,SAAS,cAAc,UAAkB,SAAiB;AACxD,SAAO,kBAAkB,QAAQ,IAAI,OAAO;AAC9C;AAEO,SAAS,iBAAiB,UAAkB,SAAiB,kBAAkB,MAAM;AAC1F,QAAM,EAAE,OAAO,UAAU,QAAQ,SAAS,IAAI;AAAA,IAC5C,cAAc,UAAU,OAAO;AAAA,IAC/B;AAAA,EACF;AACA,QAAM,cAAc,YAAY,CAAC,SAAiD;AAChF,QAAI,OAAO,SAAS,YAAY;AAC9B,eAAS,CAAC,SAAU,KAAoC,IAAI,CAAC;AAAA,IAC/D,OAAO;AACL,eAAS,IAAI;AAAA,IACf;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AACb,SAAO,EAAE,UAAU,QAAQ,YAAY;AACzC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import {
4
+ readJsonFromLocalStorage,
5
+ writeJsonToLocalStorage
6
+ } from "@open-mercato/shared/lib/browser/safeLocalStorage";
7
+ const STORAGE_PREFIX = "om:group-order:";
8
+ function getStorageKey(pageType) {
9
+ return `${STORAGE_PREFIX}${pageType}`;
10
+ }
11
+ function mergeOrder(saved, defaults) {
12
+ const known = new Set(defaults);
13
+ const result = saved.filter((id) => known.has(id));
14
+ for (const id of defaults) {
15
+ if (!result.includes(id)) result.push(id);
16
+ }
17
+ return result;
18
+ }
19
+ function arraysEqual(a, b) {
20
+ if (a.length !== b.length) return false;
21
+ for (let i = 0; i < a.length; i += 1) {
22
+ if (a[i] !== b[i]) return false;
23
+ }
24
+ return true;
25
+ }
26
+ function useGroupOrder(pageType, defaultGroupIds) {
27
+ const [orderedIds, setOrderedIds] = React.useState(defaultGroupIds);
28
+ const mounted = React.useRef(false);
29
+ React.useEffect(() => {
30
+ mounted.current = true;
31
+ const saved = readJsonFromLocalStorage(getStorageKey(pageType), null);
32
+ if (Array.isArray(saved)) {
33
+ setOrderedIds(mergeOrder(saved, defaultGroupIds));
34
+ }
35
+ }, [pageType]);
36
+ React.useEffect(() => {
37
+ setOrderedIds((prev) => {
38
+ const merged = mergeOrder(prev, defaultGroupIds);
39
+ return arraysEqual(prev, merged) ? prev : merged;
40
+ });
41
+ }, [defaultGroupIds]);
42
+ const reorder = React.useCallback(
43
+ (fromIndex, toIndex) => {
44
+ setOrderedIds((prev) => {
45
+ const next = [...prev];
46
+ const [moved] = next.splice(fromIndex, 1);
47
+ next.splice(toIndex, 0, moved);
48
+ if (mounted.current) {
49
+ writeJsonToLocalStorage(getStorageKey(pageType), next);
50
+ }
51
+ return next;
52
+ });
53
+ },
54
+ [pageType]
55
+ );
56
+ return { orderedIds, reorder };
57
+ }
58
+ export {
59
+ useGroupOrder
60
+ };
61
+ //# sourceMappingURL=useGroupOrder.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/crud/useGroupOrder.ts"],
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport {\n readJsonFromLocalStorage,\n writeJsonToLocalStorage,\n} from '@open-mercato/shared/lib/browser/safeLocalStorage'\n\nconst STORAGE_PREFIX = 'om:group-order:'\n\nfunction getStorageKey(pageType: string) {\n return `${STORAGE_PREFIX}${pageType}`\n}\n\nfunction mergeOrder(saved: string[], defaults: string[]): string[] {\n const known = new Set(defaults)\n const result = saved.filter((id) => known.has(id))\n for (const id of defaults) {\n if (!result.includes(id)) result.push(id)\n }\n return result\n}\n\nfunction arraysEqual(a: string[], b: string[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i += 1) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n\n/**\n * Returns the group IDs in the user's preferred order.\n * Falls back to the default order when no preference is stored.\n */\nexport function useGroupOrder(pageType: string, defaultGroupIds: string[]) {\n const [orderedIds, setOrderedIds] = React.useState<string[]>(defaultGroupIds)\n const mounted = React.useRef(false)\n\n React.useEffect(() => {\n mounted.current = true\n const saved = readJsonFromLocalStorage<string[] | null>(getStorageKey(pageType), null)\n if (Array.isArray(saved)) {\n setOrderedIds(mergeOrder(saved, defaultGroupIds))\n }\n // Intentionally only runs on mount (per pageType); defaultGroupIds changes are\n // handled by the sync effect below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pageType])\n\n // Sync when defaultGroupIds changes (e.g. new groups added dynamically)\n React.useEffect(() => {\n setOrderedIds((prev) => {\n const merged = mergeOrder(prev, defaultGroupIds)\n return arraysEqual(prev, merged) ? prev : merged\n })\n }, [defaultGroupIds])\n\n const reorder = React.useCallback(\n (fromIndex: number, toIndex: number) => {\n setOrderedIds((prev) => {\n const next = [...prev]\n const [moved] = next.splice(fromIndex, 1)\n next.splice(toIndex, 0, moved)\n if (mounted.current) {\n writeJsonToLocalStorage(getStorageKey(pageType), next)\n }\n return next\n })\n },\n [pageType],\n )\n\n return { orderedIds, reorder }\n}\n"],
5
+ "mappings": ";AACA,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,iBAAiB;AAEvB,SAAS,cAAc,UAAkB;AACvC,SAAO,GAAG,cAAc,GAAG,QAAQ;AACrC;AAEA,SAAS,WAAW,OAAiB,UAA8B;AACjE,QAAM,QAAQ,IAAI,IAAI,QAAQ;AAC9B,QAAM,SAAS,MAAM,OAAO,CAAC,OAAO,MAAM,IAAI,EAAE,CAAC;AACjD,aAAW,MAAM,UAAU;AACzB,QAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO,KAAK,EAAE;AAAA,EAC1C;AACA,SAAO;AACT;AAEA,SAAS,YAAY,GAAa,GAAsB;AACtD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GAAG;AACpC,QAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAO;AAAA,EAC5B;AACA,SAAO;AACT;AAMO,SAAS,cAAc,UAAkB,iBAA2B;AACzE,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAmB,eAAe;AAC5E,QAAM,UAAU,MAAM,OAAO,KAAK;AAElC,QAAM,UAAU,MAAM;AACpB,YAAQ,UAAU;AAClB,UAAM,QAAQ,yBAA0C,cAAc,QAAQ,GAAG,IAAI;AACrF,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,oBAAc,WAAW,OAAO,eAAe,CAAC;AAAA,IAClD;AAAA,EAIF,GAAG,CAAC,QAAQ,CAAC;AAGb,QAAM,UAAU,MAAM;AACpB,kBAAc,CAAC,SAAS;AACtB,YAAM,SAAS,WAAW,MAAM,eAAe;AAC/C,aAAO,YAAY,MAAM,MAAM,IAAI,OAAO;AAAA,IAC5C,CAAC;AAAA,EACH,GAAG,CAAC,eAAe,CAAC;AAEpB,QAAM,UAAU,MAAM;AAAA,IACpB,CAAC,WAAmB,YAAoB;AACtC,oBAAc,CAAC,SAAS;AACtB,cAAM,OAAO,CAAC,GAAG,IAAI;AACrB,cAAM,CAAC,KAAK,IAAI,KAAK,OAAO,WAAW,CAAC;AACxC,aAAK,OAAO,SAAS,GAAG,KAAK;AAC7B,YAAI,QAAQ,SAAS;AACnB,kCAAwB,cAAc,QAAQ,GAAG,IAAI;AAAA,QACvD;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,SAAO,EAAE,YAAY,QAAQ;AAC/B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { useState, useEffect, useCallback, useRef } from "react";
3
+ import {
4
+ readJsonFromLocalStorage,
5
+ writeJsonToLocalStorage
6
+ } from "@open-mercato/shared/lib/browser/safeLocalStorage";
7
+ function usePersistedBooleanFlag(storageKey, defaultValue) {
8
+ const [value, setValue] = useState(defaultValue);
9
+ const mounted = useRef(false);
10
+ useEffect(() => {
11
+ mounted.current = true;
12
+ const saved = readJsonFromLocalStorage(storageKey, null);
13
+ if (saved !== null) {
14
+ setValue(saved === "1");
15
+ }
16
+ }, [storageKey]);
17
+ useEffect(() => {
18
+ if (!mounted.current) return;
19
+ writeJsonToLocalStorage(storageKey, value ? "1" : "0");
20
+ }, [storageKey, value]);
21
+ const toggle = useCallback(() => {
22
+ setValue((prev) => !prev);
23
+ }, []);
24
+ return { value, toggle, setValue };
25
+ }
26
+ export {
27
+ usePersistedBooleanFlag
28
+ };
29
+ //# sourceMappingURL=usePersistedBooleanFlag.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/backend/crud/usePersistedBooleanFlag.ts"],
4
+ "sourcesContent": ["'use client'\nimport { useState, useEffect, useCallback, useRef } from 'react'\nimport {\n readJsonFromLocalStorage,\n writeJsonToLocalStorage,\n} from '@open-mercato/shared/lib/browser/safeLocalStorage'\n\n/**\n * Persists a boolean flag in localStorage under a given key.\n * Reads once on mount; writes on every change after mount.\n * Designed to back collapse/expand state for crud form groups and zones.\n */\nexport function usePersistedBooleanFlag(storageKey: string, defaultValue: boolean) {\n const [value, setValue] = useState(defaultValue)\n const mounted = useRef(false)\n\n useEffect(() => {\n mounted.current = true\n const saved = readJsonFromLocalStorage<string | null>(storageKey, null)\n if (saved !== null) {\n setValue(saved === '1')\n }\n }, [storageKey])\n\n useEffect(() => {\n if (!mounted.current) return\n writeJsonToLocalStorage(storageKey, value ? '1' : '0')\n }, [storageKey, value])\n\n const toggle = useCallback(() => {\n setValue((prev) => !prev)\n }, [])\n\n return { value, toggle, setValue }\n}\n"],
5
+ "mappings": ";AACA,SAAS,UAAU,WAAW,aAAa,cAAc;AACzD;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAOA,SAAS,wBAAwB,YAAoB,cAAuB;AACjF,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,YAAY;AAC/C,QAAM,UAAU,OAAO,KAAK;AAE5B,YAAU,MAAM;AACd,YAAQ,UAAU;AAClB,UAAM,QAAQ,yBAAwC,YAAY,IAAI;AACtE,QAAI,UAAU,MAAM;AAClB,eAAS,UAAU,GAAG;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,QAAS;AACtB,4BAAwB,YAAY,QAAQ,MAAM,GAAG;AAAA,EACvD,GAAG,CAAC,YAAY,KAAK,CAAC;AAEtB,QAAM,SAAS,YAAY,MAAM;AAC/B,aAAS,CAAC,SAAS,CAAC,IAAI;AAAA,EAC1B,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,OAAO,QAAQ,SAAS;AACnC;",
6
+ "names": []
7
+ }