@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.
package/dist/avl/index.js CHANGED
@@ -9,7 +9,7 @@ import { useTranslate } from '@almadar/ui/hooks';
9
9
  import { InMemoryPersistence, StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, normalizeCallSiteConfigToValues, interpolateValue } from '@almadar/runtime';
10
10
  import { FieldTypeSchema, isInlineTrait, isPageReference, buildResolvedTraitConfigs, schemaToIR, getPage, mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, walkSExpr, isRenderBindingMarker, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER, isFileValue, ANIMATION_NAMES } from '@almadar/core';
11
11
  import * as LucideIcons2 from 'lucide-react';
12
- import { Loader2, X, Code, FileText, WrapText, Check, Copy, Lightbulb, CheckCircle, List, Printer, ChevronRight, ChevronLeft, GitBranch, Pencil, Eye, Plus, ArrowRight, Trash, 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';
12
+ import { Loader2, X, Code, FileText, WrapText, Check, Copy, Lightbulb, CheckCircle, List, Printer, ChevronRight, ChevronLeft, GitBranch, Pencil, Eye, Plus, ArrowRight, Trash, 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';
13
13
  import { createPortal } from 'react-dom';
14
14
  import { UISlotProvider, useUISlots, useTheme } from '@almadar/ui/context';
15
15
  import { evaluateGuard, evaluateListenPayloadExpr, evaluate, createMinimalContext, executeEffects } from '@almadar/evaluator';
@@ -28536,8 +28536,9 @@ function DataGrid({
28536
28536
  const titleField = fieldDefs.find((f3) => f3.variant === "h3" || f3.variant === "h4") ?? fieldDefs[0];
28537
28537
  const badgeFields = fieldDefs.filter((f3) => f3.variant === "badge" && f3 !== titleField);
28538
28538
  const bodyFields = fieldDefs.filter((f3) => f3 !== titleField && !badgeFields.includes(f3));
28539
- const primaryActions = actionDefs.filter((a) => a.variant !== "danger");
28540
- const dangerActions = actionDefs.filter((a) => a.variant === "danger");
28539
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
28540
+ const inlineCardActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
28541
+ const menuCardActions = actionDefs.filter((a) => !inlineCardActions.includes(a));
28541
28542
  const fireAction = (action, itemData) => {
28542
28543
  if (action.navigatesTo) {
28543
28544
  const url = action.navigatesTo.replace(
@@ -28557,6 +28558,9 @@ function DataGrid({
28557
28558
  e.stopPropagation();
28558
28559
  fireAction(action, itemData);
28559
28560
  };
28561
+ const cardClickAction = actionDefs.find((a) => a.variant !== "danger");
28562
+ const handleCardClick = cardClickAction ? (itemData) => () => fireAction(cardClickAction, itemData) : void 0;
28563
+ const stopCardClick = handleCardClick ? (e) => e.stopPropagation() : void 0;
28560
28564
  const hasRenderProp = typeof children === "function";
28561
28565
  useEffect(() => {
28562
28566
  if (data.length > 0 && !hasRenderProp && fieldDefs.length === 0) {
@@ -28628,55 +28632,40 @@ function DataGrid({
28628
28632
  {
28629
28633
  "data-entity-row": true,
28630
28634
  "data-entity-id": id,
28631
- className: cn("relative group/rowactions", isSelected && "ring-2 ring-primary rounded-lg"),
28635
+ onClick: handleCardClick?.(itemData),
28636
+ className: cn("relative group/rowactions", handleCardClick && "cursor-pointer", isSelected && "ring-2 ring-primary rounded-lg"),
28632
28637
  children: [
28633
28638
  children(itemData, index),
28634
- 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: [
28635
- /* @__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: [
28636
- (maxInlineActions != null ? actionDefs.slice(0, maxInlineActions) : actionDefs).map((action, idx) => /* @__PURE__ */ jsxs(
28637
- Button,
28638
- {
28639
- variant: action.variant === "primary" ? "primary" : "ghost",
28640
- size: "sm",
28641
- onClick: handleActionClick(action, itemData),
28642
- "data-testid": `action-${action.event}`,
28643
- "data-row-id": String(itemData.id),
28644
- className: action.variant === "danger" ? "text-error hover:text-error hover:bg-error/10" : void 0,
28645
- children: [
28646
- action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
28647
- action.label
28648
- ]
28649
- },
28650
- idx
28651
- )),
28652
- maxInlineActions != null && actionDefs.length > maxInlineActions && /* @__PURE__ */ jsx(
28653
- Menu,
28654
- {
28655
- position: "bottom-end",
28656
- 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" }) }),
28657
- items: actionDefs.slice(maxInlineActions).map((action) => ({
28658
- label: action.label,
28659
- icon: action.icon,
28660
- event: action.event,
28661
- onClick: () => fireAction(action, itemData)
28662
- }))
28663
- }
28664
- )
28665
- ] }),
28666
- /* @__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(
28639
+ 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: [
28640
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsxs(
28641
+ Button,
28642
+ {
28643
+ variant: "primary",
28644
+ size: "sm",
28645
+ onClick: handleActionClick(action, itemData),
28646
+ "data-testid": `action-${action.event}`,
28647
+ "data-row-id": String(itemData.id),
28648
+ children: [
28649
+ action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
28650
+ action.label
28651
+ ]
28652
+ },
28653
+ idx
28654
+ )),
28655
+ menuCardActions.length > 0 && /* @__PURE__ */ jsx(
28667
28656
  Menu,
28668
28657
  {
28669
28658
  position: "bottom-end",
28670
28659
  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" }) }),
28671
- items: actionDefs.map((action) => ({
28660
+ items: menuCardActions.map((action) => ({
28672
28661
  label: action.label,
28673
28662
  icon: action.icon,
28674
- event: action.event,
28663
+ variant: action.variant === "danger" ? "danger" : "default",
28675
28664
  onClick: () => fireAction(action, itemData)
28676
28665
  }))
28677
28666
  }
28678
- ) })
28679
- ] })
28667
+ )
28668
+ ] }) })
28680
28669
  ]
28681
28670
  },
28682
28671
  id
@@ -28690,12 +28679,14 @@ function DataGrid({
28690
28679
  {
28691
28680
  "data-entity-row": true,
28692
28681
  "data-entity-id": id,
28682
+ onClick: handleCardClick?.(itemData),
28693
28683
  className: cn(
28694
28684
  "bg-card rounded-lg",
28695
28685
  "border border-border",
28696
28686
  "shadow-elevation-card hover:shadow-elevation-dialog",
28697
28687
  "hover:border-primary transition-all",
28698
28688
  "flex flex-col",
28689
+ handleCardClick && "cursor-pointer",
28699
28690
  isSelected && "ring-2 ring-primary border-primary"
28700
28691
  ),
28701
28692
  children: [
@@ -28745,53 +28736,35 @@ function DataGrid({
28745
28736
  ] }, field.name);
28746
28737
  }) })
28747
28738
  ] }),
28748
- (primaryActions.length > 0 || dangerActions.length > 0) && /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
28749
- (maxInlineActions != null ? primaryActions.slice(0, maxInlineActions) : primaryActions).map((action, idx) => /* @__PURE__ */ jsx(
28739
+ actionDefs.length > 0 && /* @__PURE__ */ jsxs(HStack, { gap: "xs", onClick: stopCardClick, className: "flex-shrink-0", children: [
28740
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsx(
28750
28741
  Button,
28751
28742
  {
28752
- variant: action.variant === "primary" ? "primary" : "ghost",
28743
+ variant: "primary",
28753
28744
  size: "sm",
28754
28745
  onClick: handleActionClick(action, itemData),
28755
28746
  "data-testid": `action-${action.event}`,
28756
28747
  "data-row-id": String(itemData.id),
28757
28748
  "aria-label": action.label,
28758
28749
  title: action.label,
28759
- className: cn(
28760
- action.variant === "primary" ? void 0 : "text-muted-foreground hover:text-foreground",
28761
- action.icon && "px-2"
28762
- ),
28750
+ className: cn(action.icon && "px-2"),
28763
28751
  children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
28764
28752
  },
28765
28753
  idx
28766
28754
  )),
28767
- maxInlineActions != null && primaryActions.length > maxInlineActions && /* @__PURE__ */ jsx(
28755
+ menuCardActions.length > 0 && /* @__PURE__ */ jsx(
28768
28756
  Menu,
28769
28757
  {
28770
28758
  position: "bottom-end",
28771
28759
  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" }) }),
28772
- items: primaryActions.slice(maxInlineActions).map((action) => ({
28760
+ items: menuCardActions.map((action) => ({
28773
28761
  label: action.label,
28774
28762
  icon: action.icon,
28775
- event: action.event,
28763
+ variant: action.variant === "danger" ? "danger" : "default",
28776
28764
  onClick: () => fireAction(action, itemData)
28777
28765
  }))
28778
28766
  }
28779
- ),
28780
- dangerActions.map((action, idx) => /* @__PURE__ */ jsx(
28781
- Button,
28782
- {
28783
- variant: "ghost",
28784
- size: "sm",
28785
- onClick: handleActionClick(action, itemData),
28786
- "data-testid": `action-${action.event}`,
28787
- "data-row-id": String(itemData.id),
28788
- "aria-label": action.label,
28789
- title: action.label,
28790
- className: "text-error hover:text-error hover:bg-error/10 px-2",
28791
- children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
28792
- },
28793
- `danger-${idx}`
28794
- ))
28767
+ )
28795
28768
  ] })
28796
28769
  ] }) }),
28797
28770
  bodyFields.length > 0 && /* @__PURE__ */ jsx(Box, { className: "px-4 pt-2 pb-4 flex-1", children: /* @__PURE__ */ jsxs(VStack, { gap: "xs", children: [
@@ -29033,54 +29006,59 @@ function DataList({
29033
29006
  if (!itemActions || itemActions.length === 0) return null;
29034
29007
  const inline = maxInlineActions != null ? itemActions.slice(0, maxInlineActions) : itemActions;
29035
29008
  const overflow = maxInlineActions != null ? itemActions.slice(maxInlineActions) : [];
29036
- return /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
29037
- inline.map((action, idx) => /* @__PURE__ */ jsxs(
29038
- Button,
29039
- {
29040
- variant: action.variant ?? "ghost",
29041
- size: "sm",
29042
- onClick: handleActionClick(action, itemData),
29043
- "data-testid": `action-${action.event}`,
29044
- "data-row-id": String(itemData.id),
29045
- className: cn(
29046
- action.variant === "danger" && "text-error hover:bg-error/10",
29047
- // Must sit on the Button itself: the variant's own text colour
29048
- // beats an inherited one from the row wrapper.
29049
- onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
29050
- ),
29051
- children: [
29052
- action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
29053
- action.label
29054
- ]
29055
- },
29056
- idx
29057
- )),
29058
- overflow.length > 0 && /* @__PURE__ */ jsx(
29059
- Menu,
29060
- {
29061
- position: "bottom-end",
29062
- 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" }) }),
29063
- items: overflow.map((action) => ({
29064
- label: action.label,
29065
- icon: action.icon,
29066
- event: action.event,
29067
- variant: action.variant === "danger" ? "danger" : "default",
29068
- onClick: () => eventBus.emit(`UI:${action.event}`, {
29069
- id: itemData.id,
29070
- row: itemData
29071
- })
29072
- }))
29073
- }
29074
- )
29075
- ] });
29009
+ return (
29010
+ // stopPropagation at the cluster: the "⋯" trigger opens the overflow
29011
+ // menu without also firing the row's default click (inline buttons
29012
+ // already stop it in handleActionClick; the Menu panel is portaled).
29013
+ /* @__PURE__ */ jsxs(HStack, { gap: "xs", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, className: "flex-shrink-0", children: [
29014
+ inline.map((action, idx) => /* @__PURE__ */ jsxs(
29015
+ Button,
29016
+ {
29017
+ variant: action.variant ?? "ghost",
29018
+ size: "sm",
29019
+ onClick: handleActionClick(action, itemData),
29020
+ "data-testid": `action-${action.event}`,
29021
+ "data-row-id": String(itemData.id),
29022
+ className: cn(
29023
+ action.variant === "danger" && "text-error hover:bg-error/10",
29024
+ // Must sit on the Button itself: the variant's own text colour
29025
+ // beats an inherited one from the row wrapper.
29026
+ onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
29027
+ ),
29028
+ children: [
29029
+ action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
29030
+ action.label
29031
+ ]
29032
+ },
29033
+ idx
29034
+ )),
29035
+ overflow.length > 0 && /* @__PURE__ */ jsx(
29036
+ Menu,
29037
+ {
29038
+ position: "bottom-end",
29039
+ 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" }) }),
29040
+ items: overflow.map((action) => ({
29041
+ label: action.label,
29042
+ icon: action.icon,
29043
+ variant: action.variant === "danger" ? "danger" : "default",
29044
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
29045
+ id: itemData.id,
29046
+ row: itemData
29047
+ })
29048
+ }))
29049
+ }
29050
+ )
29051
+ ] })
29052
+ );
29076
29053
  };
29054
+ const rowClickEvent = itemClickEvent || itemActions?.find((a) => a.variant !== "danger")?.event;
29077
29055
  const handleRowClick = (itemData) => () => {
29078
- if (!itemClickEvent) return;
29056
+ if (!rowClickEvent) return;
29079
29057
  const payload = {
29080
29058
  id: itemData.id,
29081
29059
  row: itemData
29082
29060
  };
29083
- eventBus.emit(`UI:${itemClickEvent}`, payload);
29061
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
29084
29062
  };
29085
29063
  if (isLoading) {
29086
29064
  return /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) });
@@ -29128,10 +29106,10 @@ function DataList({
29128
29106
  {
29129
29107
  "data-entity-row": true,
29130
29108
  "data-entity-id": id,
29131
- onClick: itemClickEvent ? handleRowClick(itemData) : void 0,
29109
+ onClick: rowClickEvent ? handleRowClick(itemData) : void 0,
29132
29110
  className: cn(
29133
29111
  "flex px-4 group/rowactions",
29134
- itemClickEvent && "cursor-pointer",
29112
+ rowClickEvent && "cursor-pointer",
29135
29113
  isSent ? "justify-end" : "justify-start"
29136
29114
  ),
29137
29115
  children: /* @__PURE__ */ jsxs(
@@ -29207,7 +29185,7 @@ function DataList({
29207
29185
  const id2 = itemData.id || String(index);
29208
29186
  const actions = renderItemActions(itemData);
29209
29187
  return wrapDnd(
29210
- /* @__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: [
29188
+ /* @__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: [
29211
29189
  children(itemData, index),
29212
29190
  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: [
29213
29191
  /* @__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 }),
@@ -29219,7 +29197,6 @@ function DataList({
29219
29197
  items: (itemActions ?? []).map((action) => ({
29220
29198
  label: action.label,
29221
29199
  icon: action.icon,
29222
- event: action.event,
29223
29200
  variant: action.variant === "danger" ? "danger" : "default",
29224
29201
  onClick: () => eventBus.emit(`UI:${action.event}`, {
29225
29202
  id: itemData.id,
@@ -29236,7 +29213,7 @@ function DataList({
29236
29213
  const id = itemData.id || String(index);
29237
29214
  const titleValue = getNestedValue(itemData, titleField?.name ?? "");
29238
29215
  return wrapDnd(
29239
- /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: itemClickEvent ? handleRowClick(itemData) : void 0, className: cn(itemClickEvent && "cursor-pointer"), children: [
29216
+ /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: rowClickEvent ? handleRowClick(itemData) : void 0, className: cn(rowClickEvent && "cursor-pointer"), children: [
29240
29217
  /* @__PURE__ */ jsxs(
29241
29218
  Box,
29242
29219
  {
@@ -34512,7 +34489,7 @@ var init_MapView = __esm({
34512
34489
  shadowSize: [41, 41]
34513
34490
  });
34514
34491
  L.Marker.prototype.options.icon = defaultIcon;
34515
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState100 } = React94__default;
34492
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState102 } = React94__default;
34516
34493
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
34517
34494
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
34518
34495
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -34557,7 +34534,7 @@ var init_MapView = __esm({
34557
34534
  showAttribution = true
34558
34535
  }) {
34559
34536
  const eventBus = useEventBus2();
34560
- const [clickedPosition, setClickedPosition] = useState100(null);
34537
+ const [clickedPosition, setClickedPosition] = useState102(null);
34561
34538
  const handleMapClick = useCallback96((lat, lng) => {
34562
34539
  if (showClickedPin) {
34563
34540
  setClickedPosition({ lat, lng });
@@ -38919,741 +38896,775 @@ var init_PositionedCanvas = __esm({
38919
38896
  PositionedCanvas.displayName = "PositionedCanvas";
38920
38897
  }
38921
38898
  });
38922
- function nextBlockId(prefix = "blk") {
38923
- _idSeq += 1;
38924
- const random = Math.random().toString(36).slice(2, 8);
38925
- return `${prefix}-${Date.now().toString(36)}-${_idSeq}-${random}`;
38926
- }
38927
- function normalizeBlocks(raw) {
38928
- if (!Array.isArray(raw) || raw.length === 0) return [createBlock("paragraph")];
38929
- return raw.map((row) => {
38930
- const entity = row;
38931
- const rawType = entity.type;
38932
- const type = typeof rawType === "string" && BLOCK_TYPES.has(rawType) ? rawType : "paragraph";
38933
- const id = typeof entity.id === "string" && entity.id ? entity.id : nextBlockId(type);
38934
- return { ...entity, id, type };
38935
- });
38936
- }
38937
- function createBlock(type) {
38938
- switch (type) {
38939
- case "bullet-list":
38940
- case "numbered-list":
38941
- return {
38942
- id: nextBlockId(type),
38943
- type,
38944
- children: [
38945
- { id: nextBlockId("li"), type: "paragraph", content: "" }
38946
- ]
38947
- };
38948
- case "image":
38949
- return {
38950
- id: nextBlockId(type),
38951
- type,
38952
- content: "",
38953
- metadata: { url: "", caption: "" }
38954
- };
38955
- case "code":
38956
- return {
38957
- id: nextBlockId(type),
38958
- type,
38959
- content: "",
38960
- metadata: { language: "plaintext" }
38961
- };
38962
- case "divider":
38963
- return { id: nextBlockId(type), type };
38964
- default:
38965
- return { id: nextBlockId(type), type, content: "" };
38899
+ function safeUrl(raw, kinds) {
38900
+ const url = raw.trim();
38901
+ if (url.startsWith("#") || url.startsWith("/")) return url;
38902
+ const lower = url.toLowerCase();
38903
+ for (const kind of kinds) {
38904
+ if (lower.startsWith(kind)) return url;
38966
38905
  }
38906
+ return null;
38967
38907
  }
38968
- function replaceBlock(blocks, id, updater) {
38969
- return blocks.map((block) => block.id === id ? updater(block) : block);
38970
- }
38971
- function removeBlock(blocks, id) {
38972
- return blocks.filter((block) => block.id !== id);
38973
- }
38974
- function duplicateBlock(block) {
38975
- return {
38976
- ...block,
38977
- id: nextBlockId(block.type),
38978
- children: block.children?.map((child) => ({
38979
- ...child,
38980
- id: nextBlockId("li")
38981
- })),
38982
- metadata: block.metadata ? { ...block.metadata } : void 0
38983
- };
38984
- }
38985
- function insertAfter(blocks, targetId, inserted) {
38986
- const idx = blocks.findIndex((b) => b.id === targetId);
38987
- if (idx === -1) return [...blocks, inserted];
38988
- const next = blocks.slice();
38989
- next.splice(idx + 1, 0, inserted);
38990
- return next;
38991
- }
38992
- function changeBlockType(block, type) {
38993
- if (block.type === type) return block;
38994
- if (type === "bullet-list" || type === "numbered-list") {
38995
- if (block.children && block.children.length > 0) {
38996
- return { ...block, type };
38908
+ function sanitizeNode(el) {
38909
+ const children = Array.from(el.children);
38910
+ for (const child of children) {
38911
+ const tag = child.tagName.toLowerCase();
38912
+ if (DROP_TAGS.has(tag)) {
38913
+ child.remove();
38914
+ continue;
38997
38915
  }
38998
- const seed2 = block.content ?? "";
38999
- return {
39000
- id: block.id,
39001
- type,
39002
- children: [
39003
- { id: nextBlockId("li"), type: "paragraph", content: seed2 }
39004
- ]
39005
- };
39006
- }
39007
- if (type === "divider") {
39008
- return { id: block.id, type };
38916
+ const allowed = ALLOWED_ATTRS[tag];
38917
+ if (!allowed) {
38918
+ sanitizeNode(child);
38919
+ child.replaceWith(...Array.from(child.childNodes));
38920
+ continue;
38921
+ }
38922
+ for (const attr of Array.from(child.attributes)) {
38923
+ const name = attr.name.toLowerCase();
38924
+ if (!allowed.includes(name)) {
38925
+ child.removeAttribute(attr.name);
38926
+ continue;
38927
+ }
38928
+ if (name === "href") {
38929
+ const url = safeUrl(attr.value, ["http://", "https://", "mailto:"]);
38930
+ if (url === null) child.removeAttribute(attr.name);
38931
+ else child.setAttribute("href", url);
38932
+ }
38933
+ if (name === "src") {
38934
+ const url = safeUrl(attr.value, ["http://", "https://", "data:image/"]);
38935
+ if (url === null) child.remove();
38936
+ else child.setAttribute("src", url);
38937
+ }
38938
+ }
38939
+ sanitizeNode(child);
39009
38940
  }
39010
- if (type === "image") {
39011
- return {
39012
- id: block.id,
39013
- type,
39014
- content: "",
39015
- metadata: { url: "", caption: block.content ?? "" }
39016
- };
38941
+ }
38942
+ function sanitizeRichHtml(html) {
38943
+ if (!html || typeof html !== "string") return "";
38944
+ if (typeof window === "undefined" || typeof DOMParser === "undefined") return "";
38945
+ const doc = new DOMParser().parseFromString(html, "text/html");
38946
+ sanitizeNode(doc.body);
38947
+ if (!doc.body.querySelector("img, hr") && (doc.body.textContent ?? "").trim().length === 0) {
38948
+ return "";
39017
38949
  }
39018
- if (type === "code") {
39019
- return {
39020
- id: block.id,
39021
- type,
39022
- content: block.content ?? "",
39023
- metadata: { language: "plaintext" }
39024
- };
38950
+ return doc.body.innerHTML.replace(/​/g, "");
38951
+ }
38952
+ function htmlIsEmpty(el) {
38953
+ if (el.querySelector("img, hr")) return false;
38954
+ return (el.textContent ?? "").trim().length === 0;
38955
+ }
38956
+ function closestBlock(node, root) {
38957
+ let cur = node;
38958
+ while (cur && cur !== root) {
38959
+ if (cur instanceof Element && BLOCK_TAGS.has(cur.tagName)) return cur;
38960
+ cur = cur.parentNode;
39025
38961
  }
39026
- const seed = block.children?.[0]?.content ?? block.content ?? "";
39027
- return { id: block.id, type, content: seed };
38962
+ return null;
39028
38963
  }
39029
- function BlockMenu({ block, readOnly, onDelete, onDuplicate, onChangeType }) {
39030
- const { t } = useTranslate();
39031
- const [open, setOpen] = useState(false);
39032
- const ref = useRef(null);
39033
- useEffect(() => {
39034
- if (!open) return;
39035
- function onDocClick(e) {
39036
- if (ref.current && !ref.current.contains(e.target)) {
39037
- setOpen(false);
38964
+ function RichTextStyles() {
38965
+ return /* @__PURE__ */ jsx("style", { children: RICH_TEXT_CSS });
38966
+ }
38967
+ function readToolbarState(root) {
38968
+ const sel = window.getSelection();
38969
+ if (!sel || sel.rangeCount === 0) return IDLE_TOOLBAR;
38970
+ const anchor = sel.anchorNode;
38971
+ if (!anchor || !root.contains(anchor)) return IDLE_TOOLBAR;
38972
+ let block = null;
38973
+ let link = false;
38974
+ let node = anchor;
38975
+ while (node && node !== root) {
38976
+ if (node instanceof Element) {
38977
+ const tag = node.tagName.toLowerCase();
38978
+ if (!block && (tag === "h1" || tag === "h2" || tag === "h3" || tag === "blockquote" || tag === "pre")) {
38979
+ block = tag;
39038
38980
  }
38981
+ if (tag === "a") link = true;
39039
38982
  }
39040
- document.addEventListener("mousedown", onDocClick);
39041
- return () => document.removeEventListener("mousedown", onDocClick);
39042
- }, [open]);
39043
- if (readOnly) return null;
39044
- return /* @__PURE__ */ jsxs(Box, { ref, className: "relative", children: [
39045
- /* @__PURE__ */ jsx(
39046
- Button,
39047
- {
39048
- type: "button",
39049
- variant: "ghost",
39050
- "aria-label": t("richBlockEditor.blockActions"),
39051
- className: cn(
39052
- "inline-flex items-center justify-center",
39053
- "h-6 w-6 rounded-sm p-0 gap-0",
39054
- "text-muted-foreground hover:bg-muted hover:text-foreground",
39055
- "opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
39056
- "transition-opacity"
39057
- ),
39058
- onClick: () => setOpen((v) => !v),
39059
- children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", className: "w-3.5 h-3.5" })
39060
- }
39061
- ),
39062
- open && /* @__PURE__ */ jsxs(
39063
- Box,
39064
- {
39065
- role: "menu",
39066
- className: cn(
39067
- "absolute right-0 z-10 mt-1 w-44",
39068
- "rounded-container border border-border bg-popover shadow-elevation-popover",
39069
- "py-1 text-sm"
39070
- ),
39071
- children: [
39072
- /* @__PURE__ */ jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t(BLOCK_TYPE_LABEL_KEY[block.type]) }),
39073
- /* @__PURE__ */ jsxs(
39074
- Button,
39075
- {
39076
- type: "button",
39077
- variant: "ghost",
39078
- role: "menuitem",
39079
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
39080
- onClick: () => {
39081
- onDuplicate();
39082
- setOpen(false);
39083
- },
39084
- children: [
39085
- /* @__PURE__ */ jsx(Icon, { name: "plus", className: "w-3.5 h-3.5" }),
39086
- " ",
39087
- t("richBlockEditor.duplicate")
39088
- ]
39089
- }
39090
- ),
39091
- /* @__PURE__ */ jsxs(
39092
- Button,
39093
- {
39094
- type: "button",
39095
- variant: "ghost",
39096
- role: "menuitem",
39097
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-destructive hover:bg-muted justify-start rounded-none",
39098
- onClick: () => {
39099
- onDelete();
39100
- setOpen(false);
39101
- },
39102
- children: [
39103
- /* @__PURE__ */ jsx(Icon, { name: "trash", className: "w-3.5 h-3.5" }),
39104
- " ",
39105
- t("common.delete")
39106
- ]
39107
- }
39108
- ),
39109
- CHANGEABLE_TYPES.includes(block.type) && /* @__PURE__ */ jsxs(Fragment, { children: [
39110
- /* @__PURE__ */ jsx(Box, { className: "my-1 border-t border-border" }),
39111
- /* @__PURE__ */ jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t("richBlockEditor.turnInto") }),
39112
- CHANGEABLE_TYPES.filter((bt) => bt !== block.type).map((bt) => /* @__PURE__ */ jsx(
39113
- Button,
39114
- {
39115
- type: "button",
39116
- variant: "ghost",
39117
- role: "menuitem",
39118
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
39119
- onClick: () => {
39120
- onChangeType(bt);
39121
- setOpen(false);
39122
- },
39123
- children: t(BLOCK_TYPE_LABEL_KEY[bt])
39124
- },
39125
- bt
39126
- ))
39127
- ] })
39128
- ]
39129
- }
39130
- )
39131
- ] });
38983
+ node = node.parentNode;
38984
+ }
38985
+ return {
38986
+ bold: document.queryCommandState("bold"),
38987
+ italic: document.queryCommandState("italic"),
38988
+ underline: document.queryCommandState("underline"),
38989
+ strikeThrough: document.queryCommandState("strikeThrough"),
38990
+ block,
38991
+ bullets: document.queryCommandState("insertUnorderedList"),
38992
+ numbers: document.queryCommandState("insertOrderedList"),
38993
+ link
38994
+ };
39132
38995
  }
39133
- function Editable({
39134
- tag,
39135
- value,
39136
- readOnly,
39137
- placeholder,
39138
- className,
39139
- ariaLabel,
39140
- onValueChange
39141
- }) {
39142
- const ref = useRef(null);
39143
- useEffect(() => {
39144
- const el = ref.current;
39145
- if (!el) return;
39146
- const isFocused = document.activeElement === el;
39147
- if (!isFocused && el.textContent !== value) {
39148
- el.textContent = value;
39149
- }
39150
- }, [value]);
39151
- const handleInput = useCallback(
39152
- (e) => {
39153
- onValueChange(e.currentTarget.textContent ?? "");
39154
- },
39155
- [onValueChange]
39156
- );
38996
+ function ToolbarButton({ icon: IconCmp, label, active, onExec }) {
39157
38997
  return /* @__PURE__ */ jsx(
39158
- Box,
38998
+ Button,
39159
38999
  {
39160
- as: tag,
39161
- ref,
39162
- contentEditable: !readOnly,
39163
- suppressContentEditableWarning: true,
39164
- role: readOnly ? void 0 : "textbox",
39165
- "aria-label": ariaLabel,
39166
- "aria-multiline": "true",
39167
- "data-placeholder": placeholder,
39000
+ type: "button",
39001
+ variant: "ghost",
39002
+ size: "sm",
39003
+ "aria-label": label,
39004
+ "aria-pressed": active,
39005
+ title: label,
39168
39006
  className: cn(
39169
- "outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-sm",
39170
- "empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground/60",
39171
- className
39007
+ "h-8 w-8 p-0 gap-0 justify-center",
39008
+ active && "bg-muted text-foreground"
39172
39009
  ),
39173
- onInput: handleInput
39010
+ onMouseDown: (e) => {
39011
+ e.preventDefault();
39012
+ onExec();
39013
+ },
39014
+ children: /* @__PURE__ */ jsx(IconCmp, { size: 15 })
39174
39015
  }
39175
39016
  );
39176
39017
  }
39177
- function BlockRow({
39178
- block,
39179
- readOnly,
39180
- showAffordances,
39181
- placeholder,
39182
- onUpdate,
39183
- onDelete,
39184
- onDuplicate,
39185
- onChangeType
39186
- }) {
39187
- const { t } = useTranslate();
39188
- const setContent = useCallback(
39189
- (next) => onUpdate((b) => ({ ...b, content: next })),
39190
- [onUpdate]
39191
- );
39192
- const setMetadata = useCallback(
39193
- (key, value) => onUpdate((b) => ({
39194
- ...b,
39195
- metadata: { ...b.metadata ?? {}, [key]: value }
39196
- })),
39197
- [onUpdate]
39198
- );
39199
- const setChildContent = useCallback(
39200
- (childId, next) => onUpdate((b) => ({
39201
- ...b,
39202
- children: (b.children ?? []).map(
39203
- (c) => c.id === childId ? { ...c, content: next } : c
39204
- )
39205
- })),
39206
- [onUpdate]
39207
- );
39208
- const addListItem = useCallback(
39209
- () => onUpdate((b) => ({
39210
- ...b,
39211
- children: [
39212
- ...b.children ?? [],
39213
- { id: nextBlockId("li"), type: "paragraph", content: "" }
39214
- ]
39215
- })),
39216
- [onUpdate]
39217
- );
39218
- const removeListItem = useCallback(
39219
- (childId) => onUpdate((b) => {
39220
- const remaining = (b.children ?? []).filter((c) => c.id !== childId);
39221
- return {
39222
- ...b,
39223
- children: remaining.length === 0 ? [{ id: nextBlockId("li"), type: "paragraph", content: "" }] : remaining
39224
- };
39225
- }),
39226
- [onUpdate]
39227
- );
39228
- const renderBody = () => {
39229
- switch (block.type) {
39230
- case "heading-1":
39231
- return /* @__PURE__ */ jsx(
39232
- Editable,
39233
- {
39234
- tag: "h1",
39235
- value: block.content ?? "",
39236
- readOnly,
39237
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading1"),
39238
- ariaLabel: t("richBlockEditor.aria.heading1Block"),
39239
- className: "text-3xl font-bold leading-tight",
39240
- onValueChange: setContent
39241
- }
39242
- );
39243
- case "heading-2":
39244
- return /* @__PURE__ */ jsx(
39245
- Editable,
39246
- {
39247
- tag: "h2",
39248
- value: block.content ?? "",
39249
- readOnly,
39250
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading2"),
39251
- ariaLabel: t("richBlockEditor.aria.heading2Block"),
39252
- className: "text-2xl font-semibold leading-tight",
39253
- onValueChange: setContent
39254
- }
39255
- );
39256
- case "heading-3":
39257
- return /* @__PURE__ */ jsx(
39258
- Editable,
39259
- {
39260
- tag: "h3",
39261
- value: block.content ?? "",
39262
- readOnly,
39263
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading3"),
39264
- ariaLabel: t("richBlockEditor.aria.heading3Block"),
39265
- className: "text-xl font-semibold leading-tight",
39266
- onValueChange: setContent
39267
- }
39268
- );
39269
- case "quote":
39270
- return /* @__PURE__ */ jsx(
39271
- Editable,
39272
- {
39273
- tag: "blockquote",
39274
- value: block.content ?? "",
39275
- readOnly,
39276
- placeholder: placeholder ?? t("richBlockEditor.placeholder.quote"),
39277
- ariaLabel: t("richBlockEditor.aria.quoteBlock"),
39278
- className: "border-l-4 border-primary/60 pl-4 italic text-muted-foreground",
39279
- onValueChange: setContent
39280
- }
39281
- );
39282
- case "code":
39283
- return /* @__PURE__ */ jsxs(Box, { className: "rounded-md border border-border bg-muted/40", children: [
39284
- /* @__PURE__ */ jsxs(Box, { className: "flex items-center justify-between border-b border-border px-3 py-1 text-xs text-muted-foreground", children: [
39285
- /* @__PURE__ */ jsx(Typography, { as: "span", variant: "caption", className: "uppercase tracking-wide", children: t("richBlockEditor.blockType.code") }),
39286
- !readOnly && /* @__PURE__ */ jsx(
39287
- Input,
39288
- {
39289
- inputType: "text",
39290
- value: String(block.metadata?.language ?? "plaintext"),
39291
- "aria-label": t("richBlockEditor.aria.codeLanguage"),
39292
- className: cn(
39293
- "h-6 w-32 rounded-sm border border-border bg-background",
39294
- "px-2 text-xs outline-none focus:ring-1 focus:ring-ring"
39295
- ),
39296
- onChange: (e) => setMetadata("language", e.target.value)
39297
- }
39298
- ),
39299
- readOnly && /* @__PURE__ */ jsx(Typography, { as: "span", variant: "caption", className: "text-xs", children: String(block.metadata?.language ?? "plaintext") })
39300
- ] }),
39301
- /* @__PURE__ */ jsx(
39302
- Editable,
39303
- {
39304
- tag: "pre",
39305
- value: block.content ?? "",
39306
- readOnly,
39307
- placeholder: placeholder ?? t("richBlockEditor.placeholder.code"),
39308
- ariaLabel: t("richBlockEditor.aria.codeBlock"),
39309
- className: "block whitespace-pre-wrap p-3 font-mono text-sm leading-relaxed",
39310
- onValueChange: setContent
39311
- }
39312
- )
39313
- ] });
39314
- case "divider":
39315
- return /* @__PURE__ */ jsx(Divider, { className: "my-2" });
39316
- case "image": {
39317
- const url = String(block.metadata?.url ?? "");
39318
- const caption = String(block.metadata?.caption ?? "");
39319
- const imgProps = {
39320
- src: url,
39321
- alt: caption || t("richBlockEditor.embeddedImage"),
39322
- className: "max-h-96 w-full rounded-md border border-border object-contain"
39018
+ var CHANGE_DEBOUNCE_MS, ALLOWED_ATTRS, DROP_TAGS, MD_BLOCK_PREFIXES, BLOCK_TAGS, MD_INLINE_RULES, RICH_TEXT_CSS, IDLE_TOOLBAR, RichTextEditor;
39019
+ var init_RichTextEditor = __esm({
39020
+ "components/core/molecules/RichTextEditor.tsx"() {
39021
+ "use client";
39022
+ init_cn();
39023
+ init_Box();
39024
+ init_Button();
39025
+ init_useEventBus();
39026
+ CHANGE_DEBOUNCE_MS = 400;
39027
+ ALLOWED_ATTRS = {
39028
+ p: [],
39029
+ h1: [],
39030
+ h2: [],
39031
+ h3: [],
39032
+ h4: [],
39033
+ ul: [],
39034
+ ol: [],
39035
+ li: [],
39036
+ blockquote: [],
39037
+ pre: [],
39038
+ code: [],
39039
+ b: [],
39040
+ strong: [],
39041
+ i: [],
39042
+ em: [],
39043
+ u: [],
39044
+ s: [],
39045
+ strike: [],
39046
+ br: [],
39047
+ hr: [],
39048
+ div: [],
39049
+ span: [],
39050
+ a: ["href"],
39051
+ img: ["src", "alt"]
39052
+ };
39053
+ DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "link", "meta", "form", "input", "button", "textarea", "select"]);
39054
+ MD_BLOCK_PREFIXES = {
39055
+ "#": { kind: "block", tag: "h1" },
39056
+ "##": { kind: "block", tag: "h2" },
39057
+ "###": { kind: "block", tag: "h3" },
39058
+ ">": { kind: "block", tag: "blockquote" },
39059
+ "```": { kind: "block", tag: "pre" },
39060
+ "-": { kind: "list", command: "insertUnorderedList" },
39061
+ "*": { kind: "list", command: "insertUnorderedList" },
39062
+ "1.": { kind: "list", command: "insertOrderedList" }
39063
+ };
39064
+ BLOCK_TAGS = /* @__PURE__ */ new Set(["P", "DIV", "LI", "H1", "H2", "H3", "H4", "BLOCKQUOTE", "PRE"]);
39065
+ MD_INLINE_RULES = [
39066
+ { pattern: /\*\*([^*\n]+)\*\*$/, wrap: "strong" },
39067
+ { pattern: /(^|[^*])\*([^*\n]+)\*$/, wrap: "em" },
39068
+ { pattern: /`([^`\n]+)`$/, wrap: "code" }
39069
+ ];
39070
+ RICH_TEXT_CSS = `
39071
+ .almadar-rich-text { line-height: 1.7; color: var(--color-foreground); }
39072
+ .almadar-rich-text h1 { font-size: 1.875rem; font-weight: 700; line-height: 1.25; margin: 1.25em 0 0.4em; }
39073
+ .almadar-rich-text h2 { font-size: 1.5rem; font-weight: 650; line-height: 1.3; margin: 1.1em 0 0.4em; }
39074
+ .almadar-rich-text h3 { font-size: 1.25rem; font-weight: 600; line-height: 1.35; margin: 1em 0 0.35em; }
39075
+ .almadar-rich-text h1:first-child, .almadar-rich-text h2:first-child, .almadar-rich-text h3:first-child { margin-top: 0; }
39076
+ .almadar-rich-text p { margin: 0.5em 0; }
39077
+ .almadar-rich-text ul { list-style: disc; padding-inline-start: 1.5rem; margin: 0.5em 0; }
39078
+ .almadar-rich-text ol { list-style: decimal; padding-inline-start: 1.5rem; margin: 0.5em 0; }
39079
+ .almadar-rich-text li { margin: 0.25em 0; }
39080
+ .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; }
39081
+ .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; }
39082
+ .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; }
39083
+ .almadar-rich-text pre code { background: transparent; padding: 0; }
39084
+ .almadar-rich-text a { color: var(--color-primary); text-decoration: underline; }
39085
+ .almadar-rich-text hr { border: none; border-top: 1px solid var(--color-border); margin: 1.25em 0; }
39086
+ .almadar-rich-text img { max-width: 100%; border-radius: 0.375rem; }
39087
+ `;
39088
+ IDLE_TOOLBAR = {
39089
+ bold: false,
39090
+ italic: false,
39091
+ underline: false,
39092
+ strikeThrough: false,
39093
+ block: null,
39094
+ bullets: false,
39095
+ numbers: false,
39096
+ link: false
39097
+ };
39098
+ RichTextEditor = ({
39099
+ value,
39100
+ onChange,
39101
+ changeEvent,
39102
+ readOnly = false,
39103
+ placeholder,
39104
+ showToolbar = true,
39105
+ className
39106
+ }) => {
39107
+ const { t } = useTranslate();
39108
+ const ref = useRef(null);
39109
+ const [toolbar, setToolbar] = useState(IDLE_TOOLBAR);
39110
+ const [empty, setEmpty] = useState(() => !value || !sanitizeRichHtml(value).trim());
39111
+ const eventBus = useEventBus();
39112
+ const onChangeRef = useRef(onChange);
39113
+ const changeEventRef = useRef(changeEvent);
39114
+ useEffect(() => {
39115
+ onChangeRef.current = onChange;
39116
+ changeEventRef.current = changeEvent;
39117
+ }, [onChange, changeEvent]);
39118
+ const emitTimerRef = useRef(null);
39119
+ const emitNow = useCallback(() => {
39120
+ const el = ref.current;
39121
+ if (!el) return;
39122
+ const html = sanitizeRichHtml(el.innerHTML);
39123
+ onChangeRef.current?.(html);
39124
+ const evt = changeEventRef.current;
39125
+ if (evt) eventBus.emit(`UI:${evt}`, { value: html });
39126
+ }, [eventBus]);
39127
+ const scheduleEmit = useCallback(() => {
39128
+ if (emitTimerRef.current) clearTimeout(emitTimerRef.current);
39129
+ emitTimerRef.current = setTimeout(() => {
39130
+ emitTimerRef.current = null;
39131
+ emitNow();
39132
+ }, CHANGE_DEBOUNCE_MS);
39133
+ }, [emitNow]);
39134
+ const flushEmit = useCallback(() => {
39135
+ if (!emitTimerRef.current) return;
39136
+ clearTimeout(emitTimerRef.current);
39137
+ emitTimerRef.current = null;
39138
+ emitNow();
39139
+ }, [emitNow]);
39140
+ useEffect(() => () => {
39141
+ if (emitTimerRef.current) {
39142
+ clearTimeout(emitTimerRef.current);
39143
+ emitTimerRef.current = null;
39144
+ emitNow();
39145
+ }
39146
+ }, [emitNow]);
39147
+ useEffect(() => {
39148
+ if (readOnly) return;
39149
+ const el = ref.current;
39150
+ if (!el) return;
39151
+ if (document.activeElement === el) return;
39152
+ const next = sanitizeRichHtml(value ?? "") || "<p><br></p>";
39153
+ if (el.innerHTML !== next) {
39154
+ el.innerHTML = next;
39155
+ setEmpty(htmlIsEmpty(el));
39156
+ }
39157
+ }, [readOnly, value]);
39158
+ useEffect(() => {
39159
+ if (readOnly) return;
39160
+ document.execCommand("defaultParagraphSeparator", false, "p");
39161
+ }, [readOnly]);
39162
+ useEffect(() => {
39163
+ if (readOnly || !showToolbar) return;
39164
+ const onSelectionChange = () => {
39165
+ const el = ref.current;
39166
+ if (!el) return;
39167
+ setToolbar(readToolbarState(el));
39323
39168
  };
39324
- return /* @__PURE__ */ jsxs(Box, { className: "space-y-2", children: [
39325
- url ? /* @__PURE__ */ jsx(Box, { as: "img", ...imgProps }) : /* @__PURE__ */ jsxs(
39326
- Box,
39327
- {
39328
- className: cn(
39329
- "flex h-32 items-center justify-center",
39330
- "rounded-md border border-dashed border-border",
39331
- "text-sm text-muted-foreground"
39332
- ),
39333
- children: [
39334
- /* @__PURE__ */ jsx(Icon, { name: "image", className: "mr-2 w-4 h-4" }),
39335
- " ",
39336
- t("richBlockEditor.noImageUrl")
39337
- ]
39338
- }
39339
- ),
39340
- !readOnly && /* @__PURE__ */ jsxs(Box, { className: "flex flex-col gap-2 sm:flex-row", children: [
39341
- /* @__PURE__ */ jsx(
39342
- Input,
39343
- {
39344
- inputType: "url",
39345
- value: url,
39346
- placeholder: "https://example.com/image.png",
39347
- "aria-label": t("richBlockEditor.aria.imageUrl"),
39348
- className: cn(
39349
- "h-8 flex-1 rounded-sm border border-border bg-background",
39350
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
39351
- ),
39352
- onChange: (e) => setMetadata("url", e.target.value)
39353
- }
39354
- ),
39355
- /* @__PURE__ */ jsx(
39356
- Input,
39357
- {
39358
- inputType: "text",
39359
- value: caption,
39360
- placeholder: t("richBlockEditor.placeholder.caption"),
39361
- "aria-label": t("richBlockEditor.aria.imageCaption"),
39362
- className: cn(
39363
- "h-8 flex-1 rounded-sm border border-border bg-background",
39364
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
39365
- ),
39366
- onChange: (e) => setMetadata("caption", e.target.value)
39367
- }
39368
- )
39369
- ] }),
39370
- readOnly && caption && /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-center text-muted-foreground", children: caption })
39169
+ document.addEventListener("selectionchange", onSelectionChange);
39170
+ return () => document.removeEventListener("selectionchange", onSelectionChange);
39171
+ }, [readOnly, showToolbar]);
39172
+ const tryInlineMarkdown = useCallback(() => {
39173
+ const root = ref.current;
39174
+ if (!root) return false;
39175
+ const sel = window.getSelection();
39176
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
39177
+ const node = sel.anchorNode;
39178
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
39179
+ if (node.parentElement?.closest("code, pre")) return false;
39180
+ const text = (node.textContent ?? "").slice(0, sel.anchorOffset);
39181
+ for (const rule of MD_INLINE_RULES) {
39182
+ const m = rule.pattern.exec(text);
39183
+ if (!m) continue;
39184
+ const inner = rule.wrap === "em" ? m[2] : m[1];
39185
+ if (!inner || !inner.trim()) continue;
39186
+ const lead = rule.wrap === "em" ? m[1]?.length ?? 0 : 0;
39187
+ const range = document.createRange();
39188
+ range.setStart(node, m.index + lead);
39189
+ range.setEnd(node, sel.anchorOffset);
39190
+ const el = document.createElement(rule.wrap);
39191
+ el.textContent = inner;
39192
+ range.deleteContents();
39193
+ range.insertNode(el);
39194
+ const after = document.createTextNode("\u200B");
39195
+ el.parentNode?.insertBefore(after, el.nextSibling);
39196
+ const caret = document.createRange();
39197
+ caret.setStart(after, 1);
39198
+ caret.collapse(true);
39199
+ sel.removeAllRanges();
39200
+ sel.addRange(caret);
39201
+ return true;
39202
+ }
39203
+ return false;
39204
+ }, []);
39205
+ const tryBlockMarkdown = useCallback(() => {
39206
+ const root = ref.current;
39207
+ if (!root) return false;
39208
+ const sel = window.getSelection();
39209
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
39210
+ const node = sel.anchorNode;
39211
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
39212
+ const block = closestBlock(node, root);
39213
+ if (block ? block.firstChild !== node : root.firstChild !== node) return false;
39214
+ const prefix = (node.textContent ?? "").slice(0, sel.anchorOffset);
39215
+ const rule = MD_BLOCK_PREFIXES[prefix];
39216
+ if (!rule) return false;
39217
+ if (node.parentElement?.closest("pre, code")) return false;
39218
+ if (rule.kind === "list" && node.parentElement?.closest("li")) return false;
39219
+ node.deleteData(0, prefix.length);
39220
+ const caret = document.createRange();
39221
+ caret.setStart(node, 0);
39222
+ caret.collapse(true);
39223
+ sel.removeAllRanges();
39224
+ sel.addRange(caret);
39225
+ if (rule.kind === "list") {
39226
+ const container = block ?? node.parentElement;
39227
+ const list = document.createElement(rule.command === "insertOrderedList" ? "ol" : "ul");
39228
+ const li = document.createElement("li");
39229
+ if (block) {
39230
+ while (block.firstChild) li.appendChild(block.firstChild);
39231
+ list.appendChild(li);
39232
+ block.replaceWith(list);
39233
+ } else if (container) {
39234
+ li.appendChild(node);
39235
+ list.appendChild(li);
39236
+ container.appendChild(list);
39237
+ }
39238
+ if (!li.firstChild) li.appendChild(document.createElement("br"));
39239
+ const liCaret = document.createRange();
39240
+ liCaret.selectNodeContents(li);
39241
+ liCaret.collapse(true);
39242
+ sel.removeAllRanges();
39243
+ sel.addRange(liCaret);
39244
+ } else {
39245
+ document.execCommand("formatBlock", false, rule.tag);
39246
+ }
39247
+ return true;
39248
+ }, []);
39249
+ const afterEdit = useCallback(() => {
39250
+ const el = ref.current;
39251
+ if (!el) return;
39252
+ tryInlineMarkdown();
39253
+ setEmpty(htmlIsEmpty(el));
39254
+ setToolbar(readToolbarState(el));
39255
+ scheduleEmit();
39256
+ }, [scheduleEmit, tryInlineMarkdown]);
39257
+ const handleKeyDown = useCallback((e) => {
39258
+ if (e.key === " " && tryBlockMarkdown()) {
39259
+ e.preventDefault();
39260
+ afterEdit();
39261
+ }
39262
+ }, [afterEdit, tryBlockMarkdown]);
39263
+ const execInline = useCallback((command) => {
39264
+ document.execCommand(command, false);
39265
+ afterEdit();
39266
+ }, [afterEdit]);
39267
+ const execBlock = useCallback((tag) => {
39268
+ const next = toolbar.block === tag ? "p" : tag;
39269
+ document.execCommand("formatBlock", false, next);
39270
+ afterEdit();
39271
+ }, [afterEdit, toolbar.block]);
39272
+ const execList = useCallback((command) => {
39273
+ document.execCommand(command, false);
39274
+ afterEdit();
39275
+ }, [afterEdit]);
39276
+ const execLink = useCallback(() => {
39277
+ if (toolbar.link) {
39278
+ document.execCommand("unlink", false);
39279
+ afterEdit();
39280
+ return;
39281
+ }
39282
+ const url = window.prompt(t("richTextEditor.linkPrompt"));
39283
+ if (!url) return;
39284
+ const safe = safeUrl(url, ["http://", "https://", "mailto:"]) ?? `https://${url}`;
39285
+ document.execCommand("createLink", false, safe);
39286
+ afterEdit();
39287
+ }, [afterEdit, t, toolbar.link]);
39288
+ const execRule = useCallback(() => {
39289
+ document.execCommand("insertHorizontalRule", false);
39290
+ afterEdit();
39291
+ }, [afterEdit]);
39292
+ if (readOnly) {
39293
+ return /* @__PURE__ */ jsxs(Box, { className: cn("almadar-rich-text max-w-none", className), children: [
39294
+ /* @__PURE__ */ jsx(RichTextStyles, {}),
39295
+ /* @__PURE__ */ jsx(Box, { dangerouslySetInnerHTML: { __html: sanitizeRichHtml(value ?? "") } })
39371
39296
  ] });
39372
39297
  }
39373
- case "bullet-list":
39374
- case "numbered-list": {
39375
- const items = block.children ?? [];
39376
- return /* @__PURE__ */ jsxs(
39298
+ return /* @__PURE__ */ jsxs(Box, { className: cn("flex flex-col gap-2", className), children: [
39299
+ /* @__PURE__ */ jsx(RichTextStyles, {}),
39300
+ showToolbar && /* @__PURE__ */ jsxs(
39377
39301
  Box,
39378
39302
  {
39379
- as: block.type === "bullet-list" ? "ul" : "ol",
39303
+ role: "toolbar",
39304
+ "aria-label": t("richTextEditor.editorToolbar"),
39380
39305
  className: cn(
39381
- "space-y-1 pl-6",
39382
- block.type === "bullet-list" ? "list-disc" : "list-decimal"
39306
+ "sticky top-0 z-10 flex flex-wrap items-center gap-0.5 self-start",
39307
+ "rounded-md border border-border bg-background/95 px-1 py-0.5 shadow-sm"
39383
39308
  ),
39384
39309
  children: [
39385
- items.map((child) => /* @__PURE__ */ jsxs(Box, { as: "li", className: "group/item flex items-start gap-2", children: [
39386
- /* @__PURE__ */ jsx(
39387
- Editable,
39388
- {
39389
- tag: "span",
39390
- value: child.content ?? "",
39391
- readOnly,
39392
- placeholder: t("richBlockEditor.placeholder.listItem"),
39393
- ariaLabel: t("richBlockEditor.aria.listItem"),
39394
- className: "inline-block min-w-[1ch] flex-1",
39395
- onValueChange: (next) => setChildContent(child.id, next)
39396
- }
39397
- ),
39398
- !readOnly && showAffordances && /* @__PURE__ */ jsx(
39399
- Button,
39400
- {
39401
- type: "button",
39402
- variant: "ghost",
39403
- "aria-label": t("richBlockEditor.aria.removeListItem"),
39404
- className: cn(
39405
- "h-5 w-5 shrink-0 rounded-sm text-muted-foreground p-0 gap-0",
39406
- "opacity-0 group-hover/item:opacity-100 hover:bg-muted hover:text-foreground"
39407
- ),
39408
- onClick: () => removeListItem(child.id),
39409
- children: /* @__PURE__ */ jsx(Icon, { name: "trash", className: "w-3 h-3" })
39410
- }
39411
- )
39412
- ] }, child.id)),
39413
- !readOnly && showAffordances && /* @__PURE__ */ jsx(Box, { as: "li", className: "list-none pl-0", children: /* @__PURE__ */ jsxs(
39414
- Button,
39415
- {
39416
- type: "button",
39417
- variant: "ghost",
39418
- className: cn(
39419
- "inline-flex items-center gap-1 text-xs text-muted-foreground",
39420
- "hover:text-foreground p-0 h-auto"
39421
- ),
39422
- onClick: addListItem,
39423
- children: [
39424
- /* @__PURE__ */ jsx(Icon, { name: "plus", className: "w-3 h-3" }),
39425
- " ",
39426
- t("richBlockEditor.addItem")
39427
- ]
39428
- }
39429
- ) })
39310
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Bold, label: t("richTextEditor.bold"), active: toolbar.bold, onExec: () => execInline("bold") }),
39311
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Italic, label: t("richTextEditor.italic"), active: toolbar.italic, onExec: () => execInline("italic") }),
39312
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Underline, label: t("richTextEditor.underline"), active: toolbar.underline, onExec: () => execInline("underline") }),
39313
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Strikethrough, label: t("richTextEditor.strikethrough"), active: toolbar.strikeThrough, onExec: () => execInline("strikeThrough") }),
39314
+ /* @__PURE__ */ jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
39315
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Heading1, label: t("richTextEditor.heading1"), active: toolbar.block === "h1", onExec: () => execBlock("h1") }),
39316
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Heading2, label: t("richTextEditor.heading2"), active: toolbar.block === "h2", onExec: () => execBlock("h2") }),
39317
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Heading3, label: t("richTextEditor.heading3"), active: toolbar.block === "h3", onExec: () => execBlock("h3") }),
39318
+ /* @__PURE__ */ jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
39319
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: List, label: t("richTextEditor.bulletList"), active: toolbar.bullets, onExec: () => execList("insertUnorderedList") }),
39320
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: ListOrdered, label: t("richTextEditor.numberedList"), active: toolbar.numbers, onExec: () => execList("insertOrderedList") }),
39321
+ /* @__PURE__ */ jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
39322
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Quote, label: t("richTextEditor.quote"), active: toolbar.block === "blockquote", onExec: () => execBlock("blockquote") }),
39323
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Code, label: t("richTextEditor.code"), active: toolbar.block === "pre", onExec: () => execBlock("pre") }),
39324
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Link$1, label: toolbar.link ? t("richTextEditor.removeLink") : t("richTextEditor.link"), active: toolbar.link, onExec: execLink }),
39325
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Minus, label: t("richTextEditor.divider"), onExec: execRule })
39430
39326
  ]
39431
39327
  }
39432
- );
39433
- }
39434
- case "paragraph":
39435
- default:
39436
- return /* @__PURE__ */ jsx(
39437
- Editable,
39328
+ ),
39329
+ /* @__PURE__ */ jsx(
39330
+ Box,
39438
39331
  {
39439
- tag: "p",
39440
- value: block.content ?? "",
39441
- readOnly,
39442
- placeholder: placeholder ?? t("richBlockEditor.placeholder.paragraph"),
39443
- ariaLabel: t("richBlockEditor.aria.paragraphBlock"),
39444
- className: "leading-7",
39445
- onValueChange: setContent
39332
+ ref,
39333
+ contentEditable: true,
39334
+ suppressContentEditableWarning: true,
39335
+ role: "textbox",
39336
+ "aria-multiline": "true",
39337
+ "aria-label": t("richTextEditor.editorSurface"),
39338
+ "data-placeholder": placeholder ?? t("richTextEditor.placeholder"),
39339
+ "data-empty": empty ? "true" : "false",
39340
+ className: cn(
39341
+ "almadar-rich-text max-w-none min-h-[8rem] outline-none",
39342
+ "data-[empty=true]:before:content-[attr(data-placeholder)]",
39343
+ "data-[empty=true]:before:text-muted-foreground/60",
39344
+ "data-[empty=true]:before:pointer-events-none",
39345
+ "data-[empty=true]:before:absolute",
39346
+ "relative cursor-text"
39347
+ ),
39348
+ onInput: afterEdit,
39349
+ onKeyDown: handleKeyDown,
39350
+ onBlur: flushEmit
39446
39351
  }
39447
- );
39448
- }
39352
+ )
39353
+ ] });
39354
+ };
39355
+ RichTextEditor.displayName = "RichTextEditor";
39356
+ }
39357
+ });
39358
+ function renderIconInput4(icon, props) {
39359
+ return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
39360
+ }
39361
+ function DocumentPanel({
39362
+ id,
39363
+ title,
39364
+ subtitle,
39365
+ value,
39366
+ actions,
39367
+ maxInlineActions,
39368
+ editing = false,
39369
+ titleCommitEvent,
39370
+ contentChangeEvent,
39371
+ editEvent,
39372
+ doneEvent,
39373
+ editLabel,
39374
+ doneLabel,
39375
+ autosaveHint,
39376
+ placeholder,
39377
+ className
39378
+ }) {
39379
+ const eventBus = useEventBus();
39380
+ const { t } = useTranslate();
39381
+ const [titleDraft, setTitleDraft] = useState(null);
39382
+ const recordId = id ?? "";
39383
+ const actionDefs = actions ?? [];
39384
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
39385
+ const inlineActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
39386
+ const menuActions = actionDefs.filter((a) => !inlineActions.includes(a));
39387
+ const fireAction = (action) => {
39388
+ if (!action.event) return;
39389
+ eventBus.emit(`UI:${action.event}`, { id: recordId, title: title ?? "" });
39449
39390
  };
39450
- return /* @__PURE__ */ jsxs(
39391
+ const commitTitle = () => {
39392
+ if (!titleCommitEvent) return;
39393
+ const next = (titleDraft ?? "").trim();
39394
+ setTitleDraft(null);
39395
+ if (!next || next === title) return;
39396
+ eventBus.emit(`UI:${titleCommitEvent}`, { title: next, id: recordId });
39397
+ };
39398
+ const untitled = t("documentPanel.untitled") || "Untitled";
39399
+ const titleNode = titleCommitEvent && titleDraft !== null ? /* @__PURE__ */ jsx(
39400
+ Input,
39401
+ {
39402
+ value: titleDraft,
39403
+ autoFocus: true,
39404
+ "aria-label": t("common.title"),
39405
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
39406
+ onFocus: (e) => e.currentTarget.select(),
39407
+ onChange: (e) => setTitleDraft(e.target.value),
39408
+ onBlur: commitTitle,
39409
+ onKeyDown: (e) => {
39410
+ if (e.key === "Enter") {
39411
+ e.preventDefault();
39412
+ commitTitle();
39413
+ } else if (e.key === "Escape") {
39414
+ e.preventDefault();
39415
+ setTitleDraft(null);
39416
+ }
39417
+ },
39418
+ "data-testid": "document-title-input"
39419
+ }
39420
+ ) : titleCommitEvent ? /* @__PURE__ */ jsx(
39451
39421
  Box,
39452
39422
  {
39453
- className: cn(
39454
- "group relative flex items-start gap-2 rounded-sm",
39455
- "px-2 py-1 hover:bg-muted/30"
39456
- ),
39457
- "data-block-id": block.id,
39458
- "data-block-type": block.type,
39459
- children: [
39460
- !readOnly && showAffordances && /* @__PURE__ */ jsx(Box, { className: "flex w-8 shrink-0 items-center pt-1", children: /* @__PURE__ */ jsx(
39461
- BlockMenu,
39423
+ role: "button",
39424
+ tabIndex: 0,
39425
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
39426
+ onClick: () => setTitleDraft(title ?? ""),
39427
+ onKeyDown: (e) => {
39428
+ if (e.key === "Enter" || e.key === " ") {
39429
+ e.preventDefault();
39430
+ setTitleDraft(title ?? "");
39431
+ }
39432
+ },
39433
+ "data-testid": "document-title-editable",
39434
+ children: /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", className: cn(!title && "text-muted-foreground"), children: title || untitled })
39435
+ }
39436
+ ) : /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", children: title || untitled });
39437
+ const emitWithId = (event) => () => {
39438
+ if (event) eventBus.emit(`UI:${event}`, { id: recordId });
39439
+ };
39440
+ return /* @__PURE__ */ jsx(Card, { variant: "elevated", className: cn("w-full", className), children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", className: "p-6 sm:p-8", children: [
39441
+ /* @__PURE__ */ jsxs(HStack, { gap: "sm", className: "justify-between items-start", children: [
39442
+ /* @__PURE__ */ jsxs(VStack, { gap: "xs", className: "flex-1 min-w-0", children: [
39443
+ titleNode,
39444
+ subtitle ? /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: subtitle }) : null
39445
+ ] }),
39446
+ /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "flex-shrink-0 pt-1 items-center", children: editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
39447
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", className: "hidden sm:block", children: autosaveHint ?? (t("documentPanel.autosaveHint") || "") }),
39448
+ /* @__PURE__ */ jsx(Button, { variant: "primary", size: "sm", onClick: emitWithId(doneEvent), "data-testid": "document-done", children: doneLabel ?? (t("documentPanel.done") || "Done") })
39449
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
39450
+ editEvent && /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: emitWithId(editEvent), "data-testid": "document-edit", children: [
39451
+ /* @__PURE__ */ jsx(Icon, { name: "edit", size: "xs", className: "mr-1" }),
39452
+ editLabel ?? (t("documentPanel.edit") || "Edit")
39453
+ ] }),
39454
+ inlineActions.map((action, idx) => /* @__PURE__ */ jsxs(
39455
+ Button,
39462
39456
  {
39463
- block,
39464
- readOnly,
39465
- onDelete,
39466
- onDuplicate,
39467
- onChangeType
39457
+ variant: "primary",
39458
+ size: "sm",
39459
+ onClick: () => fireAction(action),
39460
+ "data-testid": `action-${action.event}`,
39461
+ children: [
39462
+ action.icon && renderIconInput4(action.icon, { size: "xs", className: "mr-1" }),
39463
+ action.label
39464
+ ]
39465
+ },
39466
+ idx
39467
+ )),
39468
+ menuActions.length > 0 && /* @__PURE__ */ jsx(
39469
+ Menu,
39470
+ {
39471
+ position: "bottom-end",
39472
+ 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" }) }),
39473
+ items: menuActions.map((action) => ({
39474
+ label: action.label,
39475
+ icon: action.icon,
39476
+ variant: action.variant === "danger" ? "danger" : "default",
39477
+ onClick: () => fireAction(action)
39478
+ }))
39468
39479
  }
39469
- ) }),
39470
- /* @__PURE__ */ jsx(Box, { className: "min-w-0 flex-1", children: renderBody() })
39471
- ]
39472
- }
39473
- );
39480
+ )
39481
+ ] }) })
39482
+ ] }),
39483
+ editing ? /* @__PURE__ */ jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsx(
39484
+ Box,
39485
+ {
39486
+ onClick: emitWithId(editEvent),
39487
+ className: cn(
39488
+ "rounded-md px-1 -mx-1 min-h-[16rem]",
39489
+ editEvent && "cursor-text transition-colors hover:bg-muted/30"
39490
+ ),
39491
+ "data-testid": "document-body",
39492
+ children: value && value !== "" ? /* @__PURE__ */ jsx(RichTextEditor, { value, readOnly: true }) : /* @__PURE__ */ jsx(Typography, { variant: "body", color: "muted", children: placeholder ?? (t("documentPanel.placeholder") || "") })
39493
+ }
39494
+ )
39495
+ ] }) });
39474
39496
  }
39475
- var TOOLBAR_ENTRIES, BLOCK_TYPE_LABEL_KEY, CHANGEABLE_TYPES, _idSeq, BLOCK_TYPES, RichBlockEditor;
39476
- var init_RichBlockEditor = __esm({
39477
- "components/core/molecules/RichBlockEditor.tsx"() {
39497
+ var init_DocumentPanel = __esm({
39498
+ "components/core/molecules/DocumentPanel.tsx"() {
39478
39499
  "use client";
39479
39500
  init_cn();
39480
- init_Card();
39501
+ init_useEventBus();
39502
+ init_Box();
39503
+ init_Stack();
39481
39504
  init_Typography();
39482
39505
  init_Button();
39483
- init_Box();
39484
- init_Divider();
39485
- init_Input();
39506
+ init_Card();
39486
39507
  init_Icon();
39487
- init_useEventBus();
39488
- TOOLBAR_ENTRIES = [
39489
- { type: "paragraph", labelKey: "richBlockEditor.toolbar.text", icon: Type },
39490
- { type: "heading-1", labelKey: "richBlockEditor.toolbar.h1", icon: Heading1 },
39491
- { type: "heading-2", labelKey: "richBlockEditor.toolbar.h2", icon: Heading2 },
39492
- { type: "heading-3", labelKey: "richBlockEditor.toolbar.h3", icon: Heading3 },
39493
- { type: "bullet-list", labelKey: "richBlockEditor.toolbar.bulletList", icon: List },
39494
- { type: "numbered-list", labelKey: "richBlockEditor.toolbar.numbered", icon: ListOrdered },
39495
- { type: "quote", labelKey: "richBlockEditor.toolbar.quote", icon: Quote },
39496
- { type: "code", labelKey: "richBlockEditor.toolbar.code", icon: Code },
39497
- { type: "divider", labelKey: "richBlockEditor.toolbar.divider", icon: Minus },
39498
- { type: "image", labelKey: "richBlockEditor.toolbar.image", icon: Image$1 }
39499
- ];
39500
- BLOCK_TYPE_LABEL_KEY = {
39501
- paragraph: "richBlockEditor.blockType.paragraph",
39502
- "heading-1": "richBlockEditor.blockType.heading1",
39503
- "heading-2": "richBlockEditor.blockType.heading2",
39504
- "heading-3": "richBlockEditor.blockType.heading3",
39505
- "bullet-list": "richBlockEditor.blockType.bulletList",
39506
- "numbered-list": "richBlockEditor.blockType.numberedList",
39507
- quote: "richBlockEditor.blockType.quote",
39508
- code: "richBlockEditor.blockType.code",
39509
- divider: "richBlockEditor.blockType.divider",
39510
- image: "richBlockEditor.blockType.image"
39511
- };
39512
- CHANGEABLE_TYPES = [
39513
- "paragraph",
39514
- "heading-1",
39515
- "heading-2",
39516
- "heading-3",
39517
- "bullet-list",
39518
- "numbered-list",
39519
- "quote",
39520
- "code"
39521
- ];
39522
- _idSeq = 0;
39523
- BLOCK_TYPES = /* @__PURE__ */ new Set([
39524
- "paragraph",
39525
- "heading-1",
39526
- "heading-2",
39527
- "heading-3",
39528
- "bullet-list",
39529
- "numbered-list",
39530
- "quote",
39531
- "code",
39532
- "divider",
39533
- "image"
39534
- ]);
39535
- RichBlockEditor = ({
39536
- initialBlocks,
39537
- onChange,
39538
- changeEvent,
39539
- readOnly = false,
39540
- placeholder,
39541
- enableBlocks = true,
39542
- showToolbar = true,
39543
- className
39544
- }) => {
39545
- const { t } = useTranslate();
39546
- const [blocks, setBlocks] = useState(
39547
- () => normalizeBlocks(initialBlocks)
39548
- );
39549
- const onChangeRef = useRef(onChange);
39550
- useEffect(() => {
39551
- onChangeRef.current = onChange;
39552
- }, [onChange]);
39553
- const eventBus = useEventBus();
39554
- const changeEventRef = useRef(changeEvent);
39555
- useEffect(() => {
39556
- changeEventRef.current = changeEvent;
39557
- }, [changeEvent]);
39558
- const commit = useCallback((next) => {
39559
- setBlocks(next);
39560
- onChangeRef.current?.(next);
39561
- const evt = changeEventRef.current;
39562
- if (evt) eventBus.emit(`UI:${evt}`, { blocks: next });
39563
- }, [eventBus]);
39564
- const handleAppend = useCallback(
39565
- (type) => {
39566
- if (readOnly) return;
39567
- commit([...blocks, createBlock(type)]);
39568
- },
39569
- [blocks, commit, readOnly]
39570
- );
39571
- const handleUpdate = useCallback(
39572
- (id, updater) => {
39573
- commit(replaceBlock(blocks, id, updater));
39574
- },
39575
- [blocks, commit]
39576
- );
39577
- const handleDelete = useCallback(
39578
- (id) => {
39579
- const next = removeBlock(blocks, id);
39580
- commit(next.length > 0 ? next : [createBlock("paragraph")]);
39581
- },
39582
- [blocks, commit]
39583
- );
39584
- const handleDuplicate = useCallback(
39585
- (id) => {
39586
- const target = blocks.find((b) => b.id === id);
39587
- if (!target) return;
39588
- commit(insertAfter(blocks, id, duplicateBlock(target)));
39589
- },
39590
- [blocks, commit]
39508
+ init_Input();
39509
+ init_Menu();
39510
+ init_RichTextEditor();
39511
+ }
39512
+ });
39513
+ function renderIconInput5(icon, props) {
39514
+ return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
39515
+ }
39516
+ function fieldName(field) {
39517
+ return field.name ?? field.key ?? "";
39518
+ }
39519
+ function relationLabel(value) {
39520
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Date) return null;
39521
+ for (const key of ["name", "title", "label"]) {
39522
+ const candidate = value[key];
39523
+ if (typeof candidate === "string" && candidate !== "") return candidate;
39524
+ }
39525
+ const id = value.id;
39526
+ return id !== void 0 && id !== null ? String(id) : null;
39527
+ }
39528
+ function fieldKind(field, value) {
39529
+ if (field.kind) return field.kind;
39530
+ if (field.options && field.options.length > 0) return "select";
39531
+ if (typeof value === "boolean" || field.format === "boolean") return "boolean";
39532
+ if (Array.isArray(value) || relationLabel(value ?? null) !== null) return "readonly";
39533
+ return "text";
39534
+ }
39535
+ function DocumentDetails({
39536
+ entity,
39537
+ fields,
39538
+ metaCommitEvent,
39539
+ title,
39540
+ className
39541
+ }) {
39542
+ const eventBus = useEventBus();
39543
+ const { t } = useTranslate();
39544
+ const [fieldDraft, setFieldDraft] = useState(null);
39545
+ const recordId = entity?.id !== void 0 && entity?.id !== null ? String(entity.id) : "";
39546
+ const fieldDefs = (fields ?? []).filter((f3) => fieldName(f3) !== "");
39547
+ if (fieldDefs.length === 0) return null;
39548
+ const commitField = (name, next, current) => {
39549
+ setFieldDraft(null);
39550
+ if (!metaCommitEvent || next === current) return;
39551
+ eventBus.emit(`UI:${metaCommitEvent}`, { id: recordId, patch: { id: recordId, [name]: next } });
39552
+ };
39553
+ const renderValue = (field) => {
39554
+ const name = fieldName(field);
39555
+ const raw = getNestedValue(entity ?? {}, name);
39556
+ const kind = fieldKind(field, raw);
39557
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
39558
+ if (kind === "boolean") {
39559
+ return /* @__PURE__ */ jsx(
39560
+ Switch,
39561
+ {
39562
+ checked: Boolean(raw),
39563
+ disabled: !metaCommitEvent,
39564
+ "aria-label": label,
39565
+ onChange: (checked) => commitField(name, checked, Boolean(raw))
39566
+ }
39591
39567
  );
39592
- const handleChangeType = useCallback(
39593
- (id, type) => {
39594
- commit(
39595
- replaceBlock(blocks, id, (b) => changeBlockType(b, type))
39596
- );
39597
- },
39598
- [blocks, commit]
39568
+ }
39569
+ if (kind === "select") {
39570
+ return /* @__PURE__ */ jsx(
39571
+ Select,
39572
+ {
39573
+ value: raw !== void 0 && raw !== null ? String(raw) : "",
39574
+ disabled: !metaCommitEvent,
39575
+ "aria-label": label,
39576
+ className: "h-8 w-full",
39577
+ options: (field.options ?? []).map((opt) => ({ value: opt, label: humanizeEnumValue(opt) })),
39578
+ onChange: (e) => commitField(name, e.target.value, raw)
39579
+ }
39599
39580
  );
39600
- return /* @__PURE__ */ jsxs(
39601
- Card,
39581
+ }
39582
+ if (kind === "readonly" || !metaCommitEvent) {
39583
+ if (Array.isArray(raw)) {
39584
+ if (raw.length === 0) {
39585
+ return /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
39586
+ }
39587
+ 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)) });
39588
+ }
39589
+ const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
39590
+ return /* @__PURE__ */ jsx(Typography, { variant: "small", className: "break-words", children: shown2 });
39591
+ }
39592
+ if (fieldDraft?.name === name) {
39593
+ return /* @__PURE__ */ jsx(
39594
+ Input,
39602
39595
  {
39603
- variant: "bordered",
39604
- padding: "none",
39605
- className: cn("flex flex-col text-card-foreground", className),
39606
- children: [
39607
- enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsx(
39608
- Box,
39609
- {
39610
- role: "toolbar",
39611
- "aria-label": t("richBlockEditor.editorToolbar"),
39612
- className: cn(
39613
- "flex flex-wrap items-center gap-1",
39614
- "border-b border-border bg-muted/30 px-2 py-2"
39615
- ),
39616
- children: TOOLBAR_ENTRIES.map((entry) => {
39617
- const Icon2 = entry.icon;
39618
- const entryLabel = t(entry.labelKey);
39619
- return /* @__PURE__ */ jsxs(
39620
- Button,
39621
- {
39622
- type: "button",
39623
- variant: "ghost",
39624
- size: "sm",
39625
- "aria-label": t("richBlockEditor.insertEntry", { label: entryLabel }),
39626
- title: entryLabel,
39627
- onClick: () => handleAppend(entry.type),
39628
- children: [
39629
- /* @__PURE__ */ jsx(Icon2, { size: 14 }),
39630
- /* @__PURE__ */ jsx(Typography, { as: "span", variant: "caption", className: "ml-1 hidden text-xs sm:inline", children: entryLabel })
39631
- ]
39632
- },
39633
- entry.type
39634
- );
39635
- })
39636
- }
39637
- ),
39638
- /* @__PURE__ */ jsx(Box, { className: "flex flex-col gap-1 px-3 py-3", children: blocks.map((block) => /* @__PURE__ */ jsx(
39639
- BlockRow,
39640
- {
39641
- block,
39642
- readOnly,
39643
- showAffordances: enableBlocks,
39644
- placeholder,
39645
- onUpdate: (updater) => handleUpdate(block.id, updater),
39646
- onDelete: () => handleDelete(block.id),
39647
- onDuplicate: () => handleDuplicate(block.id),
39648
- onChangeType: (type) => handleChangeType(block.id, type)
39649
- },
39650
- block.id
39651
- )) })
39652
- ]
39596
+ value: fieldDraft.value,
39597
+ autoFocus: true,
39598
+ "aria-label": label,
39599
+ className: "h-8 w-full",
39600
+ onChange: (e) => setFieldDraft({ name, value: e.target.value }),
39601
+ onBlur: () => commitField(name, fieldDraft.value.trim(), raw),
39602
+ onKeyDown: (e) => {
39603
+ if (e.key === "Enter") {
39604
+ e.preventDefault();
39605
+ commitField(name, fieldDraft.value.trim(), raw);
39606
+ } else if (e.key === "Escape") {
39607
+ e.preventDefault();
39608
+ setFieldDraft(null);
39609
+ }
39610
+ },
39611
+ "data-testid": `document-property-input-${name}`
39653
39612
  }
39654
39613
  );
39655
- };
39656
- RichBlockEditor.displayName = "RichBlockEditor";
39614
+ }
39615
+ const shown = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
39616
+ return /* @__PURE__ */ jsx(
39617
+ Box,
39618
+ {
39619
+ role: "button",
39620
+ tabIndex: 0,
39621
+ className: cn(
39622
+ "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
39623
+ (raw === void 0 || raw === null || raw === "") && "text-muted-foreground"
39624
+ ),
39625
+ onClick: () => setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" }),
39626
+ onKeyDown: (e) => {
39627
+ if (e.key === "Enter" || e.key === " ") {
39628
+ e.preventDefault();
39629
+ setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" });
39630
+ }
39631
+ },
39632
+ "data-testid": `document-property-${name}`,
39633
+ children: /* @__PURE__ */ jsx(Typography, { variant: "small", className: "break-words", children: shown })
39634
+ }
39635
+ );
39636
+ };
39637
+ return /* @__PURE__ */ jsx(Card, { variant: "bordered", className: cn("w-full", className), children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", className: "p-4", children: [
39638
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "secondary", weight: "medium", className: "uppercase tracking-wide", children: title ?? (t("documentDetails.title") || "Details") }),
39639
+ /* @__PURE__ */ jsx(VStack, { gap: "sm", children: fieldDefs.map((field) => {
39640
+ const name = fieldName(field);
39641
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
39642
+ return /* @__PURE__ */ jsxs(VStack, { gap: "xs", children: [
39643
+ /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "items-center", children: [
39644
+ field.icon && renderIconInput5(field.icon, { size: "xs", className: "text-muted-foreground" }),
39645
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", children: label })
39646
+ ] }),
39647
+ /* @__PURE__ */ jsx(Box, { className: "min-w-0", children: renderValue(field) })
39648
+ ] }, name);
39649
+ }) })
39650
+ ] }) });
39651
+ }
39652
+ var init_DocumentDetails = __esm({
39653
+ "components/core/molecules/DocumentDetails.tsx"() {
39654
+ "use client";
39655
+ init_cn();
39656
+ init_format();
39657
+ init_getNestedValue();
39658
+ init_useEventBus();
39659
+ init_Box();
39660
+ init_Stack();
39661
+ init_Typography();
39662
+ init_Badge();
39663
+ init_Card();
39664
+ init_Icon();
39665
+ init_Input();
39666
+ init_Select();
39667
+ init_Switch();
39657
39668
  }
39658
39669
  });
39659
39670
  function collectInitiallyCollapsed(nodes, acc) {
@@ -44193,8 +44204,8 @@ var init_DataTable = __esm({
44193
44204
  DataTable.displayName = "DataTable";
44194
44205
  }
44195
44206
  });
44196
- function getBadgeVariant(fieldName, value) {
44197
- const name = fieldName.toLowerCase();
44207
+ function getBadgeVariant(fieldName2, value) {
44208
+ const name = fieldName2.toLowerCase();
44198
44209
  const val = String(value).toLowerCase();
44199
44210
  if (name.includes("status")) {
44200
44211
  if (val.includes("complete") || val.includes("done") || val.includes("active"))
@@ -44210,12 +44221,12 @@ function getBadgeVariant(fieldName, value) {
44210
44221
  }
44211
44222
  return "default";
44212
44223
  }
44213
- function formatFieldValue2(value, fieldName) {
44224
+ function formatFieldValue2(value, fieldName2) {
44214
44225
  if (typeof value === "number") {
44215
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
44226
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
44216
44227
  return `${value}%`;
44217
44228
  }
44218
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
44229
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
44219
44230
  return `$${value.toLocaleString()}`;
44220
44231
  }
44221
44232
  return value.toLocaleString();
@@ -44225,7 +44236,7 @@ function formatFieldValue2(value, fieldName) {
44225
44236
  }
44226
44237
  return String(value);
44227
44238
  }
44228
- function renderRichFieldValue(value, fieldName, fieldType, meta) {
44239
+ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
44229
44240
  if (value === void 0 || value === null) return "\u2014";
44230
44241
  const str = String(value);
44231
44242
  switch (fieldType) {
@@ -44236,7 +44247,7 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
44236
44247
  "img",
44237
44248
  {
44238
44249
  src: str,
44239
- alt: formatFieldLabel(fieldName),
44250
+ alt: formatFieldLabel(fieldName2),
44240
44251
  className: "max-w-full max-h-64 rounded-md object-contain",
44241
44252
  loading: "lazy"
44242
44253
  }
@@ -44371,9 +44382,9 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
44371
44382
  return /* @__PURE__ */ jsx("a", { href: `tel:${str}`, className: "text-primary hover:underline", children: str });
44372
44383
  default:
44373
44384
  if (meta?.values && meta.values.length > 0 && meta.values.includes(str)) {
44374
- return /* @__PURE__ */ jsx(Badge, { variant: getBadgeVariant(fieldName, str), children: humanizeEnumValue(str) });
44385
+ return /* @__PURE__ */ jsx(Badge, { variant: getBadgeVariant(fieldName2, str), children: humanizeEnumValue(str) });
44375
44386
  }
44376
- return formatFieldValue2(value, fieldName);
44387
+ return formatFieldValue2(value, fieldName2);
44377
44388
  }
44378
44389
  }
44379
44390
  function normalizeFieldDefs(fields) {
@@ -45216,7 +45227,21 @@ var init_Form = __esm({
45216
45227
  const normalizedInitialData = React94__default.useMemo(() => {
45217
45228
  const entityRowAsInitial = isPlainEntityRow(entity) ? entity : void 0;
45218
45229
  const callerInitial = initialData !== null && typeof initialData === "object" && !Array.isArray(initialData) ? initialData : {};
45219
- return entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
45230
+ const merged = entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
45231
+ const toId = (value) => {
45232
+ if (value === null || value === void 0 || typeof value !== "object" || value instanceof Date) return value;
45233
+ if (Array.isArray(value)) {
45234
+ return value.map(
45235
+ (item) => item !== null && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date) && item.id !== void 0 && item.id !== null ? String(item.id) : item
45236
+ );
45237
+ }
45238
+ return value.id !== void 0 && value.id !== null ? String(value.id) : value;
45239
+ };
45240
+ const normalized = {};
45241
+ for (const [key, value] of Object.entries(merged)) {
45242
+ normalized[key] = key === "id" ? value : toId(value);
45243
+ }
45244
+ return normalized;
45220
45245
  }, [entity, initialData]);
45221
45246
  const entityDerivedFields = React94__default.useMemo(() => {
45222
45247
  if (fields && fields.length > 0) return void 0;
@@ -45345,8 +45370,8 @@ var init_Form = __esm({
45345
45370
  checkViolations(name, newFormData);
45346
45371
  };
45347
45372
  const isFieldVisible = React94__default.useCallback(
45348
- (fieldName) => {
45349
- const condition = conditionalFields[fieldName];
45373
+ (fieldName2) => {
45374
+ const condition = conditionalFields[fieldName2];
45350
45375
  if (!condition) return true;
45351
45376
  return Boolean(evaluateFormExpression(condition, evalContext));
45352
45377
  },
@@ -45392,9 +45417,9 @@ var init_Form = __esm({
45392
45417
  };
45393
45418
  const handleInvalid = (e) => {
45394
45419
  const target = e.target;
45395
- const fieldName = target.getAttribute("data-field-name") ?? target.name ?? "";
45420
+ const fieldName2 = target.getAttribute("data-field-name") ?? target.name ?? "";
45396
45421
  const fieldMessage = target.validationMessage || "Invalid value";
45397
- debug("forms", "invalid", { mode: formMode, fieldName, fieldMessage });
45422
+ debug("forms", "invalid", { mode: formMode, fieldName: fieldName2, fieldMessage });
45398
45423
  queueMicrotask(() => {
45399
45424
  const form = formRef.current;
45400
45425
  if (!form) return;
@@ -45430,22 +45455,22 @@ var init_Form = __esm({
45430
45455
  };
45431
45456
  const renderField = React94__default.useCallback(
45432
45457
  (field) => {
45433
- const fieldName = field.name || field.field;
45434
- if (!fieldName) return null;
45435
- if (!isFieldVisible(fieldName)) {
45458
+ const fieldName2 = field.name || field.field;
45459
+ if (!fieldName2) return null;
45460
+ if (!isFieldVisible(fieldName2)) {
45436
45461
  return null;
45437
45462
  }
45438
45463
  const inputType = determineInputType(field);
45439
- const label = field.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/([A-Z])/g, " $1");
45440
- const currentValue = formData[fieldName] ?? field.defaultValue ?? "";
45441
- return /* @__PURE__ */ jsxs(VStack, { gap: "xs", "data-field": fieldName, children: [
45464
+ const label = field.label || fieldName2.charAt(0).toUpperCase() + fieldName2.slice(1).replace(/([A-Z])/g, " $1");
45465
+ const currentValue = formData[fieldName2] ?? field.defaultValue ?? "";
45466
+ return /* @__PURE__ */ jsxs(VStack, { gap: "xs", "data-field": fieldName2, children: [
45442
45467
  inputType !== "checkbox" && /* @__PURE__ */ jsxs(Typography, { as: "label", variant: "label", weight: "bold", children: [
45443
45468
  label,
45444
45469
  field.required && /* @__PURE__ */ jsx(Typography, { as: "span", color: "error", className: "ml-1", children: "*" })
45445
45470
  ] }),
45446
- renderFieldInput(field, fieldName, inputType, currentValue, label),
45471
+ renderFieldInput(field, fieldName2, inputType, currentValue, label),
45447
45472
  field.hint && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", children: field.hint })
45448
- ] }, fieldName);
45473
+ ] }, fieldName2);
45449
45474
  },
45450
45475
  [formData, isFieldVisible, relationsData, relationsLoading, isLoading]
45451
45476
  );
@@ -45474,8 +45499,8 @@ var init_Form = __esm({
45474
45499
  }
45475
45500
  return field;
45476
45501
  }).map((field) => {
45477
- const fieldName = field.name || field.field;
45478
- const override = fieldOverrides?.find((o) => o.name === fieldName);
45502
+ const fieldName2 = field.name || field.field;
45503
+ const override = fieldOverrides?.find((o) => o.name === fieldName2);
45479
45504
  if (!override) return field;
45480
45505
  return {
45481
45506
  ...field,
@@ -45531,11 +45556,11 @@ var init_Form = __esm({
45531
45556
  ] }, section.id);
45532
45557
  }).filter(Boolean);
45533
45558
  }, [sections, isSectionVisible, collapsedSections, renderField, gap]);
45534
- function renderFieldInput(field, fieldName, inputType, currentValue, label) {
45559
+ function renderFieldInput(field, fieldName2, inputType, currentValue, label) {
45535
45560
  const commonProps = {
45536
- id: fieldName,
45537
- name: fieldName,
45538
- "data-field-name": fieldName,
45561
+ id: fieldName2,
45562
+ name: fieldName2,
45563
+ "data-field-name": fieldName2,
45539
45564
  required: field.required,
45540
45565
  disabled: isLoading,
45541
45566
  placeholder: field.placeholder,
@@ -45549,7 +45574,7 @@ var init_Form = __esm({
45549
45574
  ...commonProps,
45550
45575
  label: label + (field.required ? " *" : ""),
45551
45576
  checked: Boolean(currentValue),
45552
- onChange: (e) => handleChange(fieldName, e.target.checked)
45577
+ onChange: (e) => handleChange(fieldName2, e.target.checked)
45553
45578
  }
45554
45579
  );
45555
45580
  case "textarea":
@@ -45558,7 +45583,7 @@ var init_Form = __esm({
45558
45583
  {
45559
45584
  ...commonProps,
45560
45585
  value: String(currentValue),
45561
- onChange: (e) => handleChange(fieldName, e.target.value),
45586
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45562
45587
  minLength: field.min,
45563
45588
  maxLength: field.max
45564
45589
  }
@@ -45571,14 +45596,14 @@ var init_Form = __esm({
45571
45596
  ...commonProps,
45572
45597
  options,
45573
45598
  value: String(currentValue),
45574
- onValueChange: (v) => handleChange(fieldName, v),
45599
+ onValueChange: (v) => handleChange(fieldName2, v),
45575
45600
  placeholder: field.placeholder || `Select ${label}...`
45576
45601
  }
45577
45602
  );
45578
45603
  }
45579
45604
  case "relation": {
45580
- const relationOptions = relationsData[fieldName] || [];
45581
- const relationLoading = relationsLoading[fieldName] || false;
45605
+ const relationOptions = relationsData[fieldName2] || [];
45606
+ const relationLoading = relationsLoading[fieldName2] || false;
45582
45607
  if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
45583
45608
  const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
45584
45609
  return /* @__PURE__ */ jsx(
@@ -45590,7 +45615,7 @@ var init_Form = __esm({
45590
45615
  clearable: true,
45591
45616
  options: [...relationOptions],
45592
45617
  value: selectedValues,
45593
- onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
45618
+ onValueChange: (value) => handleChange(fieldName2, Array.isArray(value) ? value : [value]),
45594
45619
  placeholder: field.placeholder || `Select ${label}...`
45595
45620
  }
45596
45621
  );
@@ -45600,7 +45625,7 @@ var init_Form = __esm({
45600
45625
  {
45601
45626
  ...commonProps,
45602
45627
  value: currentValue ? String(currentValue) : void 0,
45603
- onChange: (value) => handleChange(fieldName, value),
45628
+ onChange: (value) => handleChange(fieldName2, value),
45604
45629
  options: relationOptions,
45605
45630
  isLoading: relationLoading,
45606
45631
  placeholder: field.placeholder || `Select ${label}...`,
@@ -45617,7 +45642,7 @@ var init_Form = __esm({
45617
45642
  placeholder: field.placeholder,
45618
45643
  disabled: isLoading,
45619
45644
  value: arrayValue,
45620
- onChange: (next) => handleChange(fieldName, [...next])
45645
+ onChange: (next) => handleChange(fieldName2, [...next])
45621
45646
  }
45622
45647
  );
45623
45648
  }
@@ -45629,7 +45654,7 @@ var init_Form = __esm({
45629
45654
  type: "number",
45630
45655
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
45631
45656
  onChange: (e) => handleChange(
45632
- fieldName,
45657
+ fieldName2,
45633
45658
  e.target.value ? Number(e.target.value) : void 0
45634
45659
  ),
45635
45660
  min: field.min,
@@ -45646,7 +45671,7 @@ var init_Form = __esm({
45646
45671
  icon: DollarSign,
45647
45672
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
45648
45673
  onChange: (e) => handleChange(
45649
- fieldName,
45674
+ fieldName2,
45650
45675
  e.target.value ? Number(e.target.value) : void 0
45651
45676
  ),
45652
45677
  min: field.min,
@@ -45664,7 +45689,7 @@ var init_Form = __esm({
45664
45689
  const f3 = files[0];
45665
45690
  if (!f3) return;
45666
45691
  const reader = new FileReader();
45667
- reader.onload = () => handleChange(fieldName, {
45692
+ reader.onload = () => handleChange(fieldName2, {
45668
45693
  name: f3.name,
45669
45694
  mimeType: f3.type,
45670
45695
  sizeBytes: f3.size,
@@ -45681,7 +45706,7 @@ var init_Form = __esm({
45681
45706
  ...commonProps,
45682
45707
  type: "date",
45683
45708
  value: formatDateValue(currentValue),
45684
- onChange: (e) => handleChange(fieldName, e.target.value)
45709
+ onChange: (e) => handleChange(fieldName2, e.target.value)
45685
45710
  }
45686
45711
  );
45687
45712
  case "datetime-local":
@@ -45691,7 +45716,7 @@ var init_Form = __esm({
45691
45716
  ...commonProps,
45692
45717
  type: "datetime-local",
45693
45718
  value: formatDateTimeValue(currentValue),
45694
- onChange: (e) => handleChange(fieldName, e.target.value)
45719
+ onChange: (e) => handleChange(fieldName2, e.target.value)
45695
45720
  }
45696
45721
  );
45697
45722
  case "email":
@@ -45701,7 +45726,7 @@ var init_Form = __esm({
45701
45726
  ...commonProps,
45702
45727
  type: "email",
45703
45728
  value: String(currentValue),
45704
- onChange: (e) => handleChange(fieldName, e.target.value),
45729
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45705
45730
  minLength: field.min,
45706
45731
  maxLength: field.max
45707
45732
  }
@@ -45713,7 +45738,7 @@ var init_Form = __esm({
45713
45738
  ...commonProps,
45714
45739
  type: "url",
45715
45740
  value: String(currentValue),
45716
- onChange: (e) => handleChange(fieldName, e.target.value),
45741
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45717
45742
  minLength: field.min,
45718
45743
  maxLength: field.max
45719
45744
  }
@@ -45725,7 +45750,7 @@ var init_Form = __esm({
45725
45750
  ...commonProps,
45726
45751
  type: "password",
45727
45752
  value: String(currentValue),
45728
- onChange: (e) => handleChange(fieldName, e.target.value),
45753
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45729
45754
  minLength: field.min,
45730
45755
  maxLength: field.max
45731
45756
  }
@@ -45738,7 +45763,7 @@ var init_Form = __esm({
45738
45763
  ...commonProps,
45739
45764
  type: "text",
45740
45765
  value: String(currentValue),
45741
- onChange: (e) => handleChange(fieldName, e.target.value),
45766
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45742
45767
  minLength: field.min,
45743
45768
  maxLength: field.max
45744
45769
  }
@@ -46095,7 +46120,7 @@ function entityFieldsFromListItem(item) {
46095
46120
  }
46096
46121
  return result;
46097
46122
  }
46098
- function getStatusStyle(fieldName, value) {
46123
+ function getStatusStyle(fieldName2, value) {
46099
46124
  const val = String(value).toLowerCase();
46100
46125
  if (val.includes("complete") || val.includes("done"))
46101
46126
  return STATUS_STYLES.complete;
@@ -46111,12 +46136,12 @@ function getStatusStyle(fieldName, value) {
46111
46136
  if (val.includes("low")) return STATUS_STYLES.low;
46112
46137
  return STATUS_STYLES.default;
46113
46138
  }
46114
- function formatValue3(value, fieldName) {
46139
+ function formatValue3(value, fieldName2) {
46115
46140
  if (typeof value === "number") {
46116
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
46141
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
46117
46142
  return `${value}%`;
46118
46143
  }
46119
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
46144
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
46120
46145
  return new Intl.NumberFormat("en-US", {
46121
46146
  style: "currency",
46122
46147
  currency: "USD",
@@ -46139,8 +46164,8 @@ function formatValue3(value, fieldName) {
46139
46164
  }
46140
46165
  return String(value);
46141
46166
  }
46142
- function formatFieldLabel2(fieldName) {
46143
- return humanizeFieldName(fieldName).replace(/\sId$/, "").trim();
46167
+ function formatFieldLabel2(fieldName2) {
46168
+ return humanizeFieldName(fieldName2).replace(/\sId$/, "").trim();
46144
46169
  }
46145
46170
  var STATUS_STYLES, StatusBadge, ProgressIndicator, List3;
46146
46171
  var init_List = __esm({
@@ -46208,9 +46233,9 @@ var init_List = __esm({
46208
46233
  };
46209
46234
  StatusBadge = ({
46210
46235
  value,
46211
- fieldName
46236
+ fieldName: fieldName2
46212
46237
  }) => {
46213
- const style = getStatusStyle(fieldName, value);
46238
+ const style = getStatusStyle(fieldName2, value);
46214
46239
  return /* @__PURE__ */ jsxs(
46215
46240
  Typography,
46216
46241
  {
@@ -49142,8 +49167,8 @@ var init_StatCard = __esm({
49142
49167
  return items.length;
49143
49168
  }
49144
49169
  if (field.includes(":")) {
49145
- const [fieldName, fieldValue] = field.split(":");
49146
- return items.filter((item) => item[fieldName] === fieldValue).length;
49170
+ const [fieldName2, fieldValue] = field.split(":");
49171
+ return items.filter((item) => item[fieldName2] === fieldValue).length;
49147
49172
  }
49148
49173
  const fieldExistsOnItems = items.some((item) => field in item);
49149
49174
  if (fieldExistsOnItems) {
@@ -50663,6 +50688,8 @@ var init_component_registry_generated = __esm({
50663
50688
  init_DocSearch();
50664
50689
  init_DocSidebar();
50665
50690
  init_DocTOC();
50691
+ init_DocumentDetails();
50692
+ init_DocumentPanel();
50666
50693
  init_DocumentViewer();
50667
50694
  init_DrawFxLayer();
50668
50695
  init_DrawGroup();
@@ -50772,7 +50799,7 @@ var init_component_registry_generated = __esm({
50772
50799
  init_RelationSelect();
50773
50800
  init_RepeatableFormSection();
50774
50801
  init_ReplyTree();
50775
- init_RichBlockEditor();
50802
+ init_RichTextEditor();
50776
50803
  init_RuntimeDebugger2();
50777
50804
  init_ScaledDiagram();
50778
50805
  init_ScoreDisplay();
@@ -50935,6 +50962,8 @@ var init_component_registry_generated = __esm({
50935
50962
  "DocSearch": DocSearch,
50936
50963
  "DocSidebar": DocSidebar,
50937
50964
  "DocTOC": DocTOC,
50965
+ "DocumentDetails": DocumentDetails,
50966
+ "DocumentPanel": DocumentPanel,
50938
50967
  "DocumentViewer": DocumentViewer,
50939
50968
  "DrawFxLayer": DrawFxLayer,
50940
50969
  "DrawGroup": DrawGroup,
@@ -51046,7 +51075,7 @@ var init_component_registry_generated = __esm({
51046
51075
  "RelationSelect": RelationSelect,
51047
51076
  "RepeatableFormSection": RepeatableFormSection,
51048
51077
  "ReplyTree": ReplyTree,
51049
- "RichBlockEditor": RichBlockEditor,
51078
+ "RichTextEditor": RichTextEditor,
51050
51079
  "RuntimeDebugger": RuntimeDebugger,
51051
51080
  "ScaledDiagram": ScaledDiagram,
51052
51081
  "ScoreDisplay": ScoreDisplay,
@@ -51200,10 +51229,10 @@ function enrichFormFields(fields, entityDef) {
51200
51229
  }
51201
51230
  if (field && typeof field === "object" && !Array.isArray(field) && !React94__default.isValidElement(field) && !(field instanceof Date)) {
51202
51231
  const obj = field;
51203
- const fieldName = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
51204
- if (!fieldName) return field;
51232
+ const fieldName2 = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
51233
+ if (!fieldName2) return field;
51205
51234
  if (obj.type || obj.inputType) return field;
51206
- const entityField = fieldMap.get(fieldName);
51235
+ const entityField = fieldMap.get(fieldName2);
51207
51236
  if (!entityField) return field;
51208
51237
  const enriched = { ...obj, type: entityField.type };
51209
51238
  if (entityField.required && !("required" in obj)) {
@@ -51253,9 +51282,9 @@ function enrichDetailFields(fields, entityDef) {
51253
51282
  }
51254
51283
  if (field && typeof field === "object" && !Array.isArray(field) && !React94__default.isValidElement(field) && !(field instanceof Date)) {
51255
51284
  const obj = field;
51256
- const fieldName = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
51257
- if (!fieldName || obj.type) return field;
51258
- const meta = metaFor(fieldName);
51285
+ const fieldName2 = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
51286
+ if (!fieldName2 || obj.type) return field;
51287
+ const meta = metaFor(fieldName2);
51259
51288
  return meta ? { ...obj, ...meta } : field;
51260
51289
  }
51261
51290
  return field;
@@ -52094,9 +52123,9 @@ function UISlotRenderer({
52094
52123
  "ui-slot-renderer relative min-h-full",
52095
52124
  className
52096
52125
  ), children: [
52097
- /* @__PURE__ */ jsxs(Box, { className: "flex min-h-full", children: [
52098
- /* @__PURE__ */ jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 shrink-0" }),
52099
- /* @__PURE__ */ jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-h-[200px]" })
52126
+ /* @__PURE__ */ jsxs(Box, { className: "flex min-h-full flex-col lg:flex-row", children: [
52127
+ /* @__PURE__ */ jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 lg:shrink-0" }),
52128
+ /* @__PURE__ */ jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-w-0 min-h-[200px]" })
52100
52129
  ] }),
52101
52130
  /* @__PURE__ */ jsx(UISlotComponent, { slot: "modal", portal: true }),
52102
52131
  /* @__PURE__ */ jsx(UISlotComponent, { slot: "drawer", portal: true }),
@@ -55993,7 +56022,7 @@ function buildTraitRenderConfig(binding, traitConfigsByName, payload) {
55993
56022
  }
55994
56023
  return void 0;
55995
56024
  }
55996
- function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
56025
+ function createSharedEntityWriter(binding, tick, traitStatesRef, emit, traitConfigsByName) {
55997
56026
  return (scratch) => {
55998
56027
  const traitName = binding.trait.name;
55999
56028
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -56002,9 +56031,13 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
56002
56031
  const writes = [];
56003
56032
  const ctx = createMinimalContext(scratchEntity, {}, currentState);
56004
56033
  const declaredDefaults = collectDeclaredConfigDefaults(binding.trait);
56005
- const callSiteConfig = getBindingConfig(binding);
56006
- if (declaredDefaults || callSiteConfig) {
56007
- ctx.config = { ...declaredDefaults ?? {}, ...callSiteConfig ?? {} };
56034
+ const resolvedByName = traitConfigsByName?.[traitName];
56035
+ const callSiteRaw = getBindingConfig(binding);
56036
+ const callSiteConfig = callSiteRaw ? Object.fromEntries(
56037
+ Object.entries(callSiteRaw).filter(([, v]) => !containsConfigForward(v))
56038
+ ) : void 0;
56039
+ if (declaredDefaults || resolvedByName || callSiteConfig) {
56040
+ ctx.config = { ...declaredDefaults ?? {}, ...resolvedByName ?? {}, ...callSiteConfig ?? {} };
56008
56041
  }
56009
56042
  ctx.mutateEntity = (changes) => {
56010
56043
  for (const [field, value] of Object.entries(changes)) {
@@ -56607,7 +56640,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56607
56640
  const interval = entries[0].tick.interval;
56608
56641
  const onDue = timedTick(`tick:shared:${group.storeKey}@${String(interval)}`, () => {
56609
56642
  const writers = entries.map(
56610
- ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
56643
+ ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter, traitConfigsByName)
56611
56644
  );
56612
56645
  runTickFrame(group.storeKey, writers, sharedEntityStore);
56613
56646
  });
@@ -59120,11 +59153,11 @@ function OrbInspector({ node, schema, editable = false, userType = "builder", th
59120
59153
  const handleAddField = useCallback(() => {
59121
59154
  eventBus.emit("UI:ADD_FIELD", {});
59122
59155
  }, [eventBus]);
59123
- const handleUpdateField = useCallback((fieldName, updates) => {
59124
- eventBus.emit("UI:UPDATE_FIELD", { fieldName, updates });
59156
+ const handleUpdateField = useCallback((fieldName2, updates) => {
59157
+ eventBus.emit("UI:UPDATE_FIELD", { fieldName: fieldName2, updates });
59125
59158
  }, [eventBus]);
59126
- const handleRemoveField = useCallback((fieldName) => {
59127
- eventBus.emit("UI:REMOVE_FIELD", { fieldName });
59159
+ const handleRemoveField = useCallback((fieldName2) => {
59160
+ eventBus.emit("UI:REMOVE_FIELD", { fieldName: fieldName2 });
59128
59161
  }, [eventBus]);
59129
59162
  const handleGuardChange = useCallback((guardExpr) => {
59130
59163
  if (!editable) return;