@open-mercato/ui 0.5.1-develop.2802.9223828f7f → 0.5.1-develop.2851.2854b4507f
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.
- package/dist/backend/confirm-dialog/ConfirmDialog.js +1 -1
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +1 -1
- package/dist/backend/crud/CollapsibleGroup.js +64 -50
- package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
- package/dist/backend/crud/CollapsibleZoneLayout.js +23 -3
- package/dist/backend/crud/CollapsibleZoneLayout.js.map +2 -2
- package/dist/backend/crud/useGroupCollapse.js +2 -2
- package/dist/backend/crud/useGroupCollapse.js.map +2 -2
- package/dist/backend/crud/usePersistedBooleanFlag.js +57 -16
- package/dist/backend/crud/usePersistedBooleanFlag.js.map +2 -2
- package/dist/backend/crud/useZoneCollapse.js +2 -2
- package/dist/backend/crud/useZoneCollapse.js.map +2 -2
- package/dist/backend/messages/SendObjectMessageDialog.js +34 -13
- package/dist/backend/messages/SendObjectMessageDialog.js.map +2 -2
- package/dist/backend/version-history/VersionHistoryAction.js +3 -3
- package/dist/backend/version-history/VersionHistoryAction.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/__tests__/CollapsibleZoneLayout.test.tsx +60 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +1 -1
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +44 -0
- package/src/backend/crud/CollapsibleGroup.tsx +12 -2
- package/src/backend/crud/CollapsibleZoneLayout.tsx +29 -4
- package/src/backend/crud/__tests__/usePersistedBooleanFlag.test.ts +83 -7
- package/src/backend/crud/useGroupCollapse.ts +2 -2
- package/src/backend/crud/usePersistedBooleanFlag.ts +75 -21
- package/src/backend/crud/useZoneCollapse.ts +2 -2
- package/src/backend/messages/SendObjectMessageDialog.tsx +37 -7
- package/src/backend/messages/__tests__/SendObjectMessageDialog.test.tsx +21 -0
- package/src/backend/version-history/VersionHistoryAction.tsx +4 -4
|
@@ -119,7 +119,7 @@ function ConfirmDialog({
|
|
|
119
119
|
onClick: handleBackdropClick,
|
|
120
120
|
className: cn(
|
|
121
121
|
// Reset dialog defaults
|
|
122
|
-
"m-0 p-0 max-w-none bg-transparent border-none",
|
|
122
|
+
"m-0 p-0 max-w-none bg-transparent border-none pointer-events-auto",
|
|
123
123
|
// Backdrop styling
|
|
124
124
|
"backdrop:bg-black/50 backdrop:backdrop-blur-sm backdrop:transition-opacity",
|
|
125
125
|
// Mobile: bottom sheet
|
|
@@ -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 // 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"],
|
|
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 pointer-events-auto\",\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
5
|
"mappings": "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",
|
|
6
6
|
"names": ["handleCancel"]
|
|
7
7
|
}
|
|
@@ -9,7 +9,7 @@ import { useGroupCollapse } from "./useGroupCollapse.js";
|
|
|
9
9
|
const CollapsibleGroup = React.forwardRef(
|
|
10
10
|
function CollapsibleGroup2({ groupId, title, pageType, defaultExpanded = true, errorCount = 0, fieldCount, chevronPosition = "right", icon, children }, ref) {
|
|
11
11
|
const t = useT();
|
|
12
|
-
const { expanded, toggle, setExpanded } = useGroupCollapse(pageType, groupId, defaultExpanded);
|
|
12
|
+
const { expanded, toggle, setExpanded, isHydrated } = useGroupCollapse(pageType, groupId, defaultExpanded);
|
|
13
13
|
const contentId = `collapsible-group-${groupId}`;
|
|
14
14
|
React.useImperativeHandle(ref, () => ({
|
|
15
15
|
expand: () => setExpanded(true)
|
|
@@ -30,56 +30,70 @@ const CollapsibleGroup = React.forwardRef(
|
|
|
30
30
|
fieldCount === 1 ? t("ui.collapsible.fieldSingular", "field") : t("ui.collapsible.fieldPlural", "fields")
|
|
31
31
|
] }) : null;
|
|
32
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(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
33
|
+
return /* @__PURE__ */ jsxs(
|
|
34
|
+
"div",
|
|
35
|
+
{
|
|
36
|
+
id: `collapsible-group-wrapper-${groupId}`,
|
|
37
|
+
className: cn(
|
|
38
|
+
"rounded-lg border bg-card",
|
|
39
|
+
!isHydrated && "invisible",
|
|
40
|
+
errorCount > 0 && "border-destructive"
|
|
41
|
+
),
|
|
42
|
+
"data-collapsible-group-id": groupId,
|
|
43
|
+
"data-persistence-hydrated": isHydrated ? "true" : "false",
|
|
44
|
+
"aria-hidden": isHydrated ? void 0 : true,
|
|
45
|
+
children: [
|
|
46
|
+
title && /* @__PURE__ */ jsx(
|
|
47
|
+
Button,
|
|
48
|
+
{
|
|
49
|
+
type: "button",
|
|
50
|
+
variant: "muted",
|
|
51
|
+
onClick: toggle,
|
|
52
|
+
className: cn(
|
|
53
|
+
"w-full px-4 py-3 text-sm font-medium hover:bg-accent/50 rounded-lg",
|
|
54
|
+
chevronPosition === "left" ? "justify-start gap-2" : "justify-between"
|
|
55
|
+
),
|
|
56
|
+
"aria-expanded": expanded,
|
|
57
|
+
"aria-controls": contentId,
|
|
58
|
+
children: chevronPosition === "left" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
59
|
+
chevronIcon,
|
|
60
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
61
|
+
icon && /* @__PURE__ */ jsxs("span", { className: "relative shrink-0 text-muted-foreground", children: [
|
|
62
|
+
icon,
|
|
63
|
+
!expanded && errorCount > 0 && /* @__PURE__ */ jsx("span", { className: "absolute -right-0.5 -top-0.5 size-2 rounded-full bg-destructive" })
|
|
64
|
+
] }),
|
|
65
|
+
/* @__PURE__ */ jsx("span", { children: title }),
|
|
66
|
+
fieldCountLabel,
|
|
67
|
+
errorBadge
|
|
68
|
+
] })
|
|
69
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
70
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
71
|
+
icon && /* @__PURE__ */ jsxs("span", { className: "relative shrink-0 text-muted-foreground", children: [
|
|
72
|
+
icon,
|
|
73
|
+
!expanded && errorCount > 0 && /* @__PURE__ */ jsx("span", { className: "absolute -right-0.5 -top-0.5 size-2 rounded-full bg-destructive" })
|
|
74
|
+
] }),
|
|
75
|
+
/* @__PURE__ */ jsx("span", { children: title }),
|
|
76
|
+
fieldCountLabel,
|
|
77
|
+
errorBadge
|
|
78
|
+
] }),
|
|
79
|
+
chevronIcon
|
|
80
|
+
] })
|
|
81
|
+
}
|
|
78
82
|
),
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
/* @__PURE__ */ jsx(
|
|
84
|
+
"div",
|
|
85
|
+
{
|
|
86
|
+
id: contentId,
|
|
87
|
+
className: cn(
|
|
88
|
+
"motion-safe:transition-all motion-safe:duration-200 overflow-hidden",
|
|
89
|
+
expanded ? "max-h-[5000px] opacity-100" : "max-h-0 opacity-0"
|
|
90
|
+
),
|
|
91
|
+
children: /* @__PURE__ */ jsx("div", { className: "px-4 py-3", children })
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
);
|
|
83
97
|
}
|
|
84
98
|
);
|
|
85
99
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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
|
|
5
|
-
"mappings": ";AAmCM,
|
|
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, isHydrated } = 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\n id={`collapsible-group-wrapper-${groupId}`}\n className={cn(\n 'rounded-lg border bg-card',\n !isHydrated && 'invisible',\n errorCount > 0 && 'border-destructive',\n )}\n data-collapsible-group-id={groupId}\n data-persistence-hydrated={isHydrated ? 'true' : 'false'}\n aria-hidden={isHydrated ? undefined : true}\n >\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,SAiDQ,UAjDR,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,aAAa,WAAW,IAAI,iBAAiB,UAAU,SAAS,eAAe;AACzG,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;AAAA,MAAC;AAAA;AAAA,QACC,IAAI,6BAA6B,OAAO;AAAA,QACxC,WAAW;AAAA,UACT;AAAA,UACA,CAAC,cAAc;AAAA,UACf,aAAa,KAAK;AAAA,QACpB;AAAA,QACA,6BAA2B;AAAA,QAC3B,6BAA2B,aAAa,SAAS;AAAA,QACjD,eAAa,aAAa,SAAY;AAAA,QAErC;AAAA,mBACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,SAAS;AAAA,cACT,WAAW;AAAA,gBACT;AAAA,gBACA,oBAAoB,SAAS,wBAAwB;AAAA,cACvD;AAAA,cACA,iBAAe;AAAA,cACf,iBAAe;AAAA,cAEd,8BAAoB,SACnB,iCACG;AAAA;AAAA,gBACD,qBAAC,UAAK,WAAU,2BACb;AAAA,0BAAQ,qBAAC,UAAK,WAAU,2CAA2C;AAAA;AAAA,oBAAM,CAAC,YAAY,aAAa,KAAK,oBAAC,UAAK,WAAU,mEAAkE;AAAA,qBAAG;AAAA,kBAC9L,oBAAC,UAAM,iBAAM;AAAA,kBACZ;AAAA,kBACA;AAAA,mBACH;AAAA,iBACF,IAEA,iCACE;AAAA,qCAAC,UAAK,WAAU,2BACb;AAAA,0BAAQ,qBAAC,UAAK,WAAU,2CAA2C;AAAA;AAAA,oBAAM,CAAC,YAAY,aAAa,KAAK,oBAAC,UAAK,WAAU,mEAAkE;AAAA,qBAAG;AAAA,kBAC9L,oBAAC,UAAM,iBAAM;AAAA,kBACZ;AAAA,kBACA;AAAA,mBACH;AAAA,gBACC;AAAA,iBACH;AAAA;AAAA,UAEJ;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,IAAI;AAAA,cACJ,WAAW;AAAA,gBACT;AAAA,gBACA,WAAW,+BAA+B;AAAA,cAC5C;AAAA,cAEA,8BAAC,SAAI,WAAU,aACZ,UACH;AAAA;AAAA,UACF;AAAA;AAAA;AAAA,IACF;AAAA,EAEJ;AACF;",
|
|
6
6
|
"names": ["CollapsibleGroup"]
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import { ChevronsLeft, ChevronsRight } from "lucide-react";
|
|
|
5
5
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
6
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
7
7
|
import { Button } from "../../primitives/button.js";
|
|
8
|
+
import { IconButton } from "../../primitives/icon-button.js";
|
|
8
9
|
import { useZoneCollapse } from "./useZoneCollapse.js";
|
|
9
10
|
const SIDE_BY_SIDE_MIN_WIDTH = 1280;
|
|
10
11
|
function subscribeViewport(callback) {
|
|
@@ -29,7 +30,7 @@ function CollapsibleZoneLayout({
|
|
|
29
30
|
sections
|
|
30
31
|
}) {
|
|
31
32
|
const t = useT();
|
|
32
|
-
const { collapsed, setCollapsed } = useZoneCollapse(pageType);
|
|
33
|
+
const { collapsed, setCollapsed, isHydrated } = useZoneCollapse(pageType);
|
|
33
34
|
const canCollapse = React.useSyncExternalStore(
|
|
34
35
|
subscribeViewport,
|
|
35
36
|
getViewportSnapshot,
|
|
@@ -76,6 +77,17 @@ function CollapsibleZoneLayout({
|
|
|
76
77
|
setCollapsed(false);
|
|
77
78
|
setExpandedWhileConstrained(!canShowSideBySide);
|
|
78
79
|
}, [canCollapse, canShowSideBySide, setCollapsed]);
|
|
80
|
+
const handleSectionActivate = React.useCallback((section) => {
|
|
81
|
+
if (!canCollapse) return;
|
|
82
|
+
setCollapsed(false);
|
|
83
|
+
setExpandedWhileConstrained(!canShowSideBySide);
|
|
84
|
+
requestAnimationFrame(() => {
|
|
85
|
+
const target = document.getElementById(section.targetId ?? `collapsible-group-wrapper-${section.id}`) ?? document.getElementById(`collapsible-group-${section.id}`);
|
|
86
|
+
const headingButton = target?.querySelector("button[aria-controls]");
|
|
87
|
+
target?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
88
|
+
headingButton?.focus({ preventScroll: true });
|
|
89
|
+
});
|
|
90
|
+
}, [canCollapse, canShowSideBySide, setCollapsed]);
|
|
79
91
|
const handleCollapse = React.useCallback(() => {
|
|
80
92
|
if (!canCollapse) return;
|
|
81
93
|
setExpandedWhileConstrained(false);
|
|
@@ -89,8 +101,11 @@ function CollapsibleZoneLayout({
|
|
|
89
101
|
{
|
|
90
102
|
ref: layoutRef,
|
|
91
103
|
"data-zone-layout-mode": layoutMode,
|
|
104
|
+
"data-persistence-hydrated": isHydrated ? "true" : "false",
|
|
105
|
+
"aria-hidden": isHydrated ? void 0 : true,
|
|
92
106
|
className: cn(
|
|
93
107
|
"flex gap-4",
|
|
108
|
+
!isHydrated && "invisible",
|
|
94
109
|
showStackedExpanded ? "flex-col" : "flex-col lg:flex-row"
|
|
95
110
|
),
|
|
96
111
|
children: showCollapsedRail ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -112,10 +127,15 @@ function CollapsibleZoneLayout({
|
|
|
112
127
|
const SectionIcon = section.icon;
|
|
113
128
|
const hasErrors = Boolean(section.errorCount && section.errorCount > 0);
|
|
114
129
|
return /* @__PURE__ */ jsxs(
|
|
115
|
-
|
|
130
|
+
IconButton,
|
|
116
131
|
{
|
|
117
|
-
|
|
132
|
+
type: "button",
|
|
133
|
+
variant: "ghost",
|
|
134
|
+
size: "default",
|
|
135
|
+
onClick: () => handleSectionActivate(section),
|
|
136
|
+
className: "relative size-9 rounded-[10px] border border-transparent bg-muted/70 text-muted-foreground hover:border-border hover:bg-accent hover:text-accent-foreground",
|
|
118
137
|
title: section.label,
|
|
138
|
+
"aria-label": section.ariaLabel ?? section.label,
|
|
119
139
|
children: [
|
|
120
140
|
/* @__PURE__ */ jsx(SectionIcon, { className: "size-4" }),
|
|
121
141
|
hasErrors ? /* @__PURE__ */ jsx("span", { className: "absolute right-1.5 top-1.5 size-1.5 rounded-full bg-destructive" }) : null
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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 <
|
|
5
|
-
"mappings": ";
|
|
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 { IconButton } from '../../primitives/icon-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 targetId?: string\n ariaLabel?: 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, isHydrated } = 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 handleSectionActivate = React.useCallback((section: ZoneSectionDescriptor) => {\n if (!canCollapse) return\n setCollapsed(false)\n setExpandedWhileConstrained(!canShowSideBySide)\n requestAnimationFrame(() => {\n const target =\n document.getElementById(section.targetId ?? `collapsible-group-wrapper-${section.id}`)\n ?? document.getElementById(`collapsible-group-${section.id}`)\n const headingButton = target?.querySelector<HTMLButtonElement>('button[aria-controls]')\n target?.scrollIntoView({ behavior: 'smooth', block: 'start' })\n headingButton?.focus({ preventScroll: true })\n })\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 data-persistence-hydrated={isHydrated ? 'true' : 'false'}\n aria-hidden={isHydrated ? undefined : true}\n className={cn(\n 'flex gap-4',\n !isHydrated && 'invisible',\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 <IconButton\n key={section.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"default\"\n onClick={() => handleSectionActivate(section)}\n className=\"relative size-9 rounded-[10px] border border-transparent bg-muted/70 text-muted-foreground hover:border-border hover:bg-accent hover:text-accent-foreground\"\n title={section.label}\n aria-label={section.ariaLabel ?? 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 </IconButton>\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": ";AA2JQ,mBAWM,KAQM,YAnBZ;AA1JR,YAAY,WAAW;AACvB,SAAS,cAAc,qBAAqB;AAC5C,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAGhC,MAAM,yBAAyB;AAuB/B,SAAS,kBAAkB,UAAsB;AAC/C,QAAM,aAAa,OAAO,WAAW,qBAAqB;AAC1D,aAAW,iBAAiB,UAAU,QAAQ;AAC9C,SAAO,MAAM,WAAW,oBAAoB,UAAU,QAAQ;AAChE;AAEA,SAAS,sBAAsB;AAC7B,SAAO,OAAO,WAAW,qBAAqB,EAAE;AAClD;AAEA,SAAS,4BAA4B;AACnC,SAAO;AACT;AAEO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,UAAU;AAAA,EACV;AACF,GAA+B;AAC7B,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,WAAW,cAAc,WAAW,IAAI,gBAAgB,QAAQ;AACxE,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,YAAY,MAAM,OAAuB,IAAI;AACnD,QAAM,kBAAkB,MAAM,OAA0B,IAAI;AAC5D,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAS,MAAO,OAAO,WAAW,cAAc,IAAI,OAAO,UAAW;AACxH,QAAM,CAAC,0BAA0B,2BAA2B,IAAI,MAAM,SAAS,KAAK;AAEpF,QAAM,UAAU,MAAM;AACpB,UAAM,OAAO,UAAU;AACvB,QAAI,CAAC,KAAM;AAEX,UAAM,cAAc,CAAC,cAAsB;AACzC,wBAAkB,CAAC,SAAU,KAAK,IAAI,OAAO,SAAS,IAAI,IAAI,OAAO,SAAU;AAAA,IACjF;AAEA,UAAM,UAAU,MAAM;AACpB,kBAAY,KAAK,sBAAsB,EAAE,SAAS,OAAO,UAAU;AAAA,IACrE;AAEA,YAAQ;AAER,QAAI,OAAO,mBAAmB,aAAa;AACzC,aAAO,iBAAiB,UAAU,OAAO;AACzC,aAAO,MAAM,OAAO,oBAAoB,UAAU,OAAO;AAAA,IAC3D;AAEA,UAAM,WAAW,IAAI,eAAe,CAAC,YAAY;AAC/C,YAAM,QAAQ,QAAQ,CAAC;AACvB,UAAI,CAAC,MAAO;AACZ,kBAAY,MAAM,YAAY,KAAK;AAAA,IACrC,CAAC;AAED,aAAS,QAAQ,IAAI;AACrB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,CAAC;AAEL,QAAM,oBAAoB,kBAAkB;AAE5C,QAAM,UAAU,MAAM;AACpB,QAAI,mBAAmB;AACrB,kCAA4B,KAAK;AAAA,IACnC;AAAA,EACF,GAAG,CAAC,iBAAiB,CAAC;AAEtB,QAAM,oBAAoB,gBAAgB,aAAc,CAAC,qBAAqB,CAAC;AAC/E,QAAM,sBAAsB,CAAC,qBAAqB,CAAC;AACnD,QAAM,aAAa,oBAAoB,cAAc,sBAAsB,YAAY;AACvF,QAAM,uBAAuB,oBACzB,EAAE,OAAO,mBAAmB,WAAW,kBAAkB,IACzD;AAEJ,QAAM,eAAe,MAAM,YAAY,MAAM;AAC3C,QAAI,CAAC,YAAa;AAClB,iBAAa,KAAK;AAClB,gCAA4B,CAAC,iBAAiB;AAAA,EAChD,GAAG,CAAC,aAAa,mBAAmB,YAAY,CAAC;AAEjD,QAAM,wBAAwB,MAAM,YAAY,CAAC,YAAmC;AAClF,QAAI,CAAC,YAAa;AAClB,iBAAa,KAAK;AAClB,gCAA4B,CAAC,iBAAiB;AAC9C,0BAAsB,MAAM;AAC1B,YAAM,SACJ,SAAS,eAAe,QAAQ,YAAY,6BAA6B,QAAQ,EAAE,EAAE,KAClF,SAAS,eAAe,qBAAqB,QAAQ,EAAE,EAAE;AAC9D,YAAM,gBAAgB,QAAQ,cAAiC,uBAAuB;AACtF,cAAQ,eAAe,EAAE,UAAU,UAAU,OAAO,QAAQ,CAAC;AAC7D,qBAAe,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,IAC9C,CAAC;AAAA,EACH,GAAG,CAAC,aAAa,mBAAmB,YAAY,CAAC;AAEjD,QAAM,iBAAiB,MAAM,YAAY,MAAM;AAC7C,QAAI,CAAC,YAAa;AAClB,gCAA4B,KAAK;AACjC,iBAAa,IAAI;AACjB,0BAAsB,MAAM;AAC1B,sBAAgB,SAAS,MAAM;AAAA,IACjC,CAAC;AAAA,EACH,GAAG,CAAC,aAAa,YAAY,CAAC;AAE9B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,yBAAuB;AAAA,MACvB,6BAA2B,aAAa,SAAS;AAAA,MACjD,eAAa,aAAa,SAAY;AAAA,MACtC,WAAW;AAAA,QACT;AAAA,QACA,CAAC,cAAc;AAAA,QACf,sBAAsB,aAAa;AAAA,MACrC;AAAA,MAEC,8BACC,iCACE;AAAA,6BAAC,SAAI,WAAU,uDACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,KAAK;AAAA,cACL,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAU;AAAA,cACV,cAAY,EAAE,kBAAkB,mBAAmB;AAAA,cAEnD,8BAAC,iBAAc,WAAU,UAAS;AAAA;AAAA,UACpC;AAAA,UACC,UAAU,SACT,oBAAC,SAAI,WAAU,6FACZ,mBAAS,IAAI,CAAC,YAAY;AACzB,kBAAM,cAAc,QAAQ;AAC5B,kBAAM,YAAY,QAAQ,QAAQ,cAAc,QAAQ,aAAa,CAAC;AACtE,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,MAAK;AAAA,gBACL,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,SAAS,MAAM,sBAAsB,OAAO;AAAA,gBAC5C,WAAU;AAAA,gBACV,OAAO,QAAQ;AAAA,gBACf,cAAY,QAAQ,aAAa,QAAQ;AAAA,gBAEzC;AAAA,sCAAC,eAAY,WAAU,UAAS;AAAA,kBAC/B,YACC,oBAAC,UAAK,WAAU,mEAAkE,IAChF;AAAA;AAAA;AAAA,cAZC,QAAQ;AAAA,YAaf;AAAA,UAEJ,CAAC,GACH,IACE;AAAA,WACN;AAAA,QAEA,oBAAC,SAAI,WAAU,kBACZ,iBACH;AAAA,SACF,IACE,sBACF,iCACE;AAAA,6BAAC,SAAI,WAAU,oBACZ;AAAA,wBACC,oBAAC,SAAI,WAAU,oBACb;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAU;AAAA,cACV,cAAY,EAAE,oBAAoB,qBAAqB;AAAA,cAEvD,8BAAC,gBAAa,WAAU,UAAS;AAAA;AAAA,UACnC,GACF,IACE;AAAA,UACJ,oBAAC,SAAI,WAAU,UACZ,iBACH;AAAA,WACF;AAAA,QAEA,oBAAC,SAAI,WAAU,kBACZ,iBACH;AAAA,SACF,IAEA,iCAEE;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW,GAAG,sBAAsB,oBAAoB,SAAY,YAAY;AAAA,YAChF,OAAO;AAAA,YAEN;AAAA;AAAA,QACH;AAAA,QAGA,qBAAC,SAAI,WAAU,wEACb;AAAA,8BAAC,SAAI,WAAU,+DAA8D;AAAA,UAC7E;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAU;AAAA,cACV,cAAY,EAAE,oBAAoB,qBAAqB;AAAA,cAEvD,8BAAC,gBAAa,WAAU,UAAS;AAAA;AAAA,UACnC;AAAA,WACF;AAAA,QAGA,oBAAC,SAAI,WAAU,4BACZ,iBACH;AAAA,SACF;AAAA;AAAA,EAEJ;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,7 +5,7 @@ function getStorageKey(pageType, groupId) {
|
|
|
5
5
|
return `om:collapsible:${pageType}:${groupId}`;
|
|
6
6
|
}
|
|
7
7
|
function useGroupCollapse(pageType, groupId, defaultExpanded = true) {
|
|
8
|
-
const { value: expanded, toggle, setValue } = usePersistedBooleanFlag(
|
|
8
|
+
const { value: expanded, toggle, setValue, isHydrated } = usePersistedBooleanFlag(
|
|
9
9
|
getStorageKey(pageType, groupId),
|
|
10
10
|
defaultExpanded
|
|
11
11
|
);
|
|
@@ -16,7 +16,7 @@ function useGroupCollapse(pageType, groupId, defaultExpanded = true) {
|
|
|
16
16
|
setValue(next);
|
|
17
17
|
}
|
|
18
18
|
}, [setValue]);
|
|
19
|
-
return { expanded, toggle, setExpanded };
|
|
19
|
+
return { expanded, toggle, setExpanded, isHydrated };
|
|
20
20
|
}
|
|
21
21
|
export {
|
|
22
22
|
useGroupCollapse
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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,
|
|
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, isHydrated } = 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, isHydrated }\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,UAAU,WAAW,IAAI;AAAA,IACxD,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,aAAa,WAAW;AACrD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,27 +1,68 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import {
|
|
4
4
|
readJsonFromLocalStorage,
|
|
5
5
|
writeJsonToLocalStorage
|
|
6
6
|
} from "@open-mercato/shared/lib/browser/safeLocalStorage";
|
|
7
|
+
const LOCAL_BROADCAST_EVENT = "om:persisted-boolean-flag:change";
|
|
8
|
+
const localEmitter = typeof window !== "undefined" ? new EventTarget() : null;
|
|
9
|
+
function readCurrentValue(storageKey, defaultValue) {
|
|
10
|
+
const saved = readJsonFromLocalStorage(storageKey, null);
|
|
11
|
+
if (saved === "1") return true;
|
|
12
|
+
if (saved === "0") return false;
|
|
13
|
+
return defaultValue;
|
|
14
|
+
}
|
|
15
|
+
function persistValue(storageKey, next) {
|
|
16
|
+
writeJsonToLocalStorage(storageKey, next ? "1" : "0");
|
|
17
|
+
localEmitter?.dispatchEvent(
|
|
18
|
+
new CustomEvent(LOCAL_BROADCAST_EVENT, { detail: storageKey })
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
function subscribe(storageKey, onChange) {
|
|
22
|
+
if (typeof window === "undefined") return () => {
|
|
23
|
+
};
|
|
24
|
+
const handleStorage = (event) => {
|
|
25
|
+
if (event.key === storageKey) onChange();
|
|
26
|
+
};
|
|
27
|
+
const handleLocal = (event) => {
|
|
28
|
+
const detail = event.detail;
|
|
29
|
+
if (detail === storageKey) onChange();
|
|
30
|
+
};
|
|
31
|
+
window.addEventListener("storage", handleStorage);
|
|
32
|
+
localEmitter?.addEventListener(LOCAL_BROADCAST_EVENT, handleLocal);
|
|
33
|
+
return () => {
|
|
34
|
+
window.removeEventListener("storage", handleStorage);
|
|
35
|
+
localEmitter?.removeEventListener(LOCAL_BROADCAST_EVENT, handleLocal);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
7
38
|
function usePersistedBooleanFlag(storageKey, defaultValue) {
|
|
8
|
-
const [
|
|
9
|
-
const
|
|
39
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
40
|
+
const getSnapshot = useCallback(
|
|
41
|
+
() => readCurrentValue(storageKey, defaultValue),
|
|
42
|
+
[storageKey, defaultValue]
|
|
43
|
+
);
|
|
44
|
+
const getServerSnapshot = useCallback(() => defaultValue, [defaultValue]);
|
|
45
|
+
const subscribeKey = useCallback(
|
|
46
|
+
(onChange) => subscribe(storageKey, onChange),
|
|
47
|
+
[storageKey]
|
|
48
|
+
);
|
|
49
|
+
const value = useSyncExternalStore(subscribeKey, getSnapshot, getServerSnapshot);
|
|
10
50
|
useEffect(() => {
|
|
11
|
-
|
|
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);
|
|
51
|
+
setIsHydrated(true);
|
|
23
52
|
}, []);
|
|
24
|
-
|
|
53
|
+
const setValue = useCallback(
|
|
54
|
+
(next) => {
|
|
55
|
+
const current = readCurrentValue(storageKey, defaultValue);
|
|
56
|
+
const nextValue = typeof next === "function" ? next(current) : next;
|
|
57
|
+
persistValue(storageKey, nextValue);
|
|
58
|
+
},
|
|
59
|
+
[storageKey, defaultValue]
|
|
60
|
+
);
|
|
61
|
+
const toggle = useCallback(() => {
|
|
62
|
+
const current = readCurrentValue(storageKey, defaultValue);
|
|
63
|
+
persistValue(storageKey, !current);
|
|
64
|
+
}, [storageKey, defaultValue]);
|
|
65
|
+
return { value, toggle, setValue, isHydrated };
|
|
25
66
|
}
|
|
26
67
|
export {
|
|
27
68
|
usePersistedBooleanFlag
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/crud/usePersistedBooleanFlag.ts"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport {
|
|
5
|
-
"mappings": ";AACA,SAAS,
|
|
4
|
+
"sourcesContent": ["'use client'\nimport { useCallback, useEffect, useState, useSyncExternalStore } from 'react'\nimport {\n readJsonFromLocalStorage,\n writeJsonToLocalStorage,\n} from '@open-mercato/shared/lib/browser/safeLocalStorage'\n\nconst LOCAL_BROADCAST_EVENT = 'om:persisted-boolean-flag:change'\n\ntype SafeEventTarget = {\n addEventListener: EventTarget['addEventListener']\n removeEventListener: EventTarget['removeEventListener']\n dispatchEvent: EventTarget['dispatchEvent']\n}\n\nconst localEmitter: SafeEventTarget | null =\n typeof window !== 'undefined' ? new EventTarget() : null\n\nfunction readCurrentValue(storageKey: string, defaultValue: boolean): boolean {\n const saved = readJsonFromLocalStorage<string | null>(storageKey, null)\n if (saved === '1') return true\n if (saved === '0') return false\n return defaultValue\n}\n\nfunction persistValue(storageKey: string, next: boolean): void {\n writeJsonToLocalStorage(storageKey, next ? '1' : '0')\n localEmitter?.dispatchEvent(\n new CustomEvent(LOCAL_BROADCAST_EVENT, { detail: storageKey }),\n )\n}\n\nfunction subscribe(storageKey: string, onChange: () => void): () => void {\n if (typeof window === 'undefined') return () => {}\n\n const handleStorage = (event: StorageEvent) => {\n if (event.key === storageKey) onChange()\n }\n const handleLocal = (event: Event) => {\n const detail = (event as CustomEvent<string>).detail\n if (detail === storageKey) onChange()\n }\n\n window.addEventListener('storage', handleStorage)\n localEmitter?.addEventListener(LOCAL_BROADCAST_EVENT, handleLocal)\n\n return () => {\n window.removeEventListener('storage', handleStorage)\n localEmitter?.removeEventListener(LOCAL_BROADCAST_EVENT, handleLocal)\n }\n}\n\nexport function usePersistedBooleanFlag(storageKey: string, defaultValue: boolean) {\n const [isHydrated, setIsHydrated] = useState(false)\n const getSnapshot = useCallback(\n () => readCurrentValue(storageKey, defaultValue),\n [storageKey, defaultValue],\n )\n const getServerSnapshot = useCallback(() => defaultValue, [defaultValue])\n const subscribeKey = useCallback(\n (onChange: () => void) => subscribe(storageKey, onChange),\n [storageKey],\n )\n\n const value = useSyncExternalStore(subscribeKey, getSnapshot, getServerSnapshot)\n\n useEffect(() => {\n setIsHydrated(true)\n }, [])\n\n const setValue = useCallback(\n (next: boolean | ((prev: boolean) => boolean)) => {\n const current = readCurrentValue(storageKey, defaultValue)\n const nextValue =\n typeof next === 'function'\n ? (next as (prev: boolean) => boolean)(current)\n : next\n persistValue(storageKey, nextValue)\n },\n [storageKey, defaultValue],\n )\n\n const toggle = useCallback(() => {\n const current = readCurrentValue(storageKey, defaultValue)\n persistValue(storageKey, !current)\n }, [storageKey, defaultValue])\n\n return { value, toggle, setValue, isHydrated }\n}\n"],
|
|
5
|
+
"mappings": ";AACA,SAAS,aAAa,WAAW,UAAU,4BAA4B;AACvE;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,wBAAwB;AAQ9B,MAAM,eACJ,OAAO,WAAW,cAAc,IAAI,YAAY,IAAI;AAEtD,SAAS,iBAAiB,YAAoB,cAAgC;AAC5E,QAAM,QAAQ,yBAAwC,YAAY,IAAI;AACtE,MAAI,UAAU,IAAK,QAAO;AAC1B,MAAI,UAAU,IAAK,QAAO;AAC1B,SAAO;AACT;AAEA,SAAS,aAAa,YAAoB,MAAqB;AAC7D,0BAAwB,YAAY,OAAO,MAAM,GAAG;AACpD,gBAAc;AAAA,IACZ,IAAI,YAAY,uBAAuB,EAAE,QAAQ,WAAW,CAAC;AAAA,EAC/D;AACF;AAEA,SAAS,UAAU,YAAoB,UAAkC;AACvE,MAAI,OAAO,WAAW,YAAa,QAAO,MAAM;AAAA,EAAC;AAEjD,QAAM,gBAAgB,CAAC,UAAwB;AAC7C,QAAI,MAAM,QAAQ,WAAY,UAAS;AAAA,EACzC;AACA,QAAM,cAAc,CAAC,UAAiB;AACpC,UAAM,SAAU,MAA8B;AAC9C,QAAI,WAAW,WAAY,UAAS;AAAA,EACtC;AAEA,SAAO,iBAAiB,WAAW,aAAa;AAChD,gBAAc,iBAAiB,uBAAuB,WAAW;AAEjE,SAAO,MAAM;AACX,WAAO,oBAAoB,WAAW,aAAa;AACnD,kBAAc,oBAAoB,uBAAuB,WAAW;AAAA,EACtE;AACF;AAEO,SAAS,wBAAwB,YAAoB,cAAuB;AACjF,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,cAAc;AAAA,IAClB,MAAM,iBAAiB,YAAY,YAAY;AAAA,IAC/C,CAAC,YAAY,YAAY;AAAA,EAC3B;AACA,QAAM,oBAAoB,YAAY,MAAM,cAAc,CAAC,YAAY,CAAC;AACxE,QAAM,eAAe;AAAA,IACnB,CAAC,aAAyB,UAAU,YAAY,QAAQ;AAAA,IACxD,CAAC,UAAU;AAAA,EACb;AAEA,QAAM,QAAQ,qBAAqB,cAAc,aAAa,iBAAiB;AAE/E,YAAU,MAAM;AACd,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAC,CAAC;AAEL,QAAM,WAAW;AAAA,IACf,CAAC,SAAiD;AAChD,YAAM,UAAU,iBAAiB,YAAY,YAAY;AACzD,YAAM,YACJ,OAAO,SAAS,aACX,KAAoC,OAAO,IAC5C;AACN,mBAAa,YAAY,SAAS;AAAA,IACpC;AAAA,IACA,CAAC,YAAY,YAAY;AAAA,EAC3B;AAEA,QAAM,SAAS,YAAY,MAAM;AAC/B,UAAM,UAAU,iBAAiB,YAAY,YAAY;AACzD,iBAAa,YAAY,CAAC,OAAO;AAAA,EACnC,GAAG,CAAC,YAAY,YAAY,CAAC;AAE7B,SAAO,EAAE,OAAO,QAAQ,UAAU,WAAW;AAC/C;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,7 +5,7 @@ function getStorageKey(pageType) {
|
|
|
5
5
|
return `om:zone1-collapsed:${pageType}`;
|
|
6
6
|
}
|
|
7
7
|
function useZoneCollapse(pageType) {
|
|
8
|
-
const { value: collapsed, toggle, setValue } = usePersistedBooleanFlag(
|
|
8
|
+
const { value: collapsed, toggle, setValue, isHydrated } = usePersistedBooleanFlag(
|
|
9
9
|
getStorageKey(pageType),
|
|
10
10
|
false
|
|
11
11
|
);
|
|
@@ -16,7 +16,7 @@ function useZoneCollapse(pageType) {
|
|
|
16
16
|
setValue(next);
|
|
17
17
|
}
|
|
18
18
|
}, [setValue]);
|
|
19
|
-
return { collapsed, toggle, setCollapsed };
|
|
19
|
+
return { collapsed, toggle, setCollapsed, isHydrated };
|
|
20
20
|
}
|
|
21
21
|
export {
|
|
22
22
|
useZoneCollapse
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/crud/useZoneCollapse.ts"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport { useCallback } from 'react'\nimport { usePersistedBooleanFlag } from './usePersistedBooleanFlag'\n\nfunction getStorageKey(pageType: string) {\n return `om:zone1-collapsed:${pageType}`\n}\n\nexport function useZoneCollapse(pageType: string) {\n const { value: collapsed, toggle, setValue } = usePersistedBooleanFlag(\n getStorageKey(pageType),\n false,\n )\n const setCollapsed = 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 { collapsed, toggle, setCollapsed }\n}\n"],
|
|
5
|
-
"mappings": ";AACA,SAAS,mBAAmB;AAC5B,SAAS,+BAA+B;AAExC,SAAS,cAAc,UAAkB;AACvC,SAAO,sBAAsB,QAAQ;AACvC;AAEO,SAAS,gBAAgB,UAAkB;AAChD,QAAM,EAAE,OAAO,WAAW,QAAQ,
|
|
4
|
+
"sourcesContent": ["'use client'\nimport { useCallback } from 'react'\nimport { usePersistedBooleanFlag } from './usePersistedBooleanFlag'\n\nfunction getStorageKey(pageType: string) {\n return `om:zone1-collapsed:${pageType}`\n}\n\nexport function useZoneCollapse(pageType: string) {\n const { value: collapsed, toggle, setValue, isHydrated } = usePersistedBooleanFlag(\n getStorageKey(pageType),\n false,\n )\n const setCollapsed = 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 { collapsed, toggle, setCollapsed, isHydrated }\n}\n"],
|
|
5
|
+
"mappings": ";AACA,SAAS,mBAAmB;AAC5B,SAAS,+BAA+B;AAExC,SAAS,cAAc,UAAkB;AACvC,SAAO,sBAAsB,QAAQ;AACvC;AAEO,SAAS,gBAAgB,UAAkB;AAChD,QAAM,EAAE,OAAO,WAAW,QAAQ,UAAU,WAAW,IAAI;AAAA,IACzD,cAAc,QAAQ;AAAA,IACtB;AAAA,EACF;AACA,QAAM,eAAe,YAAY,CAAC,SAAiD;AACjF,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,WAAW,QAAQ,cAAc,WAAW;AACvD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|