@almadar/ui 5.160.0 → 5.162.0

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.
@@ -10,7 +10,7 @@ import { mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, schemaToIR
10
10
  import { clsx } from 'clsx';
11
11
  import { twMerge } from 'tailwind-merge';
12
12
  import * as LucideIcons2 from 'lucide-react';
13
- import { Loader2, X, Lightbulb, CheckCircle, List, Printer, ChevronRight, ChevronLeft, GitBranch, Pencil, Eye, Plus, ArrowRight, Trash, Code, FileText, WrapText, Check, Copy, RotateCcw, Play, Terminal, XCircle, AlertTriangle, Trash2, Link2, ArrowLeft, ZoomOut, ZoomIn, Download, DollarSign, ChevronDown, Menu as Menu$1, Package, Calendar, MoreHorizontal, Image as Image$1, Upload, HelpCircle, PauseCircle, Search, Type, Heading1, Heading2, Heading3, ListOrdered, Quote, Minus, Eraser, TrendingUp, TrendingDown, AlertCircle, Circle, Clock, CheckCircle2, ChevronUp } from 'lucide-react';
13
+ import { Loader2, X, Lightbulb, CheckCircle, List, Printer, ChevronRight, ChevronLeft, GitBranch, Pencil, Eye, Plus, ArrowRight, Trash, Code, FileText, WrapText, Check, Copy, RotateCcw, Play, Terminal, XCircle, AlertTriangle, Trash2, Link2, ArrowLeft, ZoomOut, ZoomIn, Download, DollarSign, ChevronDown, Menu as Menu$1, Package, Calendar, MoreHorizontal, Image as Image$1, Upload, HelpCircle, PauseCircle, Search, Bold, Italic, Underline, Strikethrough, Heading1, Heading2, Heading3, ListOrdered, Quote, Link as Link$1, Minus, Eraser, TrendingUp, TrendingDown, AlertCircle, Circle, Clock, CheckCircle2, ChevronUp } from 'lucide-react';
14
14
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
15
  import { createPortal } from 'react-dom';
16
16
  import { useTranslate } from '@almadar/ui/hooks';
@@ -25829,8 +25829,9 @@ function DataGrid({
25829
25829
  const titleField = fieldDefs.find((f3) => f3.variant === "h3" || f3.variant === "h4") ?? fieldDefs[0];
25830
25830
  const badgeFields = fieldDefs.filter((f3) => f3.variant === "badge" && f3 !== titleField);
25831
25831
  const bodyFields = fieldDefs.filter((f3) => f3 !== titleField && !badgeFields.includes(f3));
25832
- const primaryActions = actionDefs.filter((a) => a.variant !== "danger");
25833
- const dangerActions = actionDefs.filter((a) => a.variant === "danger");
25832
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
25833
+ const inlineCardActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
25834
+ const menuCardActions = actionDefs.filter((a) => !inlineCardActions.includes(a));
25834
25835
  const fireAction = (action, itemData) => {
25835
25836
  if (action.navigatesTo) {
25836
25837
  const url = action.navigatesTo.replace(
@@ -25850,6 +25851,9 @@ function DataGrid({
25850
25851
  e.stopPropagation();
25851
25852
  fireAction(action, itemData);
25852
25853
  };
25854
+ const cardClickAction = actionDefs.find((a) => a.variant !== "danger");
25855
+ const handleCardClick = cardClickAction ? (itemData) => () => fireAction(cardClickAction, itemData) : void 0;
25856
+ const stopCardClick = handleCardClick ? (e) => e.stopPropagation() : void 0;
25853
25857
  const hasRenderProp = typeof children === "function";
25854
25858
  useEffect(() => {
25855
25859
  if (data.length > 0 && !hasRenderProp && fieldDefs.length === 0) {
@@ -25921,55 +25925,40 @@ function DataGrid({
25921
25925
  {
25922
25926
  "data-entity-row": true,
25923
25927
  "data-entity-id": id,
25924
- className: cn("relative group/rowactions", isSelected && "ring-2 ring-primary rounded-lg"),
25928
+ onClick: handleCardClick?.(itemData),
25929
+ className: cn("relative group/rowactions", handleCardClick && "cursor-pointer", isSelected && "ring-2 ring-primary rounded-lg"),
25925
25930
  children: [
25926
25931
  children(itemData, index),
25927
- actionDefs.length > 0 && /* @__PURE__ */ jsxs(Box, { className: "absolute top-2 right-2 z-10 opacity-0 group-hover/rowactions:opacity-100 focus-within:opacity-100 [@media(pointer:coarse)]:opacity-100 transition-opacity duration-fast", children: [
25928
- /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "rounded-md border border-border bg-card/95 backdrop-blur-sm shadow-sm p-0.5 [@media(pointer:coarse)]:hidden", children: [
25929
- (maxInlineActions != null ? actionDefs.slice(0, maxInlineActions) : actionDefs).map((action, idx) => /* @__PURE__ */ jsxs(
25930
- Button,
25931
- {
25932
- variant: action.variant === "primary" ? "primary" : "ghost",
25933
- size: "sm",
25934
- onClick: handleActionClick(action, itemData),
25935
- "data-testid": `action-${action.event}`,
25936
- "data-row-id": String(itemData.id),
25937
- className: action.variant === "danger" ? "text-error hover:text-error hover:bg-error/10" : void 0,
25938
- children: [
25939
- action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
25940
- action.label
25941
- ]
25942
- },
25943
- idx
25944
- )),
25945
- maxInlineActions != null && actionDefs.length > maxInlineActions && /* @__PURE__ */ jsx(
25946
- Menu,
25947
- {
25948
- position: "bottom-end",
25949
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
25950
- items: actionDefs.slice(maxInlineActions).map((action) => ({
25951
- label: action.label,
25952
- icon: action.icon,
25953
- event: action.event,
25954
- onClick: () => fireAction(action, itemData)
25955
- }))
25956
- }
25957
- )
25958
- ] }),
25959
- /* @__PURE__ */ jsx(Box, { className: "hidden [@media(pointer:coarse)]:block rounded-md border border-border bg-card/95 backdrop-blur-sm shadow-sm p-0.5", children: /* @__PURE__ */ jsx(
25932
+ actionDefs.length > 0 && /* @__PURE__ */ jsx(Box, { onClick: stopCardClick, className: "absolute top-2 right-2 z-10 opacity-0 group-hover/rowactions:opacity-100 focus-within:opacity-100 [@media(pointer:coarse)]:opacity-100 transition-opacity duration-fast", children: /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "rounded-md border border-border bg-card/95 backdrop-blur-sm shadow-sm p-0.5", children: [
25933
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsxs(
25934
+ Button,
25935
+ {
25936
+ variant: "primary",
25937
+ size: "sm",
25938
+ onClick: handleActionClick(action, itemData),
25939
+ "data-testid": `action-${action.event}`,
25940
+ "data-row-id": String(itemData.id),
25941
+ children: [
25942
+ action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
25943
+ action.label
25944
+ ]
25945
+ },
25946
+ idx
25947
+ )),
25948
+ menuCardActions.length > 0 && /* @__PURE__ */ jsx(
25960
25949
  Menu,
25961
25950
  {
25962
25951
  position: "bottom-end",
25963
25952
  trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
25964
- items: actionDefs.map((action) => ({
25953
+ items: menuCardActions.map((action) => ({
25965
25954
  label: action.label,
25966
25955
  icon: action.icon,
25967
- event: action.event,
25956
+ variant: action.variant === "danger" ? "danger" : "default",
25968
25957
  onClick: () => fireAction(action, itemData)
25969
25958
  }))
25970
25959
  }
25971
- ) })
25972
- ] })
25960
+ )
25961
+ ] }) })
25973
25962
  ]
25974
25963
  },
25975
25964
  id
@@ -25983,12 +25972,14 @@ function DataGrid({
25983
25972
  {
25984
25973
  "data-entity-row": true,
25985
25974
  "data-entity-id": id,
25975
+ onClick: handleCardClick?.(itemData),
25986
25976
  className: cn(
25987
25977
  "bg-card rounded-lg",
25988
25978
  "border border-border",
25989
25979
  "shadow-elevation-card hover:shadow-elevation-dialog",
25990
25980
  "hover:border-primary transition-all",
25991
25981
  "flex flex-col",
25982
+ handleCardClick && "cursor-pointer",
25992
25983
  isSelected && "ring-2 ring-primary border-primary"
25993
25984
  ),
25994
25985
  children: [
@@ -26038,53 +26029,35 @@ function DataGrid({
26038
26029
  ] }, field.name);
26039
26030
  }) })
26040
26031
  ] }),
26041
- (primaryActions.length > 0 || dangerActions.length > 0) && /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
26042
- (maxInlineActions != null ? primaryActions.slice(0, maxInlineActions) : primaryActions).map((action, idx) => /* @__PURE__ */ jsx(
26032
+ actionDefs.length > 0 && /* @__PURE__ */ jsxs(HStack, { gap: "xs", onClick: stopCardClick, className: "flex-shrink-0", children: [
26033
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsx(
26043
26034
  Button,
26044
26035
  {
26045
- variant: action.variant === "primary" ? "primary" : "ghost",
26036
+ variant: "primary",
26046
26037
  size: "sm",
26047
26038
  onClick: handleActionClick(action, itemData),
26048
26039
  "data-testid": `action-${action.event}`,
26049
26040
  "data-row-id": String(itemData.id),
26050
26041
  "aria-label": action.label,
26051
26042
  title: action.label,
26052
- className: cn(
26053
- action.variant === "primary" ? void 0 : "text-muted-foreground hover:text-foreground",
26054
- action.icon && "px-2"
26055
- ),
26043
+ className: cn(action.icon && "px-2"),
26056
26044
  children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
26057
26045
  },
26058
26046
  idx
26059
26047
  )),
26060
- maxInlineActions != null && primaryActions.length > maxInlineActions && /* @__PURE__ */ jsx(
26048
+ menuCardActions.length > 0 && /* @__PURE__ */ jsx(
26061
26049
  Menu,
26062
26050
  {
26063
26051
  position: "bottom-end",
26064
26052
  trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
26065
- items: primaryActions.slice(maxInlineActions).map((action) => ({
26053
+ items: menuCardActions.map((action) => ({
26066
26054
  label: action.label,
26067
26055
  icon: action.icon,
26068
- event: action.event,
26056
+ variant: action.variant === "danger" ? "danger" : "default",
26069
26057
  onClick: () => fireAction(action, itemData)
26070
26058
  }))
26071
26059
  }
26072
- ),
26073
- dangerActions.map((action, idx) => /* @__PURE__ */ jsx(
26074
- Button,
26075
- {
26076
- variant: "ghost",
26077
- size: "sm",
26078
- onClick: handleActionClick(action, itemData),
26079
- "data-testid": `action-${action.event}`,
26080
- "data-row-id": String(itemData.id),
26081
- "aria-label": action.label,
26082
- title: action.label,
26083
- className: "text-error hover:text-error hover:bg-error/10 px-2",
26084
- children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
26085
- },
26086
- `danger-${idx}`
26087
- ))
26060
+ )
26088
26061
  ] })
26089
26062
  ] }) }),
26090
26063
  bodyFields.length > 0 && /* @__PURE__ */ jsx(Box, { className: "px-4 pt-2 pb-4 flex-1", children: /* @__PURE__ */ jsxs(VStack, { gap: "xs", children: [
@@ -26326,54 +26299,59 @@ function DataList({
26326
26299
  if (!itemActions || itemActions.length === 0) return null;
26327
26300
  const inline = maxInlineActions != null ? itemActions.slice(0, maxInlineActions) : itemActions;
26328
26301
  const overflow = maxInlineActions != null ? itemActions.slice(maxInlineActions) : [];
26329
- return /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
26330
- inline.map((action, idx) => /* @__PURE__ */ jsxs(
26331
- Button,
26332
- {
26333
- variant: action.variant ?? "ghost",
26334
- size: "sm",
26335
- onClick: handleActionClick(action, itemData),
26336
- "data-testid": `action-${action.event}`,
26337
- "data-row-id": String(itemData.id),
26338
- className: cn(
26339
- action.variant === "danger" && "text-error hover:bg-error/10",
26340
- // Must sit on the Button itself: the variant's own text colour
26341
- // beats an inherited one from the row wrapper.
26342
- onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
26343
- ),
26344
- children: [
26345
- action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
26346
- action.label
26347
- ]
26348
- },
26349
- idx
26350
- )),
26351
- overflow.length > 0 && /* @__PURE__ */ jsx(
26352
- Menu,
26353
- {
26354
- position: "bottom-end",
26355
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
26356
- items: overflow.map((action) => ({
26357
- label: action.label,
26358
- icon: action.icon,
26359
- event: action.event,
26360
- variant: action.variant === "danger" ? "danger" : "default",
26361
- onClick: () => eventBus.emit(`UI:${action.event}`, {
26362
- id: itemData.id,
26363
- row: itemData
26364
- })
26365
- }))
26366
- }
26367
- )
26368
- ] });
26302
+ return (
26303
+ // stopPropagation at the cluster: the "⋯" trigger opens the overflow
26304
+ // menu without also firing the row's default click (inline buttons
26305
+ // already stop it in handleActionClick; the Menu panel is portaled).
26306
+ /* @__PURE__ */ jsxs(HStack, { gap: "xs", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, className: "flex-shrink-0", children: [
26307
+ inline.map((action, idx) => /* @__PURE__ */ jsxs(
26308
+ Button,
26309
+ {
26310
+ variant: action.variant ?? "ghost",
26311
+ size: "sm",
26312
+ onClick: handleActionClick(action, itemData),
26313
+ "data-testid": `action-${action.event}`,
26314
+ "data-row-id": String(itemData.id),
26315
+ className: cn(
26316
+ action.variant === "danger" && "text-error hover:bg-error/10",
26317
+ // Must sit on the Button itself: the variant's own text colour
26318
+ // beats an inherited one from the row wrapper.
26319
+ onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
26320
+ ),
26321
+ children: [
26322
+ action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
26323
+ action.label
26324
+ ]
26325
+ },
26326
+ idx
26327
+ )),
26328
+ overflow.length > 0 && /* @__PURE__ */ jsx(
26329
+ Menu,
26330
+ {
26331
+ position: "bottom-end",
26332
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
26333
+ items: overflow.map((action) => ({
26334
+ label: action.label,
26335
+ icon: action.icon,
26336
+ variant: action.variant === "danger" ? "danger" : "default",
26337
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
26338
+ id: itemData.id,
26339
+ row: itemData
26340
+ })
26341
+ }))
26342
+ }
26343
+ )
26344
+ ] })
26345
+ );
26369
26346
  };
26347
+ const rowClickEvent = itemClickEvent || itemActions?.find((a) => a.variant !== "danger")?.event;
26370
26348
  const handleRowClick = (itemData) => () => {
26371
- if (!itemClickEvent) return;
26349
+ if (!rowClickEvent) return;
26372
26350
  const payload = {
26373
26351
  id: itemData.id,
26374
26352
  row: itemData
26375
26353
  };
26376
- eventBus.emit(`UI:${itemClickEvent}`, payload);
26354
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
26377
26355
  };
26378
26356
  if (isLoading) {
26379
26357
  return /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) });
@@ -26421,10 +26399,10 @@ function DataList({
26421
26399
  {
26422
26400
  "data-entity-row": true,
26423
26401
  "data-entity-id": id,
26424
- onClick: itemClickEvent ? handleRowClick(itemData) : void 0,
26402
+ onClick: rowClickEvent ? handleRowClick(itemData) : void 0,
26425
26403
  className: cn(
26426
26404
  "flex px-4 group/rowactions",
26427
- itemClickEvent && "cursor-pointer",
26405
+ rowClickEvent && "cursor-pointer",
26428
26406
  isSent ? "justify-end" : "justify-start"
26429
26407
  ),
26430
26408
  children: /* @__PURE__ */ jsxs(
@@ -26500,7 +26478,7 @@ function DataList({
26500
26478
  const id2 = itemData.id || String(index);
26501
26479
  const actions = renderItemActions(itemData);
26502
26480
  return wrapDnd(
26503
- /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id2, onClick: itemClickEvent ? handleRowClick(itemData) : void 0, className: cn("relative group/rowactions", itemClickEvent && "cursor-pointer"), children: [
26481
+ /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id2, onClick: rowClickEvent ? handleRowClick(itemData) : void 0, className: cn("relative group/rowactions", rowClickEvent && "cursor-pointer"), children: [
26504
26482
  children(itemData, index),
26505
26483
  actions && /* @__PURE__ */ jsxs(Box, { className: "absolute top-2 right-2 z-10 opacity-0 group-hover/rowactions:opacity-100 focus-within:opacity-100 [@media(pointer:coarse)]:opacity-100 transition-opacity duration-fast", children: [
26506
26484
  /* @__PURE__ */ jsx(Box, { className: "rounded-md border border-border bg-card/95 backdrop-blur-sm shadow-sm p-0.5 [@media(pointer:coarse)]:hidden", children: actions }),
@@ -26512,7 +26490,6 @@ function DataList({
26512
26490
  items: (itemActions ?? []).map((action) => ({
26513
26491
  label: action.label,
26514
26492
  icon: action.icon,
26515
- event: action.event,
26516
26493
  variant: action.variant === "danger" ? "danger" : "default",
26517
26494
  onClick: () => eventBus.emit(`UI:${action.event}`, {
26518
26495
  id: itemData.id,
@@ -26529,7 +26506,7 @@ function DataList({
26529
26506
  const id = itemData.id || String(index);
26530
26507
  const titleValue = getNestedValue(itemData, titleField?.name ?? "");
26531
26508
  return wrapDnd(
26532
- /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: itemClickEvent ? handleRowClick(itemData) : void 0, className: cn(itemClickEvent && "cursor-pointer"), children: [
26509
+ /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: rowClickEvent ? handleRowClick(itemData) : void 0, className: cn(rowClickEvent && "cursor-pointer"), children: [
26533
26510
  /* @__PURE__ */ jsxs(
26534
26511
  Box,
26535
26512
  {
@@ -31741,7 +31718,7 @@ var init_MapView = __esm({
31741
31718
  shadowSize: [41, 41]
31742
31719
  });
31743
31720
  L.Marker.prototype.options.icon = defaultIcon;
31744
- const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback90, useState: useState92 } = React85__default;
31721
+ const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback90, useState: useState94 } = React85__default;
31745
31722
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31746
31723
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31747
31724
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -31786,7 +31763,7 @@ var init_MapView = __esm({
31786
31763
  showAttribution = true
31787
31764
  }) {
31788
31765
  const eventBus = useEventBus2();
31789
- const [clickedPosition, setClickedPosition] = useState92(null);
31766
+ const [clickedPosition, setClickedPosition] = useState94(null);
31790
31767
  const handleMapClick = useCallback90((lat, lng) => {
31791
31768
  if (showClickedPin) {
31792
31769
  setClickedPosition({ lat, lng });
@@ -36010,741 +35987,775 @@ var init_PositionedCanvas = __esm({
36010
35987
  PositionedCanvas.displayName = "PositionedCanvas";
36011
35988
  }
36012
35989
  });
36013
- function nextBlockId(prefix = "blk") {
36014
- _idSeq += 1;
36015
- const random = Math.random().toString(36).slice(2, 8);
36016
- return `${prefix}-${Date.now().toString(36)}-${_idSeq}-${random}`;
36017
- }
36018
- function normalizeBlocks(raw) {
36019
- if (!Array.isArray(raw) || raw.length === 0) return [createBlock("paragraph")];
36020
- return raw.map((row) => {
36021
- const entity = row;
36022
- const rawType = entity.type;
36023
- const type = typeof rawType === "string" && BLOCK_TYPES.has(rawType) ? rawType : "paragraph";
36024
- const id = typeof entity.id === "string" && entity.id ? entity.id : nextBlockId(type);
36025
- return { ...entity, id, type };
36026
- });
36027
- }
36028
- function createBlock(type) {
36029
- switch (type) {
36030
- case "bullet-list":
36031
- case "numbered-list":
36032
- return {
36033
- id: nextBlockId(type),
36034
- type,
36035
- children: [
36036
- { id: nextBlockId("li"), type: "paragraph", content: "" }
36037
- ]
36038
- };
36039
- case "image":
36040
- return {
36041
- id: nextBlockId(type),
36042
- type,
36043
- content: "",
36044
- metadata: { url: "", caption: "" }
36045
- };
36046
- case "code":
36047
- return {
36048
- id: nextBlockId(type),
36049
- type,
36050
- content: "",
36051
- metadata: { language: "plaintext" }
36052
- };
36053
- case "divider":
36054
- return { id: nextBlockId(type), type };
36055
- default:
36056
- return { id: nextBlockId(type), type, content: "" };
35990
+ function safeUrl(raw, kinds) {
35991
+ const url = raw.trim();
35992
+ if (url.startsWith("#") || url.startsWith("/")) return url;
35993
+ const lower = url.toLowerCase();
35994
+ for (const kind of kinds) {
35995
+ if (lower.startsWith(kind)) return url;
36057
35996
  }
35997
+ return null;
36058
35998
  }
36059
- function replaceBlock(blocks, id, updater) {
36060
- return blocks.map((block) => block.id === id ? updater(block) : block);
36061
- }
36062
- function removeBlock(blocks, id) {
36063
- return blocks.filter((block) => block.id !== id);
36064
- }
36065
- function duplicateBlock(block) {
36066
- return {
36067
- ...block,
36068
- id: nextBlockId(block.type),
36069
- children: block.children?.map((child) => ({
36070
- ...child,
36071
- id: nextBlockId("li")
36072
- })),
36073
- metadata: block.metadata ? { ...block.metadata } : void 0
36074
- };
36075
- }
36076
- function insertAfter(blocks, targetId, inserted) {
36077
- const idx = blocks.findIndex((b) => b.id === targetId);
36078
- if (idx === -1) return [...blocks, inserted];
36079
- const next = blocks.slice();
36080
- next.splice(idx + 1, 0, inserted);
36081
- return next;
36082
- }
36083
- function changeBlockType(block, type) {
36084
- if (block.type === type) return block;
36085
- if (type === "bullet-list" || type === "numbered-list") {
36086
- if (block.children && block.children.length > 0) {
36087
- return { ...block, type };
35999
+ function sanitizeNode(el) {
36000
+ const children = Array.from(el.children);
36001
+ for (const child of children) {
36002
+ const tag = child.tagName.toLowerCase();
36003
+ if (DROP_TAGS.has(tag)) {
36004
+ child.remove();
36005
+ continue;
36088
36006
  }
36089
- const seed2 = block.content ?? "";
36090
- return {
36091
- id: block.id,
36092
- type,
36093
- children: [
36094
- { id: nextBlockId("li"), type: "paragraph", content: seed2 }
36095
- ]
36096
- };
36097
- }
36098
- if (type === "divider") {
36099
- return { id: block.id, type };
36007
+ const allowed = ALLOWED_ATTRS[tag];
36008
+ if (!allowed) {
36009
+ sanitizeNode(child);
36010
+ child.replaceWith(...Array.from(child.childNodes));
36011
+ continue;
36012
+ }
36013
+ for (const attr of Array.from(child.attributes)) {
36014
+ const name = attr.name.toLowerCase();
36015
+ if (!allowed.includes(name)) {
36016
+ child.removeAttribute(attr.name);
36017
+ continue;
36018
+ }
36019
+ if (name === "href") {
36020
+ const url = safeUrl(attr.value, ["http://", "https://", "mailto:"]);
36021
+ if (url === null) child.removeAttribute(attr.name);
36022
+ else child.setAttribute("href", url);
36023
+ }
36024
+ if (name === "src") {
36025
+ const url = safeUrl(attr.value, ["http://", "https://", "data:image/"]);
36026
+ if (url === null) child.remove();
36027
+ else child.setAttribute("src", url);
36028
+ }
36029
+ }
36030
+ sanitizeNode(child);
36100
36031
  }
36101
- if (type === "image") {
36102
- return {
36103
- id: block.id,
36104
- type,
36105
- content: "",
36106
- metadata: { url: "", caption: block.content ?? "" }
36107
- };
36032
+ }
36033
+ function sanitizeRichHtml(html) {
36034
+ if (!html || typeof html !== "string") return "";
36035
+ if (typeof window === "undefined" || typeof DOMParser === "undefined") return "";
36036
+ const doc = new DOMParser().parseFromString(html, "text/html");
36037
+ sanitizeNode(doc.body);
36038
+ if (!doc.body.querySelector("img, hr") && (doc.body.textContent ?? "").trim().length === 0) {
36039
+ return "";
36108
36040
  }
36109
- if (type === "code") {
36110
- return {
36111
- id: block.id,
36112
- type,
36113
- content: block.content ?? "",
36114
- metadata: { language: "plaintext" }
36115
- };
36041
+ return doc.body.innerHTML.replace(/​/g, "");
36042
+ }
36043
+ function htmlIsEmpty(el) {
36044
+ if (el.querySelector("img, hr")) return false;
36045
+ return (el.textContent ?? "").trim().length === 0;
36046
+ }
36047
+ function closestBlock(node, root) {
36048
+ let cur = node;
36049
+ while (cur && cur !== root) {
36050
+ if (cur instanceof Element && BLOCK_TAGS.has(cur.tagName)) return cur;
36051
+ cur = cur.parentNode;
36116
36052
  }
36117
- const seed = block.children?.[0]?.content ?? block.content ?? "";
36118
- return { id: block.id, type, content: seed };
36053
+ return null;
36119
36054
  }
36120
- function BlockMenu({ block, readOnly, onDelete, onDuplicate, onChangeType }) {
36121
- const { t } = useTranslate();
36122
- const [open, setOpen] = useState(false);
36123
- const ref = useRef(null);
36124
- useEffect(() => {
36125
- if (!open) return;
36126
- function onDocClick(e) {
36127
- if (ref.current && !ref.current.contains(e.target)) {
36128
- setOpen(false);
36055
+ function RichTextStyles() {
36056
+ return /* @__PURE__ */ jsx("style", { children: RICH_TEXT_CSS });
36057
+ }
36058
+ function readToolbarState(root) {
36059
+ const sel = window.getSelection();
36060
+ if (!sel || sel.rangeCount === 0) return IDLE_TOOLBAR;
36061
+ const anchor = sel.anchorNode;
36062
+ if (!anchor || !root.contains(anchor)) return IDLE_TOOLBAR;
36063
+ let block = null;
36064
+ let link = false;
36065
+ let node = anchor;
36066
+ while (node && node !== root) {
36067
+ if (node instanceof Element) {
36068
+ const tag = node.tagName.toLowerCase();
36069
+ if (!block && (tag === "h1" || tag === "h2" || tag === "h3" || tag === "blockquote" || tag === "pre")) {
36070
+ block = tag;
36129
36071
  }
36072
+ if (tag === "a") link = true;
36130
36073
  }
36131
- document.addEventListener("mousedown", onDocClick);
36132
- return () => document.removeEventListener("mousedown", onDocClick);
36133
- }, [open]);
36134
- if (readOnly) return null;
36135
- return /* @__PURE__ */ jsxs(Box, { ref, className: "relative", children: [
36136
- /* @__PURE__ */ jsx(
36137
- Button,
36138
- {
36139
- type: "button",
36140
- variant: "ghost",
36141
- "aria-label": t("richBlockEditor.blockActions"),
36142
- className: cn(
36143
- "inline-flex items-center justify-center",
36144
- "h-6 w-6 rounded-sm p-0 gap-0",
36145
- "text-muted-foreground hover:bg-muted hover:text-foreground",
36146
- "opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
36147
- "transition-opacity"
36148
- ),
36149
- onClick: () => setOpen((v) => !v),
36150
- children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", className: "w-3.5 h-3.5" })
36151
- }
36152
- ),
36153
- open && /* @__PURE__ */ jsxs(
36154
- Box,
36155
- {
36156
- role: "menu",
36157
- className: cn(
36158
- "absolute right-0 z-10 mt-1 w-44",
36159
- "rounded-container border border-border bg-popover shadow-elevation-popover",
36160
- "py-1 text-sm"
36161
- ),
36162
- children: [
36163
- /* @__PURE__ */ jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t(BLOCK_TYPE_LABEL_KEY[block.type]) }),
36164
- /* @__PURE__ */ jsxs(
36165
- Button,
36166
- {
36167
- type: "button",
36168
- variant: "ghost",
36169
- role: "menuitem",
36170
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
36171
- onClick: () => {
36172
- onDuplicate();
36173
- setOpen(false);
36174
- },
36175
- children: [
36176
- /* @__PURE__ */ jsx(Icon, { name: "plus", className: "w-3.5 h-3.5" }),
36177
- " ",
36178
- t("richBlockEditor.duplicate")
36179
- ]
36180
- }
36181
- ),
36182
- /* @__PURE__ */ jsxs(
36183
- Button,
36184
- {
36185
- type: "button",
36186
- variant: "ghost",
36187
- role: "menuitem",
36188
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-destructive hover:bg-muted justify-start rounded-none",
36189
- onClick: () => {
36190
- onDelete();
36191
- setOpen(false);
36192
- },
36193
- children: [
36194
- /* @__PURE__ */ jsx(Icon, { name: "trash", className: "w-3.5 h-3.5" }),
36195
- " ",
36196
- t("common.delete")
36197
- ]
36198
- }
36199
- ),
36200
- CHANGEABLE_TYPES.includes(block.type) && /* @__PURE__ */ jsxs(Fragment, { children: [
36201
- /* @__PURE__ */ jsx(Box, { className: "my-1 border-t border-border" }),
36202
- /* @__PURE__ */ jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t("richBlockEditor.turnInto") }),
36203
- CHANGEABLE_TYPES.filter((bt) => bt !== block.type).map((bt) => /* @__PURE__ */ jsx(
36204
- Button,
36205
- {
36206
- type: "button",
36207
- variant: "ghost",
36208
- role: "menuitem",
36209
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
36210
- onClick: () => {
36211
- onChangeType(bt);
36212
- setOpen(false);
36213
- },
36214
- children: t(BLOCK_TYPE_LABEL_KEY[bt])
36215
- },
36216
- bt
36217
- ))
36218
- ] })
36219
- ]
36220
- }
36221
- )
36222
- ] });
36074
+ node = node.parentNode;
36075
+ }
36076
+ return {
36077
+ bold: document.queryCommandState("bold"),
36078
+ italic: document.queryCommandState("italic"),
36079
+ underline: document.queryCommandState("underline"),
36080
+ strikeThrough: document.queryCommandState("strikeThrough"),
36081
+ block,
36082
+ bullets: document.queryCommandState("insertUnorderedList"),
36083
+ numbers: document.queryCommandState("insertOrderedList"),
36084
+ link
36085
+ };
36223
36086
  }
36224
- function Editable({
36225
- tag,
36226
- value,
36227
- readOnly,
36228
- placeholder,
36229
- className,
36230
- ariaLabel,
36231
- onValueChange
36232
- }) {
36233
- const ref = useRef(null);
36234
- useEffect(() => {
36235
- const el = ref.current;
36236
- if (!el) return;
36237
- const isFocused = document.activeElement === el;
36238
- if (!isFocused && el.textContent !== value) {
36239
- el.textContent = value;
36240
- }
36241
- }, [value]);
36242
- const handleInput = useCallback(
36243
- (e) => {
36244
- onValueChange(e.currentTarget.textContent ?? "");
36245
- },
36246
- [onValueChange]
36247
- );
36087
+ function ToolbarButton({ icon: IconCmp, label, active, onExec }) {
36248
36088
  return /* @__PURE__ */ jsx(
36249
- Box,
36089
+ Button,
36250
36090
  {
36251
- as: tag,
36252
- ref,
36253
- contentEditable: !readOnly,
36254
- suppressContentEditableWarning: true,
36255
- role: readOnly ? void 0 : "textbox",
36256
- "aria-label": ariaLabel,
36257
- "aria-multiline": "true",
36258
- "data-placeholder": placeholder,
36091
+ type: "button",
36092
+ variant: "ghost",
36093
+ size: "sm",
36094
+ "aria-label": label,
36095
+ "aria-pressed": active,
36096
+ title: label,
36259
36097
  className: cn(
36260
- "outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-sm",
36261
- "empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground/60",
36262
- className
36098
+ "h-8 w-8 p-0 gap-0 justify-center",
36099
+ active && "bg-muted text-foreground"
36263
36100
  ),
36264
- onInput: handleInput
36101
+ onMouseDown: (e) => {
36102
+ e.preventDefault();
36103
+ onExec();
36104
+ },
36105
+ children: /* @__PURE__ */ jsx(IconCmp, { size: 15 })
36265
36106
  }
36266
36107
  );
36267
36108
  }
36268
- function BlockRow({
36269
- block,
36270
- readOnly,
36271
- showAffordances,
36272
- placeholder,
36273
- onUpdate,
36274
- onDelete,
36275
- onDuplicate,
36276
- onChangeType
36277
- }) {
36278
- const { t } = useTranslate();
36279
- const setContent = useCallback(
36280
- (next) => onUpdate((b) => ({ ...b, content: next })),
36281
- [onUpdate]
36282
- );
36283
- const setMetadata = useCallback(
36284
- (key, value) => onUpdate((b) => ({
36285
- ...b,
36286
- metadata: { ...b.metadata ?? {}, [key]: value }
36287
- })),
36288
- [onUpdate]
36289
- );
36290
- const setChildContent = useCallback(
36291
- (childId, next) => onUpdate((b) => ({
36292
- ...b,
36293
- children: (b.children ?? []).map(
36294
- (c) => c.id === childId ? { ...c, content: next } : c
36295
- )
36296
- })),
36297
- [onUpdate]
36298
- );
36299
- const addListItem = useCallback(
36300
- () => onUpdate((b) => ({
36301
- ...b,
36302
- children: [
36303
- ...b.children ?? [],
36304
- { id: nextBlockId("li"), type: "paragraph", content: "" }
36305
- ]
36306
- })),
36307
- [onUpdate]
36308
- );
36309
- const removeListItem = useCallback(
36310
- (childId) => onUpdate((b) => {
36311
- const remaining = (b.children ?? []).filter((c) => c.id !== childId);
36312
- return {
36313
- ...b,
36314
- children: remaining.length === 0 ? [{ id: nextBlockId("li"), type: "paragraph", content: "" }] : remaining
36315
- };
36316
- }),
36317
- [onUpdate]
36318
- );
36319
- const renderBody = () => {
36320
- switch (block.type) {
36321
- case "heading-1":
36322
- return /* @__PURE__ */ jsx(
36323
- Editable,
36324
- {
36325
- tag: "h1",
36326
- value: block.content ?? "",
36327
- readOnly,
36328
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading1"),
36329
- ariaLabel: t("richBlockEditor.aria.heading1Block"),
36330
- className: "text-3xl font-bold leading-tight",
36331
- onValueChange: setContent
36332
- }
36333
- );
36334
- case "heading-2":
36335
- return /* @__PURE__ */ jsx(
36336
- Editable,
36337
- {
36338
- tag: "h2",
36339
- value: block.content ?? "",
36340
- readOnly,
36341
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading2"),
36342
- ariaLabel: t("richBlockEditor.aria.heading2Block"),
36343
- className: "text-2xl font-semibold leading-tight",
36344
- onValueChange: setContent
36345
- }
36346
- );
36347
- case "heading-3":
36348
- return /* @__PURE__ */ jsx(
36349
- Editable,
36350
- {
36351
- tag: "h3",
36352
- value: block.content ?? "",
36353
- readOnly,
36354
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading3"),
36355
- ariaLabel: t("richBlockEditor.aria.heading3Block"),
36356
- className: "text-xl font-semibold leading-tight",
36357
- onValueChange: setContent
36358
- }
36359
- );
36360
- case "quote":
36361
- return /* @__PURE__ */ jsx(
36362
- Editable,
36363
- {
36364
- tag: "blockquote",
36365
- value: block.content ?? "",
36366
- readOnly,
36367
- placeholder: placeholder ?? t("richBlockEditor.placeholder.quote"),
36368
- ariaLabel: t("richBlockEditor.aria.quoteBlock"),
36369
- className: "border-l-4 border-primary/60 pl-4 italic text-muted-foreground",
36370
- onValueChange: setContent
36371
- }
36372
- );
36373
- case "code":
36374
- return /* @__PURE__ */ jsxs(Box, { className: "rounded-md border border-border bg-muted/40", children: [
36375
- /* @__PURE__ */ jsxs(Box, { className: "flex items-center justify-between border-b border-border px-3 py-1 text-xs text-muted-foreground", children: [
36376
- /* @__PURE__ */ jsx(Typography, { as: "span", variant: "caption", className: "uppercase tracking-wide", children: t("richBlockEditor.blockType.code") }),
36377
- !readOnly && /* @__PURE__ */ jsx(
36378
- Input,
36379
- {
36380
- inputType: "text",
36381
- value: String(block.metadata?.language ?? "plaintext"),
36382
- "aria-label": t("richBlockEditor.aria.codeLanguage"),
36383
- className: cn(
36384
- "h-6 w-32 rounded-sm border border-border bg-background",
36385
- "px-2 text-xs outline-none focus:ring-1 focus:ring-ring"
36386
- ),
36387
- onChange: (e) => setMetadata("language", e.target.value)
36388
- }
36389
- ),
36390
- readOnly && /* @__PURE__ */ jsx(Typography, { as: "span", variant: "caption", className: "text-xs", children: String(block.metadata?.language ?? "plaintext") })
36391
- ] }),
36392
- /* @__PURE__ */ jsx(
36393
- Editable,
36394
- {
36395
- tag: "pre",
36396
- value: block.content ?? "",
36397
- readOnly,
36398
- placeholder: placeholder ?? t("richBlockEditor.placeholder.code"),
36399
- ariaLabel: t("richBlockEditor.aria.codeBlock"),
36400
- className: "block whitespace-pre-wrap p-3 font-mono text-sm leading-relaxed",
36401
- onValueChange: setContent
36402
- }
36403
- )
36404
- ] });
36405
- case "divider":
36406
- return /* @__PURE__ */ jsx(Divider, { className: "my-2" });
36407
- case "image": {
36408
- const url = String(block.metadata?.url ?? "");
36409
- const caption = String(block.metadata?.caption ?? "");
36410
- const imgProps = {
36411
- src: url,
36412
- alt: caption || t("richBlockEditor.embeddedImage"),
36413
- className: "max-h-96 w-full rounded-md border border-border object-contain"
36109
+ var CHANGE_DEBOUNCE_MS, ALLOWED_ATTRS, DROP_TAGS, MD_BLOCK_PREFIXES, BLOCK_TAGS, MD_INLINE_RULES, RICH_TEXT_CSS, IDLE_TOOLBAR, RichTextEditor;
36110
+ var init_RichTextEditor = __esm({
36111
+ "components/core/molecules/RichTextEditor.tsx"() {
36112
+ "use client";
36113
+ init_cn();
36114
+ init_Box();
36115
+ init_Button();
36116
+ init_useEventBus();
36117
+ CHANGE_DEBOUNCE_MS = 400;
36118
+ ALLOWED_ATTRS = {
36119
+ p: [],
36120
+ h1: [],
36121
+ h2: [],
36122
+ h3: [],
36123
+ h4: [],
36124
+ ul: [],
36125
+ ol: [],
36126
+ li: [],
36127
+ blockquote: [],
36128
+ pre: [],
36129
+ code: [],
36130
+ b: [],
36131
+ strong: [],
36132
+ i: [],
36133
+ em: [],
36134
+ u: [],
36135
+ s: [],
36136
+ strike: [],
36137
+ br: [],
36138
+ hr: [],
36139
+ div: [],
36140
+ span: [],
36141
+ a: ["href"],
36142
+ img: ["src", "alt"]
36143
+ };
36144
+ DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "link", "meta", "form", "input", "button", "textarea", "select"]);
36145
+ MD_BLOCK_PREFIXES = {
36146
+ "#": { kind: "block", tag: "h1" },
36147
+ "##": { kind: "block", tag: "h2" },
36148
+ "###": { kind: "block", tag: "h3" },
36149
+ ">": { kind: "block", tag: "blockquote" },
36150
+ "```": { kind: "block", tag: "pre" },
36151
+ "-": { kind: "list", command: "insertUnorderedList" },
36152
+ "*": { kind: "list", command: "insertUnorderedList" },
36153
+ "1.": { kind: "list", command: "insertOrderedList" }
36154
+ };
36155
+ BLOCK_TAGS = /* @__PURE__ */ new Set(["P", "DIV", "LI", "H1", "H2", "H3", "H4", "BLOCKQUOTE", "PRE"]);
36156
+ MD_INLINE_RULES = [
36157
+ { pattern: /\*\*([^*\n]+)\*\*$/, wrap: "strong" },
36158
+ { pattern: /(^|[^*])\*([^*\n]+)\*$/, wrap: "em" },
36159
+ { pattern: /`([^`\n]+)`$/, wrap: "code" }
36160
+ ];
36161
+ RICH_TEXT_CSS = `
36162
+ .almadar-rich-text { line-height: 1.7; color: var(--color-foreground); }
36163
+ .almadar-rich-text h1 { font-size: 1.875rem; font-weight: 700; line-height: 1.25; margin: 1.25em 0 0.4em; }
36164
+ .almadar-rich-text h2 { font-size: 1.5rem; font-weight: 650; line-height: 1.3; margin: 1.1em 0 0.4em; }
36165
+ .almadar-rich-text h3 { font-size: 1.25rem; font-weight: 600; line-height: 1.35; margin: 1em 0 0.35em; }
36166
+ .almadar-rich-text h1:first-child, .almadar-rich-text h2:first-child, .almadar-rich-text h3:first-child { margin-top: 0; }
36167
+ .almadar-rich-text p { margin: 0.5em 0; }
36168
+ .almadar-rich-text ul { list-style: disc; padding-inline-start: 1.5rem; margin: 0.5em 0; }
36169
+ .almadar-rich-text ol { list-style: decimal; padding-inline-start: 1.5rem; margin: 0.5em 0; }
36170
+ .almadar-rich-text li { margin: 0.25em 0; }
36171
+ .almadar-rich-text blockquote { border-inline-start: 3px solid var(--color-primary); padding-inline-start: 1rem; font-style: italic; color: var(--color-muted-foreground); margin: 0.75em 0; }
36172
+ .almadar-rich-text pre { background: var(--color-muted); border: 1px solid var(--color-border); border-radius: 0.375rem; padding: 0.75rem 1rem; font-family: ui-monospace, monospace; font-size: 0.875em; white-space: pre-wrap; margin: 0.75em 0; }
36173
+ .almadar-rich-text code { font-family: ui-monospace, monospace; font-size: 0.875em; background: var(--color-muted); border-radius: 0.25rem; padding: 0.1em 0.35em; }
36174
+ .almadar-rich-text pre code { background: transparent; padding: 0; }
36175
+ .almadar-rich-text a { color: var(--color-primary); text-decoration: underline; }
36176
+ .almadar-rich-text hr { border: none; border-top: 1px solid var(--color-border); margin: 1.25em 0; }
36177
+ .almadar-rich-text img { max-width: 100%; border-radius: 0.375rem; }
36178
+ `;
36179
+ IDLE_TOOLBAR = {
36180
+ bold: false,
36181
+ italic: false,
36182
+ underline: false,
36183
+ strikeThrough: false,
36184
+ block: null,
36185
+ bullets: false,
36186
+ numbers: false,
36187
+ link: false
36188
+ };
36189
+ RichTextEditor = ({
36190
+ value,
36191
+ onChange,
36192
+ changeEvent,
36193
+ readOnly = false,
36194
+ placeholder,
36195
+ showToolbar = true,
36196
+ className
36197
+ }) => {
36198
+ const { t } = useTranslate();
36199
+ const ref = useRef(null);
36200
+ const [toolbar, setToolbar] = useState(IDLE_TOOLBAR);
36201
+ const [empty, setEmpty] = useState(() => !value || !sanitizeRichHtml(value).trim());
36202
+ const eventBus = useEventBus();
36203
+ const onChangeRef = useRef(onChange);
36204
+ const changeEventRef = useRef(changeEvent);
36205
+ useEffect(() => {
36206
+ onChangeRef.current = onChange;
36207
+ changeEventRef.current = changeEvent;
36208
+ }, [onChange, changeEvent]);
36209
+ const emitTimerRef = useRef(null);
36210
+ const emitNow = useCallback(() => {
36211
+ const el = ref.current;
36212
+ if (!el) return;
36213
+ const html = sanitizeRichHtml(el.innerHTML);
36214
+ onChangeRef.current?.(html);
36215
+ const evt = changeEventRef.current;
36216
+ if (evt) eventBus.emit(`UI:${evt}`, { value: html });
36217
+ }, [eventBus]);
36218
+ const scheduleEmit = useCallback(() => {
36219
+ if (emitTimerRef.current) clearTimeout(emitTimerRef.current);
36220
+ emitTimerRef.current = setTimeout(() => {
36221
+ emitTimerRef.current = null;
36222
+ emitNow();
36223
+ }, CHANGE_DEBOUNCE_MS);
36224
+ }, [emitNow]);
36225
+ const flushEmit = useCallback(() => {
36226
+ if (!emitTimerRef.current) return;
36227
+ clearTimeout(emitTimerRef.current);
36228
+ emitTimerRef.current = null;
36229
+ emitNow();
36230
+ }, [emitNow]);
36231
+ useEffect(() => () => {
36232
+ if (emitTimerRef.current) {
36233
+ clearTimeout(emitTimerRef.current);
36234
+ emitTimerRef.current = null;
36235
+ emitNow();
36236
+ }
36237
+ }, [emitNow]);
36238
+ useEffect(() => {
36239
+ if (readOnly) return;
36240
+ const el = ref.current;
36241
+ if (!el) return;
36242
+ if (document.activeElement === el) return;
36243
+ const next = sanitizeRichHtml(value ?? "") || "<p><br></p>";
36244
+ if (el.innerHTML !== next) {
36245
+ el.innerHTML = next;
36246
+ setEmpty(htmlIsEmpty(el));
36247
+ }
36248
+ }, [readOnly, value]);
36249
+ useEffect(() => {
36250
+ if (readOnly) return;
36251
+ document.execCommand("defaultParagraphSeparator", false, "p");
36252
+ }, [readOnly]);
36253
+ useEffect(() => {
36254
+ if (readOnly || !showToolbar) return;
36255
+ const onSelectionChange = () => {
36256
+ const el = ref.current;
36257
+ if (!el) return;
36258
+ setToolbar(readToolbarState(el));
36414
36259
  };
36415
- return /* @__PURE__ */ jsxs(Box, { className: "space-y-2", children: [
36416
- url ? /* @__PURE__ */ jsx(Box, { as: "img", ...imgProps }) : /* @__PURE__ */ jsxs(
36417
- Box,
36418
- {
36419
- className: cn(
36420
- "flex h-32 items-center justify-center",
36421
- "rounded-md border border-dashed border-border",
36422
- "text-sm text-muted-foreground"
36423
- ),
36424
- children: [
36425
- /* @__PURE__ */ jsx(Icon, { name: "image", className: "mr-2 w-4 h-4" }),
36426
- " ",
36427
- t("richBlockEditor.noImageUrl")
36428
- ]
36429
- }
36430
- ),
36431
- !readOnly && /* @__PURE__ */ jsxs(Box, { className: "flex flex-col gap-2 sm:flex-row", children: [
36432
- /* @__PURE__ */ jsx(
36433
- Input,
36434
- {
36435
- inputType: "url",
36436
- value: url,
36437
- placeholder: "https://example.com/image.png",
36438
- "aria-label": t("richBlockEditor.aria.imageUrl"),
36439
- className: cn(
36440
- "h-8 flex-1 rounded-sm border border-border bg-background",
36441
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
36442
- ),
36443
- onChange: (e) => setMetadata("url", e.target.value)
36444
- }
36445
- ),
36446
- /* @__PURE__ */ jsx(
36447
- Input,
36448
- {
36449
- inputType: "text",
36450
- value: caption,
36451
- placeholder: t("richBlockEditor.placeholder.caption"),
36452
- "aria-label": t("richBlockEditor.aria.imageCaption"),
36453
- className: cn(
36454
- "h-8 flex-1 rounded-sm border border-border bg-background",
36455
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
36456
- ),
36457
- onChange: (e) => setMetadata("caption", e.target.value)
36458
- }
36459
- )
36460
- ] }),
36461
- readOnly && caption && /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-center text-muted-foreground", children: caption })
36260
+ document.addEventListener("selectionchange", onSelectionChange);
36261
+ return () => document.removeEventListener("selectionchange", onSelectionChange);
36262
+ }, [readOnly, showToolbar]);
36263
+ const tryInlineMarkdown = useCallback(() => {
36264
+ const root = ref.current;
36265
+ if (!root) return false;
36266
+ const sel = window.getSelection();
36267
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
36268
+ const node = sel.anchorNode;
36269
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
36270
+ if (node.parentElement?.closest("code, pre")) return false;
36271
+ const text = (node.textContent ?? "").slice(0, sel.anchorOffset);
36272
+ for (const rule of MD_INLINE_RULES) {
36273
+ const m = rule.pattern.exec(text);
36274
+ if (!m) continue;
36275
+ const inner = rule.wrap === "em" ? m[2] : m[1];
36276
+ if (!inner || !inner.trim()) continue;
36277
+ const lead = rule.wrap === "em" ? m[1]?.length ?? 0 : 0;
36278
+ const range = document.createRange();
36279
+ range.setStart(node, m.index + lead);
36280
+ range.setEnd(node, sel.anchorOffset);
36281
+ const el = document.createElement(rule.wrap);
36282
+ el.textContent = inner;
36283
+ range.deleteContents();
36284
+ range.insertNode(el);
36285
+ const after = document.createTextNode("\u200B");
36286
+ el.parentNode?.insertBefore(after, el.nextSibling);
36287
+ const caret = document.createRange();
36288
+ caret.setStart(after, 1);
36289
+ caret.collapse(true);
36290
+ sel.removeAllRanges();
36291
+ sel.addRange(caret);
36292
+ return true;
36293
+ }
36294
+ return false;
36295
+ }, []);
36296
+ const tryBlockMarkdown = useCallback(() => {
36297
+ const root = ref.current;
36298
+ if (!root) return false;
36299
+ const sel = window.getSelection();
36300
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
36301
+ const node = sel.anchorNode;
36302
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
36303
+ const block = closestBlock(node, root);
36304
+ if (block ? block.firstChild !== node : root.firstChild !== node) return false;
36305
+ const prefix = (node.textContent ?? "").slice(0, sel.anchorOffset);
36306
+ const rule = MD_BLOCK_PREFIXES[prefix];
36307
+ if (!rule) return false;
36308
+ if (node.parentElement?.closest("pre, code")) return false;
36309
+ if (rule.kind === "list" && node.parentElement?.closest("li")) return false;
36310
+ node.deleteData(0, prefix.length);
36311
+ const caret = document.createRange();
36312
+ caret.setStart(node, 0);
36313
+ caret.collapse(true);
36314
+ sel.removeAllRanges();
36315
+ sel.addRange(caret);
36316
+ if (rule.kind === "list") {
36317
+ const container = block ?? node.parentElement;
36318
+ const list = document.createElement(rule.command === "insertOrderedList" ? "ol" : "ul");
36319
+ const li = document.createElement("li");
36320
+ if (block) {
36321
+ while (block.firstChild) li.appendChild(block.firstChild);
36322
+ list.appendChild(li);
36323
+ block.replaceWith(list);
36324
+ } else if (container) {
36325
+ li.appendChild(node);
36326
+ list.appendChild(li);
36327
+ container.appendChild(list);
36328
+ }
36329
+ if (!li.firstChild) li.appendChild(document.createElement("br"));
36330
+ const liCaret = document.createRange();
36331
+ liCaret.selectNodeContents(li);
36332
+ liCaret.collapse(true);
36333
+ sel.removeAllRanges();
36334
+ sel.addRange(liCaret);
36335
+ } else {
36336
+ document.execCommand("formatBlock", false, rule.tag);
36337
+ }
36338
+ return true;
36339
+ }, []);
36340
+ const afterEdit = useCallback(() => {
36341
+ const el = ref.current;
36342
+ if (!el) return;
36343
+ tryInlineMarkdown();
36344
+ setEmpty(htmlIsEmpty(el));
36345
+ setToolbar(readToolbarState(el));
36346
+ scheduleEmit();
36347
+ }, [scheduleEmit, tryInlineMarkdown]);
36348
+ const handleKeyDown = useCallback((e) => {
36349
+ if (e.key === " " && tryBlockMarkdown()) {
36350
+ e.preventDefault();
36351
+ afterEdit();
36352
+ }
36353
+ }, [afterEdit, tryBlockMarkdown]);
36354
+ const execInline = useCallback((command) => {
36355
+ document.execCommand(command, false);
36356
+ afterEdit();
36357
+ }, [afterEdit]);
36358
+ const execBlock = useCallback((tag) => {
36359
+ const next = toolbar.block === tag ? "p" : tag;
36360
+ document.execCommand("formatBlock", false, next);
36361
+ afterEdit();
36362
+ }, [afterEdit, toolbar.block]);
36363
+ const execList = useCallback((command) => {
36364
+ document.execCommand(command, false);
36365
+ afterEdit();
36366
+ }, [afterEdit]);
36367
+ const execLink = useCallback(() => {
36368
+ if (toolbar.link) {
36369
+ document.execCommand("unlink", false);
36370
+ afterEdit();
36371
+ return;
36372
+ }
36373
+ const url = window.prompt(t("richTextEditor.linkPrompt"));
36374
+ if (!url) return;
36375
+ const safe = safeUrl(url, ["http://", "https://", "mailto:"]) ?? `https://${url}`;
36376
+ document.execCommand("createLink", false, safe);
36377
+ afterEdit();
36378
+ }, [afterEdit, t, toolbar.link]);
36379
+ const execRule = useCallback(() => {
36380
+ document.execCommand("insertHorizontalRule", false);
36381
+ afterEdit();
36382
+ }, [afterEdit]);
36383
+ if (readOnly) {
36384
+ return /* @__PURE__ */ jsxs(Box, { className: cn("almadar-rich-text max-w-none", className), children: [
36385
+ /* @__PURE__ */ jsx(RichTextStyles, {}),
36386
+ /* @__PURE__ */ jsx(Box, { dangerouslySetInnerHTML: { __html: sanitizeRichHtml(value ?? "") } })
36462
36387
  ] });
36463
36388
  }
36464
- case "bullet-list":
36465
- case "numbered-list": {
36466
- const items = block.children ?? [];
36467
- return /* @__PURE__ */ jsxs(
36389
+ return /* @__PURE__ */ jsxs(Box, { className: cn("flex flex-col gap-2", className), children: [
36390
+ /* @__PURE__ */ jsx(RichTextStyles, {}),
36391
+ showToolbar && /* @__PURE__ */ jsxs(
36468
36392
  Box,
36469
36393
  {
36470
- as: block.type === "bullet-list" ? "ul" : "ol",
36394
+ role: "toolbar",
36395
+ "aria-label": t("richTextEditor.editorToolbar"),
36471
36396
  className: cn(
36472
- "space-y-1 pl-6",
36473
- block.type === "bullet-list" ? "list-disc" : "list-decimal"
36397
+ "sticky top-0 z-10 flex flex-wrap items-center gap-0.5 self-start",
36398
+ "rounded-md border border-border bg-background/95 px-1 py-0.5 shadow-sm"
36474
36399
  ),
36475
36400
  children: [
36476
- items.map((child) => /* @__PURE__ */ jsxs(Box, { as: "li", className: "group/item flex items-start gap-2", children: [
36477
- /* @__PURE__ */ jsx(
36478
- Editable,
36479
- {
36480
- tag: "span",
36481
- value: child.content ?? "",
36482
- readOnly,
36483
- placeholder: t("richBlockEditor.placeholder.listItem"),
36484
- ariaLabel: t("richBlockEditor.aria.listItem"),
36485
- className: "inline-block min-w-[1ch] flex-1",
36486
- onValueChange: (next) => setChildContent(child.id, next)
36487
- }
36488
- ),
36489
- !readOnly && showAffordances && /* @__PURE__ */ jsx(
36490
- Button,
36491
- {
36492
- type: "button",
36493
- variant: "ghost",
36494
- "aria-label": t("richBlockEditor.aria.removeListItem"),
36495
- className: cn(
36496
- "h-5 w-5 shrink-0 rounded-sm text-muted-foreground p-0 gap-0",
36497
- "opacity-0 group-hover/item:opacity-100 hover:bg-muted hover:text-foreground"
36498
- ),
36499
- onClick: () => removeListItem(child.id),
36500
- children: /* @__PURE__ */ jsx(Icon, { name: "trash", className: "w-3 h-3" })
36501
- }
36502
- )
36503
- ] }, child.id)),
36504
- !readOnly && showAffordances && /* @__PURE__ */ jsx(Box, { as: "li", className: "list-none pl-0", children: /* @__PURE__ */ jsxs(
36505
- Button,
36506
- {
36507
- type: "button",
36508
- variant: "ghost",
36509
- className: cn(
36510
- "inline-flex items-center gap-1 text-xs text-muted-foreground",
36511
- "hover:text-foreground p-0 h-auto"
36512
- ),
36513
- onClick: addListItem,
36514
- children: [
36515
- /* @__PURE__ */ jsx(Icon, { name: "plus", className: "w-3 h-3" }),
36516
- " ",
36517
- t("richBlockEditor.addItem")
36518
- ]
36519
- }
36520
- ) })
36401
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Bold, label: t("richTextEditor.bold"), active: toolbar.bold, onExec: () => execInline("bold") }),
36402
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Italic, label: t("richTextEditor.italic"), active: toolbar.italic, onExec: () => execInline("italic") }),
36403
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Underline, label: t("richTextEditor.underline"), active: toolbar.underline, onExec: () => execInline("underline") }),
36404
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Strikethrough, label: t("richTextEditor.strikethrough"), active: toolbar.strikeThrough, onExec: () => execInline("strikeThrough") }),
36405
+ /* @__PURE__ */ jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
36406
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Heading1, label: t("richTextEditor.heading1"), active: toolbar.block === "h1", onExec: () => execBlock("h1") }),
36407
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Heading2, label: t("richTextEditor.heading2"), active: toolbar.block === "h2", onExec: () => execBlock("h2") }),
36408
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Heading3, label: t("richTextEditor.heading3"), active: toolbar.block === "h3", onExec: () => execBlock("h3") }),
36409
+ /* @__PURE__ */ jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
36410
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: List, label: t("richTextEditor.bulletList"), active: toolbar.bullets, onExec: () => execList("insertUnorderedList") }),
36411
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: ListOrdered, label: t("richTextEditor.numberedList"), active: toolbar.numbers, onExec: () => execList("insertOrderedList") }),
36412
+ /* @__PURE__ */ jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
36413
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Quote, label: t("richTextEditor.quote"), active: toolbar.block === "blockquote", onExec: () => execBlock("blockquote") }),
36414
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Code, label: t("richTextEditor.code"), active: toolbar.block === "pre", onExec: () => execBlock("pre") }),
36415
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Link$1, label: toolbar.link ? t("richTextEditor.removeLink") : t("richTextEditor.link"), active: toolbar.link, onExec: execLink }),
36416
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Minus, label: t("richTextEditor.divider"), onExec: execRule })
36521
36417
  ]
36522
36418
  }
36523
- );
36524
- }
36525
- case "paragraph":
36526
- default:
36527
- return /* @__PURE__ */ jsx(
36528
- Editable,
36419
+ ),
36420
+ /* @__PURE__ */ jsx(
36421
+ Box,
36529
36422
  {
36530
- tag: "p",
36531
- value: block.content ?? "",
36532
- readOnly,
36533
- placeholder: placeholder ?? t("richBlockEditor.placeholder.paragraph"),
36534
- ariaLabel: t("richBlockEditor.aria.paragraphBlock"),
36535
- className: "leading-7",
36536
- onValueChange: setContent
36423
+ ref,
36424
+ contentEditable: true,
36425
+ suppressContentEditableWarning: true,
36426
+ role: "textbox",
36427
+ "aria-multiline": "true",
36428
+ "aria-label": t("richTextEditor.editorSurface"),
36429
+ "data-placeholder": placeholder ?? t("richTextEditor.placeholder"),
36430
+ "data-empty": empty ? "true" : "false",
36431
+ className: cn(
36432
+ "almadar-rich-text max-w-none min-h-[8rem] outline-none",
36433
+ "data-[empty=true]:before:content-[attr(data-placeholder)]",
36434
+ "data-[empty=true]:before:text-muted-foreground/60",
36435
+ "data-[empty=true]:before:pointer-events-none",
36436
+ "data-[empty=true]:before:absolute",
36437
+ "relative cursor-text"
36438
+ ),
36439
+ onInput: afterEdit,
36440
+ onKeyDown: handleKeyDown,
36441
+ onBlur: flushEmit
36537
36442
  }
36538
- );
36539
- }
36443
+ )
36444
+ ] });
36445
+ };
36446
+ RichTextEditor.displayName = "RichTextEditor";
36447
+ }
36448
+ });
36449
+ function renderIconInput4(icon, props) {
36450
+ return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
36451
+ }
36452
+ function DocumentPanel({
36453
+ id,
36454
+ title,
36455
+ subtitle,
36456
+ value,
36457
+ actions,
36458
+ maxInlineActions,
36459
+ editing = false,
36460
+ titleCommitEvent,
36461
+ contentChangeEvent,
36462
+ editEvent,
36463
+ doneEvent,
36464
+ editLabel,
36465
+ doneLabel,
36466
+ autosaveHint,
36467
+ placeholder,
36468
+ className
36469
+ }) {
36470
+ const eventBus = useEventBus();
36471
+ const { t } = useTranslate();
36472
+ const [titleDraft, setTitleDraft] = useState(null);
36473
+ const recordId = id ?? "";
36474
+ const actionDefs = actions ?? [];
36475
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
36476
+ const inlineActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
36477
+ const menuActions = actionDefs.filter((a) => !inlineActions.includes(a));
36478
+ const fireAction = (action) => {
36479
+ if (!action.event) return;
36480
+ eventBus.emit(`UI:${action.event}`, { id: recordId, title: title ?? "" });
36540
36481
  };
36541
- return /* @__PURE__ */ jsxs(
36482
+ const commitTitle = () => {
36483
+ if (!titleCommitEvent) return;
36484
+ const next = (titleDraft ?? "").trim();
36485
+ setTitleDraft(null);
36486
+ if (!next || next === title) return;
36487
+ eventBus.emit(`UI:${titleCommitEvent}`, { title: next, id: recordId });
36488
+ };
36489
+ const untitled = t("documentPanel.untitled") || "Untitled";
36490
+ const titleNode = titleCommitEvent && titleDraft !== null ? /* @__PURE__ */ jsx(
36491
+ Input,
36492
+ {
36493
+ value: titleDraft,
36494
+ autoFocus: true,
36495
+ "aria-label": t("common.title"),
36496
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
36497
+ onFocus: (e) => e.currentTarget.select(),
36498
+ onChange: (e) => setTitleDraft(e.target.value),
36499
+ onBlur: commitTitle,
36500
+ onKeyDown: (e) => {
36501
+ if (e.key === "Enter") {
36502
+ e.preventDefault();
36503
+ commitTitle();
36504
+ } else if (e.key === "Escape") {
36505
+ e.preventDefault();
36506
+ setTitleDraft(null);
36507
+ }
36508
+ },
36509
+ "data-testid": "document-title-input"
36510
+ }
36511
+ ) : titleCommitEvent ? /* @__PURE__ */ jsx(
36542
36512
  Box,
36543
36513
  {
36544
- className: cn(
36545
- "group relative flex items-start gap-2 rounded-sm",
36546
- "px-2 py-1 hover:bg-muted/30"
36547
- ),
36548
- "data-block-id": block.id,
36549
- "data-block-type": block.type,
36550
- children: [
36551
- !readOnly && showAffordances && /* @__PURE__ */ jsx(Box, { className: "flex w-8 shrink-0 items-center pt-1", children: /* @__PURE__ */ jsx(
36552
- BlockMenu,
36514
+ role: "button",
36515
+ tabIndex: 0,
36516
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
36517
+ onClick: () => setTitleDraft(title ?? ""),
36518
+ onKeyDown: (e) => {
36519
+ if (e.key === "Enter" || e.key === " ") {
36520
+ e.preventDefault();
36521
+ setTitleDraft(title ?? "");
36522
+ }
36523
+ },
36524
+ "data-testid": "document-title-editable",
36525
+ children: /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", className: cn(!title && "text-muted-foreground"), children: title || untitled })
36526
+ }
36527
+ ) : /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", children: title || untitled });
36528
+ const emitWithId = (event) => () => {
36529
+ if (event) eventBus.emit(`UI:${event}`, { id: recordId });
36530
+ };
36531
+ return /* @__PURE__ */ jsx(Card, { variant: "elevated", className: cn("w-full", className), children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", className: "p-6 sm:p-8", children: [
36532
+ /* @__PURE__ */ jsxs(HStack, { gap: "sm", className: "justify-between items-start", children: [
36533
+ /* @__PURE__ */ jsxs(VStack, { gap: "xs", className: "flex-1 min-w-0", children: [
36534
+ titleNode,
36535
+ subtitle ? /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: subtitle }) : null
36536
+ ] }),
36537
+ /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "flex-shrink-0 pt-1 items-center", children: editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
36538
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", className: "hidden sm:block", children: autosaveHint ?? (t("documentPanel.autosaveHint") || "") }),
36539
+ /* @__PURE__ */ jsx(Button, { variant: "primary", size: "sm", onClick: emitWithId(doneEvent), "data-testid": "document-done", children: doneLabel ?? (t("documentPanel.done") || "Done") })
36540
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
36541
+ editEvent && /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: emitWithId(editEvent), "data-testid": "document-edit", children: [
36542
+ /* @__PURE__ */ jsx(Icon, { name: "edit", size: "xs", className: "mr-1" }),
36543
+ editLabel ?? (t("documentPanel.edit") || "Edit")
36544
+ ] }),
36545
+ inlineActions.map((action, idx) => /* @__PURE__ */ jsxs(
36546
+ Button,
36553
36547
  {
36554
- block,
36555
- readOnly,
36556
- onDelete,
36557
- onDuplicate,
36558
- onChangeType
36548
+ variant: "primary",
36549
+ size: "sm",
36550
+ onClick: () => fireAction(action),
36551
+ "data-testid": `action-${action.event}`,
36552
+ children: [
36553
+ action.icon && renderIconInput4(action.icon, { size: "xs", className: "mr-1" }),
36554
+ action.label
36555
+ ]
36556
+ },
36557
+ idx
36558
+ )),
36559
+ menuActions.length > 0 && /* @__PURE__ */ jsx(
36560
+ Menu,
36561
+ {
36562
+ position: "bottom-end",
36563
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
36564
+ items: menuActions.map((action) => ({
36565
+ label: action.label,
36566
+ icon: action.icon,
36567
+ variant: action.variant === "danger" ? "danger" : "default",
36568
+ onClick: () => fireAction(action)
36569
+ }))
36559
36570
  }
36560
- ) }),
36561
- /* @__PURE__ */ jsx(Box, { className: "min-w-0 flex-1", children: renderBody() })
36562
- ]
36563
- }
36564
- );
36571
+ )
36572
+ ] }) })
36573
+ ] }),
36574
+ editing ? /* @__PURE__ */ jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsx(
36575
+ Box,
36576
+ {
36577
+ onClick: emitWithId(editEvent),
36578
+ className: cn(
36579
+ "rounded-md px-1 -mx-1 min-h-[16rem]",
36580
+ editEvent && "cursor-text transition-colors hover:bg-muted/30"
36581
+ ),
36582
+ "data-testid": "document-body",
36583
+ children: value && value !== "" ? /* @__PURE__ */ jsx(RichTextEditor, { value, readOnly: true }) : /* @__PURE__ */ jsx(Typography, { variant: "body", color: "muted", children: placeholder ?? (t("documentPanel.placeholder") || "") })
36584
+ }
36585
+ )
36586
+ ] }) });
36565
36587
  }
36566
- var TOOLBAR_ENTRIES, BLOCK_TYPE_LABEL_KEY, CHANGEABLE_TYPES, _idSeq, BLOCK_TYPES, RichBlockEditor;
36567
- var init_RichBlockEditor = __esm({
36568
- "components/core/molecules/RichBlockEditor.tsx"() {
36588
+ var init_DocumentPanel = __esm({
36589
+ "components/core/molecules/DocumentPanel.tsx"() {
36569
36590
  "use client";
36570
36591
  init_cn();
36571
- init_Card();
36592
+ init_useEventBus();
36593
+ init_Box();
36594
+ init_Stack();
36572
36595
  init_Typography();
36573
36596
  init_Button();
36574
- init_Box();
36575
- init_Divider();
36576
- init_Input();
36597
+ init_Card();
36577
36598
  init_Icon();
36578
- init_useEventBus();
36579
- TOOLBAR_ENTRIES = [
36580
- { type: "paragraph", labelKey: "richBlockEditor.toolbar.text", icon: Type },
36581
- { type: "heading-1", labelKey: "richBlockEditor.toolbar.h1", icon: Heading1 },
36582
- { type: "heading-2", labelKey: "richBlockEditor.toolbar.h2", icon: Heading2 },
36583
- { type: "heading-3", labelKey: "richBlockEditor.toolbar.h3", icon: Heading3 },
36584
- { type: "bullet-list", labelKey: "richBlockEditor.toolbar.bulletList", icon: List },
36585
- { type: "numbered-list", labelKey: "richBlockEditor.toolbar.numbered", icon: ListOrdered },
36586
- { type: "quote", labelKey: "richBlockEditor.toolbar.quote", icon: Quote },
36587
- { type: "code", labelKey: "richBlockEditor.toolbar.code", icon: Code },
36588
- { type: "divider", labelKey: "richBlockEditor.toolbar.divider", icon: Minus },
36589
- { type: "image", labelKey: "richBlockEditor.toolbar.image", icon: Image$1 }
36590
- ];
36591
- BLOCK_TYPE_LABEL_KEY = {
36592
- paragraph: "richBlockEditor.blockType.paragraph",
36593
- "heading-1": "richBlockEditor.blockType.heading1",
36594
- "heading-2": "richBlockEditor.blockType.heading2",
36595
- "heading-3": "richBlockEditor.blockType.heading3",
36596
- "bullet-list": "richBlockEditor.blockType.bulletList",
36597
- "numbered-list": "richBlockEditor.blockType.numberedList",
36598
- quote: "richBlockEditor.blockType.quote",
36599
- code: "richBlockEditor.blockType.code",
36600
- divider: "richBlockEditor.blockType.divider",
36601
- image: "richBlockEditor.blockType.image"
36602
- };
36603
- CHANGEABLE_TYPES = [
36604
- "paragraph",
36605
- "heading-1",
36606
- "heading-2",
36607
- "heading-3",
36608
- "bullet-list",
36609
- "numbered-list",
36610
- "quote",
36611
- "code"
36612
- ];
36613
- _idSeq = 0;
36614
- BLOCK_TYPES = /* @__PURE__ */ new Set([
36615
- "paragraph",
36616
- "heading-1",
36617
- "heading-2",
36618
- "heading-3",
36619
- "bullet-list",
36620
- "numbered-list",
36621
- "quote",
36622
- "code",
36623
- "divider",
36624
- "image"
36625
- ]);
36626
- RichBlockEditor = ({
36627
- initialBlocks,
36628
- onChange,
36629
- changeEvent,
36630
- readOnly = false,
36631
- placeholder,
36632
- enableBlocks = true,
36633
- showToolbar = true,
36634
- className
36635
- }) => {
36636
- const { t } = useTranslate();
36637
- const [blocks, setBlocks] = useState(
36638
- () => normalizeBlocks(initialBlocks)
36639
- );
36640
- const onChangeRef = useRef(onChange);
36641
- useEffect(() => {
36642
- onChangeRef.current = onChange;
36643
- }, [onChange]);
36644
- const eventBus = useEventBus();
36645
- const changeEventRef = useRef(changeEvent);
36646
- useEffect(() => {
36647
- changeEventRef.current = changeEvent;
36648
- }, [changeEvent]);
36649
- const commit = useCallback((next) => {
36650
- setBlocks(next);
36651
- onChangeRef.current?.(next);
36652
- const evt = changeEventRef.current;
36653
- if (evt) eventBus.emit(`UI:${evt}`, { blocks: next });
36654
- }, [eventBus]);
36655
- const handleAppend = useCallback(
36656
- (type) => {
36657
- if (readOnly) return;
36658
- commit([...blocks, createBlock(type)]);
36659
- },
36660
- [blocks, commit, readOnly]
36661
- );
36662
- const handleUpdate = useCallback(
36663
- (id, updater) => {
36664
- commit(replaceBlock(blocks, id, updater));
36665
- },
36666
- [blocks, commit]
36667
- );
36668
- const handleDelete = useCallback(
36669
- (id) => {
36670
- const next = removeBlock(blocks, id);
36671
- commit(next.length > 0 ? next : [createBlock("paragraph")]);
36672
- },
36673
- [blocks, commit]
36674
- );
36675
- const handleDuplicate = useCallback(
36676
- (id) => {
36677
- const target = blocks.find((b) => b.id === id);
36678
- if (!target) return;
36679
- commit(insertAfter(blocks, id, duplicateBlock(target)));
36680
- },
36681
- [blocks, commit]
36599
+ init_Input();
36600
+ init_Menu();
36601
+ init_RichTextEditor();
36602
+ }
36603
+ });
36604
+ function renderIconInput5(icon, props) {
36605
+ return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
36606
+ }
36607
+ function fieldName(field) {
36608
+ return field.name ?? field.key ?? "";
36609
+ }
36610
+ function relationLabel(value) {
36611
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Date) return null;
36612
+ for (const key of ["name", "title", "label"]) {
36613
+ const candidate = value[key];
36614
+ if (typeof candidate === "string" && candidate !== "") return candidate;
36615
+ }
36616
+ const id = value.id;
36617
+ return id !== void 0 && id !== null ? String(id) : null;
36618
+ }
36619
+ function fieldKind(field, value) {
36620
+ if (field.kind) return field.kind;
36621
+ if (field.options && field.options.length > 0) return "select";
36622
+ if (typeof value === "boolean" || field.format === "boolean") return "boolean";
36623
+ if (Array.isArray(value) || relationLabel(value ?? null) !== null) return "readonly";
36624
+ return "text";
36625
+ }
36626
+ function DocumentDetails({
36627
+ entity,
36628
+ fields,
36629
+ metaCommitEvent,
36630
+ title,
36631
+ className
36632
+ }) {
36633
+ const eventBus = useEventBus();
36634
+ const { t } = useTranslate();
36635
+ const [fieldDraft, setFieldDraft] = useState(null);
36636
+ const recordId = entity?.id !== void 0 && entity?.id !== null ? String(entity.id) : "";
36637
+ const fieldDefs = (fields ?? []).filter((f3) => fieldName(f3) !== "");
36638
+ if (fieldDefs.length === 0) return null;
36639
+ const commitField = (name, next, current) => {
36640
+ setFieldDraft(null);
36641
+ if (!metaCommitEvent || next === current) return;
36642
+ eventBus.emit(`UI:${metaCommitEvent}`, { id: recordId, patch: { id: recordId, [name]: next } });
36643
+ };
36644
+ const renderValue = (field) => {
36645
+ const name = fieldName(field);
36646
+ const raw = getNestedValue(entity ?? {}, name);
36647
+ const kind = fieldKind(field, raw);
36648
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
36649
+ if (kind === "boolean") {
36650
+ return /* @__PURE__ */ jsx(
36651
+ Switch,
36652
+ {
36653
+ checked: Boolean(raw),
36654
+ disabled: !metaCommitEvent,
36655
+ "aria-label": label,
36656
+ onChange: (checked) => commitField(name, checked, Boolean(raw))
36657
+ }
36682
36658
  );
36683
- const handleChangeType = useCallback(
36684
- (id, type) => {
36685
- commit(
36686
- replaceBlock(blocks, id, (b) => changeBlockType(b, type))
36687
- );
36688
- },
36689
- [blocks, commit]
36659
+ }
36660
+ if (kind === "select") {
36661
+ return /* @__PURE__ */ jsx(
36662
+ Select,
36663
+ {
36664
+ value: raw !== void 0 && raw !== null ? String(raw) : "",
36665
+ disabled: !metaCommitEvent,
36666
+ "aria-label": label,
36667
+ className: "h-8 w-full",
36668
+ options: (field.options ?? []).map((opt) => ({ value: opt, label: humanizeEnumValue(opt) })),
36669
+ onChange: (e) => commitField(name, e.target.value, raw)
36670
+ }
36690
36671
  );
36691
- return /* @__PURE__ */ jsxs(
36692
- Card,
36672
+ }
36673
+ if (kind === "readonly" || !metaCommitEvent) {
36674
+ if (Array.isArray(raw)) {
36675
+ if (raw.length === 0) {
36676
+ return /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
36677
+ }
36678
+ return /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "flex-wrap", children: raw.map((item, i) => /* @__PURE__ */ jsx(Badge, { variant: "default", children: relationLabel(item) ?? humanizeEnumValue(String(item)) }, i)) });
36679
+ }
36680
+ const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
36681
+ return /* @__PURE__ */ jsx(Typography, { variant: "small", className: "break-words", children: shown2 });
36682
+ }
36683
+ if (fieldDraft?.name === name) {
36684
+ return /* @__PURE__ */ jsx(
36685
+ Input,
36693
36686
  {
36694
- variant: "bordered",
36695
- padding: "none",
36696
- className: cn("flex flex-col text-card-foreground", className),
36697
- children: [
36698
- enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsx(
36699
- Box,
36700
- {
36701
- role: "toolbar",
36702
- "aria-label": t("richBlockEditor.editorToolbar"),
36703
- className: cn(
36704
- "flex flex-wrap items-center gap-1",
36705
- "border-b border-border bg-muted/30 px-2 py-2"
36706
- ),
36707
- children: TOOLBAR_ENTRIES.map((entry) => {
36708
- const Icon2 = entry.icon;
36709
- const entryLabel = t(entry.labelKey);
36710
- return /* @__PURE__ */ jsxs(
36711
- Button,
36712
- {
36713
- type: "button",
36714
- variant: "ghost",
36715
- size: "sm",
36716
- "aria-label": t("richBlockEditor.insertEntry", { label: entryLabel }),
36717
- title: entryLabel,
36718
- onClick: () => handleAppend(entry.type),
36719
- children: [
36720
- /* @__PURE__ */ jsx(Icon2, { size: 14 }),
36721
- /* @__PURE__ */ jsx(Typography, { as: "span", variant: "caption", className: "ml-1 hidden text-xs sm:inline", children: entryLabel })
36722
- ]
36723
- },
36724
- entry.type
36725
- );
36726
- })
36727
- }
36728
- ),
36729
- /* @__PURE__ */ jsx(Box, { className: "flex flex-col gap-1 px-3 py-3", children: blocks.map((block) => /* @__PURE__ */ jsx(
36730
- BlockRow,
36731
- {
36732
- block,
36733
- readOnly,
36734
- showAffordances: enableBlocks,
36735
- placeholder,
36736
- onUpdate: (updater) => handleUpdate(block.id, updater),
36737
- onDelete: () => handleDelete(block.id),
36738
- onDuplicate: () => handleDuplicate(block.id),
36739
- onChangeType: (type) => handleChangeType(block.id, type)
36740
- },
36741
- block.id
36742
- )) })
36743
- ]
36687
+ value: fieldDraft.value,
36688
+ autoFocus: true,
36689
+ "aria-label": label,
36690
+ className: "h-8 w-full",
36691
+ onChange: (e) => setFieldDraft({ name, value: e.target.value }),
36692
+ onBlur: () => commitField(name, fieldDraft.value.trim(), raw),
36693
+ onKeyDown: (e) => {
36694
+ if (e.key === "Enter") {
36695
+ e.preventDefault();
36696
+ commitField(name, fieldDraft.value.trim(), raw);
36697
+ } else if (e.key === "Escape") {
36698
+ e.preventDefault();
36699
+ setFieldDraft(null);
36700
+ }
36701
+ },
36702
+ "data-testid": `document-property-input-${name}`
36744
36703
  }
36745
36704
  );
36746
- };
36747
- RichBlockEditor.displayName = "RichBlockEditor";
36705
+ }
36706
+ const shown = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
36707
+ return /* @__PURE__ */ jsx(
36708
+ Box,
36709
+ {
36710
+ role: "button",
36711
+ tabIndex: 0,
36712
+ className: cn(
36713
+ "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
36714
+ (raw === void 0 || raw === null || raw === "") && "text-muted-foreground"
36715
+ ),
36716
+ onClick: () => setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" }),
36717
+ onKeyDown: (e) => {
36718
+ if (e.key === "Enter" || e.key === " ") {
36719
+ e.preventDefault();
36720
+ setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" });
36721
+ }
36722
+ },
36723
+ "data-testid": `document-property-${name}`,
36724
+ children: /* @__PURE__ */ jsx(Typography, { variant: "small", className: "break-words", children: shown })
36725
+ }
36726
+ );
36727
+ };
36728
+ return /* @__PURE__ */ jsx(Card, { variant: "bordered", className: cn("w-full", className), children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", className: "p-4", children: [
36729
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "secondary", weight: "medium", className: "uppercase tracking-wide", children: title ?? (t("documentDetails.title") || "Details") }),
36730
+ /* @__PURE__ */ jsx(VStack, { gap: "sm", children: fieldDefs.map((field) => {
36731
+ const name = fieldName(field);
36732
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
36733
+ return /* @__PURE__ */ jsxs(VStack, { gap: "xs", children: [
36734
+ /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "items-center", children: [
36735
+ field.icon && renderIconInput5(field.icon, { size: "xs", className: "text-muted-foreground" }),
36736
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", children: label })
36737
+ ] }),
36738
+ /* @__PURE__ */ jsx(Box, { className: "min-w-0", children: renderValue(field) })
36739
+ ] }, name);
36740
+ }) })
36741
+ ] }) });
36742
+ }
36743
+ var init_DocumentDetails = __esm({
36744
+ "components/core/molecules/DocumentDetails.tsx"() {
36745
+ "use client";
36746
+ init_cn();
36747
+ init_format();
36748
+ init_getNestedValue();
36749
+ init_useEventBus();
36750
+ init_Box();
36751
+ init_Stack();
36752
+ init_Typography();
36753
+ init_Badge();
36754
+ init_Card();
36755
+ init_Icon();
36756
+ init_Input();
36757
+ init_Select();
36758
+ init_Switch();
36748
36759
  }
36749
36760
  });
36750
36761
  function collectInitiallyCollapsed(nodes, acc) {
@@ -41693,8 +41704,8 @@ var init_DataTable = __esm({
41693
41704
  DataTable.displayName = "DataTable";
41694
41705
  }
41695
41706
  });
41696
- function getBadgeVariant(fieldName, value) {
41697
- const name = fieldName.toLowerCase();
41707
+ function getBadgeVariant(fieldName2, value) {
41708
+ const name = fieldName2.toLowerCase();
41698
41709
  const val = String(value).toLowerCase();
41699
41710
  if (name.includes("status")) {
41700
41711
  if (val.includes("complete") || val.includes("done") || val.includes("active"))
@@ -41710,12 +41721,12 @@ function getBadgeVariant(fieldName, value) {
41710
41721
  }
41711
41722
  return "default";
41712
41723
  }
41713
- function formatFieldValue2(value, fieldName) {
41724
+ function formatFieldValue2(value, fieldName2) {
41714
41725
  if (typeof value === "number") {
41715
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
41726
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
41716
41727
  return `${value}%`;
41717
41728
  }
41718
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
41729
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
41719
41730
  return `$${value.toLocaleString()}`;
41720
41731
  }
41721
41732
  return value.toLocaleString();
@@ -41725,7 +41736,7 @@ function formatFieldValue2(value, fieldName) {
41725
41736
  }
41726
41737
  return String(value);
41727
41738
  }
41728
- function renderRichFieldValue(value, fieldName, fieldType, meta) {
41739
+ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
41729
41740
  if (value === void 0 || value === null) return "\u2014";
41730
41741
  const str = String(value);
41731
41742
  switch (fieldType) {
@@ -41736,7 +41747,7 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
41736
41747
  "img",
41737
41748
  {
41738
41749
  src: str,
41739
- alt: formatFieldLabel(fieldName),
41750
+ alt: formatFieldLabel(fieldName2),
41740
41751
  className: "max-w-full max-h-64 rounded-md object-contain",
41741
41752
  loading: "lazy"
41742
41753
  }
@@ -41871,9 +41882,9 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
41871
41882
  return /* @__PURE__ */ jsx("a", { href: `tel:${str}`, className: "text-primary hover:underline", children: str });
41872
41883
  default:
41873
41884
  if (meta?.values && meta.values.length > 0 && meta.values.includes(str)) {
41874
- return /* @__PURE__ */ jsx(Badge, { variant: getBadgeVariant(fieldName, str), children: humanizeEnumValue(str) });
41885
+ return /* @__PURE__ */ jsx(Badge, { variant: getBadgeVariant(fieldName2, str), children: humanizeEnumValue(str) });
41875
41886
  }
41876
- return formatFieldValue2(value, fieldName);
41887
+ return formatFieldValue2(value, fieldName2);
41877
41888
  }
41878
41889
  }
41879
41890
  function normalizeFieldDefs(fields) {
@@ -42716,7 +42727,21 @@ var init_Form = __esm({
42716
42727
  const normalizedInitialData = React85__default.useMemo(() => {
42717
42728
  const entityRowAsInitial = isPlainEntityRow(entity) ? entity : void 0;
42718
42729
  const callerInitial = initialData !== null && typeof initialData === "object" && !Array.isArray(initialData) ? initialData : {};
42719
- return entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
42730
+ const merged = entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
42731
+ const toId = (value) => {
42732
+ if (value === null || value === void 0 || typeof value !== "object" || value instanceof Date) return value;
42733
+ if (Array.isArray(value)) {
42734
+ return value.map(
42735
+ (item) => item !== null && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date) && item.id !== void 0 && item.id !== null ? String(item.id) : item
42736
+ );
42737
+ }
42738
+ return value.id !== void 0 && value.id !== null ? String(value.id) : value;
42739
+ };
42740
+ const normalized = {};
42741
+ for (const [key, value] of Object.entries(merged)) {
42742
+ normalized[key] = key === "id" ? value : toId(value);
42743
+ }
42744
+ return normalized;
42720
42745
  }, [entity, initialData]);
42721
42746
  const entityDerivedFields = React85__default.useMemo(() => {
42722
42747
  if (fields && fields.length > 0) return void 0;
@@ -42845,8 +42870,8 @@ var init_Form = __esm({
42845
42870
  checkViolations(name, newFormData);
42846
42871
  };
42847
42872
  const isFieldVisible = React85__default.useCallback(
42848
- (fieldName) => {
42849
- const condition = conditionalFields[fieldName];
42873
+ (fieldName2) => {
42874
+ const condition = conditionalFields[fieldName2];
42850
42875
  if (!condition) return true;
42851
42876
  return Boolean(evaluateFormExpression(condition, evalContext));
42852
42877
  },
@@ -42892,9 +42917,9 @@ var init_Form = __esm({
42892
42917
  };
42893
42918
  const handleInvalid = (e) => {
42894
42919
  const target = e.target;
42895
- const fieldName = target.getAttribute("data-field-name") ?? target.name ?? "";
42920
+ const fieldName2 = target.getAttribute("data-field-name") ?? target.name ?? "";
42896
42921
  const fieldMessage = target.validationMessage || "Invalid value";
42897
- debug("forms", "invalid", { mode: formMode, fieldName, fieldMessage });
42922
+ debug("forms", "invalid", { mode: formMode, fieldName: fieldName2, fieldMessage });
42898
42923
  queueMicrotask(() => {
42899
42924
  const form = formRef.current;
42900
42925
  if (!form) return;
@@ -42930,22 +42955,22 @@ var init_Form = __esm({
42930
42955
  };
42931
42956
  const renderField = React85__default.useCallback(
42932
42957
  (field) => {
42933
- const fieldName = field.name || field.field;
42934
- if (!fieldName) return null;
42935
- if (!isFieldVisible(fieldName)) {
42958
+ const fieldName2 = field.name || field.field;
42959
+ if (!fieldName2) return null;
42960
+ if (!isFieldVisible(fieldName2)) {
42936
42961
  return null;
42937
42962
  }
42938
42963
  const inputType = determineInputType(field);
42939
- const label = field.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/([A-Z])/g, " $1");
42940
- const currentValue = formData[fieldName] ?? field.defaultValue ?? "";
42941
- return /* @__PURE__ */ jsxs(VStack, { gap: "xs", "data-field": fieldName, children: [
42964
+ const label = field.label || fieldName2.charAt(0).toUpperCase() + fieldName2.slice(1).replace(/([A-Z])/g, " $1");
42965
+ const currentValue = formData[fieldName2] ?? field.defaultValue ?? "";
42966
+ return /* @__PURE__ */ jsxs(VStack, { gap: "xs", "data-field": fieldName2, children: [
42942
42967
  inputType !== "checkbox" && /* @__PURE__ */ jsxs(Typography, { as: "label", variant: "label", weight: "bold", children: [
42943
42968
  label,
42944
42969
  field.required && /* @__PURE__ */ jsx(Typography, { as: "span", color: "error", className: "ml-1", children: "*" })
42945
42970
  ] }),
42946
- renderFieldInput(field, fieldName, inputType, currentValue, label),
42971
+ renderFieldInput(field, fieldName2, inputType, currentValue, label),
42947
42972
  field.hint && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", children: field.hint })
42948
- ] }, fieldName);
42973
+ ] }, fieldName2);
42949
42974
  },
42950
42975
  [formData, isFieldVisible, relationsData, relationsLoading, isLoading]
42951
42976
  );
@@ -42974,8 +42999,8 @@ var init_Form = __esm({
42974
42999
  }
42975
43000
  return field;
42976
43001
  }).map((field) => {
42977
- const fieldName = field.name || field.field;
42978
- const override = fieldOverrides?.find((o) => o.name === fieldName);
43002
+ const fieldName2 = field.name || field.field;
43003
+ const override = fieldOverrides?.find((o) => o.name === fieldName2);
42979
43004
  if (!override) return field;
42980
43005
  return {
42981
43006
  ...field,
@@ -43031,11 +43056,11 @@ var init_Form = __esm({
43031
43056
  ] }, section.id);
43032
43057
  }).filter(Boolean);
43033
43058
  }, [sections, isSectionVisible, collapsedSections, renderField, gap]);
43034
- function renderFieldInput(field, fieldName, inputType, currentValue, label) {
43059
+ function renderFieldInput(field, fieldName2, inputType, currentValue, label) {
43035
43060
  const commonProps = {
43036
- id: fieldName,
43037
- name: fieldName,
43038
- "data-field-name": fieldName,
43061
+ id: fieldName2,
43062
+ name: fieldName2,
43063
+ "data-field-name": fieldName2,
43039
43064
  required: field.required,
43040
43065
  disabled: isLoading,
43041
43066
  placeholder: field.placeholder,
@@ -43049,7 +43074,7 @@ var init_Form = __esm({
43049
43074
  ...commonProps,
43050
43075
  label: label + (field.required ? " *" : ""),
43051
43076
  checked: Boolean(currentValue),
43052
- onChange: (e) => handleChange(fieldName, e.target.checked)
43077
+ onChange: (e) => handleChange(fieldName2, e.target.checked)
43053
43078
  }
43054
43079
  );
43055
43080
  case "textarea":
@@ -43058,7 +43083,7 @@ var init_Form = __esm({
43058
43083
  {
43059
43084
  ...commonProps,
43060
43085
  value: String(currentValue),
43061
- onChange: (e) => handleChange(fieldName, e.target.value),
43086
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43062
43087
  minLength: field.min,
43063
43088
  maxLength: field.max
43064
43089
  }
@@ -43071,14 +43096,14 @@ var init_Form = __esm({
43071
43096
  ...commonProps,
43072
43097
  options,
43073
43098
  value: String(currentValue),
43074
- onValueChange: (v) => handleChange(fieldName, v),
43099
+ onValueChange: (v) => handleChange(fieldName2, v),
43075
43100
  placeholder: field.placeholder || `Select ${label}...`
43076
43101
  }
43077
43102
  );
43078
43103
  }
43079
43104
  case "relation": {
43080
- const relationOptions = relationsData[fieldName] || [];
43081
- const relationLoading = relationsLoading[fieldName] || false;
43105
+ const relationOptions = relationsData[fieldName2] || [];
43106
+ const relationLoading = relationsLoading[fieldName2] || false;
43082
43107
  if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
43083
43108
  const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
43084
43109
  return /* @__PURE__ */ jsx(
@@ -43090,7 +43115,7 @@ var init_Form = __esm({
43090
43115
  clearable: true,
43091
43116
  options: [...relationOptions],
43092
43117
  value: selectedValues,
43093
- onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
43118
+ onValueChange: (value) => handleChange(fieldName2, Array.isArray(value) ? value : [value]),
43094
43119
  placeholder: field.placeholder || `Select ${label}...`
43095
43120
  }
43096
43121
  );
@@ -43100,7 +43125,7 @@ var init_Form = __esm({
43100
43125
  {
43101
43126
  ...commonProps,
43102
43127
  value: currentValue ? String(currentValue) : void 0,
43103
- onChange: (value) => handleChange(fieldName, value),
43128
+ onChange: (value) => handleChange(fieldName2, value),
43104
43129
  options: relationOptions,
43105
43130
  isLoading: relationLoading,
43106
43131
  placeholder: field.placeholder || `Select ${label}...`,
@@ -43117,7 +43142,7 @@ var init_Form = __esm({
43117
43142
  placeholder: field.placeholder,
43118
43143
  disabled: isLoading,
43119
43144
  value: arrayValue,
43120
- onChange: (next) => handleChange(fieldName, [...next])
43145
+ onChange: (next) => handleChange(fieldName2, [...next])
43121
43146
  }
43122
43147
  );
43123
43148
  }
@@ -43129,7 +43154,7 @@ var init_Form = __esm({
43129
43154
  type: "number",
43130
43155
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
43131
43156
  onChange: (e) => handleChange(
43132
- fieldName,
43157
+ fieldName2,
43133
43158
  e.target.value ? Number(e.target.value) : void 0
43134
43159
  ),
43135
43160
  min: field.min,
@@ -43146,7 +43171,7 @@ var init_Form = __esm({
43146
43171
  icon: DollarSign,
43147
43172
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
43148
43173
  onChange: (e) => handleChange(
43149
- fieldName,
43174
+ fieldName2,
43150
43175
  e.target.value ? Number(e.target.value) : void 0
43151
43176
  ),
43152
43177
  min: field.min,
@@ -43164,7 +43189,7 @@ var init_Form = __esm({
43164
43189
  const f3 = files[0];
43165
43190
  if (!f3) return;
43166
43191
  const reader = new FileReader();
43167
- reader.onload = () => handleChange(fieldName, {
43192
+ reader.onload = () => handleChange(fieldName2, {
43168
43193
  name: f3.name,
43169
43194
  mimeType: f3.type,
43170
43195
  sizeBytes: f3.size,
@@ -43181,7 +43206,7 @@ var init_Form = __esm({
43181
43206
  ...commonProps,
43182
43207
  type: "date",
43183
43208
  value: formatDateValue(currentValue),
43184
- onChange: (e) => handleChange(fieldName, e.target.value)
43209
+ onChange: (e) => handleChange(fieldName2, e.target.value)
43185
43210
  }
43186
43211
  );
43187
43212
  case "datetime-local":
@@ -43191,7 +43216,7 @@ var init_Form = __esm({
43191
43216
  ...commonProps,
43192
43217
  type: "datetime-local",
43193
43218
  value: formatDateTimeValue(currentValue),
43194
- onChange: (e) => handleChange(fieldName, e.target.value)
43219
+ onChange: (e) => handleChange(fieldName2, e.target.value)
43195
43220
  }
43196
43221
  );
43197
43222
  case "email":
@@ -43201,7 +43226,7 @@ var init_Form = __esm({
43201
43226
  ...commonProps,
43202
43227
  type: "email",
43203
43228
  value: String(currentValue),
43204
- onChange: (e) => handleChange(fieldName, e.target.value),
43229
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43205
43230
  minLength: field.min,
43206
43231
  maxLength: field.max
43207
43232
  }
@@ -43213,7 +43238,7 @@ var init_Form = __esm({
43213
43238
  ...commonProps,
43214
43239
  type: "url",
43215
43240
  value: String(currentValue),
43216
- onChange: (e) => handleChange(fieldName, e.target.value),
43241
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43217
43242
  minLength: field.min,
43218
43243
  maxLength: field.max
43219
43244
  }
@@ -43225,7 +43250,7 @@ var init_Form = __esm({
43225
43250
  ...commonProps,
43226
43251
  type: "password",
43227
43252
  value: String(currentValue),
43228
- onChange: (e) => handleChange(fieldName, e.target.value),
43253
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43229
43254
  minLength: field.min,
43230
43255
  maxLength: field.max
43231
43256
  }
@@ -43238,7 +43263,7 @@ var init_Form = __esm({
43238
43263
  ...commonProps,
43239
43264
  type: "text",
43240
43265
  value: String(currentValue),
43241
- onChange: (e) => handleChange(fieldName, e.target.value),
43266
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43242
43267
  minLength: field.min,
43243
43268
  maxLength: field.max
43244
43269
  }
@@ -43595,7 +43620,7 @@ function entityFieldsFromListItem(item) {
43595
43620
  }
43596
43621
  return result;
43597
43622
  }
43598
- function getStatusStyle(fieldName, value) {
43623
+ function getStatusStyle(fieldName2, value) {
43599
43624
  const val = String(value).toLowerCase();
43600
43625
  if (val.includes("complete") || val.includes("done"))
43601
43626
  return STATUS_STYLES.complete;
@@ -43611,12 +43636,12 @@ function getStatusStyle(fieldName, value) {
43611
43636
  if (val.includes("low")) return STATUS_STYLES.low;
43612
43637
  return STATUS_STYLES.default;
43613
43638
  }
43614
- function formatValue3(value, fieldName) {
43639
+ function formatValue3(value, fieldName2) {
43615
43640
  if (typeof value === "number") {
43616
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
43641
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
43617
43642
  return `${value}%`;
43618
43643
  }
43619
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
43644
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
43620
43645
  return new Intl.NumberFormat("en-US", {
43621
43646
  style: "currency",
43622
43647
  currency: "USD",
@@ -43639,8 +43664,8 @@ function formatValue3(value, fieldName) {
43639
43664
  }
43640
43665
  return String(value);
43641
43666
  }
43642
- function formatFieldLabel2(fieldName) {
43643
- return humanizeFieldName(fieldName).replace(/\sId$/, "").trim();
43667
+ function formatFieldLabel2(fieldName2) {
43668
+ return humanizeFieldName(fieldName2).replace(/\sId$/, "").trim();
43644
43669
  }
43645
43670
  var STATUS_STYLES, StatusBadge, ProgressIndicator, List3;
43646
43671
  var init_List = __esm({
@@ -43708,9 +43733,9 @@ var init_List = __esm({
43708
43733
  };
43709
43734
  StatusBadge = ({
43710
43735
  value,
43711
- fieldName
43736
+ fieldName: fieldName2
43712
43737
  }) => {
43713
- const style = getStatusStyle(fieldName, value);
43738
+ const style = getStatusStyle(fieldName2, value);
43714
43739
  return /* @__PURE__ */ jsxs(
43715
43740
  Typography,
43716
43741
  {
@@ -46642,8 +46667,8 @@ var init_StatCard = __esm({
46642
46667
  return items.length;
46643
46668
  }
46644
46669
  if (field.includes(":")) {
46645
- const [fieldName, fieldValue] = field.split(":");
46646
- return items.filter((item) => item[fieldName] === fieldValue).length;
46670
+ const [fieldName2, fieldValue] = field.split(":");
46671
+ return items.filter((item) => item[fieldName2] === fieldValue).length;
46647
46672
  }
46648
46673
  const fieldExistsOnItems = items.some((item) => field in item);
46649
46674
  if (fieldExistsOnItems) {
@@ -48163,6 +48188,8 @@ var init_component_registry_generated = __esm({
48163
48188
  init_DocSearch();
48164
48189
  init_DocSidebar();
48165
48190
  init_DocTOC();
48191
+ init_DocumentDetails();
48192
+ init_DocumentPanel();
48166
48193
  init_DocumentViewer();
48167
48194
  init_DrawFxLayer();
48168
48195
  init_DrawGroup();
@@ -48272,7 +48299,7 @@ var init_component_registry_generated = __esm({
48272
48299
  init_RelationSelect();
48273
48300
  init_RepeatableFormSection();
48274
48301
  init_ReplyTree();
48275
- init_RichBlockEditor();
48302
+ init_RichTextEditor();
48276
48303
  init_RuntimeDebugger2();
48277
48304
  init_ScaledDiagram();
48278
48305
  init_ScoreDisplay();
@@ -48435,6 +48462,8 @@ var init_component_registry_generated = __esm({
48435
48462
  "DocSearch": DocSearch,
48436
48463
  "DocSidebar": DocSidebar,
48437
48464
  "DocTOC": DocTOC,
48465
+ "DocumentDetails": DocumentDetails,
48466
+ "DocumentPanel": DocumentPanel,
48438
48467
  "DocumentViewer": DocumentViewer,
48439
48468
  "DrawFxLayer": DrawFxLayer,
48440
48469
  "DrawGroup": DrawGroup,
@@ -48546,7 +48575,7 @@ var init_component_registry_generated = __esm({
48546
48575
  "RelationSelect": RelationSelect,
48547
48576
  "RepeatableFormSection": RepeatableFormSection,
48548
48577
  "ReplyTree": ReplyTree,
48549
- "RichBlockEditor": RichBlockEditor,
48578
+ "RichTextEditor": RichTextEditor,
48550
48579
  "RuntimeDebugger": RuntimeDebugger,
48551
48580
  "ScaledDiagram": ScaledDiagram,
48552
48581
  "ScoreDisplay": ScoreDisplay,
@@ -48700,10 +48729,10 @@ function enrichFormFields(fields, entityDef) {
48700
48729
  }
48701
48730
  if (field && typeof field === "object" && !Array.isArray(field) && !React85__default.isValidElement(field) && !(field instanceof Date)) {
48702
48731
  const obj = field;
48703
- const fieldName = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
48704
- if (!fieldName) return field;
48732
+ const fieldName2 = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
48733
+ if (!fieldName2) return field;
48705
48734
  if (obj.type || obj.inputType) return field;
48706
- const entityField = fieldMap.get(fieldName);
48735
+ const entityField = fieldMap.get(fieldName2);
48707
48736
  if (!entityField) return field;
48708
48737
  const enriched = { ...obj, type: entityField.type };
48709
48738
  if (entityField.required && !("required" in obj)) {
@@ -48753,9 +48782,9 @@ function enrichDetailFields(fields, entityDef) {
48753
48782
  }
48754
48783
  if (field && typeof field === "object" && !Array.isArray(field) && !React85__default.isValidElement(field) && !(field instanceof Date)) {
48755
48784
  const obj = field;
48756
- const fieldName = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
48757
- if (!fieldName || obj.type) return field;
48758
- const meta = metaFor(fieldName);
48785
+ const fieldName2 = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
48786
+ if (!fieldName2 || obj.type) return field;
48787
+ const meta = metaFor(fieldName2);
48759
48788
  return meta ? { ...obj, ...meta } : field;
48760
48789
  }
48761
48790
  return field;
@@ -49594,9 +49623,9 @@ function UISlotRenderer({
49594
49623
  "ui-slot-renderer relative min-h-full",
49595
49624
  className
49596
49625
  ), children: [
49597
- /* @__PURE__ */ jsxs(Box, { className: "flex min-h-full", children: [
49598
- /* @__PURE__ */ jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 shrink-0" }),
49599
- /* @__PURE__ */ jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-h-[200px]" })
49626
+ /* @__PURE__ */ jsxs(Box, { className: "flex min-h-full flex-col lg:flex-row", children: [
49627
+ /* @__PURE__ */ jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 lg:shrink-0" }),
49628
+ /* @__PURE__ */ jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-w-0 min-h-[200px]" })
49600
49629
  ] }),
49601
49630
  /* @__PURE__ */ jsx(UISlotComponent, { slot: "modal", portal: true }),
49602
49631
  /* @__PURE__ */ jsx(UISlotComponent, { slot: "drawer", portal: true }),
@@ -50119,7 +50148,7 @@ function buildTraitRenderConfig(binding, traitConfigsByName, payload) {
50119
50148
  }
50120
50149
  return void 0;
50121
50150
  }
50122
- function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
50151
+ function createSharedEntityWriter(binding, tick, traitStatesRef, emit, traitConfigsByName) {
50123
50152
  return (scratch) => {
50124
50153
  const traitName = binding.trait.name;
50125
50154
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -50128,9 +50157,13 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
50128
50157
  const writes = [];
50129
50158
  const ctx = createMinimalContext(scratchEntity, {}, currentState);
50130
50159
  const declaredDefaults = collectDeclaredConfigDefaults(binding.trait);
50131
- const callSiteConfig = getBindingConfig(binding);
50132
- if (declaredDefaults || callSiteConfig) {
50133
- ctx.config = { ...declaredDefaults ?? {}, ...callSiteConfig ?? {} };
50160
+ const resolvedByName = traitConfigsByName?.[traitName];
50161
+ const callSiteRaw = getBindingConfig(binding);
50162
+ const callSiteConfig = callSiteRaw ? Object.fromEntries(
50163
+ Object.entries(callSiteRaw).filter(([, v]) => !containsConfigForward(v))
50164
+ ) : void 0;
50165
+ if (declaredDefaults || resolvedByName || callSiteConfig) {
50166
+ ctx.config = { ...declaredDefaults ?? {}, ...resolvedByName ?? {}, ...callSiteConfig ?? {} };
50134
50167
  }
50135
50168
  ctx.mutateEntity = (changes) => {
50136
50169
  for (const [field, value] of Object.entries(changes)) {
@@ -50733,7 +50766,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
50733
50766
  const interval = entries[0].tick.interval;
50734
50767
  const onDue = timedTick(`tick:shared:${group.storeKey}@${String(interval)}`, () => {
50735
50768
  const writers = entries.map(
50736
- ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
50769
+ ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter, traitConfigsByName)
50737
50770
  );
50738
50771
  runTickFrame(group.storeKey, writers, sharedEntityStore);
50739
50772
  });