@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.
@@ -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("div", { className: "text-base font-medium", children: title }) : null
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 &larr; {resolvedBackLabel}\n </Link>\n ) : null}\n {title ? <div className=\"text-base font-medium\">{title}</div> : 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\">&larr;</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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",
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 &larr; {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\">&larr;</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.7152.1.a69e92f9c9",
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.7152.1.a69e92f9c9",
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.7152.1.a69e92f9c9",
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",
@@ -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 />
@@ -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' ? <h2 className="text-base font-semibold">{title}</h2> : title}
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 header injection spot with routing context', () => {
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.getByText('Edit Person')).toBeInTheDocument()
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
  &larr; {resolvedBackLabel}
108
112
  </Link>
109
113
  ) : null}
110
- {title ? <div className="text-base font-medium">{title}</div> : null}
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" />,