@open-mercato/ui 0.7.1-develop.7191.1.3c9346566b → 0.7.1-develop.7194.1.ab4fc81f82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/backend/AppShell.js +66 -7
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +14 -8
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +167 -24
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/PerspectiveSidebar.js +60 -21
- package/dist/backend/PerspectiveSidebar.js.map +2 -2
- package/dist/backend/forms/ActionsDropdown.js +35 -5
- package/dist/backend/forms/ActionsDropdown.js.map +2 -2
- package/dist/backend/forms/index.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +6 -0
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/injection/spotIds.js +2 -0
- package/dist/backend/injection/spotIds.js.map +2 -2
- package/dist/backend/inputs/DurationInput.js +182 -0
- package/dist/backend/inputs/DurationInput.js.map +7 -0
- package/dist/backend/inputs/EventSelect.js.map +2 -2
- package/dist/backend/inputs/index.js +1 -0
- package/dist/backend/inputs/index.js.map +2 -2
- package/dist/backend/nav/navBadges.js +38 -0
- package/dist/backend/nav/navBadges.js.map +7 -0
- package/dist/backend/views/NewViewForm.js +46 -38
- package/dist/backend/views/NewViewForm.js.map +2 -2
- package/dist/primitives/segmented-control.js +4 -4
- package/dist/primitives/segmented-control.js.map +1 -1
- package/dist/primitives/sheet.js +21 -11
- package/dist/primitives/sheet.js.map +2 -2
- package/dist/primitives/textarea.js +54 -4
- package/dist/primitives/textarea.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +106 -2
- package/src/backend/CrudForm.tsx +28 -9
- package/src/backend/DataTable.tsx +293 -36
- package/src/backend/PerspectiveSidebar.tsx +80 -21
- package/src/backend/__tests__/CrudForm.density.test.tsx +113 -0
- package/src/backend/__tests__/DataTable.exportSectionTitles.test.tsx +134 -0
- package/src/backend/__tests__/DataTable.perspectiveReconcile.test.tsx +312 -0
- package/src/backend/__tests__/DataTable.propBulkActions.test.tsx +200 -0
- package/src/backend/__tests__/DataTable.render.test.tsx +76 -0
- package/src/backend/__tests__/PerspectiveSidebar.roleAutosave.test.tsx +252 -0
- package/src/backend/__tests__/navBadges.test.ts +57 -0
- package/src/backend/forms/ActionsDropdown.tsx +73 -6
- package/src/backend/forms/index.ts +1 -1
- package/src/backend/icons/lucideRegistry.generated.tsx +6 -0
- package/src/backend/injection/spotIds.ts +8 -0
- package/src/backend/inputs/DurationInput.tsx +207 -0
- package/src/backend/inputs/EventSelect.tsx +2 -0
- package/src/backend/inputs/__tests__/DurationInput.test.tsx +220 -0
- package/src/backend/inputs/index.ts +1 -0
- package/src/backend/nav/navBadges.ts +61 -0
- package/src/backend/views/NewViewForm.tsx +46 -32
- package/src/primitives/__tests__/segmented-control.test.tsx +2 -2
- package/src/primitives/__tests__/textarea.test.tsx +50 -0
- package/src/primitives/segmented-control.tsx +7 -7
- package/src/primitives/sheet.tsx +7 -1
- package/src/primitives/textarea.tsx +77 -4
package/dist/primitives/sheet.js
CHANGED
|
@@ -12,6 +12,7 @@ const SheetPortal = DialogPrimitive.Portal;
|
|
|
12
12
|
const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
13
13
|
DialogPrimitive.Overlay,
|
|
14
14
|
{
|
|
15
|
+
"data-slot": "sheet-overlay",
|
|
15
16
|
className: cn(
|
|
16
17
|
"fixed inset-x-0 bottom-0 top-[var(--topbar-height,0px)] z-overlay bg-black/40 backdrop-blur-sm",
|
|
17
18
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
@@ -45,17 +46,26 @@ const sheetVariants = cva(
|
|
|
45
46
|
);
|
|
46
47
|
const SheetContent = React.forwardRef(({ side = "right", className, children, hideClose = false, closeLabel = "Close", ...props }, ref) => /* @__PURE__ */ jsxs(SheetPortal, { children: [
|
|
47
48
|
/* @__PURE__ */ jsx(SheetOverlay, {}),
|
|
48
|
-
/* @__PURE__ */ jsxs(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
/* @__PURE__ */ jsxs(
|
|
50
|
+
DialogPrimitive.Content,
|
|
51
|
+
{
|
|
52
|
+
ref,
|
|
53
|
+
"data-slot": "sheet-content",
|
|
54
|
+
className: cn(sheetVariants({ side }), className),
|
|
55
|
+
...props,
|
|
56
|
+
children: [
|
|
57
|
+
children,
|
|
58
|
+
!hideClose ? /* @__PURE__ */ jsx(
|
|
59
|
+
DialogPrimitive.Close,
|
|
60
|
+
{
|
|
61
|
+
className: "absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:shadow-focus",
|
|
62
|
+
"aria-label": closeLabel,
|
|
63
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
|
|
64
|
+
}
|
|
65
|
+
) : null
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
)
|
|
59
69
|
] }));
|
|
60
70
|
SheetContent.displayName = DialogPrimitive.Content.displayName;
|
|
61
71
|
const SheetHeader = React.forwardRef(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/sheet.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { X } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst Sheet = DialogPrimitive.Root\n\nconst SheetTrigger = DialogPrimitive.Trigger\n\nconst SheetClose = DialogPrimitive.Close\n\nconst SheetPortal = DialogPrimitive.Portal\n\nconst SheetOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n className={cn(\n 'fixed inset-x-0 bottom-0 top-[var(--topbar-height,0px)] z-overlay bg-black/40 backdrop-blur-sm',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n className,\n )}\n {...props}\n ref={ref}\n />\n))\nSheetOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst sheetVariants = cva(\n cn(\n 'fixed z-modal flex flex-col gap-4 bg-background shadow-lg transition ease-in-out',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:duration-200 data-[state=open]:duration-300',\n ),\n {\n variants: {\n side: {\n top:\n 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n bottom:\n 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n left:\n 'top-[var(--topbar-height,0px)] bottom-0 left-0 w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md',\n right:\n 'top-[var(--topbar-height,0px)] bottom-0 right-0 w-full sm:max-w-md border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',\n },\n },\n defaultVariants: {\n side: 'right',\n },\n }\n)\n\nexport type SheetContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &\n VariantProps<typeof sheetVariants> & {\n /** Hide the built-in close (X) button \u2014 useful when the consumer renders its own close affordance. */\n hideClose?: boolean\n /** Override the aria-label for the built-in close button. */\n closeLabel?: string\n }\n\n/**\n * SheetContent \u2014 side-anchored Radix Dialog wrapper.\n *\n * Topbar integration: the `left` and `right` variants anchor at\n * `top: var(--topbar-height, 0px)` so a sticky app topbar stays visible above\n * the panel. Consumers that render inside `AppShell` get the correct offset\n * because the shell sets `--topbar-height` on the outer container. Surfaces\n * outside the shell (portal modals, marketing pages, standalone embeds) keep\n * the default `0px` fallback and align to the viewport top \u2014 that is the\n * correct behavior when there is no topbar to clear.\n *\n * To opt into the offset from your own layout, set the CSS variable on any\n * ancestor of `<SheetContent>`:\n *\n * ```css\n * .my-shell { --topbar-height: 60px; }\n * ```\n */\n\nconst SheetContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n SheetContentProps\n>(({ side = 'right', className, children, hideClose = false, closeLabel = 'Close', ...props }, ref) => (\n <SheetPortal>\n <SheetOverlay />\n <DialogPrimitive.Content
|
|
5
|
-
"mappings": ";AAoBE,
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { X } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst Sheet = DialogPrimitive.Root\n\nconst SheetTrigger = DialogPrimitive.Trigger\n\nconst SheetClose = DialogPrimitive.Close\n\nconst SheetPortal = DialogPrimitive.Portal\n\nconst SheetOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n data-slot=\"sheet-overlay\"\n className={cn(\n 'fixed inset-x-0 bottom-0 top-[var(--topbar-height,0px)] z-overlay bg-black/40 backdrop-blur-sm',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n className,\n )}\n {...props}\n ref={ref}\n />\n))\nSheetOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst sheetVariants = cva(\n cn(\n 'fixed z-modal flex flex-col gap-4 bg-background shadow-lg transition ease-in-out',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:duration-200 data-[state=open]:duration-300',\n ),\n {\n variants: {\n side: {\n top:\n 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n bottom:\n 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n left:\n 'top-[var(--topbar-height,0px)] bottom-0 left-0 w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md',\n right:\n 'top-[var(--topbar-height,0px)] bottom-0 right-0 w-full sm:max-w-md border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',\n },\n },\n defaultVariants: {\n side: 'right',\n },\n }\n)\n\nexport type SheetContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &\n VariantProps<typeof sheetVariants> & {\n /** Hide the built-in close (X) button \u2014 useful when the consumer renders its own close affordance. */\n hideClose?: boolean\n /** Override the aria-label for the built-in close button. */\n closeLabel?: string\n }\n\n/**\n * SheetContent \u2014 side-anchored Radix Dialog wrapper.\n *\n * Topbar integration: the `left` and `right` variants anchor at\n * `top: var(--topbar-height, 0px)` so a sticky app topbar stays visible above\n * the panel. Consumers that render inside `AppShell` get the correct offset\n * because the shell sets `--topbar-height` on the outer container. Surfaces\n * outside the shell (portal modals, marketing pages, standalone embeds) keep\n * the default `0px` fallback and align to the viewport top \u2014 that is the\n * correct behavior when there is no topbar to clear.\n *\n * To opt into the offset from your own layout, set the CSS variable on any\n * ancestor of `<SheetContent>`:\n *\n * ```css\n * .my-shell { --topbar-height: 60px; }\n * ```\n */\n\nconst SheetContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n SheetContentProps\n>(({ side = 'right', className, children, hideClose = false, closeLabel = 'Close', ...props }, ref) => (\n <SheetPortal>\n <SheetOverlay />\n <DialogPrimitive.Content\n ref={ref}\n data-slot=\"sheet-content\"\n className={cn(sheetVariants({ side }), className)}\n {...props}\n >\n {children}\n {!hideClose ? (\n <DialogPrimitive.Close\n className=\"absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:shadow-focus\"\n aria-label={closeLabel}\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </DialogPrimitive.Close>\n ) : null}\n </DialogPrimitive.Content>\n </SheetPortal>\n))\nSheetContent.displayName = DialogPrimitive.Content.displayName\n\nconst SheetHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex flex-col gap-1.5 border-b px-4 py-3 pr-12 text-left', className)}\n {...props}\n />\n ),\n)\nSheetHeader.displayName = 'SheetHeader'\n\nconst SheetFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex flex-col-reverse gap-2 border-t px-4 py-3 sm:flex-row sm:justify-end', className)}\n {...props}\n />\n ),\n)\nSheetFooter.displayName = 'SheetFooter'\n\nconst SheetTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn('text-base font-semibold text-foreground', className)}\n {...props}\n />\n))\nSheetTitle.displayName = DialogPrimitive.Title.displayName\n\nconst SheetDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn('text-sm text-muted-foreground', className)}\n {...props}\n />\n))\nSheetDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetOverlay,\n SheetPortal,\n SheetTitle,\n SheetTrigger,\n sheetVariants,\n}\n"],
|
|
5
|
+
"mappings": ";AAoBE,cAwEE,YAxEF;AAlBF,YAAY,WAAW;AACvB,YAAY,qBAAqB;AACjC,SAAS,WAA8B;AACvC,SAAS,SAAS;AAClB,SAAS,UAAU;AAEnB,MAAM,QAAQ,gBAAgB;AAE9B,MAAM,eAAe,gBAAgB;AAErC,MAAM,aAAa,gBAAgB;AAEnC,MAAM,cAAc,gBAAgB;AAEpC,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC,aAAU;AAAA,IACV,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACC,GAAG;AAAA,IACJ;AAAA;AACF,CACD;AACD,aAAa,cAAc,gBAAgB,QAAQ;AAEnD,MAAM,gBAAgB;AAAA,EACpB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,KACE;AAAA,QACF,QACE;AAAA,QACF,MACE;AAAA,QACF,OACE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AA6BA,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,OAAO,SAAS,WAAW,UAAU,YAAY,OAAO,aAAa,SAAS,GAAG,MAAM,GAAG,QAC7F,qBAAC,eACC;AAAA,sBAAC,gBAAa;AAAA,EACd;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC;AAAA,MACA,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MAC/C,GAAG;AAAA,MAEH;AAAA;AAAA,QACA,CAAC,YACA;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,WAAU;AAAA,YACV,cAAY;AAAA,YAEZ,8BAAC,KAAE,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,QAC3C,IACE;AAAA;AAAA;AAAA,EACN;AAAA,GACF,CACD;AACD,aAAa,cAAc,gBAAgB,QAAQ;AAEnD,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,4DAA4D,SAAS;AAAA,MAClF,GAAG;AAAA;AAAA,EACN;AAEJ;AACA,YAAY,cAAc;AAE1B,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,6EAA6E,SAAS;AAAA,MACnG,GAAG;AAAA;AAAA,EACN;AAEJ;AACA,YAAY,cAAc;AAE1B,MAAM,aAAa,MAAM,WAGvB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,2CAA2C,SAAS;AAAA,IACjE,GAAG;AAAA;AACN,CACD;AACD,WAAW,cAAc,gBAAgB,MAAM;AAE/C,MAAM,mBAAmB,MAAM,WAG7B,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,iCAAiC,SAAS;AAAA,IACvD,GAAG;AAAA;AACN,CACD;AACD,iBAAiB,cAAc,gBAAgB,YAAY;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,29 +3,79 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
5
5
|
const baseTextareaClass = "flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-colors placeholder:text-muted-foreground outline-none focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground hover:bg-muted/40 disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:border-border-disabled disabled:shadow-none disabled:hover:bg-bg-disabled aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive resize-y min-h-[80px]";
|
|
6
|
+
const DEFAULT_AUTO_RESIZE_MAX_ROWS = 12;
|
|
7
|
+
const FALLBACK_LINE_HEIGHT_PX = 20;
|
|
8
|
+
function readPixels(value) {
|
|
9
|
+
const parsed = Number.parseFloat(value);
|
|
10
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
11
|
+
}
|
|
12
|
+
function applyAutoResize(element, maxRows) {
|
|
13
|
+
if (!element) return;
|
|
14
|
+
const computed = typeof window !== "undefined" ? window.getComputedStyle(element) : null;
|
|
15
|
+
const lineHeight = computed ? readPixels(computed.lineHeight) || FALLBACK_LINE_HEIGHT_PX : FALLBACK_LINE_HEIGHT_PX;
|
|
16
|
+
const padding = computed ? readPixels(computed.paddingTop) + readPixels(computed.paddingBottom) : 0;
|
|
17
|
+
const border = computed ? readPixels(computed.borderTopWidth) + readPixels(computed.borderBottomWidth) : 0;
|
|
18
|
+
const maxContentHeight = lineHeight * Math.max(1, maxRows) + padding;
|
|
19
|
+
element.style.height = "auto";
|
|
20
|
+
const contentHeight = element.scrollHeight;
|
|
21
|
+
const cappedHeight = Math.min(contentHeight, maxContentHeight);
|
|
22
|
+
element.style.height = `${cappedHeight + border}px`;
|
|
23
|
+
element.style.overflowY = contentHeight > maxContentHeight ? "auto" : "hidden";
|
|
24
|
+
}
|
|
6
25
|
const Textarea = React.forwardRef(
|
|
7
|
-
({
|
|
26
|
+
({
|
|
27
|
+
className,
|
|
28
|
+
showCount,
|
|
29
|
+
wrapperClassName,
|
|
30
|
+
autoResize,
|
|
31
|
+
maxRows = DEFAULT_AUTO_RESIZE_MAX_ROWS,
|
|
32
|
+
value,
|
|
33
|
+
defaultValue,
|
|
34
|
+
maxLength,
|
|
35
|
+
onChange,
|
|
36
|
+
...props
|
|
37
|
+
}, ref) => {
|
|
8
38
|
const [internalValue, setInternalValue] = React.useState(
|
|
9
39
|
typeof defaultValue === "string" ? defaultValue : typeof value === "string" ? value : ""
|
|
10
40
|
);
|
|
11
41
|
const isControlled = value !== void 0;
|
|
12
42
|
const currentValue = isControlled ? String(value ?? "") : internalValue;
|
|
43
|
+
const elementRef = React.useRef(null);
|
|
44
|
+
const setElementRef = React.useCallback(
|
|
45
|
+
(node) => {
|
|
46
|
+
elementRef.current = node;
|
|
47
|
+
if (typeof ref === "function") ref(node);
|
|
48
|
+
else if (ref) ref.current = node;
|
|
49
|
+
},
|
|
50
|
+
[ref]
|
|
51
|
+
);
|
|
52
|
+
React.useLayoutEffect(() => {
|
|
53
|
+
if (!autoResize) return;
|
|
54
|
+
applyAutoResize(elementRef.current, maxRows);
|
|
55
|
+
}, [autoResize, maxRows, currentValue]);
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
if (!autoResize || typeof window === "undefined") return;
|
|
58
|
+
const onWindowResize = () => applyAutoResize(elementRef.current, maxRows);
|
|
59
|
+
window.addEventListener("resize", onWindowResize);
|
|
60
|
+
return () => window.removeEventListener("resize", onWindowResize);
|
|
61
|
+
}, [autoResize, maxRows]);
|
|
13
62
|
const handleChange = React.useCallback(
|
|
14
63
|
(event) => {
|
|
15
64
|
if (!isControlled) setInternalValue(event.target.value);
|
|
65
|
+
if (autoResize) applyAutoResize(event.currentTarget, maxRows);
|
|
16
66
|
onChange?.(event);
|
|
17
67
|
},
|
|
18
|
-
[isControlled, onChange]
|
|
68
|
+
[isControlled, onChange, autoResize, maxRows]
|
|
19
69
|
);
|
|
20
70
|
const textarea = /* @__PURE__ */ jsx(
|
|
21
71
|
"textarea",
|
|
22
72
|
{
|
|
23
|
-
ref,
|
|
73
|
+
ref: setElementRef,
|
|
24
74
|
value,
|
|
25
75
|
defaultValue: isControlled ? void 0 : defaultValue,
|
|
26
76
|
maxLength,
|
|
27
77
|
onChange: handleChange,
|
|
28
|
-
className: cn(baseTextareaClass, className),
|
|
78
|
+
className: cn(baseTextareaClass, autoResize && "resize-none overflow-hidden", className),
|
|
29
79
|
...props
|
|
30
80
|
}
|
|
31
81
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/primitives/textarea.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst baseTextareaClass =\n 'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-colors placeholder:text-muted-foreground outline-none focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground hover:bg-muted/40 disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:border-border-disabled disabled:shadow-none disabled:hover:bg-bg-disabled aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive resize-y min-h-[80px]'\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {\n /** Show character counter (`current/max`) below the textarea. Requires `maxLength`. */\n showCount?: boolean\n /** Optional className applied to the outer wrapper (when counter is shown). */\n wrapperClassName?: string\n}\n\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n ({
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst baseTextareaClass =\n 'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-colors placeholder:text-muted-foreground outline-none focus-visible:outline-none focus-visible:shadow-focus focus-visible:border-foreground hover:bg-muted/40 disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:border-border-disabled disabled:shadow-none disabled:hover:bg-bg-disabled aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive resize-y min-h-[80px]'\n\nconst DEFAULT_AUTO_RESIZE_MAX_ROWS = 12\nconst FALLBACK_LINE_HEIGHT_PX = 20\n\nfunction readPixels(value: string): number {\n const parsed = Number.parseFloat(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\n/**\n * Grow a textarea to fit its content, capped at `maxRows` rendered lines.\n * The cap is expressed in rows rather than pixels so it follows the element's\n * own type scale instead of a magic height, and the element only scrolls once\n * the cap is reached.\n */\nfunction applyAutoResize(element: HTMLTextAreaElement | null, maxRows: number): void {\n if (!element) return\n const computed = typeof window !== 'undefined' ? window.getComputedStyle(element) : null\n const lineHeight = computed ? readPixels(computed.lineHeight) || FALLBACK_LINE_HEIGHT_PX : FALLBACK_LINE_HEIGHT_PX\n const padding = computed ? readPixels(computed.paddingTop) + readPixels(computed.paddingBottom) : 0\n const border = computed ? readPixels(computed.borderTopWidth) + readPixels(computed.borderBottomWidth) : 0\n const maxContentHeight = lineHeight * Math.max(1, maxRows) + padding\n\n element.style.height = 'auto'\n const contentHeight = element.scrollHeight\n const cappedHeight = Math.min(contentHeight, maxContentHeight)\n element.style.height = `${cappedHeight + border}px`\n element.style.overflowY = contentHeight > maxContentHeight ? 'auto' : 'hidden'\n}\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {\n /** Show character counter (`current/max`) below the textarea. Requires `maxLength`. */\n showCount?: boolean\n /** Optional className applied to the outer wrapper (when counter is shown). */\n wrapperClassName?: string\n /**\n * Grow the field to fit its content instead of scrolling inside a fixed box.\n * Disables the manual resize handle \u2014 the height is owned by the content.\n */\n autoResize?: boolean\n /** Rows to grow to before the field starts scrolling. Only used with `autoResize`. */\n maxRows?: number\n}\n\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n (\n {\n className,\n showCount,\n wrapperClassName,\n autoResize,\n maxRows = DEFAULT_AUTO_RESIZE_MAX_ROWS,\n value,\n defaultValue,\n maxLength,\n onChange,\n ...props\n },\n ref\n ) => {\n const [internalValue, setInternalValue] = React.useState<string>(\n typeof defaultValue === 'string' ? defaultValue : typeof value === 'string' ? value : ''\n )\n\n const isControlled = value !== undefined\n const currentValue = isControlled ? String(value ?? '') : internalValue\n\n const elementRef = React.useRef<HTMLTextAreaElement | null>(null)\n const setElementRef = React.useCallback(\n (node: HTMLTextAreaElement | null) => {\n elementRef.current = node\n if (typeof ref === 'function') ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node\n },\n [ref]\n )\n\n React.useLayoutEffect(() => {\n if (!autoResize) return\n applyAutoResize(elementRef.current, maxRows)\n }, [autoResize, maxRows, currentValue])\n\n React.useEffect(() => {\n if (!autoResize || typeof window === 'undefined') return\n const onWindowResize = () => applyAutoResize(elementRef.current, maxRows)\n window.addEventListener('resize', onWindowResize)\n return () => window.removeEventListener('resize', onWindowResize)\n }, [autoResize, maxRows])\n\n const handleChange = React.useCallback(\n (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n if (!isControlled) setInternalValue(event.target.value)\n if (autoResize) applyAutoResize(event.currentTarget, maxRows)\n onChange?.(event)\n },\n [isControlled, onChange, autoResize, maxRows]\n )\n\n const textarea = (\n <textarea\n ref={setElementRef}\n value={value}\n defaultValue={isControlled ? undefined : defaultValue}\n maxLength={maxLength}\n onChange={handleChange}\n className={cn(baseTextareaClass, autoResize && 'resize-none overflow-hidden', className)}\n {...props}\n />\n )\n\n if (!showCount) return textarea\n\n const length = currentValue.length\n const max = typeof maxLength === 'number' ? maxLength : undefined\n const isError = max != null && length > max\n const isDisabled = props.disabled\n\n return (\n <div className={cn('flex flex-col gap-1', wrapperClassName)}>\n {textarea}\n <div className=\"flex justify-end\">\n <span\n className={cn(\n 'text-overline uppercase',\n isDisabled\n ? 'text-text-disabled'\n : isError\n ? 'text-destructive'\n : 'text-muted-foreground'\n )}\n aria-live=\"polite\"\n >\n {max != null ? `${length}/${max}` : `${length}`}\n </span>\n </div>\n </div>\n )\n }\n)\n\nTextarea.displayName = 'Textarea'\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7194.1.ab4fc81f82",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
158
|
+
"@open-mercato/shared": "0.7.1-develop.7194.1.ab4fc81f82",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
165
|
+
"@open-mercato/shared": "0.7.1-develop.7194.1.ab4fc81f82",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.1",
|
|
168
168
|
"@testing-library/react": "^16.3.3",
|
package/src/backend/AppShell.tsx
CHANGED
|
@@ -37,6 +37,7 @@ import { readVersionedPreference, writeVersionedPreference } from '@open-mercato
|
|
|
37
37
|
import { cloneSidebarGroups } from './sidebar/customization-helpers'
|
|
38
38
|
import type { SectionNavGroup } from './section-page/types'
|
|
39
39
|
import { InjectionSpot } from './injection/InjectionSpot'
|
|
40
|
+
import { useNavBadge } from './nav/navBadges'
|
|
40
41
|
import {
|
|
41
42
|
BackendRecordInjectionContextProvider,
|
|
42
43
|
type RecordInjectionContext,
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
BACKEND_TOPBAR_ACTIONS_INJECTION_SPOT_ID,
|
|
65
66
|
GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID,
|
|
66
67
|
GLOBAL_SIDEBAR_STATUS_BADGES_INJECTION_SPOT_ID,
|
|
68
|
+
BACKEND_NAV_BADGES_INJECTION_SPOT_ID,
|
|
67
69
|
} from './injection/spotIds'
|
|
68
70
|
|
|
69
71
|
// Versioned-envelope discriminator for the persisted sidebar open/closed group
|
|
@@ -418,6 +420,32 @@ function sidebarBlockPadding(compact: boolean): string {
|
|
|
418
420
|
return `${compact ? 'px-2' : 'px-3'} transition-[padding] duration-200 ease-out`
|
|
419
421
|
}
|
|
420
422
|
|
|
423
|
+
/**
|
|
424
|
+
* The live count a module published for this nav item, or nothing at all.
|
|
425
|
+
*
|
|
426
|
+
* Zero and absent both render nothing: a "0" chip is noise dressed as
|
|
427
|
+
* information. Collapsed sidebars keep the chip — it is the only thing left
|
|
428
|
+
* saying something needs you.
|
|
429
|
+
*/
|
|
430
|
+
function NavItemBadge({ href, compact }: { href: string; compact: boolean }) {
|
|
431
|
+
const badge = useNavBadge(href)
|
|
432
|
+
if (!badge || badge.count <= 0) return null
|
|
433
|
+
const display = badge.count > 99 ? '99+' : String(badge.count)
|
|
434
|
+
const tone = badge.tone === 'attention'
|
|
435
|
+
? 'bg-status-warning-bg text-status-warning-text'
|
|
436
|
+
: 'bg-muted text-muted-foreground'
|
|
437
|
+
return (
|
|
438
|
+
<span
|
|
439
|
+
className={`ml-auto inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-xs font-medium ${tone} ${
|
|
440
|
+
compact ? 'absolute right-0 top-0 -mr-1 -mt-1' : ''
|
|
441
|
+
}`}
|
|
442
|
+
aria-label={badge.label ?? display}
|
|
443
|
+
>
|
|
444
|
+
{display}
|
|
445
|
+
</span>
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
421
449
|
// An InjectionSpot renders nothing when no widget is registered, so its wrapper must collapse
|
|
422
450
|
// instead of surviving as an empty flex child that still costs a full `gap-3`.
|
|
423
451
|
function sidebarInjectionWrapper(compact: boolean): string {
|
|
@@ -448,6 +476,38 @@ const HeaderContext = createContext<{
|
|
|
448
476
|
setTitle: (t?: string) => void
|
|
449
477
|
} | null>(null)
|
|
450
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Additive contract that lets a page under the backend shell drive the desktop
|
|
481
|
+
* sidebar collapse without owning the shell. `requestCollapse(true)` forces the
|
|
482
|
+
* sidebar to render collapsed without overwriting the user's persisted manual
|
|
483
|
+
* preference; `releaseRequest()` drops the request and falls back to whatever
|
|
484
|
+
* the underlying state is (including any active settings/profile force-collapse).
|
|
485
|
+
*/
|
|
486
|
+
export type SidebarCollapseContextValue = {
|
|
487
|
+
collapsed: boolean
|
|
488
|
+
setCollapsed: (next: boolean) => void
|
|
489
|
+
requestCollapse: (next: boolean) => void
|
|
490
|
+
releaseRequest: () => void
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const noopSidebarCollapse: SidebarCollapseContextValue = {
|
|
494
|
+
collapsed: false,
|
|
495
|
+
setCollapsed: () => {},
|
|
496
|
+
requestCollapse: () => {},
|
|
497
|
+
releaseRequest: () => {},
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const SidebarCollapseContext = createContext<SidebarCollapseContextValue | null>(null)
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Read the backend shell's sidebar-collapse controls. Returns a safe no-op
|
|
504
|
+
* default when called outside `AppShell` so standalone renders and tests never
|
|
505
|
+
* crash.
|
|
506
|
+
*/
|
|
507
|
+
export function useSidebarCollapse(): SidebarCollapseContextValue {
|
|
508
|
+
return useContext(SidebarCollapseContext) ?? noopSidebarCollapse
|
|
509
|
+
}
|
|
510
|
+
|
|
451
511
|
export function ApplyBreadcrumb({ breadcrumb, title, titleKey }: { breadcrumb?: Array<{ label: string; href?: string; labelKey?: string }>; title?: string; titleKey?: string }) {
|
|
452
512
|
const ctx = useContext(HeaderContext)
|
|
453
513
|
const t = useT()
|
|
@@ -567,6 +627,17 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
567
627
|
}, [mobileOpen])
|
|
568
628
|
// Initialize from server-provided prop only to avoid hydration flicker
|
|
569
629
|
const [collapsed, setCollapsed] = React.useState(sidebarCollapsedDefault)
|
|
630
|
+
// External collapse request (additive). A page under the shell can force the
|
|
631
|
+
// sidebar collapsed via `useSidebarCollapse().requestCollapse(true)` without
|
|
632
|
+
// touching `collapsed` (the user's persisted manual preference) — so nothing
|
|
633
|
+
// is written to localStorage/cookie and `releaseRequest()` restores the prior
|
|
634
|
+
// underlying state. Default `null` means "no request", keeping behavior
|
|
635
|
+
// byte-for-byte identical when no page consumes the context.
|
|
636
|
+
const [externalCollapseRequest, setExternalCollapseRequest] = React.useState<boolean | null>(null)
|
|
637
|
+
// Remember the value the user manually had before an external request took
|
|
638
|
+
// over, mirroring `collapsedBeforeSectionRef`. Not required for correctness
|
|
639
|
+
// (the request never mutates `collapsed`) but keeps the intent explicit.
|
|
640
|
+
const collapsedBeforeExternalRequestRef = React.useRef<boolean | null>(null)
|
|
570
641
|
// Maintain internal nav state so we can augment it client-side
|
|
571
642
|
const [navGroups, setNavGroups] = React.useState(resolvedGroups)
|
|
572
643
|
const [openGroups, setOpenGroups] = React.useState<Record<string, boolean>>(() =>
|
|
@@ -582,9 +653,29 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
582
653
|
if (!label) return false
|
|
583
654
|
return label.toLowerCase().includes(navQueryNorm)
|
|
584
655
|
}, [navQueryActive, navQueryNorm])
|
|
585
|
-
|
|
656
|
+
// An active external collapse request wins over the manual/section state
|
|
657
|
+
// ("collapsed wins"); when released (`null`) we fall back to `collapsed`,
|
|
658
|
+
// which already reflects any settings/profile force-collapse.
|
|
659
|
+
const effectiveCollapsed = externalCollapseRequest === true ? true : collapsed
|
|
586
660
|
const expandedSidebarWidth = '240px'
|
|
587
661
|
|
|
662
|
+
const requestCollapse = React.useCallback((next: boolean) => {
|
|
663
|
+
setExternalCollapseRequest((prev) => {
|
|
664
|
+
if (prev === null) collapsedBeforeExternalRequestRef.current = collapsed
|
|
665
|
+
return next
|
|
666
|
+
})
|
|
667
|
+
}, [collapsed])
|
|
668
|
+
const releaseRequest = React.useCallback(() => {
|
|
669
|
+
collapsedBeforeExternalRequestRef.current = null
|
|
670
|
+
setExternalCollapseRequest(null)
|
|
671
|
+
}, [])
|
|
672
|
+
const sidebarCollapseValue = React.useMemo<SidebarCollapseContextValue>(() => ({
|
|
673
|
+
collapsed: effectiveCollapsed,
|
|
674
|
+
setCollapsed: (next: boolean) => setCollapsed(next),
|
|
675
|
+
requestCollapse,
|
|
676
|
+
releaseRequest,
|
|
677
|
+
}), [effectiveCollapsed, requestCollapse, releaseRequest])
|
|
678
|
+
|
|
588
679
|
// Track scroll position of the desktop sidebar's inner scroll container so we can
|
|
589
680
|
// flip the affordance chevron between down/up (and hide it entirely when content
|
|
590
681
|
// fits without scrolling). The inner div is rendered deep in renderSidebar /
|
|
@@ -918,6 +1009,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
918
1009
|
)}
|
|
919
1010
|
</span>
|
|
920
1011
|
{!compact && <span className="truncate">{label}</span>}
|
|
1012
|
+
<NavItemBadge href={item.href} compact={compact} />
|
|
921
1013
|
</Link>
|
|
922
1014
|
{showChildren ? childItems.map((child) => renderSectionItem(child, depth + 1)) : null}
|
|
923
1015
|
</React.Fragment>
|
|
@@ -1320,9 +1412,19 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
1320
1412
|
|
|
1321
1413
|
return (
|
|
1322
1414
|
<HeaderContext.Provider value={headerCtxValue}>
|
|
1415
|
+
<SidebarCollapseContext.Provider value={sidebarCollapseValue}>
|
|
1323
1416
|
<div
|
|
1324
1417
|
className={`relative min-h-svh lg:grid transition-[grid-template-columns] duration-200 ease-out ${gridColsClass}`}
|
|
1325
|
-
style={{
|
|
1418
|
+
style={{
|
|
1419
|
+
'--topbar-height': '61px',
|
|
1420
|
+
// Left offset of the content column (sidebar, plus the section sidebar on
|
|
1421
|
+
// settings/profile routes). Exposed so full-bleed pages — e.g. the
|
|
1422
|
+
// workflow visual editor — can size themselves to the content column with
|
|
1423
|
+
// `calc(100vw - var(--app-content-offset))` instead of being capped by
|
|
1424
|
+
// `main`'s centered max-width. Only meaningful at `lg` and up, where the
|
|
1425
|
+
// grid (and the sidebar) is active.
|
|
1426
|
+
'--app-content-offset': isSectionView ? `calc(${asideWidth} + 240px)` : asideWidth,
|
|
1427
|
+
} as React.CSSProperties}
|
|
1326
1428
|
>
|
|
1327
1429
|
<a
|
|
1328
1430
|
href="#main-content"
|
|
@@ -1506,6 +1608,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
1506
1608
|
</header>
|
|
1507
1609
|
<ProgressTopBar t={t} className="sticky top-0 z-sticky" completedAutoHideMs={progressCompletedAutoHideMs} />
|
|
1508
1610
|
<main id="main-content" tabIndex={-1} className="flex-1 p-4 lg:p-6 mx-auto w-full max-w-screen-2xl">
|
|
1611
|
+
<InjectionSpot spotId={BACKEND_NAV_BADGES_INJECTION_SPOT_ID} context={injectionContext} />
|
|
1509
1612
|
<InjectionSpot spotId={BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID} context={injectionContext} />
|
|
1510
1613
|
<FlashMessages />
|
|
1511
1614
|
<PartialIndexBanner />
|
|
@@ -1620,6 +1723,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
1620
1723
|
)}
|
|
1621
1724
|
</div>
|
|
1622
1725
|
<UmesDevToolsPanel />
|
|
1726
|
+
</SidebarCollapseContext.Provider>
|
|
1623
1727
|
</HeaderContext.Provider>
|
|
1624
1728
|
)
|
|
1625
1729
|
}
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -378,6 +378,17 @@ export type CrudFormProps<TValues extends Record<string, unknown>> = {
|
|
|
378
378
|
embedded?: boolean
|
|
379
379
|
// Hide the footer action bar (Save/Cancel/Delete) when embedding in a custom layout
|
|
380
380
|
hideFooterActions?: boolean
|
|
381
|
+
/**
|
|
382
|
+
* Vertical rhythm of the form body.
|
|
383
|
+
*
|
|
384
|
+
* `default` is the page-width layout every existing host renders today and is
|
|
385
|
+
* unchanged byte-for-byte. `compact` tightens the between-group and
|
|
386
|
+
* between-field spacing and the group-card padding for narrow hosts — a docked
|
|
387
|
+
* inspector rail, a side panel — where the page rhythm reads as airy and costs
|
|
388
|
+
* the host a third of its column. It changes SPACING only: no label, helper
|
|
389
|
+
* text, control size or copy differs between the two.
|
|
390
|
+
*/
|
|
391
|
+
density?: 'default' | 'compact'
|
|
381
392
|
/**
|
|
382
393
|
* Opt-in: track dirty state even when `embedded` is true, AND enable the form's built-in
|
|
383
394
|
* navigation protection (beforeunload, link-click intercept, pushState/replaceState/popstate).
|
|
@@ -738,6 +749,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
738
749
|
loadingMessage,
|
|
739
750
|
customEntity = false,
|
|
740
751
|
embedded = false,
|
|
752
|
+
density = 'default',
|
|
741
753
|
hideFooterActions = false,
|
|
742
754
|
trackDirtyWhenEmbedded = false,
|
|
743
755
|
onDirtyChange,
|
|
@@ -1330,6 +1342,13 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
1330
1342
|
// handles the overflow case on its own: when content scrolls, the footer stays pinned
|
|
1331
1343
|
// to the dialog's bottom and the user can scroll fields above it.
|
|
1332
1344
|
const dialogFormPadding = ''
|
|
1345
|
+
// One step down the DS scale each, never an arbitrary value. `default` returns
|
|
1346
|
+
// the exact strings that were inline before this prop existed.
|
|
1347
|
+
const isCompactDensity = density === 'compact'
|
|
1348
|
+
const densityStackLg = isCompactDensity ? 'space-y-3' : 'space-y-4'
|
|
1349
|
+
const densityStackMd = isCompactDensity ? 'space-y-2' : 'space-y-3'
|
|
1350
|
+
const densityCardPadding = isCompactDensity ? 'p-3' : 'p-4'
|
|
1351
|
+
const densityGroupCardPadding = isCompactDensity ? 'px-3 py-2' : 'px-4 py-3'
|
|
1333
1352
|
|
|
1334
1353
|
const buildCustomFieldsManageHref = React.useCallback(
|
|
1335
1354
|
(targetEntityId: string | null) => {
|
|
@@ -3345,7 +3364,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3345
3364
|
const sectionKey = `${entityLayout.entityId}:${section.fieldsetCode ?? 'default'}`
|
|
3346
3365
|
const manageDisabled = !manageHref
|
|
3347
3366
|
nodes.push(
|
|
3348
|
-
<div key={sectionKey} className=
|
|
3367
|
+
<div key={sectionKey} className={`rounded-lg border bg-card ${densityCardPadding} ${densityStackLg}`}>
|
|
3349
3368
|
<div className="flex items-start justify-between gap-3">
|
|
3350
3369
|
<div className="flex items-start gap-2">
|
|
3351
3370
|
{FieldsetIcon ? (
|
|
@@ -3587,7 +3606,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3587
3606
|
fieldCount={customFieldCount}
|
|
3588
3607
|
chevronPosition={collapsibleChevronPosition}
|
|
3589
3608
|
>
|
|
3590
|
-
<div className=
|
|
3609
|
+
<div className={densityStackMd}>
|
|
3591
3610
|
{customFieldsInnerNodes}
|
|
3592
3611
|
</div>
|
|
3593
3612
|
</CollapsibleGroup>,
|
|
@@ -3641,14 +3660,14 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3641
3660
|
fieldCount={groupFields.length}
|
|
3642
3661
|
chevronPosition={collapsibleChevronPosition}
|
|
3643
3662
|
>
|
|
3644
|
-
<div className=
|
|
3663
|
+
<div className={densityStackMd}>
|
|
3645
3664
|
{groupContent}
|
|
3646
3665
|
</div>
|
|
3647
3666
|
</CollapsibleGroup>,
|
|
3648
3667
|
)
|
|
3649
3668
|
} else {
|
|
3650
3669
|
nodes.push(
|
|
3651
|
-
<div key={g.id} className=
|
|
3670
|
+
<div key={g.id} className={`rounded-lg border bg-card ${densityGroupCardPadding} ${densityStackMd}`}>
|
|
3652
3671
|
{g.title ? (
|
|
3653
3672
|
<div className="text-sm font-medium">{t(g.title, g.title)}</div>
|
|
3654
3673
|
) : null}
|
|
@@ -3703,7 +3722,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3703
3722
|
className={embedded ? 'min-h-[1px]' : 'min-h-[400px]'}
|
|
3704
3723
|
>
|
|
3705
3724
|
{wrapFormBody(
|
|
3706
|
-
<form id={formId} onSubmit={handleSubmit} className={
|
|
3725
|
+
<form id={formId} onSubmit={handleSubmit} className={`${densityStackLg} ${dialogFormPadding}`}>
|
|
3707
3726
|
{resolvedInjectionSpotId ? (
|
|
3708
3727
|
<InjectionSpot
|
|
3709
3728
|
spotId={resolvedInjectionSpotId}
|
|
@@ -3722,13 +3741,13 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3722
3741
|
{sortableGroupsEnabled ? (
|
|
3723
3742
|
<DndContext sensors={sortableSensors} collisionDetection={closestCenter} onDragEnd={handleGroupDragEnd}>
|
|
3724
3743
|
<SortableContext items={col1Ids} strategy={verticalListSortingStrategy}>
|
|
3725
|
-
<div className=
|
|
3744
|
+
<div className={densityStackMd}>{col1Content}</div>
|
|
3726
3745
|
</SortableContext>
|
|
3727
3746
|
</DndContext>
|
|
3728
3747
|
) : (
|
|
3729
|
-
<div className=
|
|
3748
|
+
<div className={densityStackMd}>{col1Content}</div>
|
|
3730
3749
|
)}
|
|
3731
|
-
{hasSecondaryColumn ? <div className=
|
|
3750
|
+
{hasSecondaryColumn ? <div className={densityStackMd} data-crud-injection-region>{col2Content}</div> : null}
|
|
3732
3751
|
</div>
|
|
3733
3752
|
{formError && !Object.keys(errors).length ? <div className="text-sm text-status-error-text">{formError}</div> : null}
|
|
3734
3753
|
{hideFooterActions || formReadOnly ? null : (
|
|
@@ -3790,7 +3809,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3790
3809
|
<form
|
|
3791
3810
|
id={formId}
|
|
3792
3811
|
onSubmit={handleSubmit}
|
|
3793
|
-
className={`${embedded ?
|
|
3812
|
+
className={`${embedded ? densityStackLg : `rounded-lg border bg-card ${densityCardPadding} ${densityStackLg}`} ${dialogFormPadding}`}
|
|
3794
3813
|
>
|
|
3795
3814
|
{resolvedInjectionSpotId ? (
|
|
3796
3815
|
<InjectionSpot
|