@open-mercato/ui 0.5.1-develop.2953.6647bb2c43 → 0.5.1-develop.2964.d5ac4a6ebb

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.
Files changed (96) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +8 -0
  3. package/dist/backend/CrudForm.js +57 -29
  4. package/dist/backend/CrudForm.js.map +2 -2
  5. package/dist/backend/DataTable.js +32 -14
  6. package/dist/backend/DataTable.js.map +2 -2
  7. package/dist/backend/FilterOverlay.js +23 -17
  8. package/dist/backend/FilterOverlay.js.map +2 -2
  9. package/dist/backend/JsonBuilder.js +32 -18
  10. package/dist/backend/JsonBuilder.js.map +2 -2
  11. package/dist/backend/columns/ColumnChooserPanel.js +12 -13
  12. package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
  13. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +71 -62
  14. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
  15. package/dist/backend/date-range/DateRangeSelect.js +11 -10
  16. package/dist/backend/date-range/DateRangeSelect.js.map +2 -2
  17. package/dist/backend/date-range/InlineDateRangeSelect.js +10 -22
  18. package/dist/backend/date-range/InlineDateRangeSelect.js.map +2 -2
  19. package/dist/backend/detail/ActivitiesSection.js +20 -12
  20. package/dist/backend/detail/ActivitiesSection.js.map +2 -2
  21. package/dist/backend/detail/AddressEditor.js +24 -7
  22. package/dist/backend/detail/AddressEditor.js.map +2 -2
  23. package/dist/backend/detail/InlineEditors.js +12 -6
  24. package/dist/backend/detail/InlineEditors.js.map +2 -2
  25. package/dist/backend/detail/NotesSection.js +20 -14
  26. package/dist/backend/detail/NotesSection.js.map +2 -2
  27. package/dist/backend/filters/AdvancedFilterBuilder.js +52 -24
  28. package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
  29. package/dist/backend/injection/InjectedField.js +12 -7
  30. package/dist/backend/injection/InjectedField.js.map +2 -2
  31. package/dist/backend/inputs/ComboboxInput.js.map +2 -2
  32. package/dist/backend/inputs/EventSelect.js +22 -6
  33. package/dist/backend/inputs/EventSelect.js.map +2 -2
  34. package/dist/backend/inputs/PhoneNumberField.js +2 -2
  35. package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
  36. package/dist/backend/inputs/TimeInput.js +9 -10
  37. package/dist/backend/inputs/TimeInput.js.map +2 -2
  38. package/dist/backend/messages/message-compose-form-groups.js +12 -7
  39. package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
  40. package/dist/backend/messages/useMessageCompose.js +7 -1
  41. package/dist/backend/messages/useMessageCompose.js.map +2 -2
  42. package/dist/frontend/LanguageSwitcher.js +19 -14
  43. package/dist/frontend/LanguageSwitcher.js.map +2 -2
  44. package/dist/index.js +5 -0
  45. package/dist/index.js.map +2 -2
  46. package/dist/primitives/checkbox-field.js +17 -5
  47. package/dist/primitives/checkbox-field.js.map +2 -2
  48. package/dist/primitives/input.js +71 -14
  49. package/dist/primitives/input.js.map +2 -2
  50. package/dist/primitives/radio-field.js +74 -0
  51. package/dist/primitives/radio-field.js.map +7 -0
  52. package/dist/primitives/radio.js +37 -0
  53. package/dist/primitives/radio.js.map +7 -0
  54. package/dist/primitives/select.js +155 -0
  55. package/dist/primitives/select.js.map +7 -0
  56. package/dist/primitives/switch-field.js +76 -0
  57. package/dist/primitives/switch-field.js.map +7 -0
  58. package/dist/primitives/switch.js +17 -3
  59. package/dist/primitives/switch.js.map +2 -2
  60. package/dist/primitives/textarea.js +48 -12
  61. package/dist/primitives/textarea.js.map +2 -2
  62. package/dist/primitives/tooltip.js +44 -15
  63. package/dist/primitives/tooltip.js.map +2 -2
  64. package/package.json +5 -3
  65. package/src/backend/CrudForm.tsx +104 -37
  66. package/src/backend/DataTable.tsx +38 -20
  67. package/src/backend/FilterOverlay.tsx +35 -21
  68. package/src/backend/JsonBuilder.tsx +38 -20
  69. package/src/backend/__tests__/FieldDefinitionsEditor.test.tsx +23 -6
  70. package/src/backend/columns/ColumnChooserPanel.tsx +9 -10
  71. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +120 -87
  72. package/src/backend/date-range/DateRangeSelect.tsx +19 -12
  73. package/src/backend/date-range/InlineDateRangeSelect.tsx +16 -20
  74. package/src/backend/detail/ActivitiesSection.tsx +35 -23
  75. package/src/backend/detail/AddressEditor.tsx +30 -16
  76. package/src/backend/detail/InlineEditors.tsx +21 -11
  77. package/src/backend/detail/NotesSection.tsx +35 -25
  78. package/src/backend/filters/AdvancedFilterBuilder.tsx +60 -34
  79. package/src/backend/injection/InjectedField.tsx +21 -12
  80. package/src/backend/inputs/ComboboxInput.tsx +4 -0
  81. package/src/backend/inputs/EventSelect.tsx +30 -17
  82. package/src/backend/inputs/PhoneNumberField.tsx +2 -2
  83. package/src/backend/inputs/TimeInput.tsx +9 -10
  84. package/src/backend/messages/message-compose-form-groups.tsx +21 -12
  85. package/src/backend/messages/useMessageCompose.ts +20 -1
  86. package/src/frontend/LanguageSwitcher.tsx +20 -17
  87. package/src/index.ts +5 -0
  88. package/src/primitives/checkbox-field.tsx +10 -2
  89. package/src/primitives/input.tsx +73 -12
  90. package/src/primitives/radio-field.tsx +92 -0
  91. package/src/primitives/radio.tsx +42 -0
  92. package/src/primitives/select.tsx +200 -0
  93. package/src/primitives/switch-field.tsx +100 -0
  94. package/src/primitives/switch.tsx +17 -4
  95. package/src/primitives/textarea.tsx +67 -11
  96. package/src/primitives/tooltip.tsx +68 -24
@@ -2,24 +2,49 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5
+ import { cva } from "class-variance-authority";
5
6
  import { cn } from "@open-mercato/shared/lib/utils";
6
7
  const TooltipProvider = TooltipPrimitive.Provider;
7
8
  const Tooltip = TooltipPrimitive.Root;
8
9
  const TooltipTrigger = TooltipPrimitive.Trigger;
9
- const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsx(
10
+ const tooltipContentVariants = cva(
11
+ "z-tooltip overflow-hidden rounded-sm max-w-xs break-words shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
12
+ {
13
+ variants: {
14
+ variant: {
15
+ dark: "bg-foreground text-background",
16
+ light: "bg-popover text-popover-foreground border border-input"
17
+ },
18
+ size: {
19
+ sm: "px-1.5 py-0.5 text-xs leading-4",
20
+ default: "px-2 py-0.5 text-xs leading-4",
21
+ lg: "px-3 py-2 text-sm leading-5"
22
+ }
23
+ },
24
+ defaultVariants: {
25
+ variant: "dark",
26
+ size: "default"
27
+ }
28
+ }
29
+ );
30
+ const TooltipContent = React.forwardRef(({ className, sideOffset = 4, variant, size, arrow = true, children, ...props }, ref) => /* @__PURE__ */ jsx(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
10
31
  TooltipPrimitive.Content,
11
32
  {
12
33
  ref,
13
34
  sideOffset,
14
- className: cn(
15
- "z-tooltip overflow-hidden rounded-md bg-slate-900 px-3 py-1.5 text-xs text-slate-50 animate-in fade-in-0 zoom-in-95",
16
- "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
17
- "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
18
- "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
19
- "max-w-xs break-words",
20
- className
21
- ),
22
- ...props
35
+ className: cn(tooltipContentVariants({ variant, size }), className),
36
+ ...props,
37
+ children: [
38
+ children,
39
+ arrow ? /* @__PURE__ */ jsx(
40
+ TooltipPrimitive.Arrow,
41
+ {
42
+ width: 10,
43
+ height: 5,
44
+ className: cn(variant === "light" ? "fill-popover stroke-input" : "fill-foreground")
45
+ }
46
+ ) : null
47
+ ]
23
48
  }
24
49
  ) }));
25
50
  TooltipContent.displayName = TooltipPrimitive.Content.displayName;
@@ -31,13 +56,16 @@ function SimpleTooltip({
31
56
  align = "center",
32
57
  open,
33
58
  onOpenChange,
34
- disabled = false
59
+ disabled = false,
60
+ variant,
61
+ size,
62
+ arrow
35
63
  }) {
36
64
  const isDisabled = disabled || !content;
37
65
  if (isDisabled) {
38
66
  return /* @__PURE__ */ jsx(Fragment, { children });
39
67
  }
40
- return /* @__PURE__ */ jsxs(
68
+ return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration, children: /* @__PURE__ */ jsxs(
41
69
  Tooltip,
42
70
  {
43
71
  open,
@@ -45,16 +73,17 @@ function SimpleTooltip({
45
73
  delayDuration,
46
74
  children: [
47
75
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children }),
48
- /* @__PURE__ */ jsx(TooltipContent, { side, align, children: content })
76
+ /* @__PURE__ */ jsx(TooltipContent, { side, align, variant, size, arrow, children: content })
49
77
  ]
50
78
  }
51
- );
79
+ ) });
52
80
  }
53
81
  export {
54
82
  SimpleTooltip,
55
83
  Tooltip,
56
84
  TooltipContent,
57
85
  TooltipProvider,
58
- TooltipTrigger
86
+ TooltipTrigger,
87
+ tooltipContentVariants
59
88
  };
60
89
  //# sourceMappingURL=tooltip.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/tooltip.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport const TooltipProvider = TooltipPrimitive.Provider\n\nexport const Tooltip = TooltipPrimitive.Root\n\nexport const TooltipTrigger = TooltipPrimitive.Trigger\n\nexport const TooltipContent = React.forwardRef<\n React.ElementRef<typeof TooltipPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n 'z-tooltip overflow-hidden rounded-md bg-slate-900 px-3 py-1.5 text-xs text-slate-50 animate-in fade-in-0 zoom-in-95',\n 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',\n 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',\n 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n 'max-w-xs break-words',\n className\n )}\n {...props}\n />\n </TooltipPrimitive.Portal>\n))\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\nexport type TooltipProps = {\n content: React.ReactNode\n children: React.ReactNode\n delayDuration?: number\n side?: 'top' | 'right' | 'bottom' | 'left'\n align?: 'start' | 'center' | 'end'\n open?: boolean\n onOpenChange?: (open: boolean) => void\n disabled?: boolean\n}\n\n/**\n * Simple tooltip wrapper component for common use cases.\n *\n * @example\n * <SimpleTooltip content=\"Full text here\">\n * <span>Truncated...</span>\n * </SimpleTooltip>\n */\nexport function SimpleTooltip({\n content,\n children,\n delayDuration = 300,\n side = 'top',\n align = 'center',\n open,\n onOpenChange,\n disabled = false,\n}: TooltipProps) {\n // If disabled or no content, just render children without tooltip\n const isDisabled = disabled || !content\n\n if (isDisabled) {\n return <>{children}</>\n }\n\n return (\n <Tooltip\n open={open}\n onOpenChange={onOpenChange}\n delayDuration={delayDuration}\n >\n <TooltipTrigger asChild>\n {children}\n </TooltipTrigger>\n <TooltipContent side={side} align={align}>\n {content}\n </TooltipContent>\n </Tooltip>\n )\n}\n"],
5
- "mappings": ";AAiBI,SAkDO,UAlDP,KAsDA,YAtDA;AAfJ,YAAY,WAAW;AACvB,YAAY,sBAAsB;AAClC,SAAS,UAAU;AAEZ,MAAM,kBAAkB,iBAAiB;AAEzC,MAAM,UAAU,iBAAiB;AAEjC,MAAM,iBAAiB,iBAAiB;AAExC,MAAM,iBAAiB,MAAM,WAGlC,CAAC,EAAE,WAAW,aAAa,GAAG,GAAG,MAAM,GAAG,QAC1C,oBAAC,iBAAiB,QAAjB,EACC;AAAA,EAAC,iBAAiB;AAAA,EAAjB;AAAA,IACC;AAAA,IACA;AAAA,IACA,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACC,GAAG;AAAA;AACN,GACF,CACD;AACD,eAAe,cAAc,iBAAiB,QAAQ;AAqB/C,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAAiB;AAEf,QAAM,aAAa,YAAY,CAAC;AAEhC,MAAI,YAAY;AACd,WAAO,gCAAG,UAAS;AAAA,EACrB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MAEA;AAAA,4BAAC,kBAAe,SAAO,MACpB,UACH;AAAA,QACA,oBAAC,kBAAe,MAAY,OACzB,mBACH;AAAA;AAAA;AAAA,EACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport const TooltipProvider = TooltipPrimitive.Provider\n\nexport const Tooltip = TooltipPrimitive.Root\n\nexport const TooltipTrigger = TooltipPrimitive.Trigger\n\nconst tooltipContentVariants = cva(\n 'z-tooltip overflow-hidden rounded-sm max-w-xs break-words shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n {\n variants: {\n variant: {\n dark: 'bg-foreground text-background',\n light: 'bg-popover text-popover-foreground border border-input',\n },\n size: {\n sm: 'px-1.5 py-0.5 text-xs leading-4',\n default: 'px-2 py-0.5 text-xs leading-4',\n lg: 'px-3 py-2 text-sm leading-5',\n },\n },\n defaultVariants: {\n variant: 'dark',\n size: 'default',\n },\n }\n)\n\nexport type TooltipContentProps = React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> &\n VariantProps<typeof tooltipContentVariants> & {\n /** Show a small arrow pointing at the trigger. */\n arrow?: boolean\n }\n\nexport const TooltipContent = React.forwardRef<\n React.ElementRef<typeof TooltipPrimitive.Content>,\n TooltipContentProps\n>(({ className, sideOffset = 4, variant, size, arrow = true, children, ...props }, ref) => (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(tooltipContentVariants({ variant, size }), className)}\n {...props}\n >\n {children}\n {arrow ? (\n <TooltipPrimitive.Arrow\n width={10}\n height={5}\n className={cn(variant === 'light' ? 'fill-popover stroke-input' : 'fill-foreground')}\n />\n ) : null}\n </TooltipPrimitive.Content>\n </TooltipPrimitive.Portal>\n))\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\nexport type TooltipProps = {\n content: React.ReactNode\n children: React.ReactNode\n delayDuration?: number\n side?: 'top' | 'right' | 'bottom' | 'left'\n align?: 'start' | 'center' | 'end'\n open?: boolean\n onOpenChange?: (open: boolean) => void\n disabled?: boolean\n variant?: 'dark' | 'light'\n size?: 'sm' | 'default' | 'lg'\n arrow?: boolean\n}\n\n/**\n * Simple tooltip wrapper component for common use cases.\n *\n * @example\n * <SimpleTooltip content=\"Full text here\">\n * <span>Truncated...</span>\n * </SimpleTooltip>\n *\n * @example with arrow + light variant\n * <SimpleTooltip content=\"Help text\" variant=\"light\" arrow>\n * <InfoIcon />\n * </SimpleTooltip>\n */\nexport function SimpleTooltip({\n content,\n children,\n delayDuration = 300,\n side = 'top',\n align = 'center',\n open,\n onOpenChange,\n disabled = false,\n variant,\n size,\n arrow,\n}: TooltipProps) {\n const isDisabled = disabled || !content\n\n if (isDisabled) {\n return <>{children}</>\n }\n\n return (\n <TooltipProvider delayDuration={delayDuration}>\n <Tooltip\n open={open}\n onOpenChange={onOpenChange}\n delayDuration={delayDuration}\n >\n <TooltipTrigger asChild>\n {children}\n </TooltipTrigger>\n <TooltipContent side={side} align={align} variant={variant} size={size} arrow={arrow}>\n {content}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n )\n}\n\nexport { tooltipContentVariants }\n"],
5
+ "mappings": ";AA6CI,SA8DO,UAtDH,KARJ;AA3CJ,YAAY,WAAW;AACvB,YAAY,sBAAsB;AAClC,SAAS,WAA8B;AACvC,SAAS,UAAU;AAEZ,MAAM,kBAAkB,iBAAiB;AAEzC,MAAM,UAAU,iBAAiB;AAEjC,MAAM,iBAAiB,iBAAiB;AAE/C,MAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,MACA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,SAAS;AAAA,QACT,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAQO,MAAM,iBAAiB,MAAM,WAGlC,CAAC,EAAE,WAAW,aAAa,GAAG,SAAS,MAAM,QAAQ,MAAM,UAAU,GAAG,MAAM,GAAG,QACjF,oBAAC,iBAAiB,QAAjB,EACC;AAAA,EAAC,iBAAiB;AAAA,EAAjB;AAAA,IACC;AAAA,IACA;AAAA,IACA,WAAW,GAAG,uBAAuB,EAAE,SAAS,KAAK,CAAC,GAAG,SAAS;AAAA,IACjE,GAAG;AAAA,IAEH;AAAA;AAAA,MACA,QACC;AAAA,QAAC,iBAAiB;AAAA,QAAjB;AAAA,UACC,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,WAAW,GAAG,YAAY,UAAU,8BAA8B,iBAAiB;AAAA;AAAA,MACrF,IACE;AAAA;AAAA;AACN,GACF,CACD;AACD,eAAe,cAAc,iBAAiB,QAAQ;AA6B/C,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AACF,GAAiB;AACf,QAAM,aAAa,YAAY,CAAC;AAEhC,MAAI,YAAY;AACd,WAAO,gCAAG,UAAS;AAAA,EACrB;AAEA,SACE,oBAAC,mBAAgB,eACf;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MAEA;AAAA,4BAAC,kBAAe,SAAO,MACpB,UACH;AAAA,QACA,oBAAC,kBAAe,MAAY,OAAc,SAAkB,MAAY,OACrE,mBACH;AAAA;AAAA;AAAA,EACF,GACF;AAEJ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.5.1-develop.2953.6647bb2c43",
3
+ "version": "0.5.1-develop.2964.d5ac4a6ebb",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -124,6 +124,8 @@
124
124
  "@dnd-kit/sortable": "^10.0.0",
125
125
  "@dnd-kit/utilities": "^3.2.2",
126
126
  "@radix-ui/react-popover": "^1.1.6",
127
+ "@radix-ui/react-radio-group": "^1.2.3",
128
+ "@radix-ui/react-select": "^2.1.6",
127
129
  "@radix-ui/react-tooltip": "^1.2.8",
128
130
  "@tanstack/react-virtual": "^3.13.23",
129
131
  "date-fns": "^4.1.0",
@@ -132,12 +134,12 @@
132
134
  "recharts": "^3.8.1"
133
135
  },
134
136
  "peerDependencies": {
135
- "@open-mercato/shared": "0.5.1-develop.2953.6647bb2c43",
137
+ "@open-mercato/shared": "0.5.1-develop.2964.d5ac4a6ebb",
136
138
  "react": ">=18.0.0",
137
139
  "react-dom": ">=18.0.0"
138
140
  },
139
141
  "devDependencies": {
140
- "@open-mercato/shared": "0.5.1-develop.2953.6647bb2c43",
142
+ "@open-mercato/shared": "0.5.1-develop.2964.d5ac4a6ebb",
141
143
  "@testing-library/dom": "^10.4.1",
142
144
  "@testing-library/jest-dom": "^6.9.1",
143
145
  "@testing-library/react": "^16.3.1",
@@ -18,6 +18,15 @@ import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove }
18
18
  import { CSS } from '@dnd-kit/utilities'
19
19
  import { DataLoader } from '../primitives/DataLoader'
20
20
  import { Checkbox } from '../primitives/checkbox'
21
+ import { Input } from '../primitives/input'
22
+ import { Textarea } from '../primitives/textarea'
23
+ import {
24
+ Select,
25
+ SelectContent,
26
+ SelectItem,
27
+ SelectTrigger,
28
+ SelectValue,
29
+ } from '../primitives/select'
21
30
  import { flash } from './FlashMessages'
22
31
  import dynamic from 'next/dynamic'
23
32
  import { FormHeader } from './forms/FormHeader'
@@ -96,6 +105,10 @@ import { sanitizeHtmlRichText, sanitizeRichTextHref, sanitizeRichTextPasteConten
96
105
 
97
106
  // Stable empty options array to avoid creating a new [] every render
98
107
  const EMPTY_OPTIONS: CrudFieldOption[] = []
108
+ // Sentinel for the optional-Select clear affordance. Radix Select forbids
109
+ // empty-string item values, so we use a stable non-empty token that maps to
110
+ // `undefined` in the change handler.
111
+ const SELECT_CLEAR_SENTINEL = '__crudform_select_clear__'
99
112
  const FOCUSABLE_SELECTOR =
100
113
  '[data-crud-focus-target], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
101
114
  const CRUDFORM_EXTENDED_EVENTS_ENABLED = parseBooleanWithDefault(
@@ -203,6 +216,12 @@ export type CrudBuiltinField = CrudFieldBase & {
203
216
  suggestions?: string[]
204
217
  // for combobox fields; allow custom values or restrict to suggestions only
205
218
  allowCustomValues?: boolean
219
+ // for text/textarea fields; HTML maxLength + (textarea only) char counter when showCount=true
220
+ maxLength?: number
221
+ // for textarea fields; show character counter (requires maxLength)
222
+ showCount?: boolean
223
+ // for textarea fields; min height in rows
224
+ rows?: number
206
225
  // for datetime/time fields
207
226
  minuteStep?: number
208
227
  minDate?: Date
@@ -1918,6 +1937,18 @@ export function CrudForm<TValues extends Record<string, unknown>>({
1918
1937
 
1919
1938
  const form = document.getElementById(formId)
1920
1939
  if (!form) return
1940
+
1941
+ // Don't steal focus if the user is already typing inside the form. The auto-focus
1942
+ // is meant for "submit failed, jump to first invalid field" — not for "user is
1943
+ // editing one error field and our focus jumps to a different remaining error
1944
+ // each keystroke as the errors object shrinks". Keystrokes would otherwise land
1945
+ // in the wrong input.
1946
+ const active = document.activeElement
1947
+ if (active instanceof HTMLElement && form.contains(active)) {
1948
+ lastErrorFieldRef.current = fieldId
1949
+ return
1950
+ }
1951
+
1921
1952
  const container = form.querySelector<HTMLElement>(`[data-crud-field-id="${fieldId}"]`)
1922
1953
  const target =
1923
1954
  container?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ??
@@ -2681,22 +2712,25 @@ export function CrudForm<TValues extends Record<string, unknown>>({
2681
2712
  <label className="text-xs uppercase tracking-wide text-muted-foreground">
2682
2713
  {fieldsetSelectorLabel}
2683
2714
  </label>
2684
- <select
2685
- className="h-9 rounded border pl-3 pr-8 text-sm"
2686
- value={entityLayout.activeFieldset ?? ''}
2687
- onChange={(event) =>
2715
+ <Select
2716
+ value={entityLayout.activeFieldset || undefined}
2717
+ onValueChange={(value) =>
2688
2718
  handleFieldsetSelectionChange(
2689
2719
  entityLayout.entityId,
2690
- event.target.value || null,
2720
+ value || null,
2691
2721
  )}
2692
2722
  >
2693
- <option value="">{defaultFieldsetLabel}</option>
2694
- {entityLayout.availableFieldsets.map((fs) => (
2695
- <option key={fs.code} value={fs.code}>
2696
- {fs.label}
2697
- </option>
2698
- ))}
2699
- </select>
2723
+ <SelectTrigger className="w-auto min-w-[10rem]">
2724
+ <SelectValue placeholder={defaultFieldsetLabel} />
2725
+ </SelectTrigger>
2726
+ <SelectContent>
2727
+ {entityLayout.availableFieldsets.map((fs) => (
2728
+ <SelectItem key={fs.code} value={fs.code}>
2729
+ {fs.label}
2730
+ </SelectItem>
2731
+ ))}
2732
+ </SelectContent>
2733
+ </Select>
2700
2734
  <IconButton
2701
2735
  variant="outline"
2702
2736
  className="text-muted-foreground hover:text-foreground"
@@ -3250,9 +3284,8 @@ function RelationSelect({
3250
3284
 
3251
3285
  return (
3252
3286
  <div className="space-y-1">
3253
- <input
3287
+ <Input
3254
3288
  ref={inputRef}
3255
- className="w-full h-9 rounded border px-2 text-sm"
3256
3289
  placeholder={placeholder || t('ui.forms.listbox.searchPlaceholder', 'Search...')}
3257
3290
  value={query}
3258
3291
  onChange={(e) => setQuery(e.target.value)}
@@ -3350,9 +3383,8 @@ function TextInput({
3350
3383
 
3351
3384
  return (
3352
3385
  <>
3353
- <input
3386
+ <Input
3354
3387
  type={inputType}
3355
- className="w-full h-9 rounded border px-2 text-sm"
3356
3388
  placeholder={placeholder}
3357
3389
  value={local}
3358
3390
  onChange={handleChange}
@@ -3431,9 +3463,8 @@ function NumberInput({
3431
3463
  }, [commitIfChanged])
3432
3464
 
3433
3465
  return (
3434
- <input
3466
+ <Input
3435
3467
  type="number"
3436
- className="w-full h-9 rounded border px-2 text-sm"
3437
3468
  placeholder={placeholder}
3438
3469
  value={local}
3439
3470
  onChange={handleChange}
@@ -3452,11 +3483,19 @@ function TextAreaInput({
3452
3483
  onChange,
3453
3484
  placeholder,
3454
3485
  autoFocus,
3486
+ maxLength,
3487
+ showCount,
3488
+ rows,
3489
+ disabled,
3455
3490
  }: {
3456
3491
  value: string
3457
3492
  onChange: (v: string) => void
3458
3493
  placeholder?: string
3459
3494
  autoFocus?: boolean
3495
+ maxLength?: number
3496
+ showCount?: boolean
3497
+ rows?: number
3498
+ disabled?: boolean
3460
3499
  }) {
3461
3500
  const [local, setLocal] = React.useState<string>(value)
3462
3501
  const isFocusedRef = React.useRef(false)
@@ -3482,14 +3521,17 @@ function TextAreaInput({
3482
3521
  }, [commitIfChanged])
3483
3522
 
3484
3523
  return (
3485
- <textarea
3486
- className="w-full rounded border px-2 py-2 min-h-[80px] sm:min-h-[120px] text-sm"
3524
+ <Textarea
3487
3525
  placeholder={placeholder}
3488
3526
  value={local}
3489
3527
  onChange={handleChange}
3490
3528
  onFocus={handleFocus}
3491
3529
  onBlur={handleBlur}
3492
3530
  autoFocus={autoFocus}
3531
+ maxLength={maxLength}
3532
+ showCount={showCount}
3533
+ rows={rows}
3534
+ disabled={disabled}
3493
3535
  data-crud-focus-target=""
3494
3536
  />
3495
3537
  )
@@ -3791,8 +3833,9 @@ const ListboxMultiSelect = React.memo(function ListboxMultiSelect({
3791
3833
  )
3792
3834
  return (
3793
3835
  <div className="w-full">
3794
- <input
3795
- className="mb-2 w-full h-8 rounded border px-2 text-sm"
3836
+ <Input
3837
+ className="mb-2"
3838
+ size="sm"
3796
3839
  placeholder={searchPlaceholder}
3797
3840
  value={query}
3798
3841
  onChange={(e) => setQuery(e.target.value)}
@@ -3914,9 +3957,8 @@ const FieldControl = React.memo(function FieldControlImpl({
3914
3957
  />
3915
3958
  )}
3916
3959
  {field.type === 'date' && (
3917
- <input
3960
+ <Input
3918
3961
  type="date"
3919
- className="w-full h-9 rounded border px-2 text-sm"
3920
3962
  value={typeof value === 'string' ? value : ''}
3921
3963
  onChange={(e) => setValue(field.id, e.target.value || undefined)}
3922
3964
  autoFocus={autoFocusField}
@@ -3925,9 +3967,8 @@ const FieldControl = React.memo(function FieldControlImpl({
3925
3967
  />
3926
3968
  )}
3927
3969
  {field.type === 'datetime-local' && (
3928
- <input
3970
+ <Input
3929
3971
  type="datetime-local"
3930
- className="w-full h-9 rounded border px-2 text-sm"
3931
3972
  value={typeof value === 'string' ? value : ''}
3932
3973
  onChange={(e) => setValue(field.id, e.target.value || undefined)}
3933
3974
  autoFocus={autoFocusField}
@@ -3979,6 +4020,10 @@ const FieldControl = React.memo(function FieldControlImpl({
3979
4020
  placeholder={placeholder}
3980
4021
  onChange={(next) => fieldSetValue(next)}
3981
4022
  autoFocus={autoFocusField}
4023
+ maxLength={builtin?.maxLength}
4024
+ showCount={builtin?.showCount}
4025
+ rows={builtin?.rows}
4026
+ disabled={disabled}
3982
4027
  />
3983
4028
  )}
3984
4029
  {field.type === 'richtext' && builtin?.editor === 'simple' && (
@@ -4042,8 +4087,13 @@ const FieldControl = React.memo(function FieldControlImpl({
4042
4087
  </label>
4043
4088
  )}
4044
4089
  {field.type === 'select' && !builtin?.multiple && (
4045
- <select
4046
- className="w-full h-9 rounded border pl-3 pr-8 text-sm"
4090
+ <Select
4091
+ // Radix Select MUST be either always-controlled or always-uncontrolled.
4092
+ // Passing `value={undefined}` on first render and a string later trips
4093
+ // React's "uncontrolled → controlled" warning and breaks Radix's
4094
+ // internal state (dropdown flashes / selections no-op). Use empty
4095
+ // string for "no selection" instead — Radix treats it the same as
4096
+ // undefined for matching SelectItems but keeps the prop type stable.
4047
4097
  value={
4048
4098
  Array.isArray(value)
4049
4099
  ? String(value[0] ?? '')
@@ -4051,17 +4101,34 @@ const FieldControl = React.memo(function FieldControlImpl({
4051
4101
  ? ''
4052
4102
  : String(value)
4053
4103
  }
4054
- onChange={(e) => setValue(field.id, e.target.value || undefined)}
4055
- data-crud-focus-target=""
4104
+ onValueChange={(next) => {
4105
+ // Sentinel maps back to undefined so optional selects can be cleared.
4106
+ if (!next || next === SELECT_CLEAR_SENTINEL) {
4107
+ setValue(field.id, undefined)
4108
+ return
4109
+ }
4110
+ setValue(field.id, next)
4111
+ }}
4056
4112
  disabled={disabled}
4057
4113
  >
4058
- <option value="">{t('ui.forms.select.emptyOption', '—')}</option>
4059
- {options.map((opt) => (
4060
- <option key={opt.value} value={opt.value}>
4061
- {opt.label}
4062
- </option>
4063
- ))}
4064
- </select>
4114
+ <SelectTrigger data-crud-focus-target="">
4115
+ <SelectValue placeholder={t('ui.forms.select.emptyOption', '—')} />
4116
+ </SelectTrigger>
4117
+ <SelectContent>
4118
+ {!field.required && value != null && value !== '' && (
4119
+ <SelectItem value={SELECT_CLEAR_SENTINEL}>
4120
+ {t('ui.forms.select.clearOption', '— Clear —')}
4121
+ </SelectItem>
4122
+ )}
4123
+ {options
4124
+ .filter((opt) => opt.value !== '')
4125
+ .map((opt) => (
4126
+ <SelectItem key={opt.value} value={opt.value}>
4127
+ {opt.label}
4128
+ </SelectItem>
4129
+ ))}
4130
+ </SelectContent>
4131
+ </Select>
4065
4132
  )}
4066
4133
  {field.type === 'select' && builtin?.multiple && builtin.listbox === true && (
4067
4134
  <ListboxMultiSelect
@@ -7,6 +7,13 @@ import { RefreshCw, Loader2, SlidersHorizontal, MoreHorizontal, Circle, Filter,
7
7
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../primitives/table'
8
8
  import { Button } from '../primitives/button'
9
9
  import { Checkbox } from '../primitives/checkbox'
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from '../primitives/select'
10
17
  import { Spinner } from '../primitives/spinner'
11
18
  import { TooltipProvider } from '../primitives/tooltip'
12
19
  import { TruncatedCell } from './TruncatedCell'
@@ -1760,19 +1767,26 @@ export function DataTable<T>({
1760
1767
  : []
1761
1768
  const pageSizeSelect = pageSizeOptions.length > 0 && pagination.onPageSizeChange ? (
1762
1769
  <span className="inline-flex items-center gap-1.5">
1763
- <select
1764
- className="rounded border bg-background pl-2 pr-7 py-0.5 text-sm min-w-[3.5rem]"
1765
- value={pagination.pageSize}
1766
- onChange={(event) => {
1767
- pagination.onPageSizeChange!(Number(event.target.value))
1770
+ <Select
1771
+ value={String(pagination.pageSize)}
1772
+ onValueChange={(value) => {
1773
+ pagination.onPageSizeChange!(Number(value))
1768
1774
  scrollTableIntoView()
1769
1775
  }}
1770
- aria-label={t('ui.dataTable.pagination.rowsPerPage', 'Rows per page')}
1771
1776
  >
1772
- {pageSizeOptions.map((size) => (
1773
- <option key={size} value={size}>{size}</option>
1774
- ))}
1775
- </select>
1777
+ <SelectTrigger
1778
+ size="sm"
1779
+ className="min-w-[4rem]"
1780
+ aria-label={t('ui.dataTable.pagination.rowsPerPage', 'Rows per page')}
1781
+ >
1782
+ <SelectValue />
1783
+ </SelectTrigger>
1784
+ <SelectContent>
1785
+ {pageSizeOptions.map((size) => (
1786
+ <SelectItem key={size} value={String(size)}>{size}</SelectItem>
1787
+ ))}
1788
+ </SelectContent>
1789
+ </Select>
1776
1790
  <span className="text-muted-foreground">{t('ui.dataTable.pagination.perPage', 'per page')}</span>
1777
1791
  </span>
1778
1792
  ) : null
@@ -2105,17 +2119,21 @@ export function DataTable<T>({
2105
2119
  <div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
2106
2120
  {t('ui.dataTable.fieldset.label', 'Fieldset')}
2107
2121
  </div>
2108
- <select
2109
- className="w-full rounded border bg-background px-2 py-2 text-sm"
2110
- value={activeCustomFieldFilterFieldset ?? ''}
2111
- onChange={(event) => handleCustomFieldFilterFieldsetChange(event.target.value)}
2122
+ <Select
2123
+ value={activeCustomFieldFilterFieldset || undefined}
2124
+ onValueChange={(value) => handleCustomFieldFilterFieldsetChange(value)}
2112
2125
  >
2113
- {(cfFilterFieldsetsByEntity[resolvedEntityIds[0]] ?? []).map((fieldset) => (
2114
- <option key={fieldset.code} value={fieldset.code}>
2115
- {fieldset.label}
2116
- </option>
2117
- ))}
2118
- </select>
2126
+ <SelectTrigger>
2127
+ <SelectValue />
2128
+ </SelectTrigger>
2129
+ <SelectContent>
2130
+ {(cfFilterFieldsetsByEntity[resolvedEntityIds[0]] ?? []).map((fieldset) => (
2131
+ <SelectItem key={fieldset.code} value={fieldset.code}>
2132
+ {fieldset.label}
2133
+ </SelectItem>
2134
+ ))}
2135
+ </SelectContent>
2136
+ </Select>
2119
2137
  </div>
2120
2138
  )
2121
2139
  : null
@@ -2,6 +2,13 @@
2
2
  import * as React from 'react'
3
3
  import { Button } from '../primitives/button'
4
4
  import { Checkbox } from '../primitives/checkbox'
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from '../primitives/select'
5
12
  import { ComboboxInput } from './inputs/ComboboxInput'
6
13
  import { TagsInput, type TagsInputOption } from './inputs/TagsInput'
7
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -266,16 +273,21 @@ export function FilterOverlay({
266
273
  })}
267
274
  </div>
268
275
  ) : (
269
- <select
270
- className="w-full h-11 rounded border px-2 text-sm"
271
- value={values[f.id] ?? ''}
272
- onChange={(e) => setValue(f.id, e.target.value || undefined)}
276
+ <Select
277
+ value={values[f.id] || undefined}
278
+ onValueChange={(next) => setValue(f.id, next || undefined)}
273
279
  >
274
- <option value="">{t('ui.forms.select.emptyOption', '—')}</option>
275
- {(f.options || dynamicOptions[f.id] || []).map((opt) => (
276
- <option key={opt.value} value={opt.value}>{opt.label}</option>
277
- ))}
278
- </select>
280
+ <SelectTrigger size="lg">
281
+ <SelectValue placeholder={t('ui.forms.select.emptyOption', '—')} />
282
+ </SelectTrigger>
283
+ <SelectContent>
284
+ {(f.options || dynamicOptions[f.id] || [])
285
+ .filter((opt) => opt.value !== '')
286
+ .map((opt) => (
287
+ <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
288
+ ))}
289
+ </SelectContent>
290
+ </Select>
279
291
  )}
280
292
  </div>
281
293
  )}
@@ -354,20 +366,22 @@ export function FilterOverlay({
354
366
  })()}
355
367
  {f.type === 'checkbox' && (
356
368
  <div>
357
- <select
358
- className="w-full h-11 rounded border px-2 text-sm"
359
- value={values[f.id] === true ? 'true' : values[f.id] === false ? 'false' : ''}
360
- onChange={(e) => {
361
- const v = e.target.value
362
- if (v === '') setValue(f.id, undefined)
363
- else if (v === 'true') setValue(f.id, true)
364
- else if (v === 'false') setValue(f.id, false)
369
+ <Select
370
+ value={values[f.id] === true ? 'true' : values[f.id] === false ? 'false' : undefined}
371
+ onValueChange={(next) => {
372
+ if (!next) setValue(f.id, undefined)
373
+ else if (next === 'true') setValue(f.id, true)
374
+ else if (next === 'false') setValue(f.id, false)
365
375
  }}
366
376
  >
367
- <option value="">{t('ui.forms.select.emptyOption', '—')}</option>
368
- <option value="true">{t('common.yes', 'Yes')}</option>
369
- <option value="false">{t('common.no', 'No')}</option>
370
- </select>
377
+ <SelectTrigger size="lg">
378
+ <SelectValue placeholder={t('ui.forms.select.emptyOption', '')} />
379
+ </SelectTrigger>
380
+ <SelectContent>
381
+ <SelectItem value="true">{t('common.yes', 'Yes')}</SelectItem>
382
+ <SelectItem value="false">{t('common.no', 'No')}</SelectItem>
383
+ </SelectContent>
384
+ </Select>
371
385
  </div>
372
386
  )}
373
387
  </div>