@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.
@@ -28612,8 +28612,9 @@ function DataGrid({
28612
28612
  const titleField = fieldDefs.find((f3) => f3.variant === "h3" || f3.variant === "h4") ?? fieldDefs[0];
28613
28613
  const badgeFields = fieldDefs.filter((f3) => f3.variant === "badge" && f3 !== titleField);
28614
28614
  const bodyFields = fieldDefs.filter((f3) => f3 !== titleField && !badgeFields.includes(f3));
28615
- const primaryActions = actionDefs.filter((a) => a.variant !== "danger");
28616
- const dangerActions = actionDefs.filter((a) => a.variant === "danger");
28615
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
28616
+ const inlineCardActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
28617
+ const menuCardActions = actionDefs.filter((a) => !inlineCardActions.includes(a));
28617
28618
  const fireAction = (action, itemData) => {
28618
28619
  if (action.navigatesTo) {
28619
28620
  const url = action.navigatesTo.replace(
@@ -28633,6 +28634,9 @@ function DataGrid({
28633
28634
  e.stopPropagation();
28634
28635
  fireAction(action, itemData);
28635
28636
  };
28637
+ const cardClickAction = actionDefs.find((a) => a.variant !== "danger");
28638
+ const handleCardClick = cardClickAction ? (itemData) => () => fireAction(cardClickAction, itemData) : void 0;
28639
+ const stopCardClick = handleCardClick ? (e) => e.stopPropagation() : void 0;
28636
28640
  const hasRenderProp = typeof children === "function";
28637
28641
  React94.useEffect(() => {
28638
28642
  if (data.length > 0 && !hasRenderProp && fieldDefs.length === 0) {
@@ -28704,55 +28708,40 @@ function DataGrid({
28704
28708
  {
28705
28709
  "data-entity-row": true,
28706
28710
  "data-entity-id": id,
28707
- className: cn("relative group/rowactions", isSelected && "ring-2 ring-primary rounded-lg"),
28711
+ onClick: handleCardClick?.(itemData),
28712
+ className: cn("relative group/rowactions", handleCardClick && "cursor-pointer", isSelected && "ring-2 ring-primary rounded-lg"),
28708
28713
  children: [
28709
28714
  children(itemData, index),
28710
- actionDefs.length > 0 && /* @__PURE__ */ jsxRuntime.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: [
28711
- /* @__PURE__ */ jsxRuntime.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: [
28712
- (maxInlineActions != null ? actionDefs.slice(0, maxInlineActions) : actionDefs).map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
28713
- Button,
28714
- {
28715
- variant: action.variant === "primary" ? "primary" : "ghost",
28716
- size: "sm",
28717
- onClick: handleActionClick(action, itemData),
28718
- "data-testid": `action-${action.event}`,
28719
- "data-row-id": String(itemData.id),
28720
- className: action.variant === "danger" ? "text-error hover:text-error hover:bg-error/10" : void 0,
28721
- children: [
28722
- action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
28723
- action.label
28724
- ]
28725
- },
28726
- idx
28727
- )),
28728
- maxInlineActions != null && actionDefs.length > maxInlineActions && /* @__PURE__ */ jsxRuntime.jsx(
28729
- Menu,
28730
- {
28731
- position: "bottom-end",
28732
- trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
28733
- items: actionDefs.slice(maxInlineActions).map((action) => ({
28734
- label: action.label,
28735
- icon: action.icon,
28736
- event: action.event,
28737
- onClick: () => fireAction(action, itemData)
28738
- }))
28739
- }
28740
- )
28741
- ] }),
28742
- /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
28715
+ actionDefs.length > 0 && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "rounded-md border border-border bg-card/95 backdrop-blur-sm shadow-sm p-0.5", children: [
28716
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
28717
+ Button,
28718
+ {
28719
+ variant: "primary",
28720
+ size: "sm",
28721
+ onClick: handleActionClick(action, itemData),
28722
+ "data-testid": `action-${action.event}`,
28723
+ "data-row-id": String(itemData.id),
28724
+ children: [
28725
+ action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
28726
+ action.label
28727
+ ]
28728
+ },
28729
+ idx
28730
+ )),
28731
+ menuCardActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
28743
28732
  Menu,
28744
28733
  {
28745
28734
  position: "bottom-end",
28746
28735
  trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
28747
- items: actionDefs.map((action) => ({
28736
+ items: menuCardActions.map((action) => ({
28748
28737
  label: action.label,
28749
28738
  icon: action.icon,
28750
- event: action.event,
28739
+ variant: action.variant === "danger" ? "danger" : "default",
28751
28740
  onClick: () => fireAction(action, itemData)
28752
28741
  }))
28753
28742
  }
28754
- ) })
28755
- ] })
28743
+ )
28744
+ ] }) })
28756
28745
  ]
28757
28746
  },
28758
28747
  id
@@ -28766,12 +28755,14 @@ function DataGrid({
28766
28755
  {
28767
28756
  "data-entity-row": true,
28768
28757
  "data-entity-id": id,
28758
+ onClick: handleCardClick?.(itemData),
28769
28759
  className: cn(
28770
28760
  "bg-card rounded-lg",
28771
28761
  "border border-border",
28772
28762
  "shadow-elevation-card hover:shadow-elevation-dialog",
28773
28763
  "hover:border-primary transition-all",
28774
28764
  "flex flex-col",
28765
+ handleCardClick && "cursor-pointer",
28775
28766
  isSelected && "ring-2 ring-primary border-primary"
28776
28767
  ),
28777
28768
  children: [
@@ -28821,53 +28812,35 @@ function DataGrid({
28821
28812
  ] }, field.name);
28822
28813
  }) })
28823
28814
  ] }),
28824
- (primaryActions.length > 0 || dangerActions.length > 0) && /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
28825
- (maxInlineActions != null ? primaryActions.slice(0, maxInlineActions) : primaryActions).map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
28815
+ actionDefs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", onClick: stopCardClick, className: "flex-shrink-0", children: [
28816
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
28826
28817
  Button,
28827
28818
  {
28828
- variant: action.variant === "primary" ? "primary" : "ghost",
28819
+ variant: "primary",
28829
28820
  size: "sm",
28830
28821
  onClick: handleActionClick(action, itemData),
28831
28822
  "data-testid": `action-${action.event}`,
28832
28823
  "data-row-id": String(itemData.id),
28833
28824
  "aria-label": action.label,
28834
28825
  title: action.label,
28835
- className: cn(
28836
- action.variant === "primary" ? void 0 : "text-muted-foreground hover:text-foreground",
28837
- action.icon && "px-2"
28838
- ),
28826
+ className: cn(action.icon && "px-2"),
28839
28827
  children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
28840
28828
  },
28841
28829
  idx
28842
28830
  )),
28843
- maxInlineActions != null && primaryActions.length > maxInlineActions && /* @__PURE__ */ jsxRuntime.jsx(
28831
+ menuCardActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
28844
28832
  Menu,
28845
28833
  {
28846
28834
  position: "bottom-end",
28847
28835
  trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
28848
- items: primaryActions.slice(maxInlineActions).map((action) => ({
28836
+ items: menuCardActions.map((action) => ({
28849
28837
  label: action.label,
28850
28838
  icon: action.icon,
28851
- event: action.event,
28839
+ variant: action.variant === "danger" ? "danger" : "default",
28852
28840
  onClick: () => fireAction(action, itemData)
28853
28841
  }))
28854
28842
  }
28855
- ),
28856
- dangerActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
28857
- Button,
28858
- {
28859
- variant: "ghost",
28860
- size: "sm",
28861
- onClick: handleActionClick(action, itemData),
28862
- "data-testid": `action-${action.event}`,
28863
- "data-row-id": String(itemData.id),
28864
- "aria-label": action.label,
28865
- title: action.label,
28866
- className: "text-error hover:text-error hover:bg-error/10 px-2",
28867
- children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
28868
- },
28869
- `danger-${idx}`
28870
- ))
28843
+ )
28871
28844
  ] })
28872
28845
  ] }) }),
28873
28846
  bodyFields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-4 pt-2 pb-4 flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", children: [
@@ -29109,54 +29082,59 @@ function DataList({
29109
29082
  if (!itemActions || itemActions.length === 0) return null;
29110
29083
  const inline = maxInlineActions != null ? itemActions.slice(0, maxInlineActions) : itemActions;
29111
29084
  const overflow = maxInlineActions != null ? itemActions.slice(maxInlineActions) : [];
29112
- return /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
29113
- inline.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
29114
- Button,
29115
- {
29116
- variant: action.variant ?? "ghost",
29117
- size: "sm",
29118
- onClick: handleActionClick(action, itemData),
29119
- "data-testid": `action-${action.event}`,
29120
- "data-row-id": String(itemData.id),
29121
- className: cn(
29122
- action.variant === "danger" && "text-error hover:bg-error/10",
29123
- // Must sit on the Button itself: the variant's own text colour
29124
- // beats an inherited one from the row wrapper.
29125
- onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
29126
- ),
29127
- children: [
29128
- action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
29129
- action.label
29130
- ]
29131
- },
29132
- idx
29133
- )),
29134
- overflow.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
29135
- Menu,
29136
- {
29137
- position: "bottom-end",
29138
- trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
29139
- items: overflow.map((action) => ({
29140
- label: action.label,
29141
- icon: action.icon,
29142
- event: action.event,
29143
- variant: action.variant === "danger" ? "danger" : "default",
29144
- onClick: () => eventBus.emit(`UI:${action.event}`, {
29145
- id: itemData.id,
29146
- row: itemData
29147
- })
29148
- }))
29149
- }
29150
- )
29151
- ] });
29085
+ return (
29086
+ // stopPropagation at the cluster: the "⋯" trigger opens the overflow
29087
+ // menu without also firing the row's default click (inline buttons
29088
+ // already stop it in handleActionClick; the Menu panel is portaled).
29089
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, className: "flex-shrink-0", children: [
29090
+ inline.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
29091
+ Button,
29092
+ {
29093
+ variant: action.variant ?? "ghost",
29094
+ size: "sm",
29095
+ onClick: handleActionClick(action, itemData),
29096
+ "data-testid": `action-${action.event}`,
29097
+ "data-row-id": String(itemData.id),
29098
+ className: cn(
29099
+ action.variant === "danger" && "text-error hover:bg-error/10",
29100
+ // Must sit on the Button itself: the variant's own text colour
29101
+ // beats an inherited one from the row wrapper.
29102
+ onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
29103
+ ),
29104
+ children: [
29105
+ action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
29106
+ action.label
29107
+ ]
29108
+ },
29109
+ idx
29110
+ )),
29111
+ overflow.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
29112
+ Menu,
29113
+ {
29114
+ position: "bottom-end",
29115
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
29116
+ items: overflow.map((action) => ({
29117
+ label: action.label,
29118
+ icon: action.icon,
29119
+ variant: action.variant === "danger" ? "danger" : "default",
29120
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
29121
+ id: itemData.id,
29122
+ row: itemData
29123
+ })
29124
+ }))
29125
+ }
29126
+ )
29127
+ ] })
29128
+ );
29152
29129
  };
29130
+ const rowClickEvent = itemClickEvent || itemActions?.find((a) => a.variant !== "danger")?.event;
29153
29131
  const handleRowClick = (itemData) => () => {
29154
- if (!itemClickEvent) return;
29132
+ if (!rowClickEvent) return;
29155
29133
  const payload = {
29156
29134
  id: itemData.id,
29157
29135
  row: itemData
29158
29136
  };
29159
- eventBus.emit(`UI:${itemClickEvent}`, payload);
29137
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
29160
29138
  };
29161
29139
  if (isLoading) {
29162
29140
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) });
@@ -29204,10 +29182,10 @@ function DataList({
29204
29182
  {
29205
29183
  "data-entity-row": true,
29206
29184
  "data-entity-id": id,
29207
- onClick: itemClickEvent ? handleRowClick(itemData) : void 0,
29185
+ onClick: rowClickEvent ? handleRowClick(itemData) : void 0,
29208
29186
  className: cn(
29209
29187
  "flex px-4 group/rowactions",
29210
- itemClickEvent && "cursor-pointer",
29188
+ rowClickEvent && "cursor-pointer",
29211
29189
  isSent ? "justify-end" : "justify-start"
29212
29190
  ),
29213
29191
  children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -29283,7 +29261,7 @@ function DataList({
29283
29261
  const id2 = itemData.id || String(index);
29284
29262
  const actions = renderItemActions(itemData);
29285
29263
  return wrapDnd(
29286
- /* @__PURE__ */ jsxRuntime.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: [
29264
+ /* @__PURE__ */ jsxRuntime.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: [
29287
29265
  children(itemData, index),
29288
29266
  actions && /* @__PURE__ */ jsxRuntime.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: [
29289
29267
  /* @__PURE__ */ jsxRuntime.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 }),
@@ -29295,7 +29273,6 @@ function DataList({
29295
29273
  items: (itemActions ?? []).map((action) => ({
29296
29274
  label: action.label,
29297
29275
  icon: action.icon,
29298
- event: action.event,
29299
29276
  variant: action.variant === "danger" ? "danger" : "default",
29300
29277
  onClick: () => eventBus.emit(`UI:${action.event}`, {
29301
29278
  id: itemData.id,
@@ -29312,7 +29289,7 @@ function DataList({
29312
29289
  const id = itemData.id || String(index);
29313
29290
  const titleValue = getNestedValue(itemData, titleField?.name ?? "");
29314
29291
  return wrapDnd(
29315
- /* @__PURE__ */ jsxRuntime.jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: itemClickEvent ? handleRowClick(itemData) : void 0, className: cn(itemClickEvent && "cursor-pointer"), children: [
29292
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: rowClickEvent ? handleRowClick(itemData) : void 0, className: cn(rowClickEvent && "cursor-pointer"), children: [
29316
29293
  /* @__PURE__ */ jsxRuntime.jsxs(
29317
29294
  Box,
29318
29295
  {
@@ -34588,7 +34565,7 @@ var init_MapView = __esm({
34588
34565
  shadowSize: [41, 41]
34589
34566
  });
34590
34567
  L.Marker.prototype.options.icon = defaultIcon;
34591
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState100 } = React94__namespace.default;
34568
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState102 } = React94__namespace.default;
34592
34569
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
34593
34570
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
34594
34571
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -34633,7 +34610,7 @@ var init_MapView = __esm({
34633
34610
  showAttribution = true
34634
34611
  }) {
34635
34612
  const eventBus = useEventBus2();
34636
- const [clickedPosition, setClickedPosition] = useState100(null);
34613
+ const [clickedPosition, setClickedPosition] = useState102(null);
34637
34614
  const handleMapClick = useCallback96((lat, lng) => {
34638
34615
  if (showClickedPin) {
34639
34616
  setClickedPosition({ lat, lng });
@@ -38995,741 +38972,775 @@ var init_PositionedCanvas = __esm({
38995
38972
  PositionedCanvas.displayName = "PositionedCanvas";
38996
38973
  }
38997
38974
  });
38998
- function nextBlockId(prefix = "blk") {
38999
- _idSeq += 1;
39000
- const random = Math.random().toString(36).slice(2, 8);
39001
- return `${prefix}-${Date.now().toString(36)}-${_idSeq}-${random}`;
38975
+ function safeUrl(raw, kinds) {
38976
+ const url = raw.trim();
38977
+ if (url.startsWith("#") || url.startsWith("/")) return url;
38978
+ const lower = url.toLowerCase();
38979
+ for (const kind of kinds) {
38980
+ if (lower.startsWith(kind)) return url;
38981
+ }
38982
+ return null;
39002
38983
  }
39003
- function normalizeBlocks(raw) {
39004
- if (!Array.isArray(raw) || raw.length === 0) return [createBlock("paragraph")];
39005
- return raw.map((row) => {
39006
- const entity = row;
39007
- const rawType = entity.type;
39008
- const type = typeof rawType === "string" && BLOCK_TYPES.has(rawType) ? rawType : "paragraph";
39009
- const id = typeof entity.id === "string" && entity.id ? entity.id : nextBlockId(type);
39010
- return { ...entity, id, type };
39011
- });
38984
+ function sanitizeNode(el) {
38985
+ const children = Array.from(el.children);
38986
+ for (const child of children) {
38987
+ const tag = child.tagName.toLowerCase();
38988
+ if (DROP_TAGS.has(tag)) {
38989
+ child.remove();
38990
+ continue;
38991
+ }
38992
+ const allowed = ALLOWED_ATTRS[tag];
38993
+ if (!allowed) {
38994
+ sanitizeNode(child);
38995
+ child.replaceWith(...Array.from(child.childNodes));
38996
+ continue;
38997
+ }
38998
+ for (const attr of Array.from(child.attributes)) {
38999
+ const name = attr.name.toLowerCase();
39000
+ if (!allowed.includes(name)) {
39001
+ child.removeAttribute(attr.name);
39002
+ continue;
39003
+ }
39004
+ if (name === "href") {
39005
+ const url = safeUrl(attr.value, ["http://", "https://", "mailto:"]);
39006
+ if (url === null) child.removeAttribute(attr.name);
39007
+ else child.setAttribute("href", url);
39008
+ }
39009
+ if (name === "src") {
39010
+ const url = safeUrl(attr.value, ["http://", "https://", "data:image/"]);
39011
+ if (url === null) child.remove();
39012
+ else child.setAttribute("src", url);
39013
+ }
39014
+ }
39015
+ sanitizeNode(child);
39016
+ }
39012
39017
  }
39013
- function createBlock(type) {
39014
- switch (type) {
39015
- case "bullet-list":
39016
- case "numbered-list":
39017
- return {
39018
- id: nextBlockId(type),
39019
- type,
39020
- children: [
39021
- { id: nextBlockId("li"), type: "paragraph", content: "" }
39022
- ]
39023
- };
39024
- case "image":
39025
- return {
39026
- id: nextBlockId(type),
39027
- type,
39028
- content: "",
39029
- metadata: { url: "", caption: "" }
39030
- };
39031
- case "code":
39032
- return {
39033
- id: nextBlockId(type),
39034
- type,
39035
- content: "",
39036
- metadata: { language: "plaintext" }
39037
- };
39038
- case "divider":
39039
- return { id: nextBlockId(type), type };
39040
- default:
39041
- return { id: nextBlockId(type), type, content: "" };
39018
+ function sanitizeRichHtml(html) {
39019
+ if (!html || typeof html !== "string") return "";
39020
+ if (typeof window === "undefined" || typeof DOMParser === "undefined") return "";
39021
+ const doc = new DOMParser().parseFromString(html, "text/html");
39022
+ sanitizeNode(doc.body);
39023
+ if (!doc.body.querySelector("img, hr") && (doc.body.textContent ?? "").trim().length === 0) {
39024
+ return "";
39042
39025
  }
39026
+ return doc.body.innerHTML.replace(/​/g, "");
39043
39027
  }
39044
- function replaceBlock(blocks, id, updater) {
39045
- return blocks.map((block) => block.id === id ? updater(block) : block);
39028
+ function htmlIsEmpty(el) {
39029
+ if (el.querySelector("img, hr")) return false;
39030
+ return (el.textContent ?? "").trim().length === 0;
39031
+ }
39032
+ function closestBlock(node, root) {
39033
+ let cur = node;
39034
+ while (cur && cur !== root) {
39035
+ if (cur instanceof Element && BLOCK_TAGS.has(cur.tagName)) return cur;
39036
+ cur = cur.parentNode;
39037
+ }
39038
+ return null;
39046
39039
  }
39047
- function removeBlock(blocks, id) {
39048
- return blocks.filter((block) => block.id !== id);
39040
+ function RichTextStyles() {
39041
+ return /* @__PURE__ */ jsxRuntime.jsx("style", { children: RICH_TEXT_CSS });
39049
39042
  }
39050
- function duplicateBlock(block) {
39043
+ function readToolbarState(root) {
39044
+ const sel = window.getSelection();
39045
+ if (!sel || sel.rangeCount === 0) return IDLE_TOOLBAR;
39046
+ const anchor = sel.anchorNode;
39047
+ if (!anchor || !root.contains(anchor)) return IDLE_TOOLBAR;
39048
+ let block = null;
39049
+ let link = false;
39050
+ let node = anchor;
39051
+ while (node && node !== root) {
39052
+ if (node instanceof Element) {
39053
+ const tag = node.tagName.toLowerCase();
39054
+ if (!block && (tag === "h1" || tag === "h2" || tag === "h3" || tag === "blockquote" || tag === "pre")) {
39055
+ block = tag;
39056
+ }
39057
+ if (tag === "a") link = true;
39058
+ }
39059
+ node = node.parentNode;
39060
+ }
39051
39061
  return {
39052
- ...block,
39053
- id: nextBlockId(block.type),
39054
- children: block.children?.map((child) => ({
39055
- ...child,
39056
- id: nextBlockId("li")
39057
- })),
39058
- metadata: block.metadata ? { ...block.metadata } : void 0
39062
+ bold: document.queryCommandState("bold"),
39063
+ italic: document.queryCommandState("italic"),
39064
+ underline: document.queryCommandState("underline"),
39065
+ strikeThrough: document.queryCommandState("strikeThrough"),
39066
+ block,
39067
+ bullets: document.queryCommandState("insertUnorderedList"),
39068
+ numbers: document.queryCommandState("insertOrderedList"),
39069
+ link
39059
39070
  };
39060
39071
  }
39061
- function insertAfter(blocks, targetId, inserted) {
39062
- const idx = blocks.findIndex((b) => b.id === targetId);
39063
- if (idx === -1) return [...blocks, inserted];
39064
- const next = blocks.slice();
39065
- next.splice(idx + 1, 0, inserted);
39066
- return next;
39067
- }
39068
- function changeBlockType(block, type) {
39069
- if (block.type === type) return block;
39070
- if (type === "bullet-list" || type === "numbered-list") {
39071
- if (block.children && block.children.length > 0) {
39072
- return { ...block, type };
39072
+ function ToolbarButton({ icon: IconCmp, label, active, onExec }) {
39073
+ return /* @__PURE__ */ jsxRuntime.jsx(
39074
+ Button,
39075
+ {
39076
+ type: "button",
39077
+ variant: "ghost",
39078
+ size: "sm",
39079
+ "aria-label": label,
39080
+ "aria-pressed": active,
39081
+ title: label,
39082
+ className: cn(
39083
+ "h-8 w-8 p-0 gap-0 justify-center",
39084
+ active && "bg-muted text-foreground"
39085
+ ),
39086
+ onMouseDown: (e) => {
39087
+ e.preventDefault();
39088
+ onExec();
39089
+ },
39090
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconCmp, { size: 15 })
39073
39091
  }
39074
- const seed2 = block.content ?? "";
39075
- return {
39076
- id: block.id,
39077
- type,
39078
- children: [
39079
- { id: nextBlockId("li"), type: "paragraph", content: seed2 }
39080
- ]
39081
- };
39082
- }
39083
- if (type === "divider") {
39084
- return { id: block.id, type };
39085
- }
39086
- if (type === "image") {
39087
- return {
39088
- id: block.id,
39089
- type,
39090
- content: "",
39091
- metadata: { url: "", caption: block.content ?? "" }
39092
- };
39093
- }
39094
- if (type === "code") {
39095
- return {
39096
- id: block.id,
39097
- type,
39098
- content: block.content ?? "",
39099
- metadata: { language: "plaintext" }
39092
+ );
39093
+ }
39094
+ var CHANGE_DEBOUNCE_MS, ALLOWED_ATTRS, DROP_TAGS, MD_BLOCK_PREFIXES, BLOCK_TAGS, MD_INLINE_RULES, RICH_TEXT_CSS, IDLE_TOOLBAR, RichTextEditor;
39095
+ var init_RichTextEditor = __esm({
39096
+ "components/core/molecules/RichTextEditor.tsx"() {
39097
+ "use client";
39098
+ init_cn();
39099
+ init_Box();
39100
+ init_Button();
39101
+ init_useEventBus();
39102
+ CHANGE_DEBOUNCE_MS = 400;
39103
+ ALLOWED_ATTRS = {
39104
+ p: [],
39105
+ h1: [],
39106
+ h2: [],
39107
+ h3: [],
39108
+ h4: [],
39109
+ ul: [],
39110
+ ol: [],
39111
+ li: [],
39112
+ blockquote: [],
39113
+ pre: [],
39114
+ code: [],
39115
+ b: [],
39116
+ strong: [],
39117
+ i: [],
39118
+ em: [],
39119
+ u: [],
39120
+ s: [],
39121
+ strike: [],
39122
+ br: [],
39123
+ hr: [],
39124
+ div: [],
39125
+ span: [],
39126
+ a: ["href"],
39127
+ img: ["src", "alt"]
39128
+ };
39129
+ DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "link", "meta", "form", "input", "button", "textarea", "select"]);
39130
+ MD_BLOCK_PREFIXES = {
39131
+ "#": { kind: "block", tag: "h1" },
39132
+ "##": { kind: "block", tag: "h2" },
39133
+ "###": { kind: "block", tag: "h3" },
39134
+ ">": { kind: "block", tag: "blockquote" },
39135
+ "```": { kind: "block", tag: "pre" },
39136
+ "-": { kind: "list", command: "insertUnorderedList" },
39137
+ "*": { kind: "list", command: "insertUnorderedList" },
39138
+ "1.": { kind: "list", command: "insertOrderedList" }
39139
+ };
39140
+ BLOCK_TAGS = /* @__PURE__ */ new Set(["P", "DIV", "LI", "H1", "H2", "H3", "H4", "BLOCKQUOTE", "PRE"]);
39141
+ MD_INLINE_RULES = [
39142
+ { pattern: /\*\*([^*\n]+)\*\*$/, wrap: "strong" },
39143
+ { pattern: /(^|[^*])\*([^*\n]+)\*$/, wrap: "em" },
39144
+ { pattern: /`([^`\n]+)`$/, wrap: "code" }
39145
+ ];
39146
+ RICH_TEXT_CSS = `
39147
+ .almadar-rich-text { line-height: 1.7; color: var(--color-foreground); }
39148
+ .almadar-rich-text h1 { font-size: 1.875rem; font-weight: 700; line-height: 1.25; margin: 1.25em 0 0.4em; }
39149
+ .almadar-rich-text h2 { font-size: 1.5rem; font-weight: 650; line-height: 1.3; margin: 1.1em 0 0.4em; }
39150
+ .almadar-rich-text h3 { font-size: 1.25rem; font-weight: 600; line-height: 1.35; margin: 1em 0 0.35em; }
39151
+ .almadar-rich-text h1:first-child, .almadar-rich-text h2:first-child, .almadar-rich-text h3:first-child { margin-top: 0; }
39152
+ .almadar-rich-text p { margin: 0.5em 0; }
39153
+ .almadar-rich-text ul { list-style: disc; padding-inline-start: 1.5rem; margin: 0.5em 0; }
39154
+ .almadar-rich-text ol { list-style: decimal; padding-inline-start: 1.5rem; margin: 0.5em 0; }
39155
+ .almadar-rich-text li { margin: 0.25em 0; }
39156
+ .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; }
39157
+ .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; }
39158
+ .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; }
39159
+ .almadar-rich-text pre code { background: transparent; padding: 0; }
39160
+ .almadar-rich-text a { color: var(--color-primary); text-decoration: underline; }
39161
+ .almadar-rich-text hr { border: none; border-top: 1px solid var(--color-border); margin: 1.25em 0; }
39162
+ .almadar-rich-text img { max-width: 100%; border-radius: 0.375rem; }
39163
+ `;
39164
+ IDLE_TOOLBAR = {
39165
+ bold: false,
39166
+ italic: false,
39167
+ underline: false,
39168
+ strikeThrough: false,
39169
+ block: null,
39170
+ bullets: false,
39171
+ numbers: false,
39172
+ link: false
39173
+ };
39174
+ RichTextEditor = ({
39175
+ value,
39176
+ onChange,
39177
+ changeEvent,
39178
+ readOnly = false,
39179
+ placeholder,
39180
+ showToolbar = true,
39181
+ className
39182
+ }) => {
39183
+ const { t } = hooks.useTranslate();
39184
+ const ref = React94.useRef(null);
39185
+ const [toolbar, setToolbar] = React94.useState(IDLE_TOOLBAR);
39186
+ const [empty, setEmpty] = React94.useState(() => !value || !sanitizeRichHtml(value).trim());
39187
+ const eventBus = useEventBus();
39188
+ const onChangeRef = React94.useRef(onChange);
39189
+ const changeEventRef = React94.useRef(changeEvent);
39190
+ React94.useEffect(() => {
39191
+ onChangeRef.current = onChange;
39192
+ changeEventRef.current = changeEvent;
39193
+ }, [onChange, changeEvent]);
39194
+ const emitTimerRef = React94.useRef(null);
39195
+ const emitNow = React94.useCallback(() => {
39196
+ const el = ref.current;
39197
+ if (!el) return;
39198
+ const html = sanitizeRichHtml(el.innerHTML);
39199
+ onChangeRef.current?.(html);
39200
+ const evt = changeEventRef.current;
39201
+ if (evt) eventBus.emit(`UI:${evt}`, { value: html });
39202
+ }, [eventBus]);
39203
+ const scheduleEmit = React94.useCallback(() => {
39204
+ if (emitTimerRef.current) clearTimeout(emitTimerRef.current);
39205
+ emitTimerRef.current = setTimeout(() => {
39206
+ emitTimerRef.current = null;
39207
+ emitNow();
39208
+ }, CHANGE_DEBOUNCE_MS);
39209
+ }, [emitNow]);
39210
+ const flushEmit = React94.useCallback(() => {
39211
+ if (!emitTimerRef.current) return;
39212
+ clearTimeout(emitTimerRef.current);
39213
+ emitTimerRef.current = null;
39214
+ emitNow();
39215
+ }, [emitNow]);
39216
+ React94.useEffect(() => () => {
39217
+ if (emitTimerRef.current) {
39218
+ clearTimeout(emitTimerRef.current);
39219
+ emitTimerRef.current = null;
39220
+ emitNow();
39221
+ }
39222
+ }, [emitNow]);
39223
+ React94.useEffect(() => {
39224
+ if (readOnly) return;
39225
+ const el = ref.current;
39226
+ if (!el) return;
39227
+ if (document.activeElement === el) return;
39228
+ const next = sanitizeRichHtml(value ?? "") || "<p><br></p>";
39229
+ if (el.innerHTML !== next) {
39230
+ el.innerHTML = next;
39231
+ setEmpty(htmlIsEmpty(el));
39232
+ }
39233
+ }, [readOnly, value]);
39234
+ React94.useEffect(() => {
39235
+ if (readOnly) return;
39236
+ document.execCommand("defaultParagraphSeparator", false, "p");
39237
+ }, [readOnly]);
39238
+ React94.useEffect(() => {
39239
+ if (readOnly || !showToolbar) return;
39240
+ const onSelectionChange = () => {
39241
+ const el = ref.current;
39242
+ if (!el) return;
39243
+ setToolbar(readToolbarState(el));
39244
+ };
39245
+ document.addEventListener("selectionchange", onSelectionChange);
39246
+ return () => document.removeEventListener("selectionchange", onSelectionChange);
39247
+ }, [readOnly, showToolbar]);
39248
+ const tryInlineMarkdown = React94.useCallback(() => {
39249
+ const root = ref.current;
39250
+ if (!root) return false;
39251
+ const sel = window.getSelection();
39252
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
39253
+ const node = sel.anchorNode;
39254
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
39255
+ if (node.parentElement?.closest("code, pre")) return false;
39256
+ const text = (node.textContent ?? "").slice(0, sel.anchorOffset);
39257
+ for (const rule of MD_INLINE_RULES) {
39258
+ const m = rule.pattern.exec(text);
39259
+ if (!m) continue;
39260
+ const inner = rule.wrap === "em" ? m[2] : m[1];
39261
+ if (!inner || !inner.trim()) continue;
39262
+ const lead = rule.wrap === "em" ? m[1]?.length ?? 0 : 0;
39263
+ const range = document.createRange();
39264
+ range.setStart(node, m.index + lead);
39265
+ range.setEnd(node, sel.anchorOffset);
39266
+ const el = document.createElement(rule.wrap);
39267
+ el.textContent = inner;
39268
+ range.deleteContents();
39269
+ range.insertNode(el);
39270
+ const after = document.createTextNode("\u200B");
39271
+ el.parentNode?.insertBefore(after, el.nextSibling);
39272
+ const caret = document.createRange();
39273
+ caret.setStart(after, 1);
39274
+ caret.collapse(true);
39275
+ sel.removeAllRanges();
39276
+ sel.addRange(caret);
39277
+ return true;
39278
+ }
39279
+ return false;
39280
+ }, []);
39281
+ const tryBlockMarkdown = React94.useCallback(() => {
39282
+ const root = ref.current;
39283
+ if (!root) return false;
39284
+ const sel = window.getSelection();
39285
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
39286
+ const node = sel.anchorNode;
39287
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
39288
+ const block = closestBlock(node, root);
39289
+ if (block ? block.firstChild !== node : root.firstChild !== node) return false;
39290
+ const prefix = (node.textContent ?? "").slice(0, sel.anchorOffset);
39291
+ const rule = MD_BLOCK_PREFIXES[prefix];
39292
+ if (!rule) return false;
39293
+ if (node.parentElement?.closest("pre, code")) return false;
39294
+ if (rule.kind === "list" && node.parentElement?.closest("li")) return false;
39295
+ node.deleteData(0, prefix.length);
39296
+ const caret = document.createRange();
39297
+ caret.setStart(node, 0);
39298
+ caret.collapse(true);
39299
+ sel.removeAllRanges();
39300
+ sel.addRange(caret);
39301
+ if (rule.kind === "list") {
39302
+ const container = block ?? node.parentElement;
39303
+ const list = document.createElement(rule.command === "insertOrderedList" ? "ol" : "ul");
39304
+ const li = document.createElement("li");
39305
+ if (block) {
39306
+ while (block.firstChild) li.appendChild(block.firstChild);
39307
+ list.appendChild(li);
39308
+ block.replaceWith(list);
39309
+ } else if (container) {
39310
+ li.appendChild(node);
39311
+ list.appendChild(li);
39312
+ container.appendChild(list);
39313
+ }
39314
+ if (!li.firstChild) li.appendChild(document.createElement("br"));
39315
+ const liCaret = document.createRange();
39316
+ liCaret.selectNodeContents(li);
39317
+ liCaret.collapse(true);
39318
+ sel.removeAllRanges();
39319
+ sel.addRange(liCaret);
39320
+ } else {
39321
+ document.execCommand("formatBlock", false, rule.tag);
39322
+ }
39323
+ return true;
39324
+ }, []);
39325
+ const afterEdit = React94.useCallback(() => {
39326
+ const el = ref.current;
39327
+ if (!el) return;
39328
+ tryInlineMarkdown();
39329
+ setEmpty(htmlIsEmpty(el));
39330
+ setToolbar(readToolbarState(el));
39331
+ scheduleEmit();
39332
+ }, [scheduleEmit, tryInlineMarkdown]);
39333
+ const handleKeyDown = React94.useCallback((e) => {
39334
+ if (e.key === " " && tryBlockMarkdown()) {
39335
+ e.preventDefault();
39336
+ afterEdit();
39337
+ }
39338
+ }, [afterEdit, tryBlockMarkdown]);
39339
+ const execInline = React94.useCallback((command) => {
39340
+ document.execCommand(command, false);
39341
+ afterEdit();
39342
+ }, [afterEdit]);
39343
+ const execBlock = React94.useCallback((tag) => {
39344
+ const next = toolbar.block === tag ? "p" : tag;
39345
+ document.execCommand("formatBlock", false, next);
39346
+ afterEdit();
39347
+ }, [afterEdit, toolbar.block]);
39348
+ const execList = React94.useCallback((command) => {
39349
+ document.execCommand(command, false);
39350
+ afterEdit();
39351
+ }, [afterEdit]);
39352
+ const execLink = React94.useCallback(() => {
39353
+ if (toolbar.link) {
39354
+ document.execCommand("unlink", false);
39355
+ afterEdit();
39356
+ return;
39357
+ }
39358
+ const url = window.prompt(t("richTextEditor.linkPrompt"));
39359
+ if (!url) return;
39360
+ const safe = safeUrl(url, ["http://", "https://", "mailto:"]) ?? `https://${url}`;
39361
+ document.execCommand("createLink", false, safe);
39362
+ afterEdit();
39363
+ }, [afterEdit, t, toolbar.link]);
39364
+ const execRule = React94.useCallback(() => {
39365
+ document.execCommand("insertHorizontalRule", false);
39366
+ afterEdit();
39367
+ }, [afterEdit]);
39368
+ if (readOnly) {
39369
+ return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: cn("almadar-rich-text max-w-none", className), children: [
39370
+ /* @__PURE__ */ jsxRuntime.jsx(RichTextStyles, {}),
39371
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { dangerouslySetInnerHTML: { __html: sanitizeRichHtml(value ?? "") } })
39372
+ ] });
39373
+ }
39374
+ return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: cn("flex flex-col gap-2", className), children: [
39375
+ /* @__PURE__ */ jsxRuntime.jsx(RichTextStyles, {}),
39376
+ showToolbar && /* @__PURE__ */ jsxRuntime.jsxs(
39377
+ Box,
39378
+ {
39379
+ role: "toolbar",
39380
+ "aria-label": t("richTextEditor.editorToolbar"),
39381
+ className: cn(
39382
+ "sticky top-0 z-10 flex flex-wrap items-center gap-0.5 self-start",
39383
+ "rounded-md border border-border bg-background/95 px-1 py-0.5 shadow-sm"
39384
+ ),
39385
+ children: [
39386
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Bold, label: t("richTextEditor.bold"), active: toolbar.bold, onExec: () => execInline("bold") }),
39387
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Italic, label: t("richTextEditor.italic"), active: toolbar.italic, onExec: () => execInline("italic") }),
39388
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Underline, label: t("richTextEditor.underline"), active: toolbar.underline, onExec: () => execInline("underline") }),
39389
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Strikethrough, label: t("richTextEditor.strikethrough"), active: toolbar.strikeThrough, onExec: () => execInline("strikeThrough") }),
39390
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
39391
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Heading1, label: t("richTextEditor.heading1"), active: toolbar.block === "h1", onExec: () => execBlock("h1") }),
39392
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Heading2, label: t("richTextEditor.heading2"), active: toolbar.block === "h2", onExec: () => execBlock("h2") }),
39393
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Heading3, label: t("richTextEditor.heading3"), active: toolbar.block === "h3", onExec: () => execBlock("h3") }),
39394
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
39395
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.List, label: t("richTextEditor.bulletList"), active: toolbar.bullets, onExec: () => execList("insertUnorderedList") }),
39396
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.ListOrdered, label: t("richTextEditor.numberedList"), active: toolbar.numbers, onExec: () => execList("insertOrderedList") }),
39397
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
39398
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Quote, label: t("richTextEditor.quote"), active: toolbar.block === "blockquote", onExec: () => execBlock("blockquote") }),
39399
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Code, label: t("richTextEditor.code"), active: toolbar.block === "pre", onExec: () => execBlock("pre") }),
39400
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Link, label: toolbar.link ? t("richTextEditor.removeLink") : t("richTextEditor.link"), active: toolbar.link, onExec: execLink }),
39401
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Minus, label: t("richTextEditor.divider"), onExec: execRule })
39402
+ ]
39403
+ }
39404
+ ),
39405
+ /* @__PURE__ */ jsxRuntime.jsx(
39406
+ Box,
39407
+ {
39408
+ ref,
39409
+ contentEditable: true,
39410
+ suppressContentEditableWarning: true,
39411
+ role: "textbox",
39412
+ "aria-multiline": "true",
39413
+ "aria-label": t("richTextEditor.editorSurface"),
39414
+ "data-placeholder": placeholder ?? t("richTextEditor.placeholder"),
39415
+ "data-empty": empty ? "true" : "false",
39416
+ className: cn(
39417
+ "almadar-rich-text max-w-none min-h-[8rem] outline-none",
39418
+ "data-[empty=true]:before:content-[attr(data-placeholder)]",
39419
+ "data-[empty=true]:before:text-muted-foreground/60",
39420
+ "data-[empty=true]:before:pointer-events-none",
39421
+ "data-[empty=true]:before:absolute",
39422
+ "relative cursor-text"
39423
+ ),
39424
+ onInput: afterEdit,
39425
+ onKeyDown: handleKeyDown,
39426
+ onBlur: flushEmit
39427
+ }
39428
+ )
39429
+ ] });
39100
39430
  };
39431
+ RichTextEditor.displayName = "RichTextEditor";
39101
39432
  }
39102
- const seed = block.children?.[0]?.content ?? block.content ?? "";
39103
- return { id: block.id, type, content: seed };
39433
+ });
39434
+ function renderIconInput4(icon, props) {
39435
+ return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
39104
39436
  }
39105
- function BlockMenu({ block, readOnly, onDelete, onDuplicate, onChangeType }) {
39437
+ function DocumentPanel({
39438
+ id,
39439
+ title,
39440
+ subtitle,
39441
+ value,
39442
+ actions,
39443
+ maxInlineActions,
39444
+ editing = false,
39445
+ titleCommitEvent,
39446
+ contentChangeEvent,
39447
+ editEvent,
39448
+ doneEvent,
39449
+ editLabel,
39450
+ doneLabel,
39451
+ autosaveHint,
39452
+ placeholder,
39453
+ className
39454
+ }) {
39455
+ const eventBus = useEventBus();
39106
39456
  const { t } = hooks.useTranslate();
39107
- const [open, setOpen] = React94.useState(false);
39108
- const ref = React94.useRef(null);
39109
- React94.useEffect(() => {
39110
- if (!open) return;
39111
- function onDocClick(e) {
39112
- if (ref.current && !ref.current.contains(e.target)) {
39113
- setOpen(false);
39114
- }
39457
+ const [titleDraft, setTitleDraft] = React94.useState(null);
39458
+ const recordId = id ?? "";
39459
+ const actionDefs = actions ?? [];
39460
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
39461
+ const inlineActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
39462
+ const menuActions = actionDefs.filter((a) => !inlineActions.includes(a));
39463
+ const fireAction = (action) => {
39464
+ if (!action.event) return;
39465
+ eventBus.emit(`UI:${action.event}`, { id: recordId, title: title ?? "" });
39466
+ };
39467
+ const commitTitle = () => {
39468
+ if (!titleCommitEvent) return;
39469
+ const next = (titleDraft ?? "").trim();
39470
+ setTitleDraft(null);
39471
+ if (!next || next === title) return;
39472
+ eventBus.emit(`UI:${titleCommitEvent}`, { title: next, id: recordId });
39473
+ };
39474
+ const untitled = t("documentPanel.untitled") || "Untitled";
39475
+ const titleNode = titleCommitEvent && titleDraft !== null ? /* @__PURE__ */ jsxRuntime.jsx(
39476
+ Input,
39477
+ {
39478
+ value: titleDraft,
39479
+ autoFocus: true,
39480
+ "aria-label": t("common.title"),
39481
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
39482
+ onFocus: (e) => e.currentTarget.select(),
39483
+ onChange: (e) => setTitleDraft(e.target.value),
39484
+ onBlur: commitTitle,
39485
+ onKeyDown: (e) => {
39486
+ if (e.key === "Enter") {
39487
+ e.preventDefault();
39488
+ commitTitle();
39489
+ } else if (e.key === "Escape") {
39490
+ e.preventDefault();
39491
+ setTitleDraft(null);
39492
+ }
39493
+ },
39494
+ "data-testid": "document-title-input"
39115
39495
  }
39116
- document.addEventListener("mousedown", onDocClick);
39117
- return () => document.removeEventListener("mousedown", onDocClick);
39118
- }, [open]);
39119
- if (readOnly) return null;
39120
- return /* @__PURE__ */ jsxRuntime.jsxs(Box, { ref, className: "relative", children: [
39121
- /* @__PURE__ */ jsxRuntime.jsx(
39122
- Button,
39123
- {
39124
- type: "button",
39125
- variant: "ghost",
39126
- "aria-label": t("richBlockEditor.blockActions"),
39127
- className: cn(
39128
- "inline-flex items-center justify-center",
39129
- "h-6 w-6 rounded-sm p-0 gap-0",
39130
- "text-muted-foreground hover:bg-muted hover:text-foreground",
39131
- "opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
39132
- "transition-opacity"
39133
- ),
39134
- onClick: () => setOpen((v) => !v),
39135
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", className: "w-3.5 h-3.5" })
39136
- }
39137
- ),
39138
- open && /* @__PURE__ */ jsxRuntime.jsxs(
39496
+ ) : titleCommitEvent ? /* @__PURE__ */ jsxRuntime.jsx(
39497
+ Box,
39498
+ {
39499
+ role: "button",
39500
+ tabIndex: 0,
39501
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
39502
+ onClick: () => setTitleDraft(title ?? ""),
39503
+ onKeyDown: (e) => {
39504
+ if (e.key === "Enter" || e.key === " ") {
39505
+ e.preventDefault();
39506
+ setTitleDraft(title ?? "");
39507
+ }
39508
+ },
39509
+ "data-testid": "document-title-editable",
39510
+ children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", className: cn(!title && "text-muted-foreground"), children: title || untitled })
39511
+ }
39512
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", children: title || untitled });
39513
+ const emitWithId = (event) => () => {
39514
+ if (event) eventBus.emit(`UI:${event}`, { id: recordId });
39515
+ };
39516
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { variant: "elevated", className: cn("w-full", className), children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", className: "p-6 sm:p-8", children: [
39517
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", className: "justify-between items-start", children: [
39518
+ /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", className: "flex-1 min-w-0", children: [
39519
+ titleNode,
39520
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: subtitle }) : null
39521
+ ] }),
39522
+ /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", className: "flex-shrink-0 pt-1 items-center", children: editing ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
39523
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", className: "hidden sm:block", children: autosaveHint ?? (t("documentPanel.autosaveHint") || "") }),
39524
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", size: "sm", onClick: emitWithId(doneEvent), "data-testid": "document-done", children: doneLabel ?? (t("documentPanel.done") || "Done") })
39525
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
39526
+ editEvent && /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "ghost", size: "sm", onClick: emitWithId(editEvent), "data-testid": "document-edit", children: [
39527
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "edit", size: "xs", className: "mr-1" }),
39528
+ editLabel ?? (t("documentPanel.edit") || "Edit")
39529
+ ] }),
39530
+ inlineActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
39531
+ Button,
39532
+ {
39533
+ variant: "primary",
39534
+ size: "sm",
39535
+ onClick: () => fireAction(action),
39536
+ "data-testid": `action-${action.event}`,
39537
+ children: [
39538
+ action.icon && renderIconInput4(action.icon, { size: "xs", className: "mr-1" }),
39539
+ action.label
39540
+ ]
39541
+ },
39542
+ idx
39543
+ )),
39544
+ menuActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
39545
+ Menu,
39546
+ {
39547
+ position: "bottom-end",
39548
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
39549
+ items: menuActions.map((action) => ({
39550
+ label: action.label,
39551
+ icon: action.icon,
39552
+ variant: action.variant === "danger" ? "danger" : "default",
39553
+ onClick: () => fireAction(action)
39554
+ }))
39555
+ }
39556
+ )
39557
+ ] }) })
39558
+ ] }),
39559
+ editing ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsxRuntime.jsx(
39139
39560
  Box,
39140
39561
  {
39141
- role: "menu",
39562
+ onClick: emitWithId(editEvent),
39142
39563
  className: cn(
39143
- "absolute right-0 z-10 mt-1 w-44",
39144
- "rounded-container border border-border bg-popover shadow-elevation-popover",
39145
- "py-1 text-sm"
39564
+ "rounded-md px-1 -mx-1 min-h-[16rem]",
39565
+ editEvent && "cursor-text transition-colors hover:bg-muted/30"
39146
39566
  ),
39147
- children: [
39148
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t(BLOCK_TYPE_LABEL_KEY[block.type]) }),
39149
- /* @__PURE__ */ jsxRuntime.jsxs(
39150
- Button,
39151
- {
39152
- type: "button",
39153
- variant: "ghost",
39154
- role: "menuitem",
39155
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
39156
- onClick: () => {
39157
- onDuplicate();
39158
- setOpen(false);
39159
- },
39160
- children: [
39161
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "plus", className: "w-3.5 h-3.5" }),
39162
- " ",
39163
- t("richBlockEditor.duplicate")
39164
- ]
39165
- }
39166
- ),
39167
- /* @__PURE__ */ jsxRuntime.jsxs(
39168
- Button,
39169
- {
39170
- type: "button",
39171
- variant: "ghost",
39172
- role: "menuitem",
39173
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-destructive hover:bg-muted justify-start rounded-none",
39174
- onClick: () => {
39175
- onDelete();
39176
- setOpen(false);
39177
- },
39178
- children: [
39179
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "trash", className: "w-3.5 h-3.5" }),
39180
- " ",
39181
- t("common.delete")
39182
- ]
39183
- }
39184
- ),
39185
- CHANGEABLE_TYPES.includes(block.type) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
39186
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "my-1 border-t border-border" }),
39187
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t("richBlockEditor.turnInto") }),
39188
- CHANGEABLE_TYPES.filter((bt) => bt !== block.type).map((bt) => /* @__PURE__ */ jsxRuntime.jsx(
39189
- Button,
39190
- {
39191
- type: "button",
39192
- variant: "ghost",
39193
- role: "menuitem",
39194
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
39195
- onClick: () => {
39196
- onChangeType(bt);
39197
- setOpen(false);
39198
- },
39199
- children: t(BLOCK_TYPE_LABEL_KEY[bt])
39200
- },
39201
- bt
39202
- ))
39203
- ] })
39204
- ]
39567
+ "data-testid": "document-body",
39568
+ children: value && value !== "" ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, readOnly: true }) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "muted", children: placeholder ?? (t("documentPanel.placeholder") || "") })
39205
39569
  }
39206
39570
  )
39207
- ] });
39571
+ ] }) });
39208
39572
  }
39209
- function Editable({
39210
- tag,
39211
- value,
39212
- readOnly,
39213
- placeholder,
39214
- className,
39215
- ariaLabel,
39216
- onValueChange
39217
- }) {
39218
- const ref = React94.useRef(null);
39219
- React94.useEffect(() => {
39220
- const el = ref.current;
39221
- if (!el) return;
39222
- const isFocused = document.activeElement === el;
39223
- if (!isFocused && el.textContent !== value) {
39224
- el.textContent = value;
39225
- }
39226
- }, [value]);
39227
- const handleInput = React94.useCallback(
39228
- (e) => {
39229
- onValueChange(e.currentTarget.textContent ?? "");
39230
- },
39231
- [onValueChange]
39232
- );
39233
- return /* @__PURE__ */ jsxRuntime.jsx(
39234
- Box,
39235
- {
39236
- as: tag,
39237
- ref,
39238
- contentEditable: !readOnly,
39239
- suppressContentEditableWarning: true,
39240
- role: readOnly ? void 0 : "textbox",
39241
- "aria-label": ariaLabel,
39242
- "aria-multiline": "true",
39243
- "data-placeholder": placeholder,
39244
- className: cn(
39245
- "outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-sm",
39246
- "empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground/60",
39247
- className
39248
- ),
39249
- onInput: handleInput
39250
- }
39251
- );
39573
+ var init_DocumentPanel = __esm({
39574
+ "components/core/molecules/DocumentPanel.tsx"() {
39575
+ "use client";
39576
+ init_cn();
39577
+ init_useEventBus();
39578
+ init_Box();
39579
+ init_Stack();
39580
+ init_Typography();
39581
+ init_Button();
39582
+ init_Card();
39583
+ init_Icon();
39584
+ init_Input();
39585
+ init_Menu();
39586
+ init_RichTextEditor();
39587
+ }
39588
+ });
39589
+ function renderIconInput5(icon, props) {
39590
+ return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
39252
39591
  }
39253
- function BlockRow({
39254
- block,
39255
- readOnly,
39256
- showAffordances,
39257
- placeholder,
39258
- onUpdate,
39259
- onDelete,
39260
- onDuplicate,
39261
- onChangeType
39592
+ function fieldName(field) {
39593
+ return field.name ?? field.key ?? "";
39594
+ }
39595
+ function relationLabel(value) {
39596
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Date) return null;
39597
+ for (const key of ["name", "title", "label"]) {
39598
+ const candidate = value[key];
39599
+ if (typeof candidate === "string" && candidate !== "") return candidate;
39600
+ }
39601
+ const id = value.id;
39602
+ return id !== void 0 && id !== null ? String(id) : null;
39603
+ }
39604
+ function fieldKind(field, value) {
39605
+ if (field.kind) return field.kind;
39606
+ if (field.options && field.options.length > 0) return "select";
39607
+ if (typeof value === "boolean" || field.format === "boolean") return "boolean";
39608
+ if (Array.isArray(value) || relationLabel(value ?? null) !== null) return "readonly";
39609
+ return "text";
39610
+ }
39611
+ function DocumentDetails({
39612
+ entity,
39613
+ fields,
39614
+ metaCommitEvent,
39615
+ title,
39616
+ className
39262
39617
  }) {
39618
+ const eventBus = useEventBus();
39263
39619
  const { t } = hooks.useTranslate();
39264
- const setContent = React94.useCallback(
39265
- (next) => onUpdate((b) => ({ ...b, content: next })),
39266
- [onUpdate]
39267
- );
39268
- const setMetadata = React94.useCallback(
39269
- (key, value) => onUpdate((b) => ({
39270
- ...b,
39271
- metadata: { ...b.metadata ?? {}, [key]: value }
39272
- })),
39273
- [onUpdate]
39274
- );
39275
- const setChildContent = React94.useCallback(
39276
- (childId, next) => onUpdate((b) => ({
39277
- ...b,
39278
- children: (b.children ?? []).map(
39279
- (c) => c.id === childId ? { ...c, content: next } : c
39280
- )
39281
- })),
39282
- [onUpdate]
39283
- );
39284
- const addListItem = React94.useCallback(
39285
- () => onUpdate((b) => ({
39286
- ...b,
39287
- children: [
39288
- ...b.children ?? [],
39289
- { id: nextBlockId("li"), type: "paragraph", content: "" }
39290
- ]
39291
- })),
39292
- [onUpdate]
39293
- );
39294
- const removeListItem = React94.useCallback(
39295
- (childId) => onUpdate((b) => {
39296
- const remaining = (b.children ?? []).filter((c) => c.id !== childId);
39297
- return {
39298
- ...b,
39299
- children: remaining.length === 0 ? [{ id: nextBlockId("li"), type: "paragraph", content: "" }] : remaining
39300
- };
39301
- }),
39302
- [onUpdate]
39303
- );
39304
- const renderBody = () => {
39305
- switch (block.type) {
39306
- case "heading-1":
39307
- return /* @__PURE__ */ jsxRuntime.jsx(
39308
- Editable,
39309
- {
39310
- tag: "h1",
39311
- value: block.content ?? "",
39312
- readOnly,
39313
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading1"),
39314
- ariaLabel: t("richBlockEditor.aria.heading1Block"),
39315
- className: "text-3xl font-bold leading-tight",
39316
- onValueChange: setContent
39317
- }
39318
- );
39319
- case "heading-2":
39320
- return /* @__PURE__ */ jsxRuntime.jsx(
39321
- Editable,
39322
- {
39323
- tag: "h2",
39324
- value: block.content ?? "",
39325
- readOnly,
39326
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading2"),
39327
- ariaLabel: t("richBlockEditor.aria.heading2Block"),
39328
- className: "text-2xl font-semibold leading-tight",
39329
- onValueChange: setContent
39330
- }
39331
- );
39332
- case "heading-3":
39333
- return /* @__PURE__ */ jsxRuntime.jsx(
39334
- Editable,
39335
- {
39336
- tag: "h3",
39337
- value: block.content ?? "",
39338
- readOnly,
39339
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading3"),
39340
- ariaLabel: t("richBlockEditor.aria.heading3Block"),
39341
- className: "text-xl font-semibold leading-tight",
39342
- onValueChange: setContent
39343
- }
39344
- );
39345
- case "quote":
39346
- return /* @__PURE__ */ jsxRuntime.jsx(
39347
- Editable,
39348
- {
39349
- tag: "blockquote",
39350
- value: block.content ?? "",
39351
- readOnly,
39352
- placeholder: placeholder ?? t("richBlockEditor.placeholder.quote"),
39353
- ariaLabel: t("richBlockEditor.aria.quoteBlock"),
39354
- className: "border-l-4 border-primary/60 pl-4 italic text-muted-foreground",
39355
- onValueChange: setContent
39356
- }
39357
- );
39358
- case "code":
39359
- return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "rounded-md border border-border bg-muted/40", children: [
39360
- /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex items-center justify-between border-b border-border px-3 py-1 text-xs text-muted-foreground", children: [
39361
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", variant: "caption", className: "uppercase tracking-wide", children: t("richBlockEditor.blockType.code") }),
39362
- !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
39363
- Input,
39364
- {
39365
- inputType: "text",
39366
- value: String(block.metadata?.language ?? "plaintext"),
39367
- "aria-label": t("richBlockEditor.aria.codeLanguage"),
39368
- className: cn(
39369
- "h-6 w-32 rounded-sm border border-border bg-background",
39370
- "px-2 text-xs outline-none focus:ring-1 focus:ring-ring"
39371
- ),
39372
- onChange: (e) => setMetadata("language", e.target.value)
39373
- }
39374
- ),
39375
- readOnly && /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", variant: "caption", className: "text-xs", children: String(block.metadata?.language ?? "plaintext") })
39376
- ] }),
39377
- /* @__PURE__ */ jsxRuntime.jsx(
39378
- Editable,
39379
- {
39380
- tag: "pre",
39381
- value: block.content ?? "",
39382
- readOnly,
39383
- placeholder: placeholder ?? t("richBlockEditor.placeholder.code"),
39384
- ariaLabel: t("richBlockEditor.aria.codeBlock"),
39385
- className: "block whitespace-pre-wrap p-3 font-mono text-sm leading-relaxed",
39386
- onValueChange: setContent
39387
- }
39388
- )
39389
- ] });
39390
- case "divider":
39391
- return /* @__PURE__ */ jsxRuntime.jsx(Divider, { className: "my-2" });
39392
- case "image": {
39393
- const url = String(block.metadata?.url ?? "");
39394
- const caption = String(block.metadata?.caption ?? "");
39395
- const imgProps = {
39396
- src: url,
39397
- alt: caption || t("richBlockEditor.embeddedImage"),
39398
- className: "max-h-96 w-full rounded-md border border-border object-contain"
39399
- };
39400
- return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "space-y-2", children: [
39401
- url ? /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "img", ...imgProps }) : /* @__PURE__ */ jsxRuntime.jsxs(
39402
- Box,
39403
- {
39404
- className: cn(
39405
- "flex h-32 items-center justify-center",
39406
- "rounded-md border border-dashed border-border",
39407
- "text-sm text-muted-foreground"
39408
- ),
39409
- children: [
39410
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "image", className: "mr-2 w-4 h-4" }),
39411
- " ",
39412
- t("richBlockEditor.noImageUrl")
39413
- ]
39414
- }
39415
- ),
39416
- !readOnly && /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex flex-col gap-2 sm:flex-row", children: [
39417
- /* @__PURE__ */ jsxRuntime.jsx(
39418
- Input,
39419
- {
39420
- inputType: "url",
39421
- value: url,
39422
- placeholder: "https://example.com/image.png",
39423
- "aria-label": t("richBlockEditor.aria.imageUrl"),
39424
- className: cn(
39425
- "h-8 flex-1 rounded-sm border border-border bg-background",
39426
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
39427
- ),
39428
- onChange: (e) => setMetadata("url", e.target.value)
39429
- }
39430
- ),
39431
- /* @__PURE__ */ jsxRuntime.jsx(
39432
- Input,
39433
- {
39434
- inputType: "text",
39435
- value: caption,
39436
- placeholder: t("richBlockEditor.placeholder.caption"),
39437
- "aria-label": t("richBlockEditor.aria.imageCaption"),
39438
- className: cn(
39439
- "h-8 flex-1 rounded-sm border border-border bg-background",
39440
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
39441
- ),
39442
- onChange: (e) => setMetadata("caption", e.target.value)
39443
- }
39444
- )
39445
- ] }),
39446
- readOnly && caption && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-center text-muted-foreground", children: caption })
39447
- ] });
39620
+ const [fieldDraft, setFieldDraft] = React94.useState(null);
39621
+ const recordId = entity?.id !== void 0 && entity?.id !== null ? String(entity.id) : "";
39622
+ const fieldDefs = (fields ?? []).filter((f3) => fieldName(f3) !== "");
39623
+ if (fieldDefs.length === 0) return null;
39624
+ const commitField = (name, next, current) => {
39625
+ setFieldDraft(null);
39626
+ if (!metaCommitEvent || next === current) return;
39627
+ eventBus.emit(`UI:${metaCommitEvent}`, { id: recordId, patch: { id: recordId, [name]: next } });
39628
+ };
39629
+ const renderValue = (field) => {
39630
+ const name = fieldName(field);
39631
+ const raw = getNestedValue(entity ?? {}, name);
39632
+ const kind = fieldKind(field, raw);
39633
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
39634
+ if (kind === "boolean") {
39635
+ return /* @__PURE__ */ jsxRuntime.jsx(
39636
+ Switch,
39637
+ {
39638
+ checked: Boolean(raw),
39639
+ disabled: !metaCommitEvent,
39640
+ "aria-label": label,
39641
+ onChange: (checked) => commitField(name, checked, Boolean(raw))
39642
+ }
39643
+ );
39644
+ }
39645
+ if (kind === "select") {
39646
+ return /* @__PURE__ */ jsxRuntime.jsx(
39647
+ Select,
39648
+ {
39649
+ value: raw !== void 0 && raw !== null ? String(raw) : "",
39650
+ disabled: !metaCommitEvent,
39651
+ "aria-label": label,
39652
+ className: "h-8 w-full",
39653
+ options: (field.options ?? []).map((opt) => ({ value: opt, label: humanizeEnumValue(opt) })),
39654
+ onChange: (e) => commitField(name, e.target.value, raw)
39655
+ }
39656
+ );
39657
+ }
39658
+ if (kind === "readonly" || !metaCommitEvent) {
39659
+ if (Array.isArray(raw)) {
39660
+ if (raw.length === 0) {
39661
+ return /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
39662
+ }
39663
+ return /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", className: "flex-wrap", children: raw.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "default", children: relationLabel(item) ?? humanizeEnumValue(String(item)) }, i)) });
39448
39664
  }
39449
- case "bullet-list":
39450
- case "numbered-list": {
39451
- const items = block.children ?? [];
39452
- return /* @__PURE__ */ jsxRuntime.jsxs(
39453
- Box,
39454
- {
39455
- as: block.type === "bullet-list" ? "ul" : "ol",
39456
- className: cn(
39457
- "space-y-1 pl-6",
39458
- block.type === "bullet-list" ? "list-disc" : "list-decimal"
39459
- ),
39460
- children: [
39461
- items.map((child) => /* @__PURE__ */ jsxRuntime.jsxs(Box, { as: "li", className: "group/item flex items-start gap-2", children: [
39462
- /* @__PURE__ */ jsxRuntime.jsx(
39463
- Editable,
39464
- {
39465
- tag: "span",
39466
- value: child.content ?? "",
39467
- readOnly,
39468
- placeholder: t("richBlockEditor.placeholder.listItem"),
39469
- ariaLabel: t("richBlockEditor.aria.listItem"),
39470
- className: "inline-block min-w-[1ch] flex-1",
39471
- onValueChange: (next) => setChildContent(child.id, next)
39472
- }
39473
- ),
39474
- !readOnly && showAffordances && /* @__PURE__ */ jsxRuntime.jsx(
39475
- Button,
39476
- {
39477
- type: "button",
39478
- variant: "ghost",
39479
- "aria-label": t("richBlockEditor.aria.removeListItem"),
39480
- className: cn(
39481
- "h-5 w-5 shrink-0 rounded-sm text-muted-foreground p-0 gap-0",
39482
- "opacity-0 group-hover/item:opacity-100 hover:bg-muted hover:text-foreground"
39483
- ),
39484
- onClick: () => removeListItem(child.id),
39485
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "trash", className: "w-3 h-3" })
39486
- }
39487
- )
39488
- ] }, child.id)),
39489
- !readOnly && showAffordances && /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "li", className: "list-none pl-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
39490
- Button,
39491
- {
39492
- type: "button",
39493
- variant: "ghost",
39494
- className: cn(
39495
- "inline-flex items-center gap-1 text-xs text-muted-foreground",
39496
- "hover:text-foreground p-0 h-auto"
39497
- ),
39498
- onClick: addListItem,
39499
- children: [
39500
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "plus", className: "w-3 h-3" }),
39501
- " ",
39502
- t("richBlockEditor.addItem")
39503
- ]
39504
- }
39505
- ) })
39506
- ]
39665
+ const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
39666
+ return /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", className: "break-words", children: shown2 });
39667
+ }
39668
+ if (fieldDraft?.name === name) {
39669
+ return /* @__PURE__ */ jsxRuntime.jsx(
39670
+ Input,
39671
+ {
39672
+ value: fieldDraft.value,
39673
+ autoFocus: true,
39674
+ "aria-label": label,
39675
+ className: "h-8 w-full",
39676
+ onChange: (e) => setFieldDraft({ name, value: e.target.value }),
39677
+ onBlur: () => commitField(name, fieldDraft.value.trim(), raw),
39678
+ onKeyDown: (e) => {
39679
+ if (e.key === "Enter") {
39680
+ e.preventDefault();
39681
+ commitField(name, fieldDraft.value.trim(), raw);
39682
+ } else if (e.key === "Escape") {
39683
+ e.preventDefault();
39684
+ setFieldDraft(null);
39685
+ }
39686
+ },
39687
+ "data-testid": `document-property-input-${name}`
39688
+ }
39689
+ );
39690
+ }
39691
+ const shown = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
39692
+ return /* @__PURE__ */ jsxRuntime.jsx(
39693
+ Box,
39694
+ {
39695
+ role: "button",
39696
+ tabIndex: 0,
39697
+ className: cn(
39698
+ "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
39699
+ (raw === void 0 || raw === null || raw === "") && "text-muted-foreground"
39700
+ ),
39701
+ onClick: () => setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" }),
39702
+ onKeyDown: (e) => {
39703
+ if (e.key === "Enter" || e.key === " ") {
39704
+ e.preventDefault();
39705
+ setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" });
39507
39706
  }
39508
- );
39707
+ },
39708
+ "data-testid": `document-property-${name}`,
39709
+ children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", className: "break-words", children: shown })
39509
39710
  }
39510
- case "paragraph":
39511
- default:
39512
- return /* @__PURE__ */ jsxRuntime.jsx(
39513
- Editable,
39514
- {
39515
- tag: "p",
39516
- value: block.content ?? "",
39517
- readOnly,
39518
- placeholder: placeholder ?? t("richBlockEditor.placeholder.paragraph"),
39519
- ariaLabel: t("richBlockEditor.aria.paragraphBlock"),
39520
- className: "leading-7",
39521
- onValueChange: setContent
39522
- }
39523
- );
39524
- }
39711
+ );
39525
39712
  };
39526
- return /* @__PURE__ */ jsxRuntime.jsxs(
39527
- Box,
39528
- {
39529
- className: cn(
39530
- "group relative flex items-start gap-2 rounded-sm",
39531
- "px-2 py-1 hover:bg-muted/30"
39532
- ),
39533
- "data-block-id": block.id,
39534
- "data-block-type": block.type,
39535
- children: [
39536
- !readOnly && showAffordances && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex w-8 shrink-0 items-center pt-1", children: /* @__PURE__ */ jsxRuntime.jsx(
39537
- BlockMenu,
39538
- {
39539
- block,
39540
- readOnly,
39541
- onDelete,
39542
- onDuplicate,
39543
- onChangeType
39544
- }
39545
- ) }),
39546
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "min-w-0 flex-1", children: renderBody() })
39547
- ]
39548
- }
39549
- );
39713
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { variant: "bordered", className: cn("w-full", className), children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", className: "p-4", children: [
39714
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "secondary", weight: "medium", className: "uppercase tracking-wide", children: title ?? (t("documentDetails.title") || "Details") }),
39715
+ /* @__PURE__ */ jsxRuntime.jsx(VStack, { gap: "sm", children: fieldDefs.map((field) => {
39716
+ const name = fieldName(field);
39717
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
39718
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", children: [
39719
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "items-center", children: [
39720
+ field.icon && renderIconInput5(field.icon, { size: "xs", className: "text-muted-foreground" }),
39721
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", children: label })
39722
+ ] }),
39723
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "min-w-0", children: renderValue(field) })
39724
+ ] }, name);
39725
+ }) })
39726
+ ] }) });
39550
39727
  }
39551
- var TOOLBAR_ENTRIES, BLOCK_TYPE_LABEL_KEY, CHANGEABLE_TYPES, _idSeq, BLOCK_TYPES, RichBlockEditor;
39552
- var init_RichBlockEditor = __esm({
39553
- "components/core/molecules/RichBlockEditor.tsx"() {
39728
+ var init_DocumentDetails = __esm({
39729
+ "components/core/molecules/DocumentDetails.tsx"() {
39554
39730
  "use client";
39555
39731
  init_cn();
39556
- init_Card();
39557
- init_Typography();
39558
- init_Button();
39732
+ init_format();
39733
+ init_getNestedValue();
39734
+ init_useEventBus();
39559
39735
  init_Box();
39560
- init_Divider();
39561
- init_Input();
39736
+ init_Stack();
39737
+ init_Typography();
39738
+ init_Badge();
39739
+ init_Card();
39562
39740
  init_Icon();
39563
- init_useEventBus();
39564
- TOOLBAR_ENTRIES = [
39565
- { type: "paragraph", labelKey: "richBlockEditor.toolbar.text", icon: LucideIcons2.Type },
39566
- { type: "heading-1", labelKey: "richBlockEditor.toolbar.h1", icon: LucideIcons2.Heading1 },
39567
- { type: "heading-2", labelKey: "richBlockEditor.toolbar.h2", icon: LucideIcons2.Heading2 },
39568
- { type: "heading-3", labelKey: "richBlockEditor.toolbar.h3", icon: LucideIcons2.Heading3 },
39569
- { type: "bullet-list", labelKey: "richBlockEditor.toolbar.bulletList", icon: LucideIcons2.List },
39570
- { type: "numbered-list", labelKey: "richBlockEditor.toolbar.numbered", icon: LucideIcons2.ListOrdered },
39571
- { type: "quote", labelKey: "richBlockEditor.toolbar.quote", icon: LucideIcons2.Quote },
39572
- { type: "code", labelKey: "richBlockEditor.toolbar.code", icon: LucideIcons2.Code },
39573
- { type: "divider", labelKey: "richBlockEditor.toolbar.divider", icon: LucideIcons2.Minus },
39574
- { type: "image", labelKey: "richBlockEditor.toolbar.image", icon: LucideIcons2.Image }
39575
- ];
39576
- BLOCK_TYPE_LABEL_KEY = {
39577
- paragraph: "richBlockEditor.blockType.paragraph",
39578
- "heading-1": "richBlockEditor.blockType.heading1",
39579
- "heading-2": "richBlockEditor.blockType.heading2",
39580
- "heading-3": "richBlockEditor.blockType.heading3",
39581
- "bullet-list": "richBlockEditor.blockType.bulletList",
39582
- "numbered-list": "richBlockEditor.blockType.numberedList",
39583
- quote: "richBlockEditor.blockType.quote",
39584
- code: "richBlockEditor.blockType.code",
39585
- divider: "richBlockEditor.blockType.divider",
39586
- image: "richBlockEditor.blockType.image"
39587
- };
39588
- CHANGEABLE_TYPES = [
39589
- "paragraph",
39590
- "heading-1",
39591
- "heading-2",
39592
- "heading-3",
39593
- "bullet-list",
39594
- "numbered-list",
39595
- "quote",
39596
- "code"
39597
- ];
39598
- _idSeq = 0;
39599
- BLOCK_TYPES = /* @__PURE__ */ new Set([
39600
- "paragraph",
39601
- "heading-1",
39602
- "heading-2",
39603
- "heading-3",
39604
- "bullet-list",
39605
- "numbered-list",
39606
- "quote",
39607
- "code",
39608
- "divider",
39609
- "image"
39610
- ]);
39611
- RichBlockEditor = ({
39612
- initialBlocks,
39613
- onChange,
39614
- changeEvent,
39615
- readOnly = false,
39616
- placeholder,
39617
- enableBlocks = true,
39618
- showToolbar = true,
39619
- className
39620
- }) => {
39621
- const { t } = hooks.useTranslate();
39622
- const [blocks, setBlocks] = React94.useState(
39623
- () => normalizeBlocks(initialBlocks)
39624
- );
39625
- const onChangeRef = React94.useRef(onChange);
39626
- React94.useEffect(() => {
39627
- onChangeRef.current = onChange;
39628
- }, [onChange]);
39629
- const eventBus = useEventBus();
39630
- const changeEventRef = React94.useRef(changeEvent);
39631
- React94.useEffect(() => {
39632
- changeEventRef.current = changeEvent;
39633
- }, [changeEvent]);
39634
- const commit = React94.useCallback((next) => {
39635
- setBlocks(next);
39636
- onChangeRef.current?.(next);
39637
- const evt = changeEventRef.current;
39638
- if (evt) eventBus.emit(`UI:${evt}`, { blocks: next });
39639
- }, [eventBus]);
39640
- const handleAppend = React94.useCallback(
39641
- (type) => {
39642
- if (readOnly) return;
39643
- commit([...blocks, createBlock(type)]);
39644
- },
39645
- [blocks, commit, readOnly]
39646
- );
39647
- const handleUpdate = React94.useCallback(
39648
- (id, updater) => {
39649
- commit(replaceBlock(blocks, id, updater));
39650
- },
39651
- [blocks, commit]
39652
- );
39653
- const handleDelete = React94.useCallback(
39654
- (id) => {
39655
- const next = removeBlock(blocks, id);
39656
- commit(next.length > 0 ? next : [createBlock("paragraph")]);
39657
- },
39658
- [blocks, commit]
39659
- );
39660
- const handleDuplicate = React94.useCallback(
39661
- (id) => {
39662
- const target = blocks.find((b) => b.id === id);
39663
- if (!target) return;
39664
- commit(insertAfter(blocks, id, duplicateBlock(target)));
39665
- },
39666
- [blocks, commit]
39667
- );
39668
- const handleChangeType = React94.useCallback(
39669
- (id, type) => {
39670
- commit(
39671
- replaceBlock(blocks, id, (b) => changeBlockType(b, type))
39672
- );
39673
- },
39674
- [blocks, commit]
39675
- );
39676
- return /* @__PURE__ */ jsxRuntime.jsxs(
39677
- Card,
39678
- {
39679
- variant: "bordered",
39680
- padding: "none",
39681
- className: cn("flex flex-col text-card-foreground", className),
39682
- children: [
39683
- enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
39684
- Box,
39685
- {
39686
- role: "toolbar",
39687
- "aria-label": t("richBlockEditor.editorToolbar"),
39688
- className: cn(
39689
- "flex flex-wrap items-center gap-1",
39690
- "border-b border-border bg-muted/30 px-2 py-2"
39691
- ),
39692
- children: TOOLBAR_ENTRIES.map((entry) => {
39693
- const Icon2 = entry.icon;
39694
- const entryLabel = t(entry.labelKey);
39695
- return /* @__PURE__ */ jsxRuntime.jsxs(
39696
- Button,
39697
- {
39698
- type: "button",
39699
- variant: "ghost",
39700
- size: "sm",
39701
- "aria-label": t("richBlockEditor.insertEntry", { label: entryLabel }),
39702
- title: entryLabel,
39703
- onClick: () => handleAppend(entry.type),
39704
- children: [
39705
- /* @__PURE__ */ jsxRuntime.jsx(Icon2, { size: 14 }),
39706
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", variant: "caption", className: "ml-1 hidden text-xs sm:inline", children: entryLabel })
39707
- ]
39708
- },
39709
- entry.type
39710
- );
39711
- })
39712
- }
39713
- ),
39714
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex flex-col gap-1 px-3 py-3", children: blocks.map((block) => /* @__PURE__ */ jsxRuntime.jsx(
39715
- BlockRow,
39716
- {
39717
- block,
39718
- readOnly,
39719
- showAffordances: enableBlocks,
39720
- placeholder,
39721
- onUpdate: (updater) => handleUpdate(block.id, updater),
39722
- onDelete: () => handleDelete(block.id),
39723
- onDuplicate: () => handleDuplicate(block.id),
39724
- onChangeType: (type) => handleChangeType(block.id, type)
39725
- },
39726
- block.id
39727
- )) })
39728
- ]
39729
- }
39730
- );
39731
- };
39732
- RichBlockEditor.displayName = "RichBlockEditor";
39741
+ init_Input();
39742
+ init_Select();
39743
+ init_Switch();
39733
39744
  }
39734
39745
  });
39735
39746
  function collectInitiallyCollapsed(nodes, acc) {
@@ -44269,8 +44280,8 @@ var init_DataTable = __esm({
44269
44280
  DataTable.displayName = "DataTable";
44270
44281
  }
44271
44282
  });
44272
- function getBadgeVariant(fieldName, value) {
44273
- const name = fieldName.toLowerCase();
44283
+ function getBadgeVariant(fieldName2, value) {
44284
+ const name = fieldName2.toLowerCase();
44274
44285
  const val = String(value).toLowerCase();
44275
44286
  if (name.includes("status")) {
44276
44287
  if (val.includes("complete") || val.includes("done") || val.includes("active"))
@@ -44286,12 +44297,12 @@ function getBadgeVariant(fieldName, value) {
44286
44297
  }
44287
44298
  return "default";
44288
44299
  }
44289
- function formatFieldValue2(value, fieldName) {
44300
+ function formatFieldValue2(value, fieldName2) {
44290
44301
  if (typeof value === "number") {
44291
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
44302
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
44292
44303
  return `${value}%`;
44293
44304
  }
44294
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
44305
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
44295
44306
  return `$${value.toLocaleString()}`;
44296
44307
  }
44297
44308
  return value.toLocaleString();
@@ -44301,7 +44312,7 @@ function formatFieldValue2(value, fieldName) {
44301
44312
  }
44302
44313
  return String(value);
44303
44314
  }
44304
- function renderRichFieldValue(value, fieldName, fieldType, meta) {
44315
+ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
44305
44316
  if (value === void 0 || value === null) return "\u2014";
44306
44317
  const str = String(value);
44307
44318
  switch (fieldType) {
@@ -44312,7 +44323,7 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
44312
44323
  "img",
44313
44324
  {
44314
44325
  src: str,
44315
- alt: formatFieldLabel(fieldName),
44326
+ alt: formatFieldLabel(fieldName2),
44316
44327
  className: "max-w-full max-h-64 rounded-md object-contain",
44317
44328
  loading: "lazy"
44318
44329
  }
@@ -44447,9 +44458,9 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
44447
44458
  return /* @__PURE__ */ jsxRuntime.jsx("a", { href: `tel:${str}`, className: "text-primary hover:underline", children: str });
44448
44459
  default:
44449
44460
  if (meta?.values && meta.values.length > 0 && meta.values.includes(str)) {
44450
- return /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: getBadgeVariant(fieldName, str), children: humanizeEnumValue(str) });
44461
+ return /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: getBadgeVariant(fieldName2, str), children: humanizeEnumValue(str) });
44451
44462
  }
44452
- return formatFieldValue2(value, fieldName);
44463
+ return formatFieldValue2(value, fieldName2);
44453
44464
  }
44454
44465
  }
44455
44466
  function normalizeFieldDefs(fields) {
@@ -45292,7 +45303,21 @@ var init_Form = __esm({
45292
45303
  const normalizedInitialData = React94__namespace.default.useMemo(() => {
45293
45304
  const entityRowAsInitial = isPlainEntityRow(entity) ? entity : void 0;
45294
45305
  const callerInitial = initialData !== null && typeof initialData === "object" && !Array.isArray(initialData) ? initialData : {};
45295
- return entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
45306
+ const merged = entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
45307
+ const toId = (value) => {
45308
+ if (value === null || value === void 0 || typeof value !== "object" || value instanceof Date) return value;
45309
+ if (Array.isArray(value)) {
45310
+ return value.map(
45311
+ (item) => item !== null && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date) && item.id !== void 0 && item.id !== null ? String(item.id) : item
45312
+ );
45313
+ }
45314
+ return value.id !== void 0 && value.id !== null ? String(value.id) : value;
45315
+ };
45316
+ const normalized = {};
45317
+ for (const [key, value] of Object.entries(merged)) {
45318
+ normalized[key] = key === "id" ? value : toId(value);
45319
+ }
45320
+ return normalized;
45296
45321
  }, [entity, initialData]);
45297
45322
  const entityDerivedFields = React94__namespace.default.useMemo(() => {
45298
45323
  if (fields && fields.length > 0) return void 0;
@@ -45421,8 +45446,8 @@ var init_Form = __esm({
45421
45446
  checkViolations(name, newFormData);
45422
45447
  };
45423
45448
  const isFieldVisible = React94__namespace.default.useCallback(
45424
- (fieldName) => {
45425
- const condition = conditionalFields[fieldName];
45449
+ (fieldName2) => {
45450
+ const condition = conditionalFields[fieldName2];
45426
45451
  if (!condition) return true;
45427
45452
  return Boolean(evaluateFormExpression(condition, evalContext));
45428
45453
  },
@@ -45468,9 +45493,9 @@ var init_Form = __esm({
45468
45493
  };
45469
45494
  const handleInvalid = (e) => {
45470
45495
  const target = e.target;
45471
- const fieldName = target.getAttribute("data-field-name") ?? target.name ?? "";
45496
+ const fieldName2 = target.getAttribute("data-field-name") ?? target.name ?? "";
45472
45497
  const fieldMessage = target.validationMessage || "Invalid value";
45473
- debug("forms", "invalid", { mode: formMode, fieldName, fieldMessage });
45498
+ debug("forms", "invalid", { mode: formMode, fieldName: fieldName2, fieldMessage });
45474
45499
  queueMicrotask(() => {
45475
45500
  const form = formRef.current;
45476
45501
  if (!form) return;
@@ -45506,22 +45531,22 @@ var init_Form = __esm({
45506
45531
  };
45507
45532
  const renderField = React94__namespace.default.useCallback(
45508
45533
  (field) => {
45509
- const fieldName = field.name || field.field;
45510
- if (!fieldName) return null;
45511
- if (!isFieldVisible(fieldName)) {
45534
+ const fieldName2 = field.name || field.field;
45535
+ if (!fieldName2) return null;
45536
+ if (!isFieldVisible(fieldName2)) {
45512
45537
  return null;
45513
45538
  }
45514
45539
  const inputType = determineInputType(field);
45515
- const label = field.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/([A-Z])/g, " $1");
45516
- const currentValue = formData[fieldName] ?? field.defaultValue ?? "";
45517
- return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", "data-field": fieldName, children: [
45540
+ const label = field.label || fieldName2.charAt(0).toUpperCase() + fieldName2.slice(1).replace(/([A-Z])/g, " $1");
45541
+ const currentValue = formData[fieldName2] ?? field.defaultValue ?? "";
45542
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", "data-field": fieldName2, children: [
45518
45543
  inputType !== "checkbox" && /* @__PURE__ */ jsxRuntime.jsxs(Typography, { as: "label", variant: "label", weight: "bold", children: [
45519
45544
  label,
45520
45545
  field.required && /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", color: "error", className: "ml-1", children: "*" })
45521
45546
  ] }),
45522
- renderFieldInput(field, fieldName, inputType, currentValue, label),
45547
+ renderFieldInput(field, fieldName2, inputType, currentValue, label),
45523
45548
  field.hint && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", children: field.hint })
45524
- ] }, fieldName);
45549
+ ] }, fieldName2);
45525
45550
  },
45526
45551
  [formData, isFieldVisible, relationsData, relationsLoading, isLoading]
45527
45552
  );
@@ -45550,8 +45575,8 @@ var init_Form = __esm({
45550
45575
  }
45551
45576
  return field;
45552
45577
  }).map((field) => {
45553
- const fieldName = field.name || field.field;
45554
- const override = fieldOverrides?.find((o) => o.name === fieldName);
45578
+ const fieldName2 = field.name || field.field;
45579
+ const override = fieldOverrides?.find((o) => o.name === fieldName2);
45555
45580
  if (!override) return field;
45556
45581
  return {
45557
45582
  ...field,
@@ -45607,11 +45632,11 @@ var init_Form = __esm({
45607
45632
  ] }, section.id);
45608
45633
  }).filter(Boolean);
45609
45634
  }, [sections, isSectionVisible, collapsedSections, renderField, gap]);
45610
- function renderFieldInput(field, fieldName, inputType, currentValue, label) {
45635
+ function renderFieldInput(field, fieldName2, inputType, currentValue, label) {
45611
45636
  const commonProps = {
45612
- id: fieldName,
45613
- name: fieldName,
45614
- "data-field-name": fieldName,
45637
+ id: fieldName2,
45638
+ name: fieldName2,
45639
+ "data-field-name": fieldName2,
45615
45640
  required: field.required,
45616
45641
  disabled: isLoading,
45617
45642
  placeholder: field.placeholder,
@@ -45625,7 +45650,7 @@ var init_Form = __esm({
45625
45650
  ...commonProps,
45626
45651
  label: label + (field.required ? " *" : ""),
45627
45652
  checked: Boolean(currentValue),
45628
- onChange: (e) => handleChange(fieldName, e.target.checked)
45653
+ onChange: (e) => handleChange(fieldName2, e.target.checked)
45629
45654
  }
45630
45655
  );
45631
45656
  case "textarea":
@@ -45634,7 +45659,7 @@ var init_Form = __esm({
45634
45659
  {
45635
45660
  ...commonProps,
45636
45661
  value: String(currentValue),
45637
- onChange: (e) => handleChange(fieldName, e.target.value),
45662
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45638
45663
  minLength: field.min,
45639
45664
  maxLength: field.max
45640
45665
  }
@@ -45647,14 +45672,14 @@ var init_Form = __esm({
45647
45672
  ...commonProps,
45648
45673
  options,
45649
45674
  value: String(currentValue),
45650
- onValueChange: (v) => handleChange(fieldName, v),
45675
+ onValueChange: (v) => handleChange(fieldName2, v),
45651
45676
  placeholder: field.placeholder || `Select ${label}...`
45652
45677
  }
45653
45678
  );
45654
45679
  }
45655
45680
  case "relation": {
45656
- const relationOptions = relationsData[fieldName] || [];
45657
- const relationLoading = relationsLoading[fieldName] || false;
45681
+ const relationOptions = relationsData[fieldName2] || [];
45682
+ const relationLoading = relationsLoading[fieldName2] || false;
45658
45683
  if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
45659
45684
  const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
45660
45685
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -45666,7 +45691,7 @@ var init_Form = __esm({
45666
45691
  clearable: true,
45667
45692
  options: [...relationOptions],
45668
45693
  value: selectedValues,
45669
- onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
45694
+ onValueChange: (value) => handleChange(fieldName2, Array.isArray(value) ? value : [value]),
45670
45695
  placeholder: field.placeholder || `Select ${label}...`
45671
45696
  }
45672
45697
  );
@@ -45676,7 +45701,7 @@ var init_Form = __esm({
45676
45701
  {
45677
45702
  ...commonProps,
45678
45703
  value: currentValue ? String(currentValue) : void 0,
45679
- onChange: (value) => handleChange(fieldName, value),
45704
+ onChange: (value) => handleChange(fieldName2, value),
45680
45705
  options: relationOptions,
45681
45706
  isLoading: relationLoading,
45682
45707
  placeholder: field.placeholder || `Select ${label}...`,
@@ -45693,7 +45718,7 @@ var init_Form = __esm({
45693
45718
  placeholder: field.placeholder,
45694
45719
  disabled: isLoading,
45695
45720
  value: arrayValue,
45696
- onChange: (next) => handleChange(fieldName, [...next])
45721
+ onChange: (next) => handleChange(fieldName2, [...next])
45697
45722
  }
45698
45723
  );
45699
45724
  }
@@ -45705,7 +45730,7 @@ var init_Form = __esm({
45705
45730
  type: "number",
45706
45731
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
45707
45732
  onChange: (e) => handleChange(
45708
- fieldName,
45733
+ fieldName2,
45709
45734
  e.target.value ? Number(e.target.value) : void 0
45710
45735
  ),
45711
45736
  min: field.min,
@@ -45722,7 +45747,7 @@ var init_Form = __esm({
45722
45747
  icon: LucideIcons2.DollarSign,
45723
45748
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
45724
45749
  onChange: (e) => handleChange(
45725
- fieldName,
45750
+ fieldName2,
45726
45751
  e.target.value ? Number(e.target.value) : void 0
45727
45752
  ),
45728
45753
  min: field.min,
@@ -45740,7 +45765,7 @@ var init_Form = __esm({
45740
45765
  const f3 = files[0];
45741
45766
  if (!f3) return;
45742
45767
  const reader = new FileReader();
45743
- reader.onload = () => handleChange(fieldName, {
45768
+ reader.onload = () => handleChange(fieldName2, {
45744
45769
  name: f3.name,
45745
45770
  mimeType: f3.type,
45746
45771
  sizeBytes: f3.size,
@@ -45757,7 +45782,7 @@ var init_Form = __esm({
45757
45782
  ...commonProps,
45758
45783
  type: "date",
45759
45784
  value: formatDateValue(currentValue),
45760
- onChange: (e) => handleChange(fieldName, e.target.value)
45785
+ onChange: (e) => handleChange(fieldName2, e.target.value)
45761
45786
  }
45762
45787
  );
45763
45788
  case "datetime-local":
@@ -45767,7 +45792,7 @@ var init_Form = __esm({
45767
45792
  ...commonProps,
45768
45793
  type: "datetime-local",
45769
45794
  value: formatDateTimeValue(currentValue),
45770
- onChange: (e) => handleChange(fieldName, e.target.value)
45795
+ onChange: (e) => handleChange(fieldName2, e.target.value)
45771
45796
  }
45772
45797
  );
45773
45798
  case "email":
@@ -45777,7 +45802,7 @@ var init_Form = __esm({
45777
45802
  ...commonProps,
45778
45803
  type: "email",
45779
45804
  value: String(currentValue),
45780
- onChange: (e) => handleChange(fieldName, e.target.value),
45805
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45781
45806
  minLength: field.min,
45782
45807
  maxLength: field.max
45783
45808
  }
@@ -45789,7 +45814,7 @@ var init_Form = __esm({
45789
45814
  ...commonProps,
45790
45815
  type: "url",
45791
45816
  value: String(currentValue),
45792
- onChange: (e) => handleChange(fieldName, e.target.value),
45817
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45793
45818
  minLength: field.min,
45794
45819
  maxLength: field.max
45795
45820
  }
@@ -45801,7 +45826,7 @@ var init_Form = __esm({
45801
45826
  ...commonProps,
45802
45827
  type: "password",
45803
45828
  value: String(currentValue),
45804
- onChange: (e) => handleChange(fieldName, e.target.value),
45829
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45805
45830
  minLength: field.min,
45806
45831
  maxLength: field.max
45807
45832
  }
@@ -45814,7 +45839,7 @@ var init_Form = __esm({
45814
45839
  ...commonProps,
45815
45840
  type: "text",
45816
45841
  value: String(currentValue),
45817
- onChange: (e) => handleChange(fieldName, e.target.value),
45842
+ onChange: (e) => handleChange(fieldName2, e.target.value),
45818
45843
  minLength: field.min,
45819
45844
  maxLength: field.max
45820
45845
  }
@@ -46171,7 +46196,7 @@ function entityFieldsFromListItem(item) {
46171
46196
  }
46172
46197
  return result;
46173
46198
  }
46174
- function getStatusStyle(fieldName, value) {
46199
+ function getStatusStyle(fieldName2, value) {
46175
46200
  const val = String(value).toLowerCase();
46176
46201
  if (val.includes("complete") || val.includes("done"))
46177
46202
  return STATUS_STYLES.complete;
@@ -46187,12 +46212,12 @@ function getStatusStyle(fieldName, value) {
46187
46212
  if (val.includes("low")) return STATUS_STYLES.low;
46188
46213
  return STATUS_STYLES.default;
46189
46214
  }
46190
- function formatValue3(value, fieldName) {
46215
+ function formatValue3(value, fieldName2) {
46191
46216
  if (typeof value === "number") {
46192
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
46217
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
46193
46218
  return `${value}%`;
46194
46219
  }
46195
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
46220
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
46196
46221
  return new Intl.NumberFormat("en-US", {
46197
46222
  style: "currency",
46198
46223
  currency: "USD",
@@ -46215,8 +46240,8 @@ function formatValue3(value, fieldName) {
46215
46240
  }
46216
46241
  return String(value);
46217
46242
  }
46218
- function formatFieldLabel2(fieldName) {
46219
- return humanizeFieldName(fieldName).replace(/\sId$/, "").trim();
46243
+ function formatFieldLabel2(fieldName2) {
46244
+ return humanizeFieldName(fieldName2).replace(/\sId$/, "").trim();
46220
46245
  }
46221
46246
  var STATUS_STYLES, StatusBadge, ProgressIndicator, List3;
46222
46247
  var init_List = __esm({
@@ -46284,9 +46309,9 @@ var init_List = __esm({
46284
46309
  };
46285
46310
  StatusBadge = ({
46286
46311
  value,
46287
- fieldName
46312
+ fieldName: fieldName2
46288
46313
  }) => {
46289
- const style = getStatusStyle(fieldName, value);
46314
+ const style = getStatusStyle(fieldName2, value);
46290
46315
  return /* @__PURE__ */ jsxRuntime.jsxs(
46291
46316
  Typography,
46292
46317
  {
@@ -49218,8 +49243,8 @@ var init_StatCard = __esm({
49218
49243
  return items.length;
49219
49244
  }
49220
49245
  if (field.includes(":")) {
49221
- const [fieldName, fieldValue] = field.split(":");
49222
- return items.filter((item) => item[fieldName] === fieldValue).length;
49246
+ const [fieldName2, fieldValue] = field.split(":");
49247
+ return items.filter((item) => item[fieldName2] === fieldValue).length;
49223
49248
  }
49224
49249
  const fieldExistsOnItems = items.some((item) => field in item);
49225
49250
  if (fieldExistsOnItems) {
@@ -50739,6 +50764,8 @@ var init_component_registry_generated = __esm({
50739
50764
  init_DocSearch();
50740
50765
  init_DocSidebar();
50741
50766
  init_DocTOC();
50767
+ init_DocumentDetails();
50768
+ init_DocumentPanel();
50742
50769
  init_DocumentViewer();
50743
50770
  init_DrawFxLayer();
50744
50771
  init_DrawGroup();
@@ -50848,7 +50875,7 @@ var init_component_registry_generated = __esm({
50848
50875
  init_RelationSelect();
50849
50876
  init_RepeatableFormSection();
50850
50877
  init_ReplyTree();
50851
- init_RichBlockEditor();
50878
+ init_RichTextEditor();
50852
50879
  init_RuntimeDebugger2();
50853
50880
  init_ScaledDiagram();
50854
50881
  init_ScoreDisplay();
@@ -51011,6 +51038,8 @@ var init_component_registry_generated = __esm({
51011
51038
  "DocSearch": DocSearch,
51012
51039
  "DocSidebar": DocSidebar,
51013
51040
  "DocTOC": DocTOC,
51041
+ "DocumentDetails": DocumentDetails,
51042
+ "DocumentPanel": DocumentPanel,
51014
51043
  "DocumentViewer": DocumentViewer,
51015
51044
  "DrawFxLayer": DrawFxLayer,
51016
51045
  "DrawGroup": DrawGroup,
@@ -51122,7 +51151,7 @@ var init_component_registry_generated = __esm({
51122
51151
  "RelationSelect": RelationSelect,
51123
51152
  "RepeatableFormSection": RepeatableFormSection,
51124
51153
  "ReplyTree": ReplyTree,
51125
- "RichBlockEditor": RichBlockEditor,
51154
+ "RichTextEditor": RichTextEditor,
51126
51155
  "RuntimeDebugger": RuntimeDebugger,
51127
51156
  "ScaledDiagram": ScaledDiagram,
51128
51157
  "ScoreDisplay": ScoreDisplay,
@@ -51276,10 +51305,10 @@ function enrichFormFields(fields, entityDef) {
51276
51305
  }
51277
51306
  if (field && typeof field === "object" && !Array.isArray(field) && !React94__namespace.default.isValidElement(field) && !(field instanceof Date)) {
51278
51307
  const obj = field;
51279
- const fieldName = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
51280
- if (!fieldName) return field;
51308
+ const fieldName2 = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
51309
+ if (!fieldName2) return field;
51281
51310
  if (obj.type || obj.inputType) return field;
51282
- const entityField = fieldMap.get(fieldName);
51311
+ const entityField = fieldMap.get(fieldName2);
51283
51312
  if (!entityField) return field;
51284
51313
  const enriched = { ...obj, type: entityField.type };
51285
51314
  if (entityField.required && !("required" in obj)) {
@@ -51329,9 +51358,9 @@ function enrichDetailFields(fields, entityDef) {
51329
51358
  }
51330
51359
  if (field && typeof field === "object" && !Array.isArray(field) && !React94__namespace.default.isValidElement(field) && !(field instanceof Date)) {
51331
51360
  const obj = field;
51332
- const fieldName = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
51333
- if (!fieldName || obj.type) return field;
51334
- const meta = metaFor(fieldName);
51361
+ const fieldName2 = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
51362
+ if (!fieldName2 || obj.type) return field;
51363
+ const meta = metaFor(fieldName2);
51335
51364
  return meta ? { ...obj, ...meta } : field;
51336
51365
  }
51337
51366
  return field;
@@ -52170,9 +52199,9 @@ function UISlotRenderer({
52170
52199
  "ui-slot-renderer relative min-h-full",
52171
52200
  className
52172
52201
  ), children: [
52173
- /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex min-h-full", children: [
52174
- /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 shrink-0" }),
52175
- /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-h-[200px]" })
52202
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex min-h-full flex-col lg:flex-row", children: [
52203
+ /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 lg:shrink-0" }),
52204
+ /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-w-0 min-h-[200px]" })
52176
52205
  ] }),
52177
52206
  /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "modal", portal: true }),
52178
52207
  /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "drawer", portal: true }),
@@ -56069,7 +56098,7 @@ function buildTraitRenderConfig(binding, traitConfigsByName, payload) {
56069
56098
  }
56070
56099
  return void 0;
56071
56100
  }
56072
- function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
56101
+ function createSharedEntityWriter(binding, tick, traitStatesRef, emit, traitConfigsByName) {
56073
56102
  return (scratch) => {
56074
56103
  const traitName = binding.trait.name;
56075
56104
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -56078,9 +56107,13 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
56078
56107
  const writes = [];
56079
56108
  const ctx = evaluator.createMinimalContext(scratchEntity, {}, currentState);
56080
56109
  const declaredDefaults = runtime.collectDeclaredConfigDefaults(binding.trait);
56081
- const callSiteConfig = getBindingConfig(binding);
56082
- if (declaredDefaults || callSiteConfig) {
56083
- ctx.config = { ...declaredDefaults ?? {}, ...callSiteConfig ?? {} };
56110
+ const resolvedByName = traitConfigsByName?.[traitName];
56111
+ const callSiteRaw = getBindingConfig(binding);
56112
+ const callSiteConfig = callSiteRaw ? Object.fromEntries(
56113
+ Object.entries(callSiteRaw).filter(([, v]) => !containsConfigForward(v))
56114
+ ) : void 0;
56115
+ if (declaredDefaults || resolvedByName || callSiteConfig) {
56116
+ ctx.config = { ...declaredDefaults ?? {}, ...resolvedByName ?? {}, ...callSiteConfig ?? {} };
56084
56117
  }
56085
56118
  ctx.mutateEntity = (changes) => {
56086
56119
  for (const [field, value] of Object.entries(changes)) {
@@ -56683,7 +56716,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56683
56716
  const interval = entries[0].tick.interval;
56684
56717
  const onDue = timedTick(`tick:shared:${group.storeKey}@${String(interval)}`, () => {
56685
56718
  const writers = entries.map(
56686
- ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
56719
+ ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter, traitConfigsByName)
56687
56720
  );
56688
56721
  runTickFrame(group.storeKey, writers, sharedEntityStore);
56689
56722
  });
@@ -59196,11 +59229,11 @@ function OrbInspector({ node, schema, editable = false, userType = "builder", th
59196
59229
  const handleAddField = React94.useCallback(() => {
59197
59230
  eventBus.emit("UI:ADD_FIELD", {});
59198
59231
  }, [eventBus]);
59199
- const handleUpdateField = React94.useCallback((fieldName, updates) => {
59200
- eventBus.emit("UI:UPDATE_FIELD", { fieldName, updates });
59232
+ const handleUpdateField = React94.useCallback((fieldName2, updates) => {
59233
+ eventBus.emit("UI:UPDATE_FIELD", { fieldName: fieldName2, updates });
59201
59234
  }, [eventBus]);
59202
- const handleRemoveField = React94.useCallback((fieldName) => {
59203
- eventBus.emit("UI:REMOVE_FIELD", { fieldName });
59235
+ const handleRemoveField = React94.useCallback((fieldName2) => {
59236
+ eventBus.emit("UI:REMOVE_FIELD", { fieldName: fieldName2 });
59204
59237
  }, [eventBus]);
59205
59238
  const handleGuardChange = React94.useCallback((guardExpr) => {
59206
59239
  if (!editable) return;