@open-mercato/ui 0.6.5-develop.4772.1.d517a085d1 → 0.6.5-develop.4782.1.29bd722d3d

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.
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import Link from "next/link";
4
+ import { SearchX } from "lucide-react";
4
5
  import { Button } from "@open-mercato/ui/primitives/button";
5
- import { ErrorMessage } from "./ErrorMessage.js";
6
+ import { EmptyState } from "@open-mercato/ui/primitives/empty-state";
7
+ import { formatErrorMessageLabel } from "./ErrorMessage.js";
6
8
  import { cn } from "@open-mercato/shared/lib/utils";
7
9
  import { useT } from "@open-mercato/shared/lib/i18n/context";
8
10
  function RecordNotFoundState({
@@ -11,6 +13,7 @@ function RecordNotFoundState({
11
13
  backHref,
12
14
  backLabel,
13
15
  action,
16
+ icon,
14
17
  className
15
18
  }) {
16
19
  const t = useT();
@@ -23,11 +26,13 @@ function RecordNotFoundState({
23
26
  className
24
27
  ),
25
28
  children: /* @__PURE__ */ jsx(
26
- ErrorMessage,
29
+ EmptyState,
27
30
  {
28
- label,
29
- description,
30
- action: action ?? defaultAction
31
+ variant: "subtle",
32
+ icon: icon ?? /* @__PURE__ */ jsx(SearchX, { className: "h-6 w-6", "aria-hidden": true }),
33
+ title: formatErrorMessageLabel(label),
34
+ description: description ? formatErrorMessageLabel(description) : void 0,
35
+ actions: action ?? defaultAction
31
36
  }
32
37
  )
33
38
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/backend/detail/RecordNotFoundState.tsx"],
4
- "sourcesContent": ["\"use client\";\n\nimport * as React from \"react\";\nimport Link from \"next/link\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { ErrorMessage } from \"./ErrorMessage\";\nimport { cn } from \"@open-mercato/shared/lib/utils\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\n\nexport type RecordNotFoundStateProps = {\n label: string;\n description?: string;\n backHref?: string;\n backLabel?: string;\n action?: React.ReactNode;\n className?: string;\n};\n\nexport function RecordNotFoundState({\n label,\n description,\n backHref,\n backLabel,\n action,\n className,\n}: RecordNotFoundStateProps) {\n const t = useT();\n const defaultAction = backHref ? (\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={backHref}>{backLabel ?? t('ui.recordNotFound.backToList','Back to list')}</Link>\n </Button>\n ) : null;\n\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center min-h-[50vh] px-4\",\n className,\n )}\n >\n <ErrorMessage\n label={label}\n description={description}\n action={action ?? defaultAction}\n />\n </div>\n );\n}\n"],
5
- "mappings": ";AA6BM;AA1BN,OAAO,UAAU;AACjB,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,YAAY;AAWd,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,IAAI,KAAK;AACf,QAAM,gBAAgB,WACpB,oBAAC,UAAO,SAAO,MAAC,SAAQ,WAAU,MAAK,MACrC,8BAAC,QAAK,MAAM,UAAW,uBAAa,EAAE,gCAA+B,cAAc,GAAE,GACvF,IACE;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,QAAQ,UAAU;AAAA;AAAA,MACpB;AAAA;AAAA,EACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\";\n\nimport * as React from \"react\";\nimport Link from \"next/link\";\nimport { SearchX } from \"lucide-react\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { EmptyState } from \"@open-mercato/ui/primitives/empty-state\";\nimport { formatErrorMessageLabel } from \"./ErrorMessage\";\nimport { cn } from \"@open-mercato/shared/lib/utils\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\n\nexport type RecordNotFoundStateProps = {\n label: string;\n description?: string;\n backHref?: string;\n backLabel?: string;\n action?: React.ReactNode;\n /** Optional leading icon. Defaults to a neutral \"search / not found\" glyph. */\n icon?: React.ReactNode;\n className?: string;\n};\n\nexport function RecordNotFoundState({\n label,\n description,\n backHref,\n backLabel,\n action,\n icon,\n className,\n}: RecordNotFoundStateProps) {\n const t = useT();\n const defaultAction = backHref ? (\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={backHref}>{backLabel ?? t('ui.recordNotFound.backToList', 'Back to list')}</Link>\n </Button>\n ) : null;\n\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center min-h-[50vh] px-4\",\n className,\n )}\n >\n <EmptyState\n variant=\"subtle\"\n icon={icon ?? <SearchX className=\"h-6 w-6\" aria-hidden />}\n title={formatErrorMessageLabel(label)}\n description={description ? formatErrorMessageLabel(description) : undefined}\n actions={action ?? defaultAction}\n />\n </div>\n );\n}\n"],
5
+ "mappings": ";AAkCM;AA/BN,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,+BAA+B;AACxC,SAAS,UAAU;AACnB,SAAS,YAAY;AAad,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,IAAI,KAAK;AACf,QAAM,gBAAgB,WACpB,oBAAC,UAAO,SAAO,MAAC,SAAQ,WAAU,MAAK,MACrC,8BAAC,QAAK,MAAM,UAAW,uBAAa,EAAE,gCAAgC,cAAc,GAAE,GACxF,IACE;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,SAAQ;AAAA,UACR,MAAM,QAAQ,oBAAC,WAAQ,WAAU,WAAU,eAAW,MAAC;AAAA,UACvD,OAAO,wBAAwB,KAAK;AAAA,UACpC,aAAa,cAAc,wBAAwB,WAAW,IAAI;AAAA,UAClE,SAAS,UAAU;AAAA;AAAA,MACrB;AAAA;AAAA,EACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/alert.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { AlertCircle, CheckCircle2, Info, Rocket, X, AlertTriangle } from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type AlertStatus = 'error' | 'warning' | 'success' | 'information' | 'feature'\nexport type AlertStyle = 'filled' | 'light' | 'lighter' | 'stroke'\nexport type AlertSize = 'xs' | 'sm' | 'default'\n\n/**\n * Legacy `variant` values from the pre-Figma-`169:2358` API. Kept for BC \u2014\n * mapped to `status` inside the Alert component so existing call sites do\n * not need migration. The Figma `light` look (`#fecaca` saturated tint\n * for error \u2192 `state/{x}/light` token family) is heavier than the\n * pre-Figma look; legacy call sites that want the softer pre-Figma\n * appearance should switch explicitly to `style=\"lighter\"`.\n */\ntype LegacyVariant = 'default' | 'destructive' | 'success' | 'warning' | 'info'\n\nconst legacyVariantToStatus: Record<LegacyVariant, AlertStatus> = {\n default: 'information',\n destructive: 'error',\n success: 'success',\n warning: 'warning',\n info: 'information',\n}\n\nconst alertVariants = cva(\n 'relative flex w-full gap-2 transition-colors',\n {\n variants: {\n status: {\n error: '',\n warning: '',\n success: '',\n information: '',\n feature: '',\n },\n style: {\n filled: '',\n light: '',\n lighter: '',\n stroke: '',\n },\n size: {\n // Figma X-Small: px-[8px] py-[6px], radius-8, Paragraph/X Small (12/16)\n xs: 'min-h-8 items-center rounded-lg px-2 py-1.5 text-xs leading-4 tracking-tight',\n // Figma Small: px-[10px] py-[8px], radius-8, Paragraph/Small (14/20)\n sm: 'min-h-9 items-center rounded-lg px-2.5 py-2 text-sm leading-5 tracking-tight',\n // Figma Large: px-[12px] py-[10px], radius-12, Paragraph/Small (14/20) multi-line\n default: 'items-start rounded-xl px-3 py-2.5 text-sm leading-5 tracking-tight',\n },\n },\n compoundVariants: [\n // === filled (Figma: bg = state/{x}/base, text = static-white) ===\n { status: 'error', style: 'filled', className: 'border border-transparent bg-status-error-icon text-white' },\n { status: 'warning', style: 'filled', className: 'border border-transparent bg-status-warning-icon text-white' },\n { status: 'success', style: 'filled', className: 'border border-transparent bg-status-success-icon text-white' },\n { status: 'information', style: 'filled', className: 'border border-transparent bg-status-info-icon text-white' },\n { status: 'feature', style: 'filled', className: 'border border-transparent bg-status-neutral-icon text-white' },\n // === light (Figma: bg = state/{x}/light, text = text/strong-950 = #171717 black) ===\n { status: 'error', style: 'light', className: 'border border-transparent bg-status-error-border text-foreground' },\n { status: 'warning', style: 'light', className: 'border border-transparent bg-status-warning-border text-foreground' },\n { status: 'success', style: 'light', className: 'border border-transparent bg-status-success-border text-foreground' },\n { status: 'information', style: 'light', className: 'border border-transparent bg-status-info-border text-foreground' },\n { status: 'feature', style: 'light', className: 'border border-transparent bg-status-neutral-border text-foreground' },\n // === lighter (Figma: bg = state/{x}/lighter, text = text/strong-950 = #171717 black) ===\n { status: 'error', style: 'lighter', className: 'border border-transparent bg-status-error-bg text-foreground' },\n { status: 'warning', style: 'lighter', className: 'border border-transparent bg-status-warning-bg text-foreground' },\n { status: 'success', style: 'lighter', className: 'border border-transparent bg-status-success-bg text-foreground' },\n { status: 'information', style: 'lighter', className: 'border border-transparent bg-status-info-bg text-foreground' },\n { status: 'feature', style: 'lighter', className: 'border border-transparent bg-status-neutral-bg text-foreground' },\n // === stroke (Figma: bg = white, border = stroke/soft-200, text = strong-950, shadow = regular-shadow/medium) ===\n { style: 'stroke', className: 'border border-border bg-background text-foreground shadow-lg' },\n ],\n defaultVariants: {\n status: 'information',\n style: 'light',\n size: 'sm',\n },\n },\n)\n\n// Dismiss button (X) color per Figma: text/strong-950 (#171717 black) at\n// `opacity-40` for light/lighter/stroke \u2014 matches the Figma `close-line`\n// icon which is the dark icon-strong-950 fill rendered at 40% alpha\n// (visually \u2248 #5c5c5c gray). White at full opacity for filled so it\n// stays legible over the saturated bg.\nconst alertDismissVariants = cva(\n 'shrink-0 rounded-md p-0.5 transition-opacity focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n {\n variants: {\n style: {\n filled: 'text-white opacity-90 hover:opacity-100',\n light: 'text-foreground opacity-40 hover:opacity-100',\n lighter: 'text-foreground opacity-40 hover:opacity-100',\n stroke: 'text-foreground opacity-40 hover:opacity-100',\n },\n },\n defaultVariants: { style: 'light' },\n },\n)\n\nconst alertIconBadgeVariants = cva(\n 'inline-flex shrink-0 items-center justify-center rounded-full text-white',\n {\n variants: {\n status: {\n error: 'bg-status-error-icon',\n warning: 'bg-status-warning-icon',\n success: 'bg-status-success-icon',\n information: 'bg-status-info-icon',\n feature: 'bg-status-neutral-icon',\n },\n size: {\n xs: 'size-4 [&>svg]:size-2.5',\n sm: 'size-5 [&>svg]:size-3',\n default: 'size-6 [&>svg]:size-3.5',\n },\n },\n defaultVariants: { status: 'information', size: 'sm' },\n },\n)\n\nconst defaultStatusIcons: Record<AlertStatus, React.ComponentType<{ className?: string; 'aria-hidden'?: 'true' }>> = {\n error: AlertCircle,\n warning: AlertTriangle,\n success: CheckCircle2,\n information: Info,\n feature: Rocket,\n}\n\nexport type AlertProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'role'> &\n Omit<VariantProps<typeof alertVariants>, 'status' | 'style' | 'size'> & {\n status?: AlertStatus\n style?: AlertStyle\n size?: AlertSize\n /** Show the leading icon. Default `true`. */\n showIcon?: boolean\n /** Override the default per-status Lucide icon. */\n icon?: React.ReactNode\n /** Render a trailing close (X) button. Pair with `onDismiss`. */\n dismissible?: boolean\n onDismiss?: () => void\n dismissAriaLabel?: string\n /** Inline action slot (link buttons) rendered to the right of the body. */\n action?: React.ReactNode\n /**\n * @deprecated Use `status` + `style` instead. Kept for BC. Maps the\n * legacy single-prop variants onto `status` and picks up the new\n * `light` + `sm` defaults.\n */\n variant?: LegacyVariant\n }\n\nconst Alert = React.forwardRef<HTMLDivElement, AlertProps>(\n (\n {\n className,\n status,\n style,\n size,\n showIcon = true,\n icon,\n dismissible = false,\n onDismiss,\n dismissAriaLabel = 'Dismiss',\n action,\n variant,\n children,\n ...props\n },\n ref,\n ) => {\n const resolvedStatus: AlertStatus = status ?? (variant ? legacyVariantToStatus[variant] : 'information')\n const resolvedStyle: AlertStyle = style ?? 'light'\n const resolvedSize: AlertSize = size ?? 'sm'\n const IconComponent = defaultStatusIcons[resolvedStatus]\n const rawIcon = icon ?? <IconComponent aria-hidden=\"true\" />\n\n let iconSlot: React.ReactNode = null\n if (showIcon) {\n if (resolvedStyle === 'filled') {\n // Filled: plain icon over saturated bg \u2014 no badge wrap, inherit text-white.\n iconSlot = (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center justify-center',\n resolvedSize === 'default' ? '[&>svg]:size-5' : '[&>svg]:size-4',\n )}\n data-slot=\"alert-icon\"\n >\n {rawIcon}\n </span>\n )\n } else {\n // light / lighter / stroke: badge wrap with status color bg + white icon.\n iconSlot = (\n <span\n className={alertIconBadgeVariants({ status: resolvedStatus, size: resolvedSize })}\n data-slot=\"alert-icon-badge\"\n data-status={resolvedStatus}\n >\n {rawIcon}\n </span>\n )\n }\n }\n\n return (\n <div\n ref={ref}\n role=\"alert\"\n className={cn(\n alertVariants({ status: resolvedStatus, style: resolvedStyle, size: resolvedSize }),\n className,\n )}\n data-slot=\"alert\"\n data-status={resolvedStatus}\n data-style={resolvedStyle}\n {...props}\n >\n {iconSlot}\n <div className=\"min-w-0 flex-1\">{children}</div>\n {action ? <div className=\"shrink-0\">{action}</div> : null}\n {dismissible ? (\n <button\n type=\"button\"\n onClick={onDismiss}\n aria-label={dismissAriaLabel}\n className={alertDismissVariants({ style: resolvedStyle })}\n data-slot=\"alert-dismiss\"\n >\n <X aria-hidden=\"true\" className=\"size-4\" />\n </button>\n ) : null}\n </div>\n )\n },\n)\nAlert.displayName = 'Alert'\n\n// Figma \"Label/Small\": Inter Medium 14 / 20.\nconst AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(\n ({ className, ...props }, ref) => (\n <h5 ref={ref} className={cn('mb-1 text-sm font-medium leading-5', className)} {...props} />\n ),\n)\nAlertTitle.displayName = 'AlertTitle'\n\n// Figma \"Paragraph/Small\": Inter Regular 14 / 20.\nconst AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n ({ className, ...props }, ref) => (\n <p ref={ref} className={cn('text-sm leading-5', className)} {...props} />\n ),\n)\nAlertDescription.displayName = 'AlertDescription'\n\nexport { Alert, AlertDescription, AlertTitle, alertVariants, alertIconBadgeVariants }\n"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { AlertCircle, CheckCircle2, Info, Rocket, X, AlertTriangle } from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type AlertStatus = 'error' | 'warning' | 'success' | 'information' | 'feature'\nexport type AlertStyle = 'filled' | 'light' | 'lighter' | 'stroke'\nexport type AlertSize = 'xs' | 'sm' | 'default'\n\n/**\n * Legacy `variant` values from the pre-Figma-`169:2358` API. Kept for BC \u2014\n * mapped to `status` inside the Alert component so existing call sites do\n * not need migration. The Figma `light` look (`#fecaca` saturated tint\n * for error \u2192 `state/{x}/light` token family) is heavier than the\n * pre-Figma look; legacy call sites that want the softer pre-Figma\n * appearance should switch explicitly to `style=\"lighter\"`.\n */\ntype LegacyVariant = 'default' | 'destructive' | 'success' | 'warning' | 'info'\n\nconst legacyVariantToStatus: Record<LegacyVariant, AlertStatus> = {\n default: 'information',\n destructive: 'error',\n success: 'success',\n warning: 'warning',\n info: 'information',\n}\n\nconst alertVariants = cva(\n 'relative flex w-full gap-2 transition-colors',\n {\n variants: {\n status: {\n error: '',\n warning: '',\n success: '',\n information: '',\n feature: '',\n },\n style: {\n filled: '',\n light: '',\n lighter: '',\n stroke: '',\n },\n size: {\n // Figma X-Small: px-[8px] py-[6px], radius-8, Paragraph/X Small (12/16)\n xs: 'min-h-8 items-center rounded-lg px-2 py-1.5 text-xs leading-4 tracking-tight',\n // Figma Small: px-[10px] py-[8px], radius-8, Paragraph/Small (14/20)\n sm: 'min-h-9 items-center rounded-lg px-2.5 py-2 text-sm leading-5 tracking-tight',\n // Figma Large: px-[12px] py-[10px], radius-12, Paragraph/Small (14/20) multi-line\n default: 'items-start rounded-xl px-3 py-2.5 text-sm leading-5 tracking-tight',\n },\n },\n compoundVariants: [\n // === filled (Figma: bg = state/{x}/base, text = static-white) ===\n { status: 'error', style: 'filled', className: 'border border-transparent bg-status-error-icon text-white' },\n { status: 'warning', style: 'filled', className: 'border border-transparent bg-status-warning-icon text-white' },\n { status: 'success', style: 'filled', className: 'border border-transparent bg-status-success-icon text-white' },\n { status: 'information', style: 'filled', className: 'border border-transparent bg-status-info-icon text-white' },\n { status: 'feature', style: 'filled', className: 'border border-transparent bg-status-neutral-icon text-white' },\n // === light (Figma: bg = state/{x}/light, text = text/strong-950 = #171717 black) ===\n { status: 'error', style: 'light', className: 'border border-transparent bg-status-error-border text-foreground' },\n { status: 'warning', style: 'light', className: 'border border-transparent bg-status-warning-border text-foreground' },\n { status: 'success', style: 'light', className: 'border border-transparent bg-status-success-border text-foreground' },\n { status: 'information', style: 'light', className: 'border border-transparent bg-status-info-border text-foreground' },\n { status: 'feature', style: 'light', className: 'border border-transparent bg-status-neutral-border text-foreground' },\n // === lighter (Figma: bg = state/{x}/lighter, text = text/strong-950 = #171717 black) ===\n { status: 'error', style: 'lighter', className: 'border border-transparent bg-status-error-bg text-foreground' },\n { status: 'warning', style: 'lighter', className: 'border border-transparent bg-status-warning-bg text-foreground' },\n { status: 'success', style: 'lighter', className: 'border border-transparent bg-status-success-bg text-foreground' },\n { status: 'information', style: 'lighter', className: 'border border-transparent bg-status-info-bg text-foreground' },\n { status: 'feature', style: 'lighter', className: 'border border-transparent bg-status-neutral-bg text-foreground' },\n // === stroke (Figma: bg = white, border = stroke/soft-200, text = strong-950, shadow = regular-shadow/medium) ===\n { style: 'stroke', className: 'border border-border bg-background text-foreground shadow-lg' },\n ],\n defaultVariants: {\n status: 'information',\n style: 'light',\n size: 'sm',\n },\n },\n)\n\n// Dismiss button (X) color per Figma: text/strong-950 (#171717 black) at\n// `opacity-40` for light/lighter/stroke \u2014 matches the Figma `close-line`\n// icon which is the dark icon-strong-950 fill rendered at 40% alpha\n// (visually \u2248 #5c5c5c gray). White at full opacity for filled so it\n// stays legible over the saturated bg.\nconst alertDismissVariants = cva(\n 'shrink-0 rounded-md p-0.5 transition-opacity focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n {\n variants: {\n style: {\n filled: 'text-white opacity-90 hover:opacity-100',\n light: 'text-foreground opacity-40 hover:opacity-100',\n lighter: 'text-foreground opacity-40 hover:opacity-100',\n stroke: 'text-foreground opacity-40 hover:opacity-100',\n },\n },\n defaultVariants: { style: 'light' },\n },\n)\n\nconst alertIconBadgeVariants = cva(\n 'inline-flex shrink-0 items-center justify-center rounded-full text-white',\n {\n variants: {\n status: {\n error: 'bg-status-error-icon',\n warning: 'bg-status-warning-icon',\n success: 'bg-status-success-icon',\n information: 'bg-status-info-icon',\n feature: 'bg-status-neutral-icon',\n },\n size: {\n xs: 'size-4 [&>svg]:size-2.5',\n sm: 'size-5 [&>svg]:size-3',\n default: 'size-6 [&>svg]:size-3.5',\n },\n },\n defaultVariants: { status: 'information', size: 'sm' },\n },\n)\n\nconst defaultStatusIcons: Record<AlertStatus, React.ComponentType<{ className?: string; 'aria-hidden'?: 'true' }>> = {\n error: AlertCircle,\n warning: AlertTriangle,\n success: CheckCircle2,\n information: Info,\n feature: Rocket,\n}\n\nexport type AlertProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'role' | 'style'> &\n Omit<VariantProps<typeof alertVariants>, 'status' | 'style' | 'size'> & {\n status?: AlertStatus\n style?: AlertStyle\n size?: AlertSize\n /** Show the leading icon. Default `true`. */\n showIcon?: boolean\n /** Override the default per-status Lucide icon. */\n icon?: React.ReactNode\n /** Render a trailing close (X) button. Pair with `onDismiss`. */\n dismissible?: boolean\n onDismiss?: () => void\n dismissAriaLabel?: string\n /** Inline action slot (link buttons) rendered to the right of the body. */\n action?: React.ReactNode\n /**\n * @deprecated Use `status` + `style` instead. Kept for BC. Maps the\n * legacy single-prop variants onto `status` and picks up the new\n * `light` + `sm` defaults.\n */\n variant?: LegacyVariant\n }\n\nconst Alert = React.forwardRef<HTMLDivElement, AlertProps>(\n (\n {\n className,\n status,\n style,\n size,\n showIcon = true,\n icon,\n dismissible = false,\n onDismiss,\n dismissAriaLabel = 'Dismiss',\n action,\n variant,\n children,\n ...props\n },\n ref,\n ) => {\n const resolvedStatus: AlertStatus = status ?? (variant ? legacyVariantToStatus[variant] : 'information')\n const resolvedStyle: AlertStyle = style ?? 'light'\n const resolvedSize: AlertSize = size ?? 'sm'\n const IconComponent = defaultStatusIcons[resolvedStatus]\n const rawIcon = icon ?? <IconComponent aria-hidden=\"true\" />\n\n let iconSlot: React.ReactNode = null\n if (showIcon) {\n if (resolvedStyle === 'filled') {\n // Filled: plain icon over saturated bg \u2014 no badge wrap, inherit text-white.\n iconSlot = (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center justify-center',\n resolvedSize === 'default' ? '[&>svg]:size-5' : '[&>svg]:size-4',\n )}\n data-slot=\"alert-icon\"\n >\n {rawIcon}\n </span>\n )\n } else {\n // light / lighter / stroke: badge wrap with status color bg + white icon.\n iconSlot = (\n <span\n className={alertIconBadgeVariants({ status: resolvedStatus, size: resolvedSize })}\n data-slot=\"alert-icon-badge\"\n data-status={resolvedStatus}\n >\n {rawIcon}\n </span>\n )\n }\n }\n\n return (\n <div\n ref={ref}\n role=\"alert\"\n className={cn(\n alertVariants({ status: resolvedStatus, style: resolvedStyle, size: resolvedSize }),\n className,\n )}\n data-slot=\"alert\"\n data-status={resolvedStatus}\n data-style={resolvedStyle}\n {...props}\n >\n {iconSlot}\n <div className=\"min-w-0 flex-1\">{children}</div>\n {action ? <div className=\"shrink-0\">{action}</div> : null}\n {dismissible ? (\n <button\n type=\"button\"\n onClick={onDismiss}\n aria-label={dismissAriaLabel}\n className={alertDismissVariants({ style: resolvedStyle })}\n data-slot=\"alert-dismiss\"\n >\n <X aria-hidden=\"true\" className=\"size-4\" />\n </button>\n ) : null}\n </div>\n )\n },\n)\nAlert.displayName = 'Alert'\n\n// Figma \"Label/Small\": Inter Medium 14 / 20.\nconst AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(\n ({ className, ...props }, ref) => (\n <h5 ref={ref} className={cn('mb-1 text-sm font-medium leading-5', className)} {...props} />\n ),\n)\nAlertTitle.displayName = 'AlertTitle'\n\n// Figma \"Paragraph/Small\": Inter Regular 14 / 20.\nconst AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n ({ className, ...props }, ref) => (\n <p ref={ref} className={cn('text-sm leading-5', className)} {...props} />\n ),\n)\nAlertDescription.displayName = 'AlertDescription'\n\nexport { Alert, AlertDescription, AlertTitle, alertVariants, alertIconBadgeVariants }\n"],
5
5
  "mappings": ";AAqL4B,cAgCtB,YAhCsB;AAnL5B,YAAY,WAAW;AACvB,SAAS,aAAa,cAAc,MAAM,QAAQ,GAAG,qBAAqB;AAC1E,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAgBnB,MAAM,wBAA4D;AAAA,EAChE,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AAEA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,QAAQ;AAAA,QACN,OAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,aAAa;AAAA,QACb,SAAS;AAAA,MACX;AAAA,MACA,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,SAAS;AAAA,QACT,QAAQ;AAAA,MACV;AAAA,MACA,MAAM;AAAA;AAAA,QAEJ,IAAI;AAAA;AAAA,QAEJ,IAAI;AAAA;AAAA,QAEJ,SAAS;AAAA,MACX;AAAA,IACF;AAAA,IACA,kBAAkB;AAAA;AAAA,MAEhB,EAAE,QAAQ,SAAe,OAAO,UAAU,WAAW,4DAA4D;AAAA,MACjH,EAAE,QAAQ,WAAe,OAAO,UAAU,WAAW,8DAA8D;AAAA,MACnH,EAAE,QAAQ,WAAe,OAAO,UAAU,WAAW,8DAA8D;AAAA,MACnH,EAAE,QAAQ,eAAe,OAAO,UAAU,WAAW,2DAA2D;AAAA,MAChH,EAAE,QAAQ,WAAe,OAAO,UAAU,WAAW,8DAA8D;AAAA;AAAA,MAEnH,EAAE,QAAQ,SAAe,OAAO,SAAS,WAAW,mEAAmE;AAAA,MACvH,EAAE,QAAQ,WAAe,OAAO,SAAS,WAAW,qEAAqE;AAAA,MACzH,EAAE,QAAQ,WAAe,OAAO,SAAS,WAAW,qEAAqE;AAAA,MACzH,EAAE,QAAQ,eAAe,OAAO,SAAS,WAAW,kEAAkE;AAAA,MACtH,EAAE,QAAQ,WAAe,OAAO,SAAS,WAAW,qEAAqE;AAAA;AAAA,MAEzH,EAAE,QAAQ,SAAe,OAAO,WAAW,WAAW,+DAA+D;AAAA,MACrH,EAAE,QAAQ,WAAe,OAAO,WAAW,WAAW,iEAAiE;AAAA,MACvH,EAAE,QAAQ,WAAe,OAAO,WAAW,WAAW,iEAAiE;AAAA,MACvH,EAAE,QAAQ,eAAe,OAAO,WAAW,WAAW,8DAA8D;AAAA,MACpH,EAAE,QAAQ,WAAe,OAAO,WAAW,WAAW,iEAAiE;AAAA;AAAA,MAEvH,EAAE,OAAO,UAAU,WAAW,+DAA+D;AAAA,IAC/F;AAAA,IACA,iBAAiB;AAAA,MACf,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAOA,MAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,SAAS;AAAA,QACT,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,OAAO,QAAQ;AAAA,EACpC;AACF;AAEA,MAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,QAAQ;AAAA,QACN,OAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,aAAa;AAAA,QACb,SAAS;AAAA,MACX;AAAA,MACA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,SAAS;AAAA,MACX;AAAA,IACF;AAAA,IACA,iBAAiB,EAAE,QAAQ,eAAe,MAAM,KAAK;AAAA,EACvD;AACF;AAEA,MAAM,qBAA+G;AAAA,EACnH,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AACX;AAyBA,MAAM,QAAQ,MAAM;AAAA,EAClB,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA,mBAAmB;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,GACA,QACG;AACH,UAAM,iBAA8B,WAAW,UAAU,sBAAsB,OAAO,IAAI;AAC1F,UAAM,gBAA4B,SAAS;AAC3C,UAAM,eAA0B,QAAQ;AACxC,UAAM,gBAAgB,mBAAmB,cAAc;AACvD,UAAM,UAAU,QAAQ,oBAAC,iBAAc,eAAY,QAAO;AAE1D,QAAI,WAA4B;AAChC,QAAI,UAAU;AACZ,UAAI,kBAAkB,UAAU;AAE9B,mBACE;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,iBAAiB,YAAY,mBAAmB;AAAA,YAClD;AAAA,YACA,aAAU;AAAA,YAET;AAAA;AAAA,QACH;AAAA,MAEJ,OAAO;AAEL,mBACE;AAAA,UAAC;AAAA;AAAA,YACC,WAAW,uBAAuB,EAAE,QAAQ,gBAAgB,MAAM,aAAa,CAAC;AAAA,YAChF,aAAU;AAAA,YACV,eAAa;AAAA,YAEZ;AAAA;AAAA,QACH;AAAA,MAEJ;AAAA,IACF;AAEA,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,WAAW;AAAA,UACT,cAAc,EAAE,QAAQ,gBAAgB,OAAO,eAAe,MAAM,aAAa,CAAC;AAAA,UAClF;AAAA,QACF;AAAA,QACA,aAAU;AAAA,QACV,eAAa;AAAA,QACb,cAAY;AAAA,QACX,GAAG;AAAA,QAEH;AAAA;AAAA,UACD,oBAAC,SAAI,WAAU,kBAAkB,UAAS;AAAA,UACzC,SAAS,oBAAC,SAAI,WAAU,YAAY,kBAAO,IAAS;AAAA,UACpD,cACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,cAAY;AAAA,cACZ,WAAW,qBAAqB,EAAE,OAAO,cAAc,CAAC;AAAA,cACxD,aAAU;AAAA,cAEV,8BAAC,KAAE,eAAY,QAAO,WAAU,UAAS;AAAA;AAAA,UAC3C,IACE;AAAA;AAAA;AAAA,IACN;AAAA,EAEJ;AACF;AACA,MAAM,cAAc;AAGpB,MAAM,aAAa,MAAM;AAAA,EACvB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB,oBAAC,QAAG,KAAU,WAAW,GAAG,sCAAsC,SAAS,GAAI,GAAG,OAAO;AAE7F;AACA,WAAW,cAAc;AAGzB,MAAM,mBAAmB,MAAM;AAAA,EAC7B,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB,oBAAC,OAAE,KAAU,WAAW,GAAG,qBAAqB,SAAS,GAAI,GAAG,OAAO;AAE3E;AACA,iBAAiB,cAAc;",
6
6
  "names": []
7
7
  }
@@ -22,12 +22,13 @@ const indicatorIconBySize = {
22
22
  sm: "size-3.5",
23
23
  md: "size-4"
24
24
  };
25
- const Checkbox = React.forwardRef(({ className, size, ...props }, ref) => {
25
+ const Checkbox = React.forwardRef(({ className, size, type = "button", ...props }, ref) => {
26
26
  const iconClass = indicatorIconBySize[size ?? "sm"];
27
27
  return /* @__PURE__ */ jsx(
28
28
  CheckboxPrimitive.Root,
29
29
  {
30
30
  ref,
31
+ type,
31
32
  className: cn(checkboxVariants({ size, className })),
32
33
  ...props,
33
34
  children: /* @__PURE__ */ jsx(CheckboxPrimitive.Indicator, { className: cn("flex items-center justify-center text-current"), children: props.checked === "indeterminate" ? /* @__PURE__ */ jsx(Minus, { className: iconClass, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Check, { className: iconClass, "aria-hidden": "true" }) })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/checkbox.tsx"],
4
- "sourcesContent": ["import * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check, Minus } from \"lucide-react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@open-mercato/shared/lib/utils\"\n\nconst checkboxVariants = cva(\n \"peer shrink-0 rounded-[4px] border border-input bg-background shadow-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-indigo/40 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-accent-indigo data-[state=checked]:text-accent-indigo-foreground data-[state=checked]:border-accent-indigo data-[state=indeterminate]:bg-accent-indigo data-[state=indeterminate]:text-accent-indigo-foreground data-[state=indeterminate]:border-accent-indigo hover:border-accent-indigo/60 transition-colors\",\n {\n variants: {\n size: {\n sm: \"size-4\",\n md: \"size-5\",\n },\n },\n defaultVariants: {\n size: \"sm\",\n },\n }\n)\n\nconst indicatorIconBySize = {\n sm: \"size-3.5\",\n md: \"size-4\",\n} as const\n\nexport type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> &\n VariantProps<typeof checkboxVariants>\n\nconst Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n CheckboxProps\n>(({ className, size, ...props }, ref) => {\n const iconClass = indicatorIconBySize[size ?? \"sm\"]\n return (\n <CheckboxPrimitive.Root\n ref={ref}\n className={cn(checkboxVariants({ size, className }))}\n {...props}\n >\n <CheckboxPrimitive.Indicator className={cn(\"flex items-center justify-center text-current\")}>\n {props.checked === \"indeterminate\" ? (\n <Minus className={iconClass} aria-hidden=\"true\" />\n ) : (\n <Check className={iconClass} aria-hidden=\"true\" />\n )}\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n )\n})\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox, checkboxVariants }\n"],
5
- "mappings": "AA2CU;AA3CV,YAAY,WAAW;AACvB,YAAY,uBAAuB;AACnC,SAAS,OAAO,aAAa;AAC7B,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAEnB,MAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,MAAM,sBAAsB;AAAA,EAC1B,IAAI;AAAA,EACJ,IAAI;AACN;AAKA,MAAM,WAAW,MAAM,WAGrB,CAAC,EAAE,WAAW,MAAM,GAAG,MAAM,GAAG,QAAQ;AACxC,QAAM,YAAY,oBAAoB,QAAQ,IAAI;AAClD,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC;AAAA,MACA,WAAW,GAAG,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC;AAAA,MAClD,GAAG;AAAA,MAEJ,8BAAC,kBAAkB,WAAlB,EAA4B,WAAW,GAAG,+CAA+C,GACvF,gBAAM,YAAY,kBACjB,oBAAC,SAAM,WAAW,WAAW,eAAY,QAAO,IAEhD,oBAAC,SAAM,WAAW,WAAW,eAAY,QAAO,GAEpD;AAAA;AAAA,EACF;AAEJ,CAAC;AACD,SAAS,cAAc,kBAAkB,KAAK;",
4
+ "sourcesContent": ["import * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check, Minus } from \"lucide-react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@open-mercato/shared/lib/utils\"\n\nconst checkboxVariants = cva(\n \"peer shrink-0 rounded-[4px] border border-input bg-background shadow-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-indigo/40 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-accent-indigo data-[state=checked]:text-accent-indigo-foreground data-[state=checked]:border-accent-indigo data-[state=indeterminate]:bg-accent-indigo data-[state=indeterminate]:text-accent-indigo-foreground data-[state=indeterminate]:border-accent-indigo hover:border-accent-indigo/60 transition-colors\",\n {\n variants: {\n size: {\n sm: \"size-4\",\n md: \"size-5\",\n },\n },\n defaultVariants: {\n size: \"sm\",\n },\n }\n)\n\nconst indicatorIconBySize = {\n sm: \"size-3.5\",\n md: \"size-4\",\n} as const\n\nexport type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> &\n VariantProps<typeof checkboxVariants>\n\nconst Checkbox = React.forwardRef<\n React.ElementRef<typeof CheckboxPrimitive.Root>,\n CheckboxProps\n>(({ className, size, type = \"button\", ...props }, ref) => {\n const iconClass = indicatorIconBySize[size ?? \"sm\"]\n return (\n <CheckboxPrimitive.Root\n ref={ref}\n type={type}\n className={cn(checkboxVariants({ size, className }))}\n {...props}\n >\n <CheckboxPrimitive.Indicator className={cn(\"flex items-center justify-center text-current\")}>\n {props.checked === \"indeterminate\" ? (\n <Minus className={iconClass} aria-hidden=\"true\" />\n ) : (\n <Check className={iconClass} aria-hidden=\"true\" />\n )}\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n )\n})\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox, checkboxVariants }\n"],
5
+ "mappings": "AA4CU;AA5CV,YAAY,WAAW;AACvB,YAAY,uBAAuB;AACnC,SAAS,OAAO,aAAa;AAC7B,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAEnB,MAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,MAAM,sBAAsB;AAAA,EAC1B,IAAI;AAAA,EACJ,IAAI;AACN;AAKA,MAAM,WAAW,MAAM,WAGrB,CAAC,EAAE,WAAW,MAAM,OAAO,UAAU,GAAG,MAAM,GAAG,QAAQ;AACzD,QAAM,YAAY,oBAAoB,QAAQ,IAAI;AAClD,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC;AAAA,MACA;AAAA,MACA,WAAW,GAAG,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC;AAAA,MAClD,GAAG;AAAA,MAEJ,8BAAC,kBAAkB,WAAlB,EAA4B,WAAW,GAAG,+CAA+C,GACvF,gBAAM,YAAY,kBACjB,oBAAC,SAAM,WAAW,WAAW,eAAY,QAAO,IAEhD,oBAAC,SAAM,WAAW,WAAW,eAAY,QAAO,GAEpD;AAAA;AAAA,EACF;AAEJ,CAAC;AACD,SAAS,cAAc,kBAAkB,KAAK;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.5-develop.4772.1.d517a085d1",
3
+ "version": "0.6.5-develop.4782.1.29bd722d3d",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -149,13 +149,13 @@
149
149
  "remark-gfm": "^4.0.1"
150
150
  },
151
151
  "peerDependencies": {
152
- "@open-mercato/shared": "0.6.5-develop.4772.1.d517a085d1",
152
+ "@open-mercato/shared": "0.6.5-develop.4782.1.29bd722d3d",
153
153
  "react": ">=18.0.0",
154
154
  "react-dom": ">=18.0.0",
155
155
  "react-is": ">=18.0.0"
156
156
  },
157
157
  "devDependencies": {
158
- "@open-mercato/shared": "0.6.5-develop.4772.1.d517a085d1",
158
+ "@open-mercato/shared": "0.6.5-develop.4782.1.29bd722d3d",
159
159
  "@testing-library/dom": "^10.4.1",
160
160
  "@testing-library/jest-dom": "^6.9.1",
161
161
  "@testing-library/react": "^16.3.1",
@@ -4132,6 +4132,7 @@ const ListboxMultiSelect = React.memo(function ListboxMultiSelect({
4132
4132
  return (
4133
4133
  <Button
4134
4134
  key={opt.value}
4135
+ type="button"
4135
4136
  variant="ghost"
4136
4137
  size="sm"
4137
4138
  onClick={() => toggle(opt.value)}
@@ -26,7 +26,7 @@ jest.mock('../injection/useInjectionDataWidgets', () => ({
26
26
 
27
27
  import * as React from 'react'
28
28
  import { renderToString } from 'react-dom/server'
29
- import { act, fireEvent, waitFor } from '@testing-library/react'
29
+ import { act, fireEvent, screen, waitFor } from '@testing-library/react'
30
30
  import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
31
31
  import { CrudForm, type CrudField } from '../CrudForm'
32
32
  import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
@@ -118,6 +118,41 @@ describe('CrudForm initialValues', () => {
118
118
  })
119
119
  })
120
120
 
121
+ it('does not submit when a listbox multi-select option is clicked', async () => {
122
+ const onSubmit = jest.fn()
123
+ const fields: CrudField[] = [
124
+ {
125
+ id: 'assignees',
126
+ label: 'Assignees',
127
+ type: 'select',
128
+ multiple: true,
129
+ listbox: true,
130
+ options: [
131
+ { value: 'alice', label: 'alice' },
132
+ { value: 'bob', label: 'bob' },
133
+ ],
134
+ },
135
+ ]
136
+
137
+ renderWithProviders(
138
+ <CrudForm title="Form" fields={fields} initialValues={{ assignees: [] }} onSubmit={onSubmit} />,
139
+ {
140
+ dict: {
141
+ 'ui.forms.actions.save': 'Save',
142
+ 'ui.forms.listbox.searchPlaceholder': 'Search...',
143
+ 'ui.forms.listbox.noMatches': 'No matches',
144
+ },
145
+ },
146
+ )
147
+
148
+ const aliceOption = screen.getByRole('button', { name: 'alice' })
149
+ expect(aliceOption).toHaveAttribute('type', 'button')
150
+
151
+ fireEvent.click(aliceOption)
152
+
153
+ expect(onSubmit).not.toHaveBeenCalled()
154
+ })
155
+
121
156
  it('does not re-invoke loadOptions on parent re-render (#814)', async () => {
122
157
  const loader = jest.fn().mockResolvedValue([{ label: 'A', value: 'a' }])
123
158
  const baseFields: CrudField[] = [
@@ -2,8 +2,10 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import Link from "next/link";
5
+ import { SearchX } from "lucide-react";
5
6
  import { Button } from "@open-mercato/ui/primitives/button";
6
- import { ErrorMessage } from "./ErrorMessage";
7
+ import { EmptyState } from "@open-mercato/ui/primitives/empty-state";
8
+ import { formatErrorMessageLabel } from "./ErrorMessage";
7
9
  import { cn } from "@open-mercato/shared/lib/utils";
8
10
  import { useT } from "@open-mercato/shared/lib/i18n/context";
9
11
 
@@ -13,6 +15,8 @@ export type RecordNotFoundStateProps = {
13
15
  backHref?: string;
14
16
  backLabel?: string;
15
17
  action?: React.ReactNode;
18
+ /** Optional leading icon. Defaults to a neutral "search / not found" glyph. */
19
+ icon?: React.ReactNode;
16
20
  className?: string;
17
21
  };
18
22
 
@@ -22,12 +26,13 @@ export function RecordNotFoundState({
22
26
  backHref,
23
27
  backLabel,
24
28
  action,
29
+ icon,
25
30
  className,
26
31
  }: RecordNotFoundStateProps) {
27
32
  const t = useT();
28
33
  const defaultAction = backHref ? (
29
34
  <Button asChild variant="outline" size="sm">
30
- <Link href={backHref}>{backLabel ?? t('ui.recordNotFound.backToList','Back to list')}</Link>
35
+ <Link href={backHref}>{backLabel ?? t('ui.recordNotFound.backToList', 'Back to list')}</Link>
31
36
  </Button>
32
37
  ) : null;
33
38
 
@@ -38,10 +43,12 @@ export function RecordNotFoundState({
38
43
  className,
39
44
  )}
40
45
  >
41
- <ErrorMessage
42
- label={label}
43
- description={description}
44
- action={action ?? defaultAction}
46
+ <EmptyState
47
+ variant="subtle"
48
+ icon={icon ?? <SearchX className="h-6 w-6" aria-hidden />}
49
+ title={formatErrorMessageLabel(label)}
50
+ description={description ? formatErrorMessageLabel(description) : undefined}
51
+ actions={action ?? defaultAction}
45
52
  />
46
53
  </div>
47
54
  );
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import * as React from 'react'
6
+ import { render, screen } from '@testing-library/react'
7
+ import { RecordNotFoundState } from '../RecordNotFoundState'
8
+
9
+ jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
10
+ useT: () => (_key: string, fallback?: string) => fallback ?? _key,
11
+ }))
12
+
13
+ jest.mock('next/link', () => {
14
+ const React = require('react')
15
+ return React.forwardRef(
16
+ ({ children, href, ...rest }: any, ref: React.ForwardedRef<HTMLAnchorElement>) => (
17
+ <a href={typeof href === 'string' ? href : href?.toString?.()} ref={ref} {...rest}>
18
+ {children}
19
+ </a>
20
+ ),
21
+ )
22
+ })
23
+
24
+ describe('RecordNotFoundState', () => {
25
+ it('renders the label as the empty-state title', () => {
26
+ render(<RecordNotFoundState label="Company not found." />)
27
+ expect(screen.getByText('Company not found.')).toBeInTheDocument()
28
+ })
29
+
30
+ it('renders a back link (role=link) with the given href and default label', () => {
31
+ render(<RecordNotFoundState label="Company not found." backHref="/backend/customers/companies" />)
32
+ const backLink = screen.getByRole('link', { name: /back to list/i })
33
+ expect(backLink).toBeInTheDocument()
34
+ expect(backLink).toHaveAttribute('href', '/backend/customers/companies')
35
+ })
36
+
37
+ it('uses a custom backLabel when provided', () => {
38
+ render(
39
+ <RecordNotFoundState
40
+ label="Company not found."
41
+ backHref="/backend/customers/companies"
42
+ backLabel="Back to companies"
43
+ />,
44
+ )
45
+ expect(screen.getByRole('link', { name: /back to companies/i })).toBeInTheDocument()
46
+ })
47
+
48
+ it('renders a custom action instead of the default back link', () => {
49
+ render(
50
+ <RecordNotFoundState
51
+ label="Company not found."
52
+ backHref="/ignored"
53
+ action={<button type="button">Retry</button>}
54
+ />,
55
+ )
56
+ expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument()
57
+ expect(screen.queryByRole('link')).not.toBeInTheDocument()
58
+ })
59
+
60
+ it('renders no action when neither backHref nor action is provided', () => {
61
+ render(<RecordNotFoundState label="Company not found." />)
62
+ expect(screen.queryByRole('link')).not.toBeInTheDocument()
63
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
64
+ })
65
+
66
+ it('is a neutral empty state, not a destructive alert (regression guard for #2127)', () => {
67
+ render(<RecordNotFoundState label="Company not found." backHref="/backend/customers/companies" />)
68
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument()
69
+ })
70
+
71
+ it('renders a custom icon when provided', () => {
72
+ render(
73
+ <RecordNotFoundState
74
+ label="Company not found."
75
+ icon={<svg data-testid="custom-icon" />}
76
+ />,
77
+ )
78
+ expect(screen.getByTestId('custom-icon')).toBeInTheDocument()
79
+ })
80
+ })
@@ -0,0 +1,28 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import * as React from 'react'
4
+ import { fireEvent, render, screen } from '@testing-library/react'
5
+ import { Checkbox } from '../checkbox'
6
+
7
+ describe('Checkbox', () => {
8
+ it('does not submit a parent form by default', () => {
9
+ const onSubmit = jest.fn((event: React.FormEvent<HTMLFormElement>) => {
10
+ event.preventDefault()
11
+ })
12
+ const onCheckedChange = jest.fn()
13
+
14
+ render(
15
+ <form onSubmit={onSubmit}>
16
+ <Checkbox aria-label="Assignee alice" onCheckedChange={onCheckedChange} />
17
+ </form>,
18
+ )
19
+
20
+ const checkbox = screen.getByRole('checkbox', { name: 'Assignee alice' })
21
+ expect(checkbox).toHaveAttribute('type', 'button')
22
+
23
+ fireEvent.click(checkbox)
24
+
25
+ expect(onCheckedChange).toHaveBeenCalled()
26
+ expect(onSubmit).not.toHaveBeenCalled()
27
+ })
28
+ })
@@ -133,7 +133,7 @@ const defaultStatusIcons: Record<AlertStatus, React.ComponentType<{ className?:
133
133
  feature: Rocket,
134
134
  }
135
135
 
136
- export type AlertProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'role'> &
136
+ export type AlertProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'role' | 'style'> &
137
137
  Omit<VariantProps<typeof alertVariants>, 'status' | 'style' | 'size'> & {
138
138
  status?: AlertStatus
139
139
  style?: AlertStyle
@@ -31,11 +31,12 @@ export type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimit
31
31
  const Checkbox = React.forwardRef<
32
32
  React.ElementRef<typeof CheckboxPrimitive.Root>,
33
33
  CheckboxProps
34
- >(({ className, size, ...props }, ref) => {
34
+ >(({ className, size, type = "button", ...props }, ref) => {
35
35
  const iconClass = indicatorIconBySize[size ?? "sm"]
36
36
  return (
37
37
  <CheckboxPrimitive.Root
38
38
  ref={ref}
39
+ type={type}
39
40
  className={cn(checkboxVariants({ size, className }))}
40
41
  {...props}
41
42
  >