@open-mercato/ui 0.6.4-develop.4382.1.6b4f656b77 → 0.6.4
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/.turbo/turbo-build.log +2 -2
- package/AGENTS.md +2 -1
- package/dist/backend/AppShell.js +6 -0
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +212 -85
- package/dist/backend/CrudForm.js.map +3 -3
- package/dist/backend/DataTable.js +77 -26
- package/dist/backend/DataTable.js.map +3 -3
- package/dist/backend/FlashMessages.js +21 -2
- package/dist/backend/FlashMessages.js.map +2 -2
- package/dist/backend/JsonBuilder.js +64 -41
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/PerspectiveSidebar.js +8 -1
- package/dist/backend/PerspectiveSidebar.js.map +2 -2
- package/dist/backend/RowActions.js +1 -1
- package/dist/backend/RowActions.js.map +2 -2
- package/dist/backend/conflicts/RecordConflictBanner.js +73 -0
- package/dist/backend/conflicts/RecordConflictBanner.js.map +7 -0
- package/dist/backend/conflicts/index.js +32 -0
- package/dist/backend/conflicts/index.js.map +7 -0
- package/dist/backend/conflicts/store.js +50 -0
- package/dist/backend/conflicts/store.js.map +7 -0
- package/dist/backend/detail/ActivitiesSection.js +1 -0
- package/dist/backend/detail/ActivitiesSection.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +2 -51
- package/dist/backend/detail/InlineEditors.js.map +3 -3
- package/dist/backend/detail/RecordNotFoundState.js +10 -5
- package/dist/backend/detail/RecordNotFoundState.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +14 -0
- package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
- package/dist/backend/filters/FilteredEmptyResults.js +12 -4
- package/dist/backend/filters/FilteredEmptyResults.js.map +2 -2
- package/dist/backend/filters/ListEmptyState.js +44 -0
- package/dist/backend/filters/ListEmptyState.js.map +7 -0
- package/dist/backend/filters/SearchEmptyResults.js +28 -0
- package/dist/backend/filters/SearchEmptyResults.js.map +7 -0
- package/dist/backend/icons/lucideRegistry.generated.js +8 -0
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/injection/useGuardedMutation.js +8 -1
- package/dist/backend/injection/useGuardedMutation.js.map +2 -2
- package/dist/backend/inputs/MarkdownField.js +20 -0
- package/dist/backend/inputs/MarkdownField.js.map +7 -0
- package/dist/backend/inputs/MdxEditorImpl.js +98 -0
- package/dist/backend/inputs/MdxEditorImpl.js.map +7 -0
- package/dist/backend/inputs/SwitchableMarkdownInput.js +6 -38
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
- package/dist/backend/markdown/markdownToPlainText.js +29 -0
- package/dist/backend/markdown/markdownToPlainText.js.map +2 -2
- package/dist/backend/messages/EmailThreadsPanel.js +212 -0
- package/dist/backend/messages/EmailThreadsPanel.js.map +7 -0
- package/dist/backend/messages/index.js +4 -0
- package/dist/backend/messages/index.js.map +2 -2
- package/dist/backend/messages/mergeOptimisticEmailThreads.js +55 -0
- package/dist/backend/messages/mergeOptimisticEmailThreads.js.map +7 -0
- package/dist/backend/notifications/NotificationDispatcher.js +1 -0
- package/dist/backend/notifications/NotificationDispatcher.js.map +2 -2
- package/dist/backend/notifications/useNotificationsPoll.js +7 -0
- package/dist/backend/notifications/useNotificationsPoll.js.map +2 -2
- package/dist/backend/notifications/useNotificationsSse.js +8 -0
- package/dist/backend/notifications/useNotificationsSse.js.map +2 -2
- package/dist/backend/sidebar/SidebarCustomizationEditor.js +23 -7
- package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
- package/dist/backend/utils/optimisticLock.js +34 -0
- package/dist/backend/utils/optimisticLock.js.map +7 -0
- package/dist/portal/hooks/useTenantContext.js +1 -1
- package/dist/portal/hooks/useTenantContext.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/dist/types/css.d.js +1 -0
- package/dist/types/css.d.js.map +7 -0
- package/package.json +17 -13
- package/src/backend/AppShell.tsx +10 -0
- package/src/backend/CrudForm.tsx +320 -97
- package/src/backend/DataTable.tsx +113 -34
- package/src/backend/FlashMessages.tsx +31 -2
- package/src/backend/JsonBuilder.tsx +79 -26
- package/src/backend/PerspectiveSidebar.tsx +8 -1
- package/src/backend/RowActions.tsx +1 -1
- package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.datetime.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.defaults.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.dotPath.test.tsx +311 -0
- package/src/backend/__tests__/CrudForm.doubleSubmit.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.navigation.test.tsx +120 -1
- package/src/backend/__tests__/CrudForm.optimisticLock.test.tsx +287 -0
- package/src/backend/__tests__/CrudForm.render.test.tsx +74 -2
- package/src/backend/__tests__/CrudForm.sortable.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.transformData.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.unsavedNavigation.test.tsx +51 -3
- package/src/backend/__tests__/CrudForm.validation.test.tsx +0 -1
- package/src/backend/__tests__/CustomDataSection.test.tsx +0 -1
- package/src/backend/__tests__/FlashMessages.test.tsx +47 -0
- package/src/backend/__tests__/JsonBuilder.test.tsx +135 -0
- package/src/backend/__tests__/lazy-heavy-libraries.test.ts +16 -3
- package/src/backend/conflicts/RecordConflictBanner.tsx +77 -0
- package/src/backend/conflicts/__tests__/RecordConflictBanner.test.tsx +61 -0
- package/src/backend/conflicts/__tests__/store.test.ts +78 -0
- package/src/backend/conflicts/index.ts +49 -0
- package/src/backend/conflicts/store.ts +94 -0
- package/src/backend/detail/ActivitiesSection.tsx +1 -0
- package/src/backend/detail/InlineEditors.tsx +14 -76
- package/src/backend/detail/RecordNotFoundState.tsx +13 -6
- package/src/backend/detail/__tests__/RecordNotFoundState.test.tsx +80 -0
- package/src/backend/filters/AdvancedFilterPanel.tsx +27 -0
- package/src/backend/filters/FilteredEmptyResults.tsx +23 -10
- package/src/backend/filters/ListEmptyState.tsx +69 -0
- package/src/backend/filters/SearchEmptyResults.tsx +42 -0
- package/src/backend/filters/__tests__/AdvancedFilterPanel.test.tsx +98 -0
- package/src/backend/filters/__tests__/ListEmptyState.test.tsx +53 -0
- package/src/backend/icons/lucideRegistry.generated.tsx +8 -0
- package/src/backend/injection/__tests__/useGuardedMutation.optimisticLock.test.tsx +118 -0
- package/src/backend/injection/useGuardedMutation.ts +16 -1
- package/src/backend/inputs/MarkdownField.tsx +34 -0
- package/src/backend/inputs/MdxEditorImpl.tsx +113 -0
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +5 -54
- package/src/backend/markdown/__tests__/markdownToPlainText.test.ts +13 -0
- package/src/backend/markdown/markdownToPlainText.ts +41 -0
- package/src/backend/messages/EmailThreadsPanel.tsx +351 -0
- package/src/backend/messages/__tests__/mergeOptimisticEmailThreads.test.ts +126 -0
- package/src/backend/messages/index.ts +12 -0
- package/src/backend/messages/mergeOptimisticEmailThreads.ts +84 -0
- package/src/backend/notifications/NotificationDispatcher.ts +2 -0
- package/src/backend/notifications/useNotificationsPoll.ts +5 -0
- package/src/backend/notifications/useNotificationsSse.ts +6 -0
- package/src/backend/sidebar/SidebarCustomizationEditor.tsx +35 -11
- package/src/backend/utils/__tests__/optimisticLock.test.ts +126 -0
- package/src/backend/utils/optimisticLock.ts +70 -0
- package/src/portal/hooks/useTenantContext.ts +7 -2
- package/src/primitives/__tests__/checkbox.test.tsx +28 -0
- package/src/primitives/alert.tsx +1 -1
- package/src/primitives/checkbox.tsx +2 -1
- package/src/types/css.d.ts +5 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { SearchX, X } from "lucide-react";
|
|
4
|
+
import { Button } from "../../primitives/button.js";
|
|
5
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
|
+
function SearchEmptyResults({ query, entityNamePlural, onClearSearch }) {
|
|
7
|
+
const t = useT();
|
|
8
|
+
const entity = entityNamePlural ?? t("ui.dataTable.search.empty.genericEntity", "results");
|
|
9
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex w-full max-w-md flex-col items-center gap-4 py-10 text-center", "data-testid": "search-empty-results", children: [
|
|
10
|
+
/* @__PURE__ */ jsx("div", { className: "flex size-16 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(SearchX, { className: "size-7 text-muted-foreground", "aria-hidden": true }) }),
|
|
11
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1", children: [
|
|
12
|
+
/* @__PURE__ */ jsx("div", { className: "text-base font-semibold text-foreground", children: t("ui.dataTable.search.empty.title", "No results found") }),
|
|
13
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t(
|
|
14
|
+
"ui.dataTable.search.empty.description",
|
|
15
|
+
"We couldn\u2019t find any {entity} matching \u201C{query}\u201D. Try adjusting your search or filters.",
|
|
16
|
+
{ entity, query }
|
|
17
|
+
) })
|
|
18
|
+
] }),
|
|
19
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", onClick: onClearSearch, children: [
|
|
20
|
+
/* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": true }),
|
|
21
|
+
t("ui.dataTable.search.empty.clear", "Clear search")
|
|
22
|
+
] })
|
|
23
|
+
] });
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
SearchEmptyResults
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=SearchEmptyResults.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/filters/SearchEmptyResults.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { SearchX, X } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type SearchEmptyResultsProps = {\n /** The active search query, shown back to the user. */\n query: string\n /** Plural entity label (e.g. \"companies\"); falls back to a generic word. */\n entityNamePlural?: string\n /** Clears the active search. */\n onClearSearch: () => void\n}\n\nexport function SearchEmptyResults({ query, entityNamePlural, onClearSearch }: SearchEmptyResultsProps) {\n const t = useT()\n const entity = entityNamePlural ?? t('ui.dataTable.search.empty.genericEntity', 'results')\n return (\n <div className=\"flex w-full max-w-md flex-col items-center gap-4 py-10 text-center\" data-testid=\"search-empty-results\">\n <div className=\"flex size-16 items-center justify-center rounded-full bg-muted\">\n <SearchX className=\"size-7 text-muted-foreground\" aria-hidden />\n </div>\n <div className=\"flex flex-col items-center gap-1\">\n <div className=\"text-base font-semibold text-foreground\">\n {t('ui.dataTable.search.empty.title', 'No results found')}\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'ui.dataTable.search.empty.description',\n 'We couldn\u2019t find any {entity} matching \u201C{query}\u201D. Try adjusting your search or filters.',\n { entity, query },\n )}\n </p>\n </div>\n <Button type=\"button\" onClick={onClearSearch}>\n <X className=\"size-4\" aria-hidden />\n {t('ui.dataTable.search.empty.clear', 'Clear search')}\n </Button>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAqBQ,cAEF,YAFE;AAnBR,SAAS,SAAS,SAAS;AAC3B,SAAS,cAAc;AACvB,SAAS,YAAY;AAWd,SAAS,mBAAmB,EAAE,OAAO,kBAAkB,cAAc,GAA4B;AACtG,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,oBAAoB,EAAE,2CAA2C,SAAS;AACzF,SACE,qBAAC,SAAI,WAAU,sEAAqE,eAAY,wBAC9F;AAAA,wBAAC,SAAI,WAAU,kEACb,8BAAC,WAAQ,WAAU,gCAA+B,eAAW,MAAC,GAChE;AAAA,IACA,qBAAC,SAAI,WAAU,oCACb;AAAA,0BAAC,SAAI,WAAU,2CACZ,YAAE,mCAAmC,kBAAkB,GAC1D;AAAA,MACA,oBAAC,OAAE,WAAU,iCACV;AAAA,QACC;AAAA,QACA;AAAA,QACA,EAAE,QAAQ,MAAM;AAAA,MAClB,GACF;AAAA,OACF;AAAA,IACA,qBAAC,UAAO,MAAK,UAAS,SAAS,eAC7B;AAAA,0BAAC,KAAE,WAAU,UAAS,eAAW,MAAC;AAAA,MACjC,EAAE,mCAAmC,cAAc;AAAA,OACtD;AAAA,KACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
BarChart3,
|
|
14
14
|
Bell,
|
|
15
15
|
Bolt,
|
|
16
|
+
Book,
|
|
16
17
|
Bookmark,
|
|
17
18
|
Box,
|
|
18
19
|
Briefcase,
|
|
@@ -78,6 +79,7 @@ import {
|
|
|
78
79
|
Mail,
|
|
79
80
|
MailOpen,
|
|
80
81
|
MapPin,
|
|
82
|
+
MessageCircle,
|
|
81
83
|
MinusCircle,
|
|
82
84
|
Notebook,
|
|
83
85
|
Package,
|
|
@@ -93,6 +95,7 @@ import {
|
|
|
93
95
|
Receipt,
|
|
94
96
|
ReceiptText,
|
|
95
97
|
RefreshCcw,
|
|
98
|
+
RefreshCw,
|
|
96
99
|
Reply,
|
|
97
100
|
RotateCcw,
|
|
98
101
|
Ruler,
|
|
@@ -134,6 +137,7 @@ import {
|
|
|
134
137
|
UserX,
|
|
135
138
|
Wallet,
|
|
136
139
|
Webhook,
|
|
140
|
+
Workflow,
|
|
137
141
|
Wrench,
|
|
138
142
|
X,
|
|
139
143
|
XCircle
|
|
@@ -152,6 +156,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
152
156
|
"bar-chart-3": BarChart3,
|
|
153
157
|
"bell": Bell,
|
|
154
158
|
"bolt": Bolt,
|
|
159
|
+
"book": Book,
|
|
155
160
|
"bookmark": Bookmark,
|
|
156
161
|
"box": Box,
|
|
157
162
|
"briefcase": Briefcase,
|
|
@@ -217,6 +222,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
217
222
|
"mail": Mail,
|
|
218
223
|
"mail-open": MailOpen,
|
|
219
224
|
"map-pin": MapPin,
|
|
225
|
+
"message-circle": MessageCircle,
|
|
220
226
|
"minus-circle": MinusCircle,
|
|
221
227
|
"notebook": Notebook,
|
|
222
228
|
"package": Package,
|
|
@@ -232,6 +238,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
232
238
|
"receipt": Receipt,
|
|
233
239
|
"receipt-text": ReceiptText,
|
|
234
240
|
"refresh-ccw": RefreshCcw,
|
|
241
|
+
"refresh-cw": RefreshCw,
|
|
235
242
|
"reply": Reply,
|
|
236
243
|
"rotate-ccw": RotateCcw,
|
|
237
244
|
"ruler": Ruler,
|
|
@@ -273,6 +280,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
273
280
|
"users": Users,
|
|
274
281
|
"wallet": Wallet,
|
|
275
282
|
"webhook": Webhook,
|
|
283
|
+
"workflow": Workflow,
|
|
276
284
|
"wrench": Wrench,
|
|
277
285
|
"x": X,
|
|
278
286
|
"x-circle": XCircle
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/icons/lucideRegistry.generated.tsx"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Activity,\n AlertCircle,\n AlertOctagon,\n AlertTriangle,\n Archive,\n Award,\n BadgeCheck,\n Ban,\n Banknote,\n BarChart2,\n BarChart3,\n Bell,\n Bolt,\n Bookmark,\n Box,\n Briefcase,\n BriefcaseBusiness,\n Building,\n Building2,\n Calendar,\n CalendarCheck,\n CalendarClock,\n CalendarCog,\n CalendarMinus,\n CalendarOff,\n CalendarX,\n Check,\n CheckCircle,\n CheckCircle2,\n CheckSquare,\n Circle,\n ClipboardCheck,\n ClipboardList,\n Clock,\n Clock3,\n Coins,\n Copy,\n CreditCard,\n Crown,\n Database,\n DollarSign,\n Download,\n ExternalLink,\n Eye,\n FileMinus,\n FilePenLine,\n FileSpreadsheet,\n FileText,\n FilterX,\n Flag,\n Flame,\n FolderTree,\n Gauge,\n GitBranch,\n GitCompareArrows,\n GitPullRequestArrow,\n Globe,\n GraduationCap,\n Hand,\n Handshake,\n Headphones,\n Heart,\n HelpCircle,\n Hourglass,\n Inbox,\n Key,\n KeyRound,\n Layers,\n Lightbulb,\n LineChart,\n Link,\n List,\n Loader,\n Loader2,\n Lock,\n Mail,\n MailOpen,\n MapPin,\n MinusCircle,\n Notebook,\n Package,\n PackageCheck,\n PackagePlus,\n PackageX,\n PauseCircle,\n Percent,\n Phone,\n PhoneCall,\n PieChart,\n PlusSquare,\n Receipt,\n ReceiptText,\n RefreshCcw,\n Reply,\n RotateCcw,\n Ruler,\n Send,\n Settings,\n Shapes,\n Share2,\n Shield,\n ShieldAlert,\n ShieldCheck,\n ShoppingBag,\n ShoppingCart,\n Shuffle,\n Sliders,\n Smartphone,\n Snowflake,\n Sparkles,\n Star,\n StickyNote,\n Store,\n Tag,\n Target,\n ThumbsUp,\n Ticket,\n Timer,\n Trash2,\n TrendingUp,\n TriangleAlert,\n Trophy,\n Truck,\n Undo2,\n Unlock,\n User,\n UserCheck,\n UserMinus,\n UserPlus,\n UserRound,\n Users,\n UserX,\n Wallet,\n Webhook,\n Wrench,\n X,\n XCircle,\n} from 'lucide-react'\n\nexport const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {\n 'activity': Activity,\n 'alert-circle': AlertCircle,\n 'alert-octagon': AlertOctagon,\n 'alert-triangle': AlertTriangle,\n 'archive': Archive,\n 'award': Award,\n 'badge-check': BadgeCheck,\n 'ban': Ban,\n 'banknote': Banknote,\n 'bar-chart-2': BarChart2,\n 'bar-chart-3': BarChart3,\n 'bell': Bell,\n 'bolt': Bolt,\n 'bookmark': Bookmark,\n 'box': Box,\n 'briefcase': Briefcase,\n 'briefcase-business': BriefcaseBusiness,\n 'building': Building,\n 'building2': Building2,\n 'calendar': Calendar,\n 'calendar-check': CalendarCheck,\n 'calendar-clock': CalendarClock,\n 'calendar-cog': CalendarCog,\n 'calendar-minus': CalendarMinus,\n 'calendar-off': CalendarOff,\n 'calendar-x': CalendarX,\n 'check': Check,\n 'check-circle': CheckCircle,\n 'check-circle-2': CheckCircle2,\n 'check-square': CheckSquare,\n 'circle': Circle,\n 'clipboard-check': ClipboardCheck,\n 'clipboard-list': ClipboardList,\n 'clock': Clock,\n 'clock-3': Clock3,\n 'coins': Coins,\n 'copy': Copy,\n 'credit-card': CreditCard,\n 'crown': Crown,\n 'database': Database,\n 'dollar-sign': DollarSign,\n 'download': Download,\n 'external-link': ExternalLink,\n 'eye': Eye,\n 'file-minus': FileMinus,\n 'file-pen-line': FilePenLine,\n 'file-spreadsheet': FileSpreadsheet,\n 'file-text': FileText,\n 'filter-x': FilterX,\n 'flag': Flag,\n 'flame': Flame,\n 'folder-tree': FolderTree,\n 'gauge': Gauge,\n 'git-branch': GitBranch,\n 'git-compare-arrows': GitCompareArrows,\n 'git-pull-request-arrow': GitPullRequestArrow,\n 'globe': Globe,\n 'graduation-cap': GraduationCap,\n 'hand': Hand,\n 'handshake': Handshake,\n 'headphones': Headphones,\n 'heart': Heart,\n 'help-circle': HelpCircle,\n 'hourglass': Hourglass,\n 'inbox': Inbox,\n 'key': Key,\n 'key-round': KeyRound,\n 'layers': Layers,\n 'lightbulb': Lightbulb,\n 'line-chart': LineChart,\n 'link': Link,\n 'list': List,\n 'loader': Loader,\n 'loader-2': Loader2,\n 'lock': Lock,\n 'mail': Mail,\n 'mail-open': MailOpen,\n 'map-pin': MapPin,\n 'minus-circle': MinusCircle,\n 'notebook': Notebook,\n 'package': Package,\n 'package-check': PackageCheck,\n 'package-plus': PackagePlus,\n 'package-x': PackageX,\n 'pause-circle': PauseCircle,\n 'percent': Percent,\n 'phone': Phone,\n 'phone-call': PhoneCall,\n 'pie-chart': PieChart,\n 'plus-square': PlusSquare,\n 'receipt': Receipt,\n 'receipt-text': ReceiptText,\n 'refresh-ccw': RefreshCcw,\n 'reply': Reply,\n 'rotate-ccw': RotateCcw,\n 'ruler': Ruler,\n 'send': Send,\n 'settings': Settings,\n 'shapes': Shapes,\n 'share-2': Share2,\n 'shield': Shield,\n 'shield-alert': ShieldAlert,\n 'shield-check': ShieldCheck,\n 'shopping-bag': ShoppingBag,\n 'shopping-cart': ShoppingCart,\n 'shuffle': Shuffle,\n 'sliders': Sliders,\n 'smartphone': Smartphone,\n 'snowflake': Snowflake,\n 'sparkles': Sparkles,\n 'star': Star,\n 'sticky-note': StickyNote,\n 'store': Store,\n 'tag': Tag,\n 'target': Target,\n 'thumbs-up': ThumbsUp,\n 'ticket': Ticket,\n 'timer': Timer,\n 'trash-2': Trash2,\n 'trending-up': TrendingUp,\n 'triangle-alert': TriangleAlert,\n 'trophy': Trophy,\n 'truck': Truck,\n 'undo-2': Undo2,\n 'unlock': Unlock,\n 'user': User,\n 'user-check': UserCheck,\n 'user-minus': UserMinus,\n 'user-plus': UserPlus,\n 'user-round': UserRound,\n 'user-x': UserX,\n 'users': Users,\n 'wallet': Wallet,\n 'webhook': Webhook,\n 'wrench': Wrench,\n 'x': X,\n 'x-circle': XCircle,\n}\n\nfunction normalizeKebabIconName(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return ''\n const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed\n if (!withoutPrefix) return ''\n if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {\n return withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')\n .toLowerCase()\n }\n return withoutPrefix\n .replace(/[_\\s]+/g, '-')\n .replace(/-+/g, '-')\n .toLowerCase()\n}\n\nexport function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {\n if (!name) return null\n const normalized = normalizeKebabIconName(name)\n if (!normalized) return null\n return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null\n}\n\nexport function resolveRegisteredLucideIconNode(\n name: string | undefined,\n className: string\n): React.ReactNode | null {\n const Icon = resolveRegisteredLucideIcon(name)\n if (!Icon) return null\n return <Icon className={className} />\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Activity,\n AlertCircle,\n AlertOctagon,\n AlertTriangle,\n Archive,\n Award,\n BadgeCheck,\n Ban,\n Banknote,\n BarChart2,\n BarChart3,\n Bell,\n Bolt,\n Book,\n Bookmark,\n Box,\n Briefcase,\n BriefcaseBusiness,\n Building,\n Building2,\n Calendar,\n CalendarCheck,\n CalendarClock,\n CalendarCog,\n CalendarMinus,\n CalendarOff,\n CalendarX,\n Check,\n CheckCircle,\n CheckCircle2,\n CheckSquare,\n Circle,\n ClipboardCheck,\n ClipboardList,\n Clock,\n Clock3,\n Coins,\n Copy,\n CreditCard,\n Crown,\n Database,\n DollarSign,\n Download,\n ExternalLink,\n Eye,\n FileMinus,\n FilePenLine,\n FileSpreadsheet,\n FileText,\n FilterX,\n Flag,\n Flame,\n FolderTree,\n Gauge,\n GitBranch,\n GitCompareArrows,\n GitPullRequestArrow,\n Globe,\n GraduationCap,\n Hand,\n Handshake,\n Headphones,\n Heart,\n HelpCircle,\n Hourglass,\n Inbox,\n Key,\n KeyRound,\n Layers,\n Lightbulb,\n LineChart,\n Link,\n List,\n Loader,\n Loader2,\n Lock,\n Mail,\n MailOpen,\n MapPin,\n MessageCircle,\n MinusCircle,\n Notebook,\n Package,\n PackageCheck,\n PackagePlus,\n PackageX,\n PauseCircle,\n Percent,\n Phone,\n PhoneCall,\n PieChart,\n PlusSquare,\n Receipt,\n ReceiptText,\n RefreshCcw,\n RefreshCw,\n Reply,\n RotateCcw,\n Ruler,\n Send,\n Settings,\n Shapes,\n Share2,\n Shield,\n ShieldAlert,\n ShieldCheck,\n ShoppingBag,\n ShoppingCart,\n Shuffle,\n Sliders,\n Smartphone,\n Snowflake,\n Sparkles,\n Star,\n StickyNote,\n Store,\n Tag,\n Target,\n ThumbsUp,\n Ticket,\n Timer,\n Trash2,\n TrendingUp,\n TriangleAlert,\n Trophy,\n Truck,\n Undo2,\n Unlock,\n User,\n UserCheck,\n UserMinus,\n UserPlus,\n UserRound,\n Users,\n UserX,\n Wallet,\n Webhook,\n Workflow,\n Wrench,\n X,\n XCircle,\n} from 'lucide-react'\n\nexport const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {\n 'activity': Activity,\n 'alert-circle': AlertCircle,\n 'alert-octagon': AlertOctagon,\n 'alert-triangle': AlertTriangle,\n 'archive': Archive,\n 'award': Award,\n 'badge-check': BadgeCheck,\n 'ban': Ban,\n 'banknote': Banknote,\n 'bar-chart-2': BarChart2,\n 'bar-chart-3': BarChart3,\n 'bell': Bell,\n 'bolt': Bolt,\n 'book': Book,\n 'bookmark': Bookmark,\n 'box': Box,\n 'briefcase': Briefcase,\n 'briefcase-business': BriefcaseBusiness,\n 'building': Building,\n 'building2': Building2,\n 'calendar': Calendar,\n 'calendar-check': CalendarCheck,\n 'calendar-clock': CalendarClock,\n 'calendar-cog': CalendarCog,\n 'calendar-minus': CalendarMinus,\n 'calendar-off': CalendarOff,\n 'calendar-x': CalendarX,\n 'check': Check,\n 'check-circle': CheckCircle,\n 'check-circle-2': CheckCircle2,\n 'check-square': CheckSquare,\n 'circle': Circle,\n 'clipboard-check': ClipboardCheck,\n 'clipboard-list': ClipboardList,\n 'clock': Clock,\n 'clock-3': Clock3,\n 'coins': Coins,\n 'copy': Copy,\n 'credit-card': CreditCard,\n 'crown': Crown,\n 'database': Database,\n 'dollar-sign': DollarSign,\n 'download': Download,\n 'external-link': ExternalLink,\n 'eye': Eye,\n 'file-minus': FileMinus,\n 'file-pen-line': FilePenLine,\n 'file-spreadsheet': FileSpreadsheet,\n 'file-text': FileText,\n 'filter-x': FilterX,\n 'flag': Flag,\n 'flame': Flame,\n 'folder-tree': FolderTree,\n 'gauge': Gauge,\n 'git-branch': GitBranch,\n 'git-compare-arrows': GitCompareArrows,\n 'git-pull-request-arrow': GitPullRequestArrow,\n 'globe': Globe,\n 'graduation-cap': GraduationCap,\n 'hand': Hand,\n 'handshake': Handshake,\n 'headphones': Headphones,\n 'heart': Heart,\n 'help-circle': HelpCircle,\n 'hourglass': Hourglass,\n 'inbox': Inbox,\n 'key': Key,\n 'key-round': KeyRound,\n 'layers': Layers,\n 'lightbulb': Lightbulb,\n 'line-chart': LineChart,\n 'link': Link,\n 'list': List,\n 'loader': Loader,\n 'loader-2': Loader2,\n 'lock': Lock,\n 'mail': Mail,\n 'mail-open': MailOpen,\n 'map-pin': MapPin,\n 'message-circle': MessageCircle,\n 'minus-circle': MinusCircle,\n 'notebook': Notebook,\n 'package': Package,\n 'package-check': PackageCheck,\n 'package-plus': PackagePlus,\n 'package-x': PackageX,\n 'pause-circle': PauseCircle,\n 'percent': Percent,\n 'phone': Phone,\n 'phone-call': PhoneCall,\n 'pie-chart': PieChart,\n 'plus-square': PlusSquare,\n 'receipt': Receipt,\n 'receipt-text': ReceiptText,\n 'refresh-ccw': RefreshCcw,\n 'refresh-cw': RefreshCw,\n 'reply': Reply,\n 'rotate-ccw': RotateCcw,\n 'ruler': Ruler,\n 'send': Send,\n 'settings': Settings,\n 'shapes': Shapes,\n 'share-2': Share2,\n 'shield': Shield,\n 'shield-alert': ShieldAlert,\n 'shield-check': ShieldCheck,\n 'shopping-bag': ShoppingBag,\n 'shopping-cart': ShoppingCart,\n 'shuffle': Shuffle,\n 'sliders': Sliders,\n 'smartphone': Smartphone,\n 'snowflake': Snowflake,\n 'sparkles': Sparkles,\n 'star': Star,\n 'sticky-note': StickyNote,\n 'store': Store,\n 'tag': Tag,\n 'target': Target,\n 'thumbs-up': ThumbsUp,\n 'ticket': Ticket,\n 'timer': Timer,\n 'trash-2': Trash2,\n 'trending-up': TrendingUp,\n 'triangle-alert': TriangleAlert,\n 'trophy': Trophy,\n 'truck': Truck,\n 'undo-2': Undo2,\n 'unlock': Unlock,\n 'user': User,\n 'user-check': UserCheck,\n 'user-minus': UserMinus,\n 'user-plus': UserPlus,\n 'user-round': UserRound,\n 'user-x': UserX,\n 'users': Users,\n 'wallet': Wallet,\n 'webhook': Webhook,\n 'workflow': Workflow,\n 'wrench': Wrench,\n 'x': X,\n 'x-circle': XCircle,\n}\n\nfunction normalizeKebabIconName(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return ''\n const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed\n if (!withoutPrefix) return ''\n if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {\n return withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')\n .toLowerCase()\n }\n return withoutPrefix\n .replace(/[_\\s]+/g, '-')\n .replace(/-+/g, '-')\n .toLowerCase()\n}\n\nexport function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {\n if (!name) return null\n const normalized = normalizeKebabIconName(name)\n if (!normalized) return null\n return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null\n}\n\nexport function resolveRegisteredLucideIconNode(\n name: string | undefined,\n className: string\n): React.ReactNode | null {\n const Icon = resolveRegisteredLucideIcon(name)\n if (!Icon) return null\n return <Icon className={className} />\n}\n"],
|
|
5
|
+
"mappings": "AAoUS;AA9TT;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAC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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,11 +3,14 @@ import * as React from "react";
|
|
|
3
3
|
import { useInjectionSpotEvents } from "./InjectionSpot.js";
|
|
4
4
|
import { GLOBAL_MUTATION_INJECTION_SPOT_ID, dispatchBackendMutationError } from "./mutationEvents.js";
|
|
5
5
|
import { withScopedApiRequestHeaders } from "../utils/apiCall.js";
|
|
6
|
+
import { surfaceRecordConflict } from "../conflicts/index.js";
|
|
7
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
8
|
function useGuardedMutation({
|
|
7
9
|
contextId,
|
|
8
10
|
blockedMessage = "Save blocked by validation",
|
|
9
11
|
spotId = GLOBAL_MUTATION_INJECTION_SPOT_ID
|
|
10
12
|
}) {
|
|
13
|
+
const t = useT();
|
|
11
14
|
const { triggerEvent } = useInjectionSpotEvents(spotId);
|
|
12
15
|
const lastMutationRef = React.useRef(null);
|
|
13
16
|
const emitMutationSaveError = React.useCallback((error) => {
|
|
@@ -16,7 +19,11 @@ function useGuardedMutation({
|
|
|
16
19
|
formId: contextId,
|
|
17
20
|
error
|
|
18
21
|
});
|
|
19
|
-
|
|
22
|
+
try {
|
|
23
|
+
surfaceRecordConflict(error, t);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}, [contextId, t]);
|
|
20
27
|
const runMutation = React.useCallback(async ({
|
|
21
28
|
operation,
|
|
22
29
|
context,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/injection/useGuardedMutation.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useInjectionSpotEvents } from './InjectionSpot'\nimport { GLOBAL_MUTATION_INJECTION_SPOT_ID, dispatchBackendMutationError } from './mutationEvents'\nimport { withScopedApiRequestHeaders } from '../utils/apiCall'\n\ntype GuardedMutationContext = Record<string, unknown>\n\ntype GuardedMutationRunInput<TResult, TContext extends GuardedMutationContext> = {\n operation: () => Promise<TResult>\n context: TContext\n mutationPayload?: Record<string, unknown>\n}\n\ntype UseGuardedMutationOptions = {\n contextId: string\n blockedMessage?: string\n spotId?: string\n}\n\ntype StoredMutation<TContext extends GuardedMutationContext> = {\n operation: () => Promise<unknown>\n context: TContext\n mutationPayload: Record<string, unknown>\n}\n\nexport function useGuardedMutation<TContext extends GuardedMutationContext>({\n contextId,\n blockedMessage = 'Save blocked by validation',\n spotId = GLOBAL_MUTATION_INJECTION_SPOT_ID,\n}: UseGuardedMutationOptions) {\n const { triggerEvent } = useInjectionSpotEvents<TContext, Record<string, unknown>>(spotId)\n const lastMutationRef = React.useRef<StoredMutation<TContext> | null>(null)\n\n const emitMutationSaveError = React.useCallback((error: unknown) => {\n dispatchBackendMutationError({\n contextId,\n formId: contextId,\n error,\n })\n }, [contextId])\n\n const runMutation = React.useCallback(async <TResult,>({\n operation,\n context,\n mutationPayload,\n }: GuardedMutationRunInput<TResult, TContext>): Promise<TResult> => {\n const payload = mutationPayload ?? {}\n lastMutationRef.current = {\n operation: operation as () => Promise<unknown>,\n context,\n mutationPayload: payload,\n }\n\n const beforeSave = await triggerEvent('onBeforeSave', payload, context)\n if (!beforeSave.ok) {\n emitMutationSaveError(beforeSave.details ?? beforeSave)\n throw new Error(beforeSave.message || blockedMessage)\n }\n\n try {\n const result =\n beforeSave.requestHeaders && Object.keys(beforeSave.requestHeaders).length > 0\n ? await withScopedApiRequestHeaders(beforeSave.requestHeaders, operation)\n : await operation()\n\n try {\n await triggerEvent('onAfterSave', payload, context)\n } catch (error) {\n console.error('[useGuardedMutation] Error in onAfterSave injection event:', error)\n }\n\n return result\n } catch (error) {\n emitMutationSaveError(error)\n throw error\n }\n }, [blockedMessage, emitMutationSaveError, triggerEvent])\n\n const retryLastMutation = React.useCallback(async (): Promise<boolean> => {\n const lastMutation = lastMutationRef.current\n if (!lastMutation) return false\n\n try {\n await runMutation({\n operation: lastMutation.operation,\n context: lastMutation.context,\n mutationPayload: lastMutation.mutationPayload,\n })\n return true\n } catch {\n return false\n }\n }, [runMutation])\n\n return {\n runMutation,\n retryLastMutation,\n }\n}\n"],
|
|
5
|
-
"mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,8BAA8B;AACvC,SAAS,mCAAmC,oCAAoC;AAChF,SAAS,mCAAmC;
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useInjectionSpotEvents } from './InjectionSpot'\nimport { GLOBAL_MUTATION_INJECTION_SPOT_ID, dispatchBackendMutationError } from './mutationEvents'\nimport { withScopedApiRequestHeaders } from '../utils/apiCall'\nimport { surfaceRecordConflict } from '../conflicts'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype GuardedMutationContext = Record<string, unknown>\n\ntype GuardedMutationRunInput<TResult, TContext extends GuardedMutationContext> = {\n operation: () => Promise<TResult>\n context: TContext\n mutationPayload?: Record<string, unknown>\n}\n\ntype UseGuardedMutationOptions = {\n contextId: string\n blockedMessage?: string\n spotId?: string\n}\n\ntype StoredMutation<TContext extends GuardedMutationContext> = {\n operation: () => Promise<unknown>\n context: TContext\n mutationPayload: Record<string, unknown>\n}\n\nexport function useGuardedMutation<TContext extends GuardedMutationContext>({\n contextId,\n blockedMessage = 'Save blocked by validation',\n spotId = GLOBAL_MUTATION_INJECTION_SPOT_ID,\n}: UseGuardedMutationOptions) {\n const t = useT()\n const { triggerEvent } = useInjectionSpotEvents<TContext, Record<string, unknown>>(spotId)\n const lastMutationRef = React.useRef<StoredMutation<TContext> | null>(null)\n\n const emitMutationSaveError = React.useCallback((error: unknown) => {\n dispatchBackendMutationError({\n contextId,\n formId: contextId,\n error,\n })\n // Default UX for OSS optimistic-lock conflicts (spec\n // .ai/specs/2026-05-25-oss-optimistic-locking.md \u00A73.6): when the\n // server returns 409 with `code: 'optimistic_lock_conflict'`,\n // surface the conflict on the unified, persistent, error-styled\n // RecordConflictBanner (rendered in AppShell) instead of a transient\n // toast. Callers that have their own handler still see the dispatched\n // event above and can suppress/override by catching the error first.\n try {\n surfaceRecordConflict(error, t)\n } catch {\n // ignore conflict-bar dispatch failures\n }\n }, [contextId, t])\n\n const runMutation = React.useCallback(async <TResult,>({\n operation,\n context,\n mutationPayload,\n }: GuardedMutationRunInput<TResult, TContext>): Promise<TResult> => {\n const payload = mutationPayload ?? {}\n lastMutationRef.current = {\n operation: operation as () => Promise<unknown>,\n context,\n mutationPayload: payload,\n }\n\n const beforeSave = await triggerEvent('onBeforeSave', payload, context)\n if (!beforeSave.ok) {\n emitMutationSaveError(beforeSave.details ?? beforeSave)\n throw new Error(beforeSave.message || blockedMessage)\n }\n\n try {\n const result =\n beforeSave.requestHeaders && Object.keys(beforeSave.requestHeaders).length > 0\n ? await withScopedApiRequestHeaders(beforeSave.requestHeaders, operation)\n : await operation()\n\n try {\n await triggerEvent('onAfterSave', payload, context)\n } catch (error) {\n console.error('[useGuardedMutation] Error in onAfterSave injection event:', error)\n }\n\n return result\n } catch (error) {\n emitMutationSaveError(error)\n throw error\n }\n }, [blockedMessage, emitMutationSaveError, triggerEvent])\n\n const retryLastMutation = React.useCallback(async (): Promise<boolean> => {\n const lastMutation = lastMutationRef.current\n if (!lastMutation) return false\n\n try {\n await runMutation({\n operation: lastMutation.operation,\n context: lastMutation.context,\n mutationPayload: lastMutation.mutationPayload,\n })\n return true\n } catch {\n return false\n }\n }, [runMutation])\n\n return {\n runMutation,\n retryLastMutation,\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,8BAA8B;AACvC,SAAS,mCAAmC,oCAAoC;AAChF,SAAS,mCAAmC;AAC5C,SAAS,6BAA6B;AACtC,SAAS,YAAY;AAsBd,SAAS,mBAA4D;AAAA,EAC1E;AAAA,EACA,iBAAiB;AAAA,EACjB,SAAS;AACX,GAA8B;AAC5B,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,aAAa,IAAI,uBAA0D,MAAM;AACzF,QAAM,kBAAkB,MAAM,OAAwC,IAAI;AAE1E,QAAM,wBAAwB,MAAM,YAAY,CAAC,UAAmB;AAClE,iCAA6B;AAAA,MAC3B;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,IACF,CAAC;AAQD,QAAI;AACF,4BAAsB,OAAO,CAAC;AAAA,IAChC,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,WAAW,CAAC,CAAC;AAEjB,QAAM,cAAc,MAAM,YAAY,OAAiB;AAAA,IACrD;AAAA,IACA;AAAA,IACA;AAAA,EACF,MAAoE;AAClE,UAAM,UAAU,mBAAmB,CAAC;AACpC,oBAAgB,UAAU;AAAA,MACxB;AAAA,MACA;AAAA,MACA,iBAAiB;AAAA,IACnB;AAEA,UAAM,aAAa,MAAM,aAAa,gBAAgB,SAAS,OAAO;AACtE,QAAI,CAAC,WAAW,IAAI;AAClB,4BAAsB,WAAW,WAAW,UAAU;AACtD,YAAM,IAAI,MAAM,WAAW,WAAW,cAAc;AAAA,IACtD;AAEA,QAAI;AACF,YAAM,SACJ,WAAW,kBAAkB,OAAO,KAAK,WAAW,cAAc,EAAE,SAAS,IACzE,MAAM,4BAA4B,WAAW,gBAAgB,SAAS,IACtE,MAAM,UAAU;AAEtB,UAAI;AACF,cAAM,aAAa,eAAe,SAAS,OAAO;AAAA,MACpD,SAAS,OAAO;AACd,gBAAQ,MAAM,8DAA8D,KAAK;AAAA,MACnF;AAEA,aAAO;AAAA,IACT,SAAS,OAAO;AACd,4BAAsB,KAAK;AAC3B,YAAM;AAAA,IACR;AAAA,EACF,GAAG,CAAC,gBAAgB,uBAAuB,YAAY,CAAC;AAExD,QAAM,oBAAoB,MAAM,YAAY,YAA8B;AACxE,UAAM,eAAe,gBAAgB;AACrC,QAAI,CAAC,aAAc,QAAO;AAE1B,QAAI;AACF,YAAM,YAAY;AAAA,QAChB,WAAW,aAAa;AAAA,QACxB,SAAS,aAAa;AAAA,QACtB,iBAAiB,aAAa;AAAA,MAChC,CAAC;AACD,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAEhB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
4
|
+
const isTestEnv = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || typeof process.env.JEST_WORKER_ID !== "undefined");
|
|
5
|
+
const MarkdownFieldTestStub = ({ value, onChange }) => /* @__PURE__ */ jsx(
|
|
6
|
+
"textarea",
|
|
7
|
+
{
|
|
8
|
+
"data-testid": "markdown-field",
|
|
9
|
+
className: "min-h-[160px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
10
|
+
value: value ?? "",
|
|
11
|
+
onChange: (event) => onChange(event.target.value)
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
const MarkdownFieldLive = dynamic(() => import("./MdxEditorImpl.js"), { ssr: false });
|
|
15
|
+
const MarkdownField = isTestEnv ? MarkdownFieldTestStub : MarkdownFieldLive;
|
|
16
|
+
var MarkdownField_default = MarkdownField;
|
|
17
|
+
export {
|
|
18
|
+
MarkdownField_default as default
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=MarkdownField.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/inputs/MarkdownField.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport dynamic from 'next/dynamic'\nimport type { ComponentType } from 'react'\n\nexport type MarkdownFieldProps = {\n value?: string\n onChange: (markdown: string) => void\n}\n\nconst isTestEnv =\n typeof process !== 'undefined' &&\n (process.env.NODE_ENV === 'test' || typeof process.env.JEST_WORKER_ID !== 'undefined')\n\n// Lightweight stand-in for unit tests (jsdom): MDXEditor/Lexical pull in ESM + CSS that jest\n// would have to transform, so under test we render a plain controlled textarea instead.\nconst MarkdownFieldTestStub: ComponentType<MarkdownFieldProps> = ({ value, onChange }) => (\n <textarea\n data-testid=\"markdown-field\"\n className=\"min-h-[160px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={value ?? ''}\n onChange={(event) => onChange(event.target.value)}\n />\n)\n\nconst MarkdownFieldLive = dynamic(() => import('./MdxEditorImpl'), { ssr: false }) as ComponentType<MarkdownFieldProps>\n\n// Canonical Markdown editor for the whole app: a WYSIWYG MDXEditor (Lexical) that emits Markdown.\n// Client-only (Lexical needs the DOM), loaded via next/dynamic with ssr disabled. Use this\n// everywhere a Markdown editing field is needed.\nconst MarkdownField: ComponentType<MarkdownFieldProps> = isTestEnv ? MarkdownFieldTestStub : MarkdownFieldLive\n\nexport default MarkdownField\n"],
|
|
5
|
+
"mappings": ";AAkBE;AAfF,OAAO,aAAa;AAQpB,MAAM,YACJ,OAAO,YAAY,gBAClB,QAAQ,IAAI,aAAa,UAAU,OAAO,QAAQ,IAAI,mBAAmB;AAI5E,MAAM,wBAA2D,CAAC,EAAE,OAAO,SAAS,MAClF;AAAA,EAAC;AAAA;AAAA,IACC,eAAY;AAAA,IACZ,WAAU;AAAA,IACV,OAAO,SAAS;AAAA,IAChB,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA;AAClD;AAGF,MAAM,oBAAoB,QAAQ,MAAM,OAAO,iBAAiB,GAAG,EAAE,KAAK,MAAM,CAAC;AAKjF,MAAM,gBAAmD,YAAY,wBAAwB;AAE7F,IAAO,wBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
MDXEditor,
|
|
6
|
+
headingsPlugin,
|
|
7
|
+
listsPlugin,
|
|
8
|
+
quotePlugin,
|
|
9
|
+
thematicBreakPlugin,
|
|
10
|
+
linkPlugin,
|
|
11
|
+
linkDialogPlugin,
|
|
12
|
+
imagePlugin,
|
|
13
|
+
tablePlugin,
|
|
14
|
+
markdownShortcutPlugin,
|
|
15
|
+
diffSourcePlugin,
|
|
16
|
+
toolbarPlugin,
|
|
17
|
+
UndoRedo,
|
|
18
|
+
BoldItalicUnderlineToggles,
|
|
19
|
+
CodeToggle,
|
|
20
|
+
StrikeThroughSupSubToggles,
|
|
21
|
+
ListsToggle,
|
|
22
|
+
BlockTypeSelect,
|
|
23
|
+
CreateLink,
|
|
24
|
+
InsertImage,
|
|
25
|
+
InsertTable,
|
|
26
|
+
InsertThematicBreak,
|
|
27
|
+
Separator,
|
|
28
|
+
DiffSourceToggleWrapper
|
|
29
|
+
} from "@mdxeditor/editor";
|
|
30
|
+
import "@mdxeditor/editor/style.css";
|
|
31
|
+
import { cn } from "@open-mercato/shared/lib/utils";
|
|
32
|
+
import { useTheme } from "../../theme/index.js";
|
|
33
|
+
function MdxEditorImpl({ value = "", onChange }) {
|
|
34
|
+
const editorRef = React.useRef(null);
|
|
35
|
+
const latestRef = React.useRef(value);
|
|
36
|
+
const typingRef = React.useRef(false);
|
|
37
|
+
const { resolvedTheme } = useTheme();
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (typingRef.current) return;
|
|
40
|
+
if (value !== latestRef.current) {
|
|
41
|
+
latestRef.current = value;
|
|
42
|
+
editorRef.current?.setMarkdown(value ?? "");
|
|
43
|
+
}
|
|
44
|
+
}, [value]);
|
|
45
|
+
const commit = React.useCallback(() => {
|
|
46
|
+
if (!typingRef.current) return;
|
|
47
|
+
typingRef.current = false;
|
|
48
|
+
onChange(latestRef.current);
|
|
49
|
+
}, [onChange]);
|
|
50
|
+
return /* @__PURE__ */ jsx("div", { className: "w-full overflow-hidden rounded-md border border-input bg-background", onBlur: commit, children: /* @__PURE__ */ jsx(
|
|
51
|
+
MDXEditor,
|
|
52
|
+
{
|
|
53
|
+
ref: editorRef,
|
|
54
|
+
markdown: value ?? "",
|
|
55
|
+
onChange: (markdown) => {
|
|
56
|
+
typingRef.current = true;
|
|
57
|
+
latestRef.current = markdown;
|
|
58
|
+
},
|
|
59
|
+
className: cn("om-mdx-editor", resolvedTheme === "dark" && "dark-theme"),
|
|
60
|
+
contentEditableClassName: "om-mdx-prose",
|
|
61
|
+
plugins: [
|
|
62
|
+
headingsPlugin(),
|
|
63
|
+
listsPlugin(),
|
|
64
|
+
quotePlugin(),
|
|
65
|
+
thematicBreakPlugin(),
|
|
66
|
+
linkPlugin(),
|
|
67
|
+
linkDialogPlugin(),
|
|
68
|
+
imagePlugin(),
|
|
69
|
+
tablePlugin(),
|
|
70
|
+
markdownShortcutPlugin(),
|
|
71
|
+
diffSourcePlugin({ viewMode: "rich-text" }),
|
|
72
|
+
toolbarPlugin({
|
|
73
|
+
toolbarContents: () => /* @__PURE__ */ jsxs(DiffSourceToggleWrapper, { children: [
|
|
74
|
+
/* @__PURE__ */ jsx(UndoRedo, {}),
|
|
75
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
76
|
+
/* @__PURE__ */ jsx(BoldItalicUnderlineToggles, {}),
|
|
77
|
+
/* @__PURE__ */ jsx(CodeToggle, {}),
|
|
78
|
+
/* @__PURE__ */ jsx(StrikeThroughSupSubToggles, {}),
|
|
79
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
80
|
+
/* @__PURE__ */ jsx(ListsToggle, {}),
|
|
81
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
82
|
+
/* @__PURE__ */ jsx(BlockTypeSelect, {}),
|
|
83
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
84
|
+
/* @__PURE__ */ jsx(CreateLink, {}),
|
|
85
|
+
/* @__PURE__ */ jsx(InsertImage, {}),
|
|
86
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
87
|
+
/* @__PURE__ */ jsx(InsertTable, {}),
|
|
88
|
+
/* @__PURE__ */ jsx(InsertThematicBreak, {})
|
|
89
|
+
] })
|
|
90
|
+
})
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
) });
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
MdxEditorImpl as default
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=MdxEditorImpl.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/inputs/MdxEditorImpl.tsx"],
|
|
4
|
+
"sourcesContent": ["/// <reference path=\"../../types/css.d.ts\" />\n\"use client\"\n\nimport * as React from 'react'\nimport {\n MDXEditor,\n type MDXEditorMethods,\n headingsPlugin,\n listsPlugin,\n quotePlugin,\n thematicBreakPlugin,\n linkPlugin,\n linkDialogPlugin,\n imagePlugin,\n tablePlugin,\n markdownShortcutPlugin,\n diffSourcePlugin,\n toolbarPlugin,\n UndoRedo,\n BoldItalicUnderlineToggles,\n CodeToggle,\n StrikeThroughSupSubToggles,\n ListsToggle,\n BlockTypeSelect,\n CreateLink,\n InsertImage,\n InsertTable,\n InsertThematicBreak,\n Separator,\n DiffSourceToggleWrapper,\n} from '@mdxeditor/editor'\nimport '@mdxeditor/editor/style.css'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useTheme } from '../../theme'\n\ntype MdxEditorImplProps = {\n value?: string\n onChange: (markdown: string) => void\n}\n\n// WYSIWYG Markdown editor (MDXEditor / Lexical) wired to the CrudForm value/onChange\n// contract. Markdown is the source of truth. Edits are buffered locally and committed\n// on blur to avoid re-rendering the whole form on every keystroke.\nexport default function MdxEditorImpl({ value = '', onChange }: MdxEditorImplProps) {\n const editorRef = React.useRef<MDXEditorMethods>(null)\n const latestRef = React.useRef<string>(value)\n const typingRef = React.useRef(false)\n const { resolvedTheme } = useTheme()\n\n // External value changes (form reset / async initial load) are pushed into the editor,\n // but never while the user is actively typing (would reset the caret).\n React.useEffect(() => {\n if (typingRef.current) return\n if (value !== latestRef.current) {\n latestRef.current = value\n editorRef.current?.setMarkdown(value ?? '')\n }\n }, [value])\n\n const commit = React.useCallback(() => {\n if (!typingRef.current) return\n typingRef.current = false\n onChange(latestRef.current)\n }, [onChange])\n\n return (\n <div className=\"w-full overflow-hidden rounded-md border border-input bg-background\" onBlur={commit}>\n <MDXEditor\n ref={editorRef}\n markdown={value ?? ''}\n onChange={(markdown) => {\n typingRef.current = true\n latestRef.current = markdown\n }}\n className={cn('om-mdx-editor', resolvedTheme === 'dark' && 'dark-theme')}\n contentEditableClassName=\"om-mdx-prose\"\n plugins={[\n headingsPlugin(),\n listsPlugin(),\n quotePlugin(),\n thematicBreakPlugin(),\n linkPlugin(),\n linkDialogPlugin(),\n imagePlugin(),\n tablePlugin(),\n markdownShortcutPlugin(),\n diffSourcePlugin({ viewMode: 'rich-text' }),\n toolbarPlugin({\n toolbarContents: () => (\n <DiffSourceToggleWrapper>\n <UndoRedo />\n <Separator />\n <BoldItalicUnderlineToggles />\n <CodeToggle />\n <StrikeThroughSupSubToggles />\n <Separator />\n <ListsToggle />\n <Separator />\n <BlockTypeSelect />\n <Separator />\n <CreateLink />\n <InsertImage />\n <Separator />\n <InsertTable />\n <InsertThematicBreak />\n </DiffSourceToggleWrapper>\n ),\n }),\n ]}\n />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAyFc,SACE,KADF;AAtFd,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO;AACP,SAAS,UAAU;AACnB,SAAS,gBAAgB;AAUV,SAAR,cAA+B,EAAE,QAAQ,IAAI,SAAS,GAAuB;AAClF,QAAM,YAAY,MAAM,OAAyB,IAAI;AACrD,QAAM,YAAY,MAAM,OAAe,KAAK;AAC5C,QAAM,YAAY,MAAM,OAAO,KAAK;AACpC,QAAM,EAAE,cAAc,IAAI,SAAS;AAInC,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU,QAAS;AACvB,QAAI,UAAU,UAAU,SAAS;AAC/B,gBAAU,UAAU;AACpB,gBAAU,SAAS,YAAY,SAAS,EAAE;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,SAAS,MAAM,YAAY,MAAM;AACrC,QAAI,CAAC,UAAU,QAAS;AACxB,cAAU,UAAU;AACpB,aAAS,UAAU,OAAO;AAAA,EAC5B,GAAG,CAAC,QAAQ,CAAC;AAEb,SACE,oBAAC,SAAI,WAAU,uEAAsE,QAAQ,QAC3F;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,UAAU,SAAS;AAAA,MACnB,UAAU,CAAC,aAAa;AACtB,kBAAU,UAAU;AACpB,kBAAU,UAAU;AAAA,MACtB;AAAA,MACA,WAAW,GAAG,iBAAiB,kBAAkB,UAAU,YAAY;AAAA,MACvE,0BAAyB;AAAA,MACzB,SAAS;AAAA,QACP,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,oBAAoB;AAAA,QACpB,WAAW;AAAA,QACX,iBAAiB;AAAA,QACjB,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,uBAAuB;AAAA,QACvB,iBAAiB,EAAE,UAAU,YAAY,CAAC;AAAA,QAC1C,cAAc;AAAA,UACZ,iBAAiB,MACf,qBAAC,2BACC;AAAA,gCAAC,YAAS;AAAA,YACV,oBAAC,aAAU;AAAA,YACX,oBAAC,8BAA2B;AAAA,YAC5B,oBAAC,cAAW;AAAA,YACZ,oBAAC,8BAA2B;AAAA,YAC5B,oBAAC,aAAU;AAAA,YACX,oBAAC,eAAY;AAAA,YACb,oBAAC,aAAU;AAAA,YACX,oBAAC,mBAAgB;AAAA,YACjB,oBAAC,aAAU;AAAA,YACX,oBAAC,cAAW;AAAA,YACZ,oBAAC,eAAY;AAAA,YACb,oBAAC,aAAU;AAAA,YACX,oBAAC,eAAY;AAAA,YACb,oBAAC,uBAAoB;AAAA,aACvB;AAAA,QAEJ,CAAC;AAAA,MACH;AAAA;AAAA,EACF,GACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,59 +1,27 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import
|
|
4
|
-
import { LoadingMessage } from "../detail/LoadingMessage.js";
|
|
5
|
-
import { useMarkdownRemarkPlugins } from "../markdown/useMarkdownRemarkPlugins.js";
|
|
6
|
-
import { useTheme } from "../../theme/index.js";
|
|
7
|
-
const isTestEnv = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || typeof process.env.JEST_WORKER_ID !== "undefined");
|
|
8
|
-
const MarkdownEditorTestStub = ({ value, onChange }) => /* @__PURE__ */ jsx(
|
|
9
|
-
"textarea",
|
|
10
|
-
{
|
|
11
|
-
className: "min-h-[160px] w-full rounded border px-3 py-2 text-sm",
|
|
12
|
-
value: value ?? "",
|
|
13
|
-
onChange: (event) => onChange?.(event.target.value)
|
|
14
|
-
}
|
|
15
|
-
);
|
|
16
|
-
const UiMarkdownEditor = isTestEnv ? MarkdownEditorTestStub : dynamic(() => import("@uiw/react-md-editor"), {
|
|
17
|
-
ssr: false,
|
|
18
|
-
loading: () => /* @__PURE__ */ jsx(
|
|
19
|
-
LoadingMessage,
|
|
20
|
-
{
|
|
21
|
-
label: "Loading editor...",
|
|
22
|
-
className: "min-h-[220px] justify-center"
|
|
23
|
-
}
|
|
24
|
-
)
|
|
25
|
-
});
|
|
3
|
+
import MarkdownField from "./MarkdownField.js";
|
|
26
4
|
function SwitchableMarkdownInput({
|
|
27
5
|
value,
|
|
28
6
|
onChange,
|
|
29
7
|
isMarkdownEnabled,
|
|
30
8
|
disableMarkdown,
|
|
31
|
-
height = 220,
|
|
32
9
|
placeholder,
|
|
33
10
|
rows = 3,
|
|
34
11
|
textareaRef,
|
|
35
12
|
onTextareaInput,
|
|
36
13
|
textareaClassName,
|
|
37
|
-
|
|
38
|
-
editorClassName,
|
|
39
|
-
disabled,
|
|
40
|
-
remarkPlugins
|
|
14
|
+
disabled
|
|
41
15
|
}) {
|
|
42
|
-
const resolvedPlugins = useMarkdownRemarkPlugins(remarkPlugins);
|
|
43
|
-
const { resolvedTheme } = useTheme();
|
|
44
|
-
const editorWrapperClasses = editorWrapperClassName ?? "w-full rounded-lg border border-muted-foreground/20 bg-background p-2";
|
|
45
|
-
const editorClasses = editorClassName ?? "w-full";
|
|
46
16
|
const textareaClasses = textareaClassName ?? "w-full resize-none overflow-hidden rounded-lg border border-muted-foreground/20 bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
47
17
|
if (isMarkdownEnabled && !disableMarkdown) {
|
|
48
|
-
return /* @__PURE__ */ jsx(
|
|
49
|
-
|
|
18
|
+
return /* @__PURE__ */ jsx(
|
|
19
|
+
MarkdownField,
|
|
50
20
|
{
|
|
51
21
|
value,
|
|
52
|
-
|
|
53
|
-
onChange: (nextValue) => onChange(typeof nextValue === "string" ? nextValue : ""),
|
|
54
|
-
previewOptions: resolvedPlugins.length ? { remarkPlugins: resolvedPlugins } : void 0
|
|
22
|
+
onChange: (nextValue) => onChange(typeof nextValue === "string" ? nextValue : "")
|
|
55
23
|
}
|
|
56
|
-
)
|
|
24
|
+
);
|
|
57
25
|
}
|
|
58
26
|
return /* @__PURE__ */ jsx(
|
|
59
27
|
"textarea",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/SwitchableMarkdownInput.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n * @deprecated Prefer the DS `RichEditor` primitive from\n * `@open-mercato/ui/primitives/rich-editor` for any new rich-text input.\n *\n * `RichEditor` produces sanitized HTML (with the DS toolbar, color palette,\n * Figma-aligned styling, and auto-overflow toolbar variants). The DS direction\n * is to consolidate on a single rich-text format (HTML) so that user-authored\n * content renders consistently across email, exports, and the customer portal.\n *\n * `SwitchableMarkdownInput` is kept as a backward-compatibility shim for\n * existing Markdown-backed surfaces (notably `customers` Notes). It will be\n * removed once those surfaces migrate their storage format from Markdown to\n * sanitized HTML \u2014 track the migration in the spec referenced from\n * `customers/AGENTS.md`.\n *\n * Migration:\n *\n * ```diff\n * - import { SwitchableMarkdownInput } from '@open-mercato/ui/backend/inputs/SwitchableMarkdownInput'\n * - <SwitchableMarkdownInput value={md} onChange={setMd} isMarkdownEnabled={enabled} />\n * + import { RichEditor } from '@open-mercato/ui/primitives/rich-editor'\n * + <RichEditor value={html} onChange={setHtml} variant=\"basic\" />\n * ```\n *\n * For surfaces that genuinely need plain Markdown storage (e.g. agent prompts,\n * code-adjacent docs), keep using `SwitchableMarkdownInput` until a Markdown\n * mode lands on `RichEditor` \u2014 but do NOT introduce new Markdown surfaces.\n */\n\"use client\"\n\nimport * as React from 'react'\nimport
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["/**\n * @deprecated Prefer the DS `RichEditor` primitive from\n * `@open-mercato/ui/primitives/rich-editor` for any new rich-text input.\n *\n * `RichEditor` produces sanitized HTML (with the DS toolbar, color palette,\n * Figma-aligned styling, and auto-overflow toolbar variants). The DS direction\n * is to consolidate on a single rich-text format (HTML) so that user-authored\n * content renders consistently across email, exports, and the customer portal.\n *\n * `SwitchableMarkdownInput` is kept as a backward-compatibility shim for\n * existing Markdown-backed surfaces (notably `customers` Notes). It will be\n * removed once those surfaces migrate their storage format from Markdown to\n * sanitized HTML \u2014 track the migration in the spec referenced from\n * `customers/AGENTS.md`.\n *\n * Migration:\n *\n * ```diff\n * - import { SwitchableMarkdownInput } from '@open-mercato/ui/backend/inputs/SwitchableMarkdownInput'\n * - <SwitchableMarkdownInput value={md} onChange={setMd} isMarkdownEnabled={enabled} />\n * + import { RichEditor } from '@open-mercato/ui/primitives/rich-editor'\n * + <RichEditor value={html} onChange={setHtml} variant=\"basic\" />\n * ```\n *\n * For surfaces that genuinely need plain Markdown storage (e.g. agent prompts,\n * code-adjacent docs), keep using `SwitchableMarkdownInput` until a Markdown\n * mode lands on `RichEditor` \u2014 but do NOT introduce new Markdown surfaces.\n */\n\"use client\"\n\nimport * as React from 'react'\nimport type { PluggableList } from 'unified'\nimport MarkdownField from './MarkdownField'\n\n/** @deprecated See module-level JSDoc. */\nexport type SwitchableMarkdownInputProps = {\n value: string\n onChange: (value: string) => void\n isMarkdownEnabled: boolean\n disableMarkdown?: boolean\n height?: number\n placeholder?: string\n rows?: number\n textareaRef?: React.Ref<HTMLTextAreaElement>\n onTextareaInput?: React.FormEventHandler<HTMLTextAreaElement>\n textareaClassName?: string\n editorWrapperClassName?: string\n editorClassName?: string\n disabled?: boolean\n remarkPlugins?: PluggableList\n}\n\n/** @deprecated Use `RichEditor` from `@open-mercato/ui/primitives/rich-editor` for new code. See module JSDoc. */\nexport function SwitchableMarkdownInput({\n value,\n onChange,\n isMarkdownEnabled,\n disableMarkdown,\n placeholder,\n rows = 3,\n textareaRef,\n onTextareaInput,\n textareaClassName,\n disabled,\n}: SwitchableMarkdownInputProps) {\n const textareaClasses =\n textareaClassName\n ?? 'w-full resize-none overflow-hidden rounded-lg border border-muted-foreground/20 bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\n if (isMarkdownEnabled && !disableMarkdown) {\n return (\n <MarkdownField\n value={value}\n onChange={(nextValue) => onChange(typeof nextValue === 'string' ? nextValue : '')}\n />\n )\n }\n\n return (\n <textarea\n ref={textareaRef}\n rows={rows}\n className={textareaClasses}\n placeholder={placeholder}\n value={value}\n onChange={(event) => onChange(event.target.value)}\n onInput={onTextareaInput}\n disabled={disabled}\n />\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAuEM;AAvCN,OAAO,mBAAmB;AAqBnB,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiC;AAC/B,QAAM,kBACJ,qBACG;AAEL,MAAI,qBAAqB,CAAC,iBAAiB;AACzC,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,UAAU,CAAC,cAAc,SAAS,OAAO,cAAc,WAAW,YAAY,EAAE;AAAA;AAAA,IAClF;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA,WAAW;AAAA,MACX;AAAA,MACA;AAAA,MACA,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,MAChD,SAAS;AAAA,MACT;AAAA;AAAA,EACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,35 @@
|
|
|
1
|
+
const NAMED_HTML_ENTITIES = {
|
|
2
|
+
amp: "&",
|
|
3
|
+
lt: "<",
|
|
4
|
+
gt: ">",
|
|
5
|
+
quot: '"',
|
|
6
|
+
apos: "'",
|
|
7
|
+
nbsp: " ",
|
|
8
|
+
hellip: "\u2026",
|
|
9
|
+
mdash: "\u2014",
|
|
10
|
+
ndash: "\u2013",
|
|
11
|
+
rsquo: "\u2019",
|
|
12
|
+
lsquo: "\u2018",
|
|
13
|
+
rdquo: "\u201D",
|
|
14
|
+
ldquo: "\u201C"
|
|
15
|
+
};
|
|
16
|
+
function decodeHtmlEntities(value) {
|
|
17
|
+
return value.replace(/&#(\d+);/g, (match, dec) => {
|
|
18
|
+
const code = Number(dec);
|
|
19
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : match;
|
|
20
|
+
}).replace(/&#x([0-9a-fA-F]+);/g, (match, hex) => {
|
|
21
|
+
const code = parseInt(hex, 16);
|
|
22
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : match;
|
|
23
|
+
}).replace(
|
|
24
|
+
/&([a-zA-Z][a-zA-Z0-9]*);/g,
|
|
25
|
+
(match, name) => Object.prototype.hasOwnProperty.call(NAMED_HTML_ENTITIES, name) ? NAMED_HTML_ENTITIES[name] : match
|
|
26
|
+
);
|
|
27
|
+
}
|
|
1
28
|
function markdownToPlainText(input) {
|
|
2
29
|
if (!input) return "";
|
|
3
30
|
let text = String(input);
|
|
4
31
|
text = text.replace(/```[\s\S]*?```/g, " ");
|
|
32
|
+
text = text.replace(/<[^>]+>/g, " ");
|
|
5
33
|
text = text.replace(/`([^`]+)`/g, "$1");
|
|
6
34
|
text = text.replace(/!\[[^\]]*\]\([^)]*\)/g, " ");
|
|
7
35
|
text = text.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
@@ -15,6 +43,7 @@ function markdownToPlainText(input) {
|
|
|
15
43
|
text = text.replace(/\*([^*]+)\*/g, "$1");
|
|
16
44
|
text = text.replace(/_([^_]+)_/g, "$1");
|
|
17
45
|
text = text.replace(/\|/g, " ");
|
|
46
|
+
text = decodeHtmlEntities(text);
|
|
18
47
|
text = text.replace(/\s+/g, " ").trim();
|
|
19
48
|
return text;
|
|
20
49
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/markdown/markdownToPlainText.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["const NAMED_HTML_ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n nbsp: ' ',\n hellip: '\u2026',\n mdash: '\u2014',\n ndash: '\u2013',\n rsquo: '\u2019',\n lsquo: '\u2018',\n rdquo: '\u201D',\n ldquo: '\u201C',\n}\n\nfunction decodeHtmlEntities(value: string): string {\n return value\n .replace(/&#(\\d+);/g, (match, dec: string) => {\n const code = Number(dec)\n return Number.isFinite(code) ? String.fromCodePoint(code) : match\n })\n .replace(/&#x([0-9a-fA-F]+);/g, (match, hex: string) => {\n const code = parseInt(hex, 16)\n return Number.isFinite(code) ? String.fromCodePoint(code) : match\n })\n .replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (match, name: string) =>\n Object.prototype.hasOwnProperty.call(NAMED_HTML_ENTITIES, name) ? NAMED_HTML_ENTITIES[name] : match,\n )\n}\n\n/**\n * Converts a Markdown (or legacy HTML) string into a plain-text preview suitable for compact,\n * read-only contexts such as DataTable list cells. Strips Markdown syntax, strips HTML tags, and\n * decodes HTML entities, then collapses whitespace. Handling legacy HTML keeps list previews clean\n * for records authored before the editor moved back to Markdown \u2014 no data migration required.\n *\n * This is NOT a sanitizer \u2014 render rich content with the Markdown components, not this helper.\n */\nexport function markdownToPlainText(input: string | null | undefined): string {\n if (!input) return ''\n let text = String(input)\n text = text.replace(/```[\\s\\S]*?```/g, ' ')\n text = text.replace(/<[^>]+>/g, ' ')\n text = text.replace(/`([^`]+)`/g, '$1')\n text = text.replace(/!\\[[^\\]]*\\]\\([^)]*\\)/g, ' ')\n text = text.replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, '$1')\n text = text.replace(/^>\\s?/gm, '')\n text = text.replace(/^#{1,6}\\s+/gm, '')\n text = text.replace(/^\\s*[-*+]\\s+/gm, '')\n text = text.replace(/^\\s*\\d+\\.\\s+/gm, '')\n text = text.replace(/~~([^~]+)~~/g, '$1')\n text = text.replace(/\\*\\*([^*]+)\\*\\*/g, '$1')\n text = text.replace(/__([^_]+)__/g, '$1')\n text = text.replace(/\\*([^*]+)\\*/g, '$1')\n text = text.replace(/_([^_]+)_/g, '$1')\n text = text.replace(/\\|/g, ' ')\n text = decodeHtmlEntities(text)\n text = text.replace(/\\s+/g, ' ').trim()\n return text\n}\n"],
|
|
5
|
+
"mappings": "AAAA,MAAM,sBAA8C;AAAA,EAClD,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACT;AAEA,SAAS,mBAAmB,OAAuB;AACjD,SAAO,MACJ,QAAQ,aAAa,CAAC,OAAO,QAAgB;AAC5C,UAAM,OAAO,OAAO,GAAG;AACvB,WAAO,OAAO,SAAS,IAAI,IAAI,OAAO,cAAc,IAAI,IAAI;AAAA,EAC9D,CAAC,EACA,QAAQ,uBAAuB,CAAC,OAAO,QAAgB;AACtD,UAAM,OAAO,SAAS,KAAK,EAAE;AAC7B,WAAO,OAAO,SAAS,IAAI,IAAI,OAAO,cAAc,IAAI,IAAI;AAAA,EAC9D,CAAC,EACA;AAAA,IAAQ;AAAA,IAA6B,CAAC,OAAO,SAC5C,OAAO,UAAU,eAAe,KAAK,qBAAqB,IAAI,IAAI,oBAAoB,IAAI,IAAI;AAAA,EAChG;AACJ;AAUO,SAAS,oBAAoB,OAA0C;AAC5E,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,OAAO,OAAO,KAAK;AACvB,SAAO,KAAK,QAAQ,mBAAmB,GAAG;AAC1C,SAAO,KAAK,QAAQ,YAAY,GAAG;AACnC,SAAO,KAAK,QAAQ,cAAc,IAAI;AACtC,SAAO,KAAK,QAAQ,yBAAyB,GAAG;AAChD,SAAO,KAAK,QAAQ,0BAA0B,IAAI;AAClD,SAAO,KAAK,QAAQ,WAAW,EAAE;AACjC,SAAO,KAAK,QAAQ,gBAAgB,EAAE;AACtC,SAAO,KAAK,QAAQ,kBAAkB,EAAE;AACxC,SAAO,KAAK,QAAQ,kBAAkB,EAAE;AACxC,SAAO,KAAK,QAAQ,gBAAgB,IAAI;AACxC,SAAO,KAAK,QAAQ,oBAAoB,IAAI;AAC5C,SAAO,KAAK,QAAQ,gBAAgB,IAAI;AACxC,SAAO,KAAK,QAAQ,gBAAgB,IAAI;AACxC,SAAO,KAAK,QAAQ,cAAc,IAAI;AACtC,SAAO,KAAK,QAAQ,OAAO,GAAG;AAC9B,SAAO,mBAAmB,IAAI;AAC9B,SAAO,KAAK,QAAQ,QAAQ,GAAG,EAAE,KAAK;AACtC,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|