@open-mercato/ui 0.7.1-develop.7152.1.a69e92f9c9 → 0.7.1-develop.7153.1.7145d295e6
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/AppShell.js +9 -1
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +3 -0
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +3 -1
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/forms/FormHeader.js +3 -1
- package/dist/backend/forms/FormHeader.js.map +2 -2
- package/dist/backend/messages/MessageComposer.js +1 -0
- package/dist/backend/messages/MessageComposer.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +7 -1
- package/src/backend/CrudForm.tsx +5 -0
- package/src/backend/DataTable.tsx +10 -1
- package/src/backend/__tests__/AppShell.test.tsx +14 -0
- package/src/backend/__tests__/CrudForm.defaults.test.tsx +26 -0
- package/src/backend/__tests__/DataTable.render.test.tsx +30 -0
- package/src/backend/__tests__/FormHeader.test.tsx +9 -2
- package/src/backend/forms/FormHeader.tsx +5 -1
- package/src/backend/messages/MessageComposer.tsx +1 -0
- package/src/backend/messages/__tests__/MessageComposer.test.tsx +10 -0
|
@@ -36,16 +36,18 @@ function EditHeader({
|
|
|
36
36
|
backHref,
|
|
37
37
|
resolvedBackLabel,
|
|
38
38
|
title,
|
|
39
|
+
titleHeadingLevel = 2,
|
|
39
40
|
actions,
|
|
40
41
|
actionsContent
|
|
41
42
|
}) {
|
|
43
|
+
const TitleHeading = titleHeadingLevel === 1 ? "h1" : "h2";
|
|
42
44
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3", children: [
|
|
43
45
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
44
46
|
backHref ? /* @__PURE__ */ jsxs(Link, { href: backHref, className: "text-sm text-muted-foreground hover:text-foreground", children: [
|
|
45
47
|
"\u2190 ",
|
|
46
48
|
resolvedBackLabel
|
|
47
49
|
] }) : null,
|
|
48
|
-
title ? /* @__PURE__ */ jsx(
|
|
50
|
+
title ? /* @__PURE__ */ jsx(TitleHeading, { className: "text-base font-medium", children: title }) : null
|
|
49
51
|
] }),
|
|
50
52
|
actionsContent ?? (actions ? /* @__PURE__ */ jsx(FormActionButtons, { ...actions }) : null)
|
|
51
53
|
] });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/forms/FormHeader.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname, useSearchParams } from 'next/navigation'\nimport { Trash2, Loader2 } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FormActionButtons, type FormActionButtonsProps } from './FormActionButtons'\nimport { ActionsDropdown, type ActionItem } from './ActionsDropdown'\nimport { InjectionSpot } from '../injection/InjectionSpot'\n\n/** Base props shared by both modes */\ntype FormHeaderBaseProps = {\n /** Back link URL */\n backHref?: string\n /** Back link label */\n backLabel?: string\n}\n\n/** Edit mode: compact header for CrudForm pages */\nexport type FormHeaderEditProps = FormHeaderBaseProps & {\n mode?: 'edit'\n /** Small title next to the back link */\n title?: string\n /** Structured action buttons (Delete/Cancel/Save) */\n actions?: FormActionButtonsProps\n /** Custom right-side content (overrides `actions`) */\n actionsContent?: React.ReactNode\n}\n\n/** Detail mode: large header for view/detail pages */\nexport type FormHeaderDetailProps = FormHeaderBaseProps & {\n mode: 'detail'\n /** Large title -- string renders as h1; ReactNode for InlineTextEditor */\n title?: React.ReactNode\n /** Small uppercase entity type label above the title */\n entityTypeLabel?: string\n /** Subtitle text below the title */\n subtitle?: string\n /** Status badge or similar element below the title */\n statusBadge?: React.ReactNode\n /** Context actions grouped into an \"Actions\" dropdown (preferred) */\n menuActions?: ActionItem[]\n /** Optional label for actions dropdown trigger */\n menuLabel?: string\n /** Trigger style for actions dropdown */\n menuTriggerMode?: 'label' | 'icon'\n /** Accessible label used when trigger is icon-only */\n menuAriaLabel?: string\n /** Optional utility actions (icon-only) displayed before menu actions */\n utilityActions?: React.ReactNode\n /** Delete action -- rendered as a standalone destructive button next to the dropdown */\n onDelete?: () => void\n /** Delete button label */\n deleteLabel?: string\n /** Whether delete is in progress */\n isDeleting?: boolean\n /** Fallback: fully custom right-side content (overrides menuActions + onDelete) */\n actionsContent?: React.ReactNode\n}\n\nexport type FormHeaderProps = FormHeaderEditProps | FormHeaderDetailProps\n\nexport function FormHeader(props: FormHeaderProps) {\n const t = useT()\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const resolvedBackLabel = props.backLabel ?? t('ui.navigation.back')\n const injectionContext = React.useMemo(\n () => ({\n path: pathname ?? '',\n query: searchParams?.toString() ?? '',\n }),\n [pathname, searchParams],\n )\n\n if (props.mode === 'detail') {\n return (\n <>\n <DetailHeader {...props} resolvedBackLabel={resolvedBackLabel} />\n <InjectionSpot spotId=\"form-header:detail\" context={injectionContext} />\n </>\n )\n }\n\n return (\n <>\n <EditHeader {...props} resolvedBackLabel={resolvedBackLabel} />\n <InjectionSpot spotId=\"form-header:edit\" context={injectionContext} />\n </>\n )\n}\n\nfunction EditHeader({\n backHref,\n resolvedBackLabel,\n title,\n actions,\n actionsContent,\n}: FormHeaderEditProps & { resolvedBackLabel: string }) {\n return (\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3\">\n <div className=\"flex items-center gap-3\">\n {backHref ? (\n <Link href={backHref} className=\"text-sm text-muted-foreground hover:text-foreground\">\n ← {resolvedBackLabel}\n </Link>\n ) : null}\n {title ? <
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname, useSearchParams } from 'next/navigation'\nimport { Trash2, Loader2 } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { FormActionButtons, type FormActionButtonsProps } from './FormActionButtons'\nimport { ActionsDropdown, type ActionItem } from './ActionsDropdown'\nimport { InjectionSpot } from '../injection/InjectionSpot'\n\n/** Base props shared by both modes */\ntype FormHeaderBaseProps = {\n /** Back link URL */\n backHref?: string\n /** Back link label */\n backLabel?: string\n}\n\n/** Edit mode: compact header for CrudForm pages */\nexport type FormHeaderEditProps = FormHeaderBaseProps & {\n mode?: 'edit'\n /** Small title next to the back link */\n title?: string\n /** Semantic level for the title. Defaults to 2 for section-level compatibility. */\n titleHeadingLevel?: 1 | 2\n /** Structured action buttons (Delete/Cancel/Save) */\n actions?: FormActionButtonsProps\n /** Custom right-side content (overrides `actions`) */\n actionsContent?: React.ReactNode\n}\n\n/** Detail mode: large header for view/detail pages */\nexport type FormHeaderDetailProps = FormHeaderBaseProps & {\n mode: 'detail'\n /** Large title -- string renders as h1; ReactNode for InlineTextEditor */\n title?: React.ReactNode\n /** Small uppercase entity type label above the title */\n entityTypeLabel?: string\n /** Subtitle text below the title */\n subtitle?: string\n /** Status badge or similar element below the title */\n statusBadge?: React.ReactNode\n /** Context actions grouped into an \"Actions\" dropdown (preferred) */\n menuActions?: ActionItem[]\n /** Optional label for actions dropdown trigger */\n menuLabel?: string\n /** Trigger style for actions dropdown */\n menuTriggerMode?: 'label' | 'icon'\n /** Accessible label used when trigger is icon-only */\n menuAriaLabel?: string\n /** Optional utility actions (icon-only) displayed before menu actions */\n utilityActions?: React.ReactNode\n /** Delete action -- rendered as a standalone destructive button next to the dropdown */\n onDelete?: () => void\n /** Delete button label */\n deleteLabel?: string\n /** Whether delete is in progress */\n isDeleting?: boolean\n /** Fallback: fully custom right-side content (overrides menuActions + onDelete) */\n actionsContent?: React.ReactNode\n}\n\nexport type FormHeaderProps = FormHeaderEditProps | FormHeaderDetailProps\n\nexport function FormHeader(props: FormHeaderProps) {\n const t = useT()\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const resolvedBackLabel = props.backLabel ?? t('ui.navigation.back')\n const injectionContext = React.useMemo(\n () => ({\n path: pathname ?? '',\n query: searchParams?.toString() ?? '',\n }),\n [pathname, searchParams],\n )\n\n if (props.mode === 'detail') {\n return (\n <>\n <DetailHeader {...props} resolvedBackLabel={resolvedBackLabel} />\n <InjectionSpot spotId=\"form-header:detail\" context={injectionContext} />\n </>\n )\n }\n\n return (\n <>\n <EditHeader {...props} resolvedBackLabel={resolvedBackLabel} />\n <InjectionSpot spotId=\"form-header:edit\" context={injectionContext} />\n </>\n )\n}\n\nfunction EditHeader({\n backHref,\n resolvedBackLabel,\n title,\n titleHeadingLevel = 2,\n actions,\n actionsContent,\n}: FormHeaderEditProps & { resolvedBackLabel: string }) {\n const TitleHeading = titleHeadingLevel === 1 ? 'h1' : 'h2'\n return (\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3\">\n <div className=\"flex items-center gap-3\">\n {backHref ? (\n <Link href={backHref} className=\"text-sm text-muted-foreground hover:text-foreground\">\n ← {resolvedBackLabel}\n </Link>\n ) : null}\n {title ? <TitleHeading className=\"text-base font-medium\">{title}</TitleHeading> : null}\n </div>\n {actionsContent ?? (actions ? <FormActionButtons {...actions} /> : null)}\n </div>\n )\n}\n\nfunction DetailHeader({\n backHref,\n resolvedBackLabel,\n title,\n entityTypeLabel,\n subtitle,\n statusBadge,\n menuActions,\n menuLabel,\n menuTriggerMode,\n menuAriaLabel,\n utilityActions,\n onDelete,\n deleteLabel,\n isDeleting,\n actionsContent,\n}: FormHeaderDetailProps & { resolvedBackLabel: string }) {\n const t = useT()\n const resolvedDeleteLabel = deleteLabel ?? t('ui.forms.actions.delete')\n\n const hasActions = actionsContent || utilityActions || menuActions?.length || onDelete\n\n return (\n <div className=\"flex flex-col gap-2 md:gap-3 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex flex-wrap items-center gap-2 md:gap-3 min-w-0\">\n {backHref ? (\n <Link\n href={backHref}\n className=\"inline-flex items-center text-sm text-muted-foreground hover:text-foreground shrink-0\"\n >\n <span aria-hidden className=\"mr-1 text-base\">←</span>\n <span className=\"sr-only\">{resolvedBackLabel}</span>\n </Link>\n ) : null}\n <div className=\"space-y-0.5 md:space-y-1 min-w-0\">\n {entityTypeLabel ? (\n <p className=\"text-xs uppercase text-muted-foreground\">{entityTypeLabel}</p>\n ) : null}\n {title ? (\n typeof title === 'string' ? (\n <h1 className=\"text-lg md:text-2xl font-semibold leading-tight truncate\">{title}</h1>\n ) : (\n <div className=\"text-lg md:text-2xl font-semibold leading-tight\">{title}</div>\n )\n ) : null}\n {statusBadge}\n {subtitle ? (\n <p className=\"text-xs md:text-sm text-muted-foreground\">{subtitle}</p>\n ) : null}\n </div>\n </div>\n {hasActions ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n {actionsContent ? actionsContent : (\n <>\n {utilityActions}\n {menuActions?.length ? (\n <ActionsDropdown\n items={menuActions}\n label={menuLabel}\n triggerMode={menuTriggerMode}\n ariaLabel={menuAriaLabel}\n />\n ) : null}\n {onDelete ? (\n <Button\n type=\"button\"\n variant=\"destructive\"\n size=\"sm\"\n onClick={onDelete}\n disabled={isDeleting}\n >\n {isDeleting ? (\n <Loader2 className=\"size-4 mr-2 animate-spin\" />\n ) : (\n <Trash2 className=\"size-4 mr-2\" />\n )}\n {resolvedDeleteLabel}\n </Button>\n ) : null}\n </>\n )}\n </div>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -104,6 +104,7 @@ function MessageComposer(props) {
|
|
|
104
104
|
{
|
|
105
105
|
backHref,
|
|
106
106
|
title: composeWithContextPreview.composerTitle,
|
|
107
|
+
titleHeadingLevel: 2,
|
|
107
108
|
fields: createMessageComposeFormGroups(composeWithContextPreview),
|
|
108
109
|
initialValues: {},
|
|
109
110
|
submitLabel: composeWithContextPreview.submitLabel,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/messages/MessageComposer.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Forward, Reply, Send } from 'lucide-react'\nimport { CrudForm } from '../CrudForm'\nimport { Button } from '../../primitives/button'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../primitives/dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getMessageUiComponentRegistry } from '@open-mercato/core/modules/messages/components/utils/typeUiRegistry'\nimport { getMessageObjectType } from '@open-mercato/core/modules/messages/lib/message-objects-registry'\nimport { createMessageComposeFormGroups } from './message-compose-form-groups'\nimport { useMessageCompose } from './useMessageCompose'\nimport type { MessageComposerContextObject, MessageComposerProps } from './message-composer.types'\n\nexport type {\n MessageComposerContextObject,\n MessageComposerProps,\n MessageComposerRequiredActionConfig,\n MessageComposerRequiredActionOption,\n MessageComposerVariant,\n MessageTypeItem,\n} from './message-composer.types'\n\nfunction ContextObjectPreview({ contextObject }: { contextObject: MessageComposerContextObject }) {\n const t = useT()\n const registry = getMessageUiComponentRegistry()\n const previewComponentKey = `${contextObject.entityModule}:${contextObject.entityType}`\n const PreviewComponent = registry.objectPreviewComponents[previewComponentKey]\n ?? registry.objectPreviewComponents['messages:default']\n const objectType = getMessageObjectType(contextObject.entityModule, contextObject.entityType)\n\n if (PreviewComponent) {\n return (\n <PreviewComponent\n entityId={contextObject.entityId}\n entityModule={contextObject.entityModule}\n entityType={contextObject.entityType}\n actionRequired={contextObject.actionRequired}\n actionType={contextObject.actionType}\n actionLabel={contextObject.actionLabel}\n previewData={contextObject.previewData ?? undefined}\n icon={objectType?.icon}\n />\n )\n }\n\n return (\n <div className=\"space-y-1\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('messages.composer.contextPreview.title', 'Context object')}\n </p>\n <p className=\"text-sm font-medium\">\n {contextObject.entityModule}:{contextObject.entityType}\n </p>\n <p className=\"text-xs font-mono text-muted-foreground\">\n {contextObject.entityId}\n </p>\n </div>\n )\n}\n\nexport function MessageComposer(props: MessageComposerProps) {\n const compose = useMessageCompose(props)\n const inlineBackHref = props.inlineBackHref\n const SubmitIcon = compose.variant === 'reply'\n ? Reply\n : compose.variant === 'forward'\n ? Forward\n : Send\n\n const inlineExtraActions = compose.inline\n ? (\n <>\n {compose.variant === 'compose' ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleSaveDraft}\n disabled={compose.submitting}\n >\n {compose.t('messages.saveDraft', 'Save draft')}\n </Button>\n ) : null}\n {compose.variant !== 'compose' ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleBack}\n disabled={compose.submitting}\n >\n {compose.t('ui.forms.actions.cancel', 'Cancel')}\n </Button>\n ) : null}\n </>\n )\n : null\n\n const dialogExtraActions = !compose.inline\n ? (\n <>\n {compose.variant === 'compose' ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleSaveDraft}\n disabled={compose.submitting}\n >\n {compose.t('messages.saveDraft', 'Save draft')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleBack}\n disabled={compose.submitting}\n >\n {compose.t('ui.forms.actions.cancel', 'Cancel')}\n </Button>\n </>\n )\n : null\n\n const backHref = compose.inline\n ? inlineBackHref === undefined\n ? '/backend/messages'\n : inlineBackHref ?? undefined\n : undefined\n\n const fallbackContextPreview = React.useMemo(() => {\n if (compose.contextPreview) return compose.contextPreview\n if (!props.contextObject) return null\n return <ContextObjectPreview contextObject={props.contextObject} />\n }, [compose.contextPreview, props.contextObject])\n\n const composeWithContextPreview = React.useMemo(\n () => ({ ...compose, contextPreview: fallbackContextPreview }),\n [compose, fallbackContextPreview],\n )\n\n const composePanel = (\n <CrudForm<Record<string, unknown>>\n backHref={backHref}\n title={composeWithContextPreview.composerTitle}\n fields={createMessageComposeFormGroups(composeWithContextPreview)}\n initialValues={{}}\n submitLabel={composeWithContextPreview.submitLabel}\n submitIcon={SubmitIcon}\n extraActions={composeWithContextPreview.inline ? inlineExtraActions : dialogExtraActions}\n hideFooterActions\n onSubmit={async () => {\n await composeWithContextPreview.handleSubmit()\n }}\n />\n )\n\n if (compose.inline) {\n return (\n <div className=\"space-y-4\">\n {composePanel}\n </div>\n )\n }\n\n return (\n <Dialog open={Boolean(compose.open)} onOpenChange={compose.handleDialogOpenChange}>\n <DialogContent className=\"sm:max-w-3xl [&>button]:hidden\">\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{compose.composerTitle}</DialogTitle>\n <DialogDescription>\n {compose.t('messages.composer.dialogDescription', 'Compose and send a message.')}\n </DialogDescription>\n </DialogHeader>\n {composePanel}\n <div className=\"flex items-center justify-end gap-2 border-t pt-4\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleBack}\n disabled={compose.submitting}\n >\n {compose.t('ui.forms.actions.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n onClick={() => {\n void compose.handleSubmit()\n }}\n disabled={compose.submitting}\n >\n <SubmitIcon className=\"mr-2 size-4\" />\n {compose.submitLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Forward, Reply, Send } from 'lucide-react'\nimport { CrudForm } from '../CrudForm'\nimport { Button } from '../../primitives/button'\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../primitives/dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { getMessageUiComponentRegistry } from '@open-mercato/core/modules/messages/components/utils/typeUiRegistry'\nimport { getMessageObjectType } from '@open-mercato/core/modules/messages/lib/message-objects-registry'\nimport { createMessageComposeFormGroups } from './message-compose-form-groups'\nimport { useMessageCompose } from './useMessageCompose'\nimport type { MessageComposerContextObject, MessageComposerProps } from './message-composer.types'\n\nexport type {\n MessageComposerContextObject,\n MessageComposerProps,\n MessageComposerRequiredActionConfig,\n MessageComposerRequiredActionOption,\n MessageComposerVariant,\n MessageTypeItem,\n} from './message-composer.types'\n\nfunction ContextObjectPreview({ contextObject }: { contextObject: MessageComposerContextObject }) {\n const t = useT()\n const registry = getMessageUiComponentRegistry()\n const previewComponentKey = `${contextObject.entityModule}:${contextObject.entityType}`\n const PreviewComponent = registry.objectPreviewComponents[previewComponentKey]\n ?? registry.objectPreviewComponents['messages:default']\n const objectType = getMessageObjectType(contextObject.entityModule, contextObject.entityType)\n\n if (PreviewComponent) {\n return (\n <PreviewComponent\n entityId={contextObject.entityId}\n entityModule={contextObject.entityModule}\n entityType={contextObject.entityType}\n actionRequired={contextObject.actionRequired}\n actionType={contextObject.actionType}\n actionLabel={contextObject.actionLabel}\n previewData={contextObject.previewData ?? undefined}\n icon={objectType?.icon}\n />\n )\n }\n\n return (\n <div className=\"space-y-1\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('messages.composer.contextPreview.title', 'Context object')}\n </p>\n <p className=\"text-sm font-medium\">\n {contextObject.entityModule}:{contextObject.entityType}\n </p>\n <p className=\"text-xs font-mono text-muted-foreground\">\n {contextObject.entityId}\n </p>\n </div>\n )\n}\n\nexport function MessageComposer(props: MessageComposerProps) {\n const compose = useMessageCompose(props)\n const inlineBackHref = props.inlineBackHref\n const SubmitIcon = compose.variant === 'reply'\n ? Reply\n : compose.variant === 'forward'\n ? Forward\n : Send\n\n const inlineExtraActions = compose.inline\n ? (\n <>\n {compose.variant === 'compose' ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleSaveDraft}\n disabled={compose.submitting}\n >\n {compose.t('messages.saveDraft', 'Save draft')}\n </Button>\n ) : null}\n {compose.variant !== 'compose' ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleBack}\n disabled={compose.submitting}\n >\n {compose.t('ui.forms.actions.cancel', 'Cancel')}\n </Button>\n ) : null}\n </>\n )\n : null\n\n const dialogExtraActions = !compose.inline\n ? (\n <>\n {compose.variant === 'compose' ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleSaveDraft}\n disabled={compose.submitting}\n >\n {compose.t('messages.saveDraft', 'Save draft')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleBack}\n disabled={compose.submitting}\n >\n {compose.t('ui.forms.actions.cancel', 'Cancel')}\n </Button>\n </>\n )\n : null\n\n const backHref = compose.inline\n ? inlineBackHref === undefined\n ? '/backend/messages'\n : inlineBackHref ?? undefined\n : undefined\n\n const fallbackContextPreview = React.useMemo(() => {\n if (compose.contextPreview) return compose.contextPreview\n if (!props.contextObject) return null\n return <ContextObjectPreview contextObject={props.contextObject} />\n }, [compose.contextPreview, props.contextObject])\n\n const composeWithContextPreview = React.useMemo(\n () => ({ ...compose, contextPreview: fallbackContextPreview }),\n [compose, fallbackContextPreview],\n )\n\n const composePanel = (\n <CrudForm<Record<string, unknown>>\n backHref={backHref}\n title={composeWithContextPreview.composerTitle}\n titleHeadingLevel={2}\n fields={createMessageComposeFormGroups(composeWithContextPreview)}\n initialValues={{}}\n submitLabel={composeWithContextPreview.submitLabel}\n submitIcon={SubmitIcon}\n extraActions={composeWithContextPreview.inline ? inlineExtraActions : dialogExtraActions}\n hideFooterActions\n onSubmit={async () => {\n await composeWithContextPreview.handleSubmit()\n }}\n />\n )\n\n if (compose.inline) {\n return (\n <div className=\"space-y-4\">\n {composePanel}\n </div>\n )\n }\n\n return (\n <Dialog open={Boolean(compose.open)} onOpenChange={compose.handleDialogOpenChange}>\n <DialogContent className=\"sm:max-w-3xl [&>button]:hidden\">\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{compose.composerTitle}</DialogTitle>\n <DialogDescription>\n {compose.t('messages.composer.dialogDescription', 'Compose and send a message.')}\n </DialogDescription>\n </DialogHeader>\n {composePanel}\n <div className=\"flex items-center justify-end gap-2 border-t pt-4\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={compose.handleBack}\n disabled={compose.submitting}\n >\n {compose.t('ui.forms.actions.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n onClick={() => {\n void compose.handleSubmit()\n }}\n disabled={compose.submitting}\n >\n <SubmitIcon className=\"mr-2 size-4\" />\n {compose.submitLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\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.7153.1.7145d295e6",
|
|
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.7153.1.7145d295e6",
|
|
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.7153.1.7145d295e6",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.0",
|
|
168
168
|
"@testing-library/react": "^16.3.1",
|
package/src/backend/AppShell.tsx
CHANGED
|
@@ -1324,6 +1324,12 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
1324
1324
|
className={`relative min-h-svh lg:grid transition-[grid-template-columns] duration-200 ease-out ${gridColsClass}`}
|
|
1325
1325
|
style={{ '--topbar-height': '61px' } as React.CSSProperties}
|
|
1326
1326
|
>
|
|
1327
|
+
<a
|
|
1328
|
+
href="#main-content"
|
|
1329
|
+
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-tooltip focus:rounded-md focus:border focus:bg-background focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-foreground focus:shadow-focus"
|
|
1330
|
+
>
|
|
1331
|
+
{t('appShell.skipToMainContent', 'Skip to main content')}
|
|
1332
|
+
</a>
|
|
1327
1333
|
{/* Desktop sidebar collapse/expand toggle — sits on the divider line between
|
|
1328
1334
|
sidebar and content, like Notion/Vercel. Hidden on mobile (hamburger in
|
|
1329
1335
|
topbar handles the drawer). */}
|
|
@@ -1499,7 +1505,7 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
|
|
|
1499
1505
|
</div>
|
|
1500
1506
|
</header>
|
|
1501
1507
|
<ProgressTopBar t={t} className="sticky top-0 z-sticky" completedAutoHideMs={progressCompletedAutoHideMs} />
|
|
1502
|
-
<main className="flex-1 p-4 lg:p-6 mx-auto w-full max-w-screen-2xl">
|
|
1508
|
+
<main id="main-content" tabIndex={-1} className="flex-1 p-4 lg:p-6 mx-auto w-full max-w-screen-2xl">
|
|
1503
1509
|
<InjectionSpot spotId={BACKEND_LAYOUT_TOP_INJECTION_SPOT_ID} context={injectionContext} />
|
|
1504
1510
|
<FlashMessages />
|
|
1505
1511
|
<PartialIndexBanner />
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -349,6 +349,8 @@ export type CrudFormProps<TValues extends Record<string, unknown>> = {
|
|
|
349
349
|
// Legacy field-only grid toggle. Use `groups` for advanced layout.
|
|
350
350
|
twoColumn?: boolean
|
|
351
351
|
title?: string
|
|
352
|
+
/** Semantic level for the form title. Defaults to 2 for section-level compatibility. */
|
|
353
|
+
titleHeadingLevel?: 1 | 2
|
|
352
354
|
backHref?: string
|
|
353
355
|
// Optional extra action buttons rendered next to Delete/Cancel/Save
|
|
354
356
|
// Useful for custom links like "Show Records" etc.
|
|
@@ -726,6 +728,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
726
728
|
disableOptimisticLock = false,
|
|
727
729
|
twoColumn = false,
|
|
728
730
|
title,
|
|
731
|
+
titleHeadingLevel = 2,
|
|
729
732
|
backHref,
|
|
730
733
|
entityId,
|
|
731
734
|
entityIds,
|
|
@@ -3677,6 +3680,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3677
3680
|
backHref={backHref}
|
|
3678
3681
|
backLabel={backLabel}
|
|
3679
3682
|
title={title}
|
|
3683
|
+
titleHeadingLevel={titleHeadingLevel}
|
|
3680
3684
|
actions={{
|
|
3681
3685
|
extraActions: headerExtraActions,
|
|
3682
3686
|
showDelete: !formReadOnly && showDelete,
|
|
@@ -3759,6 +3763,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
|
|
|
3759
3763
|
backHref={backHref}
|
|
3760
3764
|
backLabel={backLabel}
|
|
3761
3765
|
title={title}
|
|
3766
|
+
titleHeadingLevel={titleHeadingLevel}
|
|
3762
3767
|
actions={{
|
|
3763
3768
|
extraActions: headerExtraActions,
|
|
3764
3769
|
showDelete: !formReadOnly && showDelete,
|
|
@@ -296,6 +296,11 @@ export type DataTableProps<T extends RowData> = {
|
|
|
296
296
|
data: T[]
|
|
297
297
|
toolbar?: React.ReactNode
|
|
298
298
|
title?: React.ReactNode
|
|
299
|
+
/**
|
|
300
|
+
* Semantic level for the title. String titles default to 2 for section-level
|
|
301
|
+
* compatibility; ReactNode titles remain caller-owned unless this is set.
|
|
302
|
+
*/
|
|
303
|
+
titleHeadingLevel?: 1 | 2
|
|
299
304
|
actions?: React.ReactNode
|
|
300
305
|
refreshButton?: DataTableRefreshButton
|
|
301
306
|
sortable?: boolean
|
|
@@ -1209,6 +1214,7 @@ export function DataTable<T extends RowData>({
|
|
|
1209
1214
|
data,
|
|
1210
1215
|
toolbar,
|
|
1211
1216
|
title,
|
|
1217
|
+
titleHeadingLevel,
|
|
1212
1218
|
actions,
|
|
1213
1219
|
refreshButton,
|
|
1214
1220
|
sortable,
|
|
@@ -3270,9 +3276,12 @@ export function DataTable<T extends RowData>({
|
|
|
3270
3276
|
}
|
|
3271
3277
|
: undefined
|
|
3272
3278
|
|
|
3279
|
+
const TitleHeading = titleHeadingLevel === 1 ? 'h1' : 'h2'
|
|
3273
3280
|
const titleContent = hasTitle ? (
|
|
3274
3281
|
<div className="text-base font-semibold leading-tight min-h-[2.25rem] flex items-center">
|
|
3275
|
-
{typeof title === 'string'
|
|
3282
|
+
{typeof title === 'string' || titleHeadingLevel
|
|
3283
|
+
? <TitleHeading className="text-base font-semibold">{title}</TitleHeading>
|
|
3284
|
+
: title}
|
|
3276
3285
|
</div>
|
|
3277
3286
|
) : <div className="min-h-[2.25rem]" />
|
|
3278
3287
|
|
|
@@ -92,6 +92,7 @@ jest.mock('../devtools', () => ({
|
|
|
92
92
|
|
|
93
93
|
const dict = {
|
|
94
94
|
'appShell.productName': 'Mercato',
|
|
95
|
+
'appShell.skipToMainContent': 'Skip to main content',
|
|
95
96
|
'appShell.menu': 'Menu',
|
|
96
97
|
'appShell.toggleSidebar': 'Toggle sidebar',
|
|
97
98
|
'appShell.collapseSidebar': 'Collapse',
|
|
@@ -212,6 +213,19 @@ describe('AppShell', () => {
|
|
|
212
213
|
)
|
|
213
214
|
})
|
|
214
215
|
|
|
216
|
+
it('provides a skip link targeting the focusable main content landmark', () => {
|
|
217
|
+
renderWithProviders(
|
|
218
|
+
<AppShell email="demo@example.com" groups={groups}>
|
|
219
|
+
<div>Child content</div>
|
|
220
|
+
</AppShell>,
|
|
221
|
+
{ dict },
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
expect(screen.getByRole('link', { name: 'Skip to main content' })).toHaveAttribute('href', '#main-content')
|
|
225
|
+
expect(screen.getByRole('main')).toHaveAttribute('id', 'main-content')
|
|
226
|
+
expect(screen.getByRole('main')).toHaveAttribute('tabindex', '-1')
|
|
227
|
+
})
|
|
228
|
+
|
|
215
229
|
it('keeps the incoming page breadcrumb when the pathname change and ApplyBreadcrumb land in the same commit', () => {
|
|
216
230
|
const { rerender } = renderWithProviders(
|
|
217
231
|
<AppShell email="demo@example.com" groups={groups}>
|
|
@@ -64,6 +64,32 @@ describe('CrudForm custom field default application', () => {
|
|
|
64
64
|
})
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
+
it('forwards explicit title heading semantics while preserving the section-level default', () => {
|
|
68
|
+
const { rerender } = renderWithProviders(
|
|
69
|
+
<CrudForm
|
|
70
|
+
title="Edit"
|
|
71
|
+
fields={fields}
|
|
72
|
+
initialValues={{ name: 'Test' }}
|
|
73
|
+
onSubmit={() => {}}
|
|
74
|
+
/>,
|
|
75
|
+
{ dict },
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Edit' })).toBeInTheDocument()
|
|
79
|
+
|
|
80
|
+
rerender(
|
|
81
|
+
<CrudForm
|
|
82
|
+
title="Edit"
|
|
83
|
+
titleHeadingLevel={1}
|
|
84
|
+
fields={fields}
|
|
85
|
+
initialValues={{ name: 'Test' }}
|
|
86
|
+
onSubmit={() => {}}
|
|
87
|
+
/>,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Edit' })).toBeInTheDocument()
|
|
91
|
+
})
|
|
92
|
+
|
|
67
93
|
it('applies defaultValue to text inputs on create form', async () => {
|
|
68
94
|
setupMockDefinitions([
|
|
69
95
|
{ key: 'status', kind: 'text', label: 'Status', defaultValue: 'active', formEditable: true },
|
|
@@ -19,6 +19,36 @@ jest.mock('../injection/useInjectionDataWidgets', () => ({
|
|
|
19
19
|
type Row = { id: string; name: string }
|
|
20
20
|
|
|
21
21
|
describe('DataTable SSR render', () => {
|
|
22
|
+
it('keeps section-level title semantics by default and supports an explicit page heading', () => {
|
|
23
|
+
const columns: ColumnDef<Row>[] = [
|
|
24
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
25
|
+
]
|
|
26
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 0 } } })
|
|
27
|
+
try {
|
|
28
|
+
const { rerender } = render(
|
|
29
|
+
<QueryClientProvider client={queryClient}>
|
|
30
|
+
<I18nProvider locale="en" dict={{}}>
|
|
31
|
+
<DataTable columns={columns} data={[]} title="People" />
|
|
32
|
+
</I18nProvider>
|
|
33
|
+
</QueryClientProvider>,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(screen.getByRole('heading', { level: 2, name: 'People' })).toBeInTheDocument()
|
|
37
|
+
|
|
38
|
+
rerender(
|
|
39
|
+
<QueryClientProvider client={queryClient}>
|
|
40
|
+
<I18nProvider locale="en" dict={{}}>
|
|
41
|
+
<DataTable columns={columns} data={[]} title="People" titleHeadingLevel={1} embedded />
|
|
42
|
+
</I18nProvider>
|
|
43
|
+
</QueryClientProvider>,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(screen.getByRole('heading', { level: 1, name: 'People' })).toBeInTheDocument()
|
|
47
|
+
} finally {
|
|
48
|
+
queryClient.clear()
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
22
52
|
it('renders built-in FilterBar when search/filters provided', () => {
|
|
23
53
|
const columns: ColumnDef<Row>[] = [
|
|
24
54
|
{ accessorKey: 'name', header: 'Name' },
|
|
@@ -59,13 +59,13 @@ describe('FormHeader', () => {
|
|
|
59
59
|
)
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
it('renders edit
|
|
62
|
+
it('renders edit titles as section headings by default with an explicit page-heading option', () => {
|
|
63
63
|
renderWithProviders(
|
|
64
64
|
<FormHeader title="Edit Person" />,
|
|
65
65
|
{ dict: {} },
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
expect(screen.
|
|
68
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Edit Person' })).toBeInTheDocument()
|
|
69
69
|
expect(screen.getByTestId('injection-spot:form-header:edit')).toBeInTheDocument()
|
|
70
70
|
expect(mockInjectionSpot).toHaveBeenCalledWith(
|
|
71
71
|
expect.objectContaining({
|
|
@@ -76,5 +76,12 @@ describe('FormHeader', () => {
|
|
|
76
76
|
},
|
|
77
77
|
}),
|
|
78
78
|
)
|
|
79
|
+
|
|
80
|
+
renderWithProviders(
|
|
81
|
+
<FormHeader title="Edit Company" titleHeadingLevel={1} />,
|
|
82
|
+
{ dict: {} },
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Edit Company' })).toBeInTheDocument()
|
|
79
86
|
})
|
|
80
87
|
})
|
|
@@ -23,6 +23,8 @@ export type FormHeaderEditProps = FormHeaderBaseProps & {
|
|
|
23
23
|
mode?: 'edit'
|
|
24
24
|
/** Small title next to the back link */
|
|
25
25
|
title?: string
|
|
26
|
+
/** Semantic level for the title. Defaults to 2 for section-level compatibility. */
|
|
27
|
+
titleHeadingLevel?: 1 | 2
|
|
26
28
|
/** Structured action buttons (Delete/Cancel/Save) */
|
|
27
29
|
actions?: FormActionButtonsProps
|
|
28
30
|
/** Custom right-side content (overrides `actions`) */
|
|
@@ -96,9 +98,11 @@ function EditHeader({
|
|
|
96
98
|
backHref,
|
|
97
99
|
resolvedBackLabel,
|
|
98
100
|
title,
|
|
101
|
+
titleHeadingLevel = 2,
|
|
99
102
|
actions,
|
|
100
103
|
actionsContent,
|
|
101
104
|
}: FormHeaderEditProps & { resolvedBackLabel: string }) {
|
|
105
|
+
const TitleHeading = titleHeadingLevel === 1 ? 'h1' : 'h2'
|
|
102
106
|
return (
|
|
103
107
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
|
104
108
|
<div className="flex items-center gap-3">
|
|
@@ -107,7 +111,7 @@ function EditHeader({
|
|
|
107
111
|
← {resolvedBackLabel}
|
|
108
112
|
</Link>
|
|
109
113
|
) : null}
|
|
110
|
-
{title ? <
|
|
114
|
+
{title ? <TitleHeading className="text-base font-medium">{title}</TitleHeading> : null}
|
|
111
115
|
</div>
|
|
112
116
|
{actionsContent ?? (actions ? <FormActionButtons {...actions} /> : null)}
|
|
113
117
|
</div>
|
|
@@ -141,6 +141,7 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
141
141
|
<CrudForm<Record<string, unknown>>
|
|
142
142
|
backHref={backHref}
|
|
143
143
|
title={composeWithContextPreview.composerTitle}
|
|
144
|
+
titleHeadingLevel={2}
|
|
144
145
|
fields={createMessageComposeFormGroups(composeWithContextPreview)}
|
|
145
146
|
initialValues={{}}
|
|
146
147
|
submitLabel={composeWithContextPreview.submitLabel}
|
|
@@ -526,6 +526,16 @@ describe('MessageComposer draft flow', () => {
|
|
|
526
526
|
})
|
|
527
527
|
})
|
|
528
528
|
|
|
529
|
+
it('keeps inline composer titles at section level', async () => {
|
|
530
|
+
renderWithProviders(
|
|
531
|
+
<MessageComposer inline variant="reply" messageId="message-1" />,
|
|
532
|
+
{ dict: {} },
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
expect(await screen.findByRole('heading', { level: 2, name: 'Reply' })).toBeInTheDocument()
|
|
536
|
+
expect(screen.queryByRole('heading', { level: 1, name: 'Reply' })).not.toBeInTheDocument()
|
|
537
|
+
})
|
|
538
|
+
|
|
529
539
|
it('does not render back link when inlineBackHref is null', async () => {
|
|
530
540
|
const { container } = renderWithProviders(
|
|
531
541
|
<MessageComposer inline inlineBackHref={null} variant="reply" messageId="message-1" />,
|