@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.
- package/AGENTS.md +1 -1
- package/dist/backend/CrudForm.js +1 -0
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/detail/RecordNotFoundState.js +10 -5
- package/dist/backend/detail/RecordNotFoundState.js.map +2 -2
- package/dist/primitives/alert.js.map +1 -1
- package/dist/primitives/checkbox.js +2 -1
- package/dist/primitives/checkbox.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/CrudForm.tsx +1 -0
- package/src/backend/__tests__/CrudForm.render.test.tsx +36 -1
- package/src/backend/detail/RecordNotFoundState.tsx +13 -6
- package/src/backend/detail/__tests__/RecordNotFoundState.test.tsx +80 -0
- package/src/primitives/__tests__/checkbox.test.tsx +28 -0
- package/src/primitives/alert.tsx +1 -1
- package/src/primitives/checkbox.tsx +2 -1
|
@@ -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 {
|
|
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
|
-
|
|
29
|
+
EmptyState,
|
|
27
30
|
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 {
|
|
5
|
-
"mappings": ";
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
})
|
package/src/primitives/alert.tsx
CHANGED
|
@@ -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
|
>
|