@almadar/ui 5.160.0 → 5.161.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.
@@ -25903,8 +25903,9 @@ function DataGrid({
25903
25903
  const titleField = fieldDefs.find((f3) => f3.variant === "h3" || f3.variant === "h4") ?? fieldDefs[0];
25904
25904
  const badgeFields = fieldDefs.filter((f3) => f3.variant === "badge" && f3 !== titleField);
25905
25905
  const bodyFields = fieldDefs.filter((f3) => f3 !== titleField && !badgeFields.includes(f3));
25906
- const primaryActions = actionDefs.filter((a) => a.variant !== "danger");
25907
- const dangerActions = actionDefs.filter((a) => a.variant === "danger");
25906
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
25907
+ const inlineCardActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
25908
+ const menuCardActions = actionDefs.filter((a) => !inlineCardActions.includes(a));
25908
25909
  const fireAction = (action, itemData) => {
25909
25910
  if (action.navigatesTo) {
25910
25911
  const url = action.navigatesTo.replace(
@@ -25924,6 +25925,9 @@ function DataGrid({
25924
25925
  e.stopPropagation();
25925
25926
  fireAction(action, itemData);
25926
25927
  };
25928
+ const cardClickAction = actionDefs.find((a) => a.variant !== "danger");
25929
+ const handleCardClick = cardClickAction ? (itemData) => () => fireAction(cardClickAction, itemData) : void 0;
25930
+ const stopCardClick = handleCardClick ? (e) => e.stopPropagation() : void 0;
25927
25931
  const hasRenderProp = typeof children === "function";
25928
25932
  React85.useEffect(() => {
25929
25933
  if (data.length > 0 && !hasRenderProp && fieldDefs.length === 0) {
@@ -25995,55 +25999,40 @@ function DataGrid({
25995
25999
  {
25996
26000
  "data-entity-row": true,
25997
26001
  "data-entity-id": id,
25998
- className: cn("relative group/rowactions", isSelected && "ring-2 ring-primary rounded-lg"),
26002
+ onClick: handleCardClick?.(itemData),
26003
+ className: cn("relative group/rowactions", handleCardClick && "cursor-pointer", isSelected && "ring-2 ring-primary rounded-lg"),
25999
26004
  children: [
26000
26005
  children(itemData, index),
26001
- 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: [
26002
- /* @__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: [
26003
- (maxInlineActions != null ? actionDefs.slice(0, maxInlineActions) : actionDefs).map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
26004
- Button,
26005
- {
26006
- variant: action.variant === "primary" ? "primary" : "ghost",
26007
- size: "sm",
26008
- onClick: handleActionClick(action, itemData),
26009
- "data-testid": `action-${action.event}`,
26010
- "data-row-id": String(itemData.id),
26011
- className: action.variant === "danger" ? "text-error hover:text-error hover:bg-error/10" : void 0,
26012
- children: [
26013
- action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
26014
- action.label
26015
- ]
26016
- },
26017
- idx
26018
- )),
26019
- maxInlineActions != null && actionDefs.length > maxInlineActions && /* @__PURE__ */ jsxRuntime.jsx(
26020
- Menu,
26021
- {
26022
- position: "bottom-end",
26023
- 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" }) }),
26024
- items: actionDefs.slice(maxInlineActions).map((action) => ({
26025
- label: action.label,
26026
- icon: action.icon,
26027
- event: action.event,
26028
- onClick: () => fireAction(action, itemData)
26029
- }))
26030
- }
26031
- )
26032
- ] }),
26033
- /* @__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(
26006
+ 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: [
26007
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
26008
+ Button,
26009
+ {
26010
+ variant: "primary",
26011
+ size: "sm",
26012
+ onClick: handleActionClick(action, itemData),
26013
+ "data-testid": `action-${action.event}`,
26014
+ "data-row-id": String(itemData.id),
26015
+ children: [
26016
+ action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
26017
+ action.label
26018
+ ]
26019
+ },
26020
+ idx
26021
+ )),
26022
+ menuCardActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
26034
26023
  Menu,
26035
26024
  {
26036
26025
  position: "bottom-end",
26037
26026
  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" }) }),
26038
- items: actionDefs.map((action) => ({
26027
+ items: menuCardActions.map((action) => ({
26039
26028
  label: action.label,
26040
26029
  icon: action.icon,
26041
- event: action.event,
26030
+ variant: action.variant === "danger" ? "danger" : "default",
26042
26031
  onClick: () => fireAction(action, itemData)
26043
26032
  }))
26044
26033
  }
26045
- ) })
26046
- ] })
26034
+ )
26035
+ ] }) })
26047
26036
  ]
26048
26037
  },
26049
26038
  id
@@ -26057,12 +26046,14 @@ function DataGrid({
26057
26046
  {
26058
26047
  "data-entity-row": true,
26059
26048
  "data-entity-id": id,
26049
+ onClick: handleCardClick?.(itemData),
26060
26050
  className: cn(
26061
26051
  "bg-card rounded-lg",
26062
26052
  "border border-border",
26063
26053
  "shadow-elevation-card hover:shadow-elevation-dialog",
26064
26054
  "hover:border-primary transition-all",
26065
26055
  "flex flex-col",
26056
+ handleCardClick && "cursor-pointer",
26066
26057
  isSelected && "ring-2 ring-primary border-primary"
26067
26058
  ),
26068
26059
  children: [
@@ -26112,53 +26103,35 @@ function DataGrid({
26112
26103
  ] }, field.name);
26113
26104
  }) })
26114
26105
  ] }),
26115
- (primaryActions.length > 0 || dangerActions.length > 0) && /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
26116
- (maxInlineActions != null ? primaryActions.slice(0, maxInlineActions) : primaryActions).map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
26106
+ actionDefs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", onClick: stopCardClick, className: "flex-shrink-0", children: [
26107
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
26117
26108
  Button,
26118
26109
  {
26119
- variant: action.variant === "primary" ? "primary" : "ghost",
26110
+ variant: "primary",
26120
26111
  size: "sm",
26121
26112
  onClick: handleActionClick(action, itemData),
26122
26113
  "data-testid": `action-${action.event}`,
26123
26114
  "data-row-id": String(itemData.id),
26124
26115
  "aria-label": action.label,
26125
26116
  title: action.label,
26126
- className: cn(
26127
- action.variant === "primary" ? void 0 : "text-muted-foreground hover:text-foreground",
26128
- action.icon && "px-2"
26129
- ),
26117
+ className: cn(action.icon && "px-2"),
26130
26118
  children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
26131
26119
  },
26132
26120
  idx
26133
26121
  )),
26134
- maxInlineActions != null && primaryActions.length > maxInlineActions && /* @__PURE__ */ jsxRuntime.jsx(
26122
+ menuCardActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
26135
26123
  Menu,
26136
26124
  {
26137
26125
  position: "bottom-end",
26138
26126
  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" }) }),
26139
- items: primaryActions.slice(maxInlineActions).map((action) => ({
26127
+ items: menuCardActions.map((action) => ({
26140
26128
  label: action.label,
26141
26129
  icon: action.icon,
26142
- event: action.event,
26130
+ variant: action.variant === "danger" ? "danger" : "default",
26143
26131
  onClick: () => fireAction(action, itemData)
26144
26132
  }))
26145
26133
  }
26146
- ),
26147
- dangerActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
26148
- Button,
26149
- {
26150
- variant: "ghost",
26151
- size: "sm",
26152
- onClick: handleActionClick(action, itemData),
26153
- "data-testid": `action-${action.event}`,
26154
- "data-row-id": String(itemData.id),
26155
- "aria-label": action.label,
26156
- title: action.label,
26157
- className: "text-error hover:text-error hover:bg-error/10 px-2",
26158
- children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
26159
- },
26160
- `danger-${idx}`
26161
- ))
26134
+ )
26162
26135
  ] })
26163
26136
  ] }) }),
26164
26137
  bodyFields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-4 pt-2 pb-4 flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", children: [
@@ -26400,54 +26373,59 @@ function DataList({
26400
26373
  if (!itemActions || itemActions.length === 0) return null;
26401
26374
  const inline = maxInlineActions != null ? itemActions.slice(0, maxInlineActions) : itemActions;
26402
26375
  const overflow = maxInlineActions != null ? itemActions.slice(maxInlineActions) : [];
26403
- return /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
26404
- inline.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
26405
- Button,
26406
- {
26407
- variant: action.variant ?? "ghost",
26408
- size: "sm",
26409
- onClick: handleActionClick(action, itemData),
26410
- "data-testid": `action-${action.event}`,
26411
- "data-row-id": String(itemData.id),
26412
- className: cn(
26413
- action.variant === "danger" && "text-error hover:bg-error/10",
26414
- // Must sit on the Button itself: the variant's own text colour
26415
- // beats an inherited one from the row wrapper.
26416
- onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
26417
- ),
26418
- children: [
26419
- action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
26420
- action.label
26421
- ]
26422
- },
26423
- idx
26424
- )),
26425
- overflow.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
26426
- Menu,
26427
- {
26428
- position: "bottom-end",
26429
- 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" }) }),
26430
- items: overflow.map((action) => ({
26431
- label: action.label,
26432
- icon: action.icon,
26433
- event: action.event,
26434
- variant: action.variant === "danger" ? "danger" : "default",
26435
- onClick: () => eventBus.emit(`UI:${action.event}`, {
26436
- id: itemData.id,
26437
- row: itemData
26438
- })
26439
- }))
26440
- }
26441
- )
26442
- ] });
26376
+ return (
26377
+ // stopPropagation at the cluster: the "⋯" trigger opens the overflow
26378
+ // menu without also firing the row's default click (inline buttons
26379
+ // already stop it in handleActionClick; the Menu panel is portaled).
26380
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, className: "flex-shrink-0", children: [
26381
+ inline.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
26382
+ Button,
26383
+ {
26384
+ variant: action.variant ?? "ghost",
26385
+ size: "sm",
26386
+ onClick: handleActionClick(action, itemData),
26387
+ "data-testid": `action-${action.event}`,
26388
+ "data-row-id": String(itemData.id),
26389
+ className: cn(
26390
+ action.variant === "danger" && "text-error hover:bg-error/10",
26391
+ // Must sit on the Button itself: the variant's own text colour
26392
+ // beats an inherited one from the row wrapper.
26393
+ onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
26394
+ ),
26395
+ children: [
26396
+ action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
26397
+ action.label
26398
+ ]
26399
+ },
26400
+ idx
26401
+ )),
26402
+ overflow.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
26403
+ Menu,
26404
+ {
26405
+ position: "bottom-end",
26406
+ 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" }) }),
26407
+ items: overflow.map((action) => ({
26408
+ label: action.label,
26409
+ icon: action.icon,
26410
+ variant: action.variant === "danger" ? "danger" : "default",
26411
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
26412
+ id: itemData.id,
26413
+ row: itemData
26414
+ })
26415
+ }))
26416
+ }
26417
+ )
26418
+ ] })
26419
+ );
26443
26420
  };
26421
+ const rowClickEvent = itemClickEvent || itemActions?.find((a) => a.variant !== "danger")?.event;
26444
26422
  const handleRowClick = (itemData) => () => {
26445
- if (!itemClickEvent) return;
26423
+ if (!rowClickEvent) return;
26446
26424
  const payload = {
26447
26425
  id: itemData.id,
26448
26426
  row: itemData
26449
26427
  };
26450
- eventBus.emit(`UI:${itemClickEvent}`, payload);
26428
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
26451
26429
  };
26452
26430
  if (isLoading) {
26453
26431
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) });
@@ -26495,10 +26473,10 @@ function DataList({
26495
26473
  {
26496
26474
  "data-entity-row": true,
26497
26475
  "data-entity-id": id,
26498
- onClick: itemClickEvent ? handleRowClick(itemData) : void 0,
26476
+ onClick: rowClickEvent ? handleRowClick(itemData) : void 0,
26499
26477
  className: cn(
26500
26478
  "flex px-4 group/rowactions",
26501
- itemClickEvent && "cursor-pointer",
26479
+ rowClickEvent && "cursor-pointer",
26502
26480
  isSent ? "justify-end" : "justify-start"
26503
26481
  ),
26504
26482
  children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -26574,7 +26552,7 @@ function DataList({
26574
26552
  const id2 = itemData.id || String(index);
26575
26553
  const actions = renderItemActions(itemData);
26576
26554
  return wrapDnd(
26577
- /* @__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: [
26555
+ /* @__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: [
26578
26556
  children(itemData, index),
26579
26557
  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: [
26580
26558
  /* @__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 }),
@@ -26586,7 +26564,6 @@ function DataList({
26586
26564
  items: (itemActions ?? []).map((action) => ({
26587
26565
  label: action.label,
26588
26566
  icon: action.icon,
26589
- event: action.event,
26590
26567
  variant: action.variant === "danger" ? "danger" : "default",
26591
26568
  onClick: () => eventBus.emit(`UI:${action.event}`, {
26592
26569
  id: itemData.id,
@@ -26603,7 +26580,7 @@ function DataList({
26603
26580
  const id = itemData.id || String(index);
26604
26581
  const titleValue = getNestedValue(itemData, titleField?.name ?? "");
26605
26582
  return wrapDnd(
26606
- /* @__PURE__ */ jsxRuntime.jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: itemClickEvent ? handleRowClick(itemData) : void 0, className: cn(itemClickEvent && "cursor-pointer"), children: [
26583
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: rowClickEvent ? handleRowClick(itemData) : void 0, className: cn(rowClickEvent && "cursor-pointer"), children: [
26607
26584
  /* @__PURE__ */ jsxRuntime.jsxs(
26608
26585
  Box,
26609
26586
  {
@@ -31815,7 +31792,7 @@ var init_MapView = __esm({
31815
31792
  shadowSize: [41, 41]
31816
31793
  });
31817
31794
  L.Marker.prototype.options.icon = defaultIcon;
31818
- const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback90, useState: useState92 } = React85__namespace.default;
31795
+ const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback90, useState: useState94 } = React85__namespace.default;
31819
31796
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31820
31797
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31821
31798
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -31860,7 +31837,7 @@ var init_MapView = __esm({
31860
31837
  showAttribution = true
31861
31838
  }) {
31862
31839
  const eventBus = useEventBus2();
31863
- const [clickedPosition, setClickedPosition] = useState92(null);
31840
+ const [clickedPosition, setClickedPosition] = useState94(null);
31864
31841
  const handleMapClick = useCallback90((lat, lng) => {
31865
31842
  if (showClickedPin) {
31866
31843
  setClickedPosition({ lat, lng });
@@ -36084,741 +36061,775 @@ var init_PositionedCanvas = __esm({
36084
36061
  PositionedCanvas.displayName = "PositionedCanvas";
36085
36062
  }
36086
36063
  });
36087
- function nextBlockId(prefix = "blk") {
36088
- _idSeq += 1;
36089
- const random = Math.random().toString(36).slice(2, 8);
36090
- return `${prefix}-${Date.now().toString(36)}-${_idSeq}-${random}`;
36064
+ function safeUrl(raw, kinds) {
36065
+ const url = raw.trim();
36066
+ if (url.startsWith("#") || url.startsWith("/")) return url;
36067
+ const lower = url.toLowerCase();
36068
+ for (const kind of kinds) {
36069
+ if (lower.startsWith(kind)) return url;
36070
+ }
36071
+ return null;
36091
36072
  }
36092
- function normalizeBlocks(raw) {
36093
- if (!Array.isArray(raw) || raw.length === 0) return [createBlock("paragraph")];
36094
- return raw.map((row) => {
36095
- const entity = row;
36096
- const rawType = entity.type;
36097
- const type = typeof rawType === "string" && BLOCK_TYPES.has(rawType) ? rawType : "paragraph";
36098
- const id = typeof entity.id === "string" && entity.id ? entity.id : nextBlockId(type);
36099
- return { ...entity, id, type };
36100
- });
36073
+ function sanitizeNode(el) {
36074
+ const children = Array.from(el.children);
36075
+ for (const child of children) {
36076
+ const tag = child.tagName.toLowerCase();
36077
+ if (DROP_TAGS.has(tag)) {
36078
+ child.remove();
36079
+ continue;
36080
+ }
36081
+ const allowed = ALLOWED_ATTRS[tag];
36082
+ if (!allowed) {
36083
+ sanitizeNode(child);
36084
+ child.replaceWith(...Array.from(child.childNodes));
36085
+ continue;
36086
+ }
36087
+ for (const attr of Array.from(child.attributes)) {
36088
+ const name = attr.name.toLowerCase();
36089
+ if (!allowed.includes(name)) {
36090
+ child.removeAttribute(attr.name);
36091
+ continue;
36092
+ }
36093
+ if (name === "href") {
36094
+ const url = safeUrl(attr.value, ["http://", "https://", "mailto:"]);
36095
+ if (url === null) child.removeAttribute(attr.name);
36096
+ else child.setAttribute("href", url);
36097
+ }
36098
+ if (name === "src") {
36099
+ const url = safeUrl(attr.value, ["http://", "https://", "data:image/"]);
36100
+ if (url === null) child.remove();
36101
+ else child.setAttribute("src", url);
36102
+ }
36103
+ }
36104
+ sanitizeNode(child);
36105
+ }
36101
36106
  }
36102
- function createBlock(type) {
36103
- switch (type) {
36104
- case "bullet-list":
36105
- case "numbered-list":
36106
- return {
36107
- id: nextBlockId(type),
36108
- type,
36109
- children: [
36110
- { id: nextBlockId("li"), type: "paragraph", content: "" }
36111
- ]
36112
- };
36113
- case "image":
36114
- return {
36115
- id: nextBlockId(type),
36116
- type,
36117
- content: "",
36118
- metadata: { url: "", caption: "" }
36119
- };
36120
- case "code":
36121
- return {
36122
- id: nextBlockId(type),
36123
- type,
36124
- content: "",
36125
- metadata: { language: "plaintext" }
36126
- };
36127
- case "divider":
36128
- return { id: nextBlockId(type), type };
36129
- default:
36130
- return { id: nextBlockId(type), type, content: "" };
36107
+ function sanitizeRichHtml(html) {
36108
+ if (!html || typeof html !== "string") return "";
36109
+ if (typeof window === "undefined" || typeof DOMParser === "undefined") return "";
36110
+ const doc = new DOMParser().parseFromString(html, "text/html");
36111
+ sanitizeNode(doc.body);
36112
+ if (!doc.body.querySelector("img, hr") && (doc.body.textContent ?? "").trim().length === 0) {
36113
+ return "";
36131
36114
  }
36115
+ return doc.body.innerHTML.replace(/​/g, "");
36132
36116
  }
36133
- function replaceBlock(blocks, id, updater) {
36134
- return blocks.map((block) => block.id === id ? updater(block) : block);
36117
+ function htmlIsEmpty(el) {
36118
+ if (el.querySelector("img, hr")) return false;
36119
+ return (el.textContent ?? "").trim().length === 0;
36120
+ }
36121
+ function closestBlock(node, root) {
36122
+ let cur = node;
36123
+ while (cur && cur !== root) {
36124
+ if (cur instanceof Element && BLOCK_TAGS.has(cur.tagName)) return cur;
36125
+ cur = cur.parentNode;
36126
+ }
36127
+ return null;
36135
36128
  }
36136
- function removeBlock(blocks, id) {
36137
- return blocks.filter((block) => block.id !== id);
36129
+ function RichTextStyles() {
36130
+ return /* @__PURE__ */ jsxRuntime.jsx("style", { children: RICH_TEXT_CSS });
36138
36131
  }
36139
- function duplicateBlock(block) {
36132
+ function readToolbarState(root) {
36133
+ const sel = window.getSelection();
36134
+ if (!sel || sel.rangeCount === 0) return IDLE_TOOLBAR;
36135
+ const anchor = sel.anchorNode;
36136
+ if (!anchor || !root.contains(anchor)) return IDLE_TOOLBAR;
36137
+ let block = null;
36138
+ let link = false;
36139
+ let node = anchor;
36140
+ while (node && node !== root) {
36141
+ if (node instanceof Element) {
36142
+ const tag = node.tagName.toLowerCase();
36143
+ if (!block && (tag === "h1" || tag === "h2" || tag === "h3" || tag === "blockquote" || tag === "pre")) {
36144
+ block = tag;
36145
+ }
36146
+ if (tag === "a") link = true;
36147
+ }
36148
+ node = node.parentNode;
36149
+ }
36140
36150
  return {
36141
- ...block,
36142
- id: nextBlockId(block.type),
36143
- children: block.children?.map((child) => ({
36144
- ...child,
36145
- id: nextBlockId("li")
36146
- })),
36147
- metadata: block.metadata ? { ...block.metadata } : void 0
36151
+ bold: document.queryCommandState("bold"),
36152
+ italic: document.queryCommandState("italic"),
36153
+ underline: document.queryCommandState("underline"),
36154
+ strikeThrough: document.queryCommandState("strikeThrough"),
36155
+ block,
36156
+ bullets: document.queryCommandState("insertUnorderedList"),
36157
+ numbers: document.queryCommandState("insertOrderedList"),
36158
+ link
36148
36159
  };
36149
36160
  }
36150
- function insertAfter(blocks, targetId, inserted) {
36151
- const idx = blocks.findIndex((b) => b.id === targetId);
36152
- if (idx === -1) return [...blocks, inserted];
36153
- const next = blocks.slice();
36154
- next.splice(idx + 1, 0, inserted);
36155
- return next;
36156
- }
36157
- function changeBlockType(block, type) {
36158
- if (block.type === type) return block;
36159
- if (type === "bullet-list" || type === "numbered-list") {
36160
- if (block.children && block.children.length > 0) {
36161
- return { ...block, type };
36161
+ function ToolbarButton({ icon: IconCmp, label, active, onExec }) {
36162
+ return /* @__PURE__ */ jsxRuntime.jsx(
36163
+ Button,
36164
+ {
36165
+ type: "button",
36166
+ variant: "ghost",
36167
+ size: "sm",
36168
+ "aria-label": label,
36169
+ "aria-pressed": active,
36170
+ title: label,
36171
+ className: cn(
36172
+ "h-8 w-8 p-0 gap-0 justify-center",
36173
+ active && "bg-muted text-foreground"
36174
+ ),
36175
+ onMouseDown: (e) => {
36176
+ e.preventDefault();
36177
+ onExec();
36178
+ },
36179
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconCmp, { size: 15 })
36162
36180
  }
36163
- const seed2 = block.content ?? "";
36164
- return {
36165
- id: block.id,
36166
- type,
36167
- children: [
36168
- { id: nextBlockId("li"), type: "paragraph", content: seed2 }
36169
- ]
36170
- };
36171
- }
36172
- if (type === "divider") {
36173
- return { id: block.id, type };
36174
- }
36175
- if (type === "image") {
36176
- return {
36177
- id: block.id,
36178
- type,
36179
- content: "",
36180
- metadata: { url: "", caption: block.content ?? "" }
36181
- };
36182
- }
36183
- if (type === "code") {
36184
- return {
36185
- id: block.id,
36186
- type,
36187
- content: block.content ?? "",
36188
- metadata: { language: "plaintext" }
36181
+ );
36182
+ }
36183
+ var CHANGE_DEBOUNCE_MS, ALLOWED_ATTRS, DROP_TAGS, MD_BLOCK_PREFIXES, BLOCK_TAGS, MD_INLINE_RULES, RICH_TEXT_CSS, IDLE_TOOLBAR, RichTextEditor;
36184
+ var init_RichTextEditor = __esm({
36185
+ "components/core/molecules/RichTextEditor.tsx"() {
36186
+ "use client";
36187
+ init_cn();
36188
+ init_Box();
36189
+ init_Button();
36190
+ init_useEventBus();
36191
+ CHANGE_DEBOUNCE_MS = 400;
36192
+ ALLOWED_ATTRS = {
36193
+ p: [],
36194
+ h1: [],
36195
+ h2: [],
36196
+ h3: [],
36197
+ h4: [],
36198
+ ul: [],
36199
+ ol: [],
36200
+ li: [],
36201
+ blockquote: [],
36202
+ pre: [],
36203
+ code: [],
36204
+ b: [],
36205
+ strong: [],
36206
+ i: [],
36207
+ em: [],
36208
+ u: [],
36209
+ s: [],
36210
+ strike: [],
36211
+ br: [],
36212
+ hr: [],
36213
+ div: [],
36214
+ span: [],
36215
+ a: ["href"],
36216
+ img: ["src", "alt"]
36217
+ };
36218
+ DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "link", "meta", "form", "input", "button", "textarea", "select"]);
36219
+ MD_BLOCK_PREFIXES = {
36220
+ "#": { kind: "block", tag: "h1" },
36221
+ "##": { kind: "block", tag: "h2" },
36222
+ "###": { kind: "block", tag: "h3" },
36223
+ ">": { kind: "block", tag: "blockquote" },
36224
+ "```": { kind: "block", tag: "pre" },
36225
+ "-": { kind: "list", command: "insertUnorderedList" },
36226
+ "*": { kind: "list", command: "insertUnorderedList" },
36227
+ "1.": { kind: "list", command: "insertOrderedList" }
36228
+ };
36229
+ BLOCK_TAGS = /* @__PURE__ */ new Set(["P", "DIV", "LI", "H1", "H2", "H3", "H4", "BLOCKQUOTE", "PRE"]);
36230
+ MD_INLINE_RULES = [
36231
+ { pattern: /\*\*([^*\n]+)\*\*$/, wrap: "strong" },
36232
+ { pattern: /(^|[^*])\*([^*\n]+)\*$/, wrap: "em" },
36233
+ { pattern: /`([^`\n]+)`$/, wrap: "code" }
36234
+ ];
36235
+ RICH_TEXT_CSS = `
36236
+ .almadar-rich-text { line-height: 1.7; color: var(--color-foreground); }
36237
+ .almadar-rich-text h1 { font-size: 1.875rem; font-weight: 700; line-height: 1.25; margin: 1.25em 0 0.4em; }
36238
+ .almadar-rich-text h2 { font-size: 1.5rem; font-weight: 650; line-height: 1.3; margin: 1.1em 0 0.4em; }
36239
+ .almadar-rich-text h3 { font-size: 1.25rem; font-weight: 600; line-height: 1.35; margin: 1em 0 0.35em; }
36240
+ .almadar-rich-text h1:first-child, .almadar-rich-text h2:first-child, .almadar-rich-text h3:first-child { margin-top: 0; }
36241
+ .almadar-rich-text p { margin: 0.5em 0; }
36242
+ .almadar-rich-text ul { list-style: disc; padding-inline-start: 1.5rem; margin: 0.5em 0; }
36243
+ .almadar-rich-text ol { list-style: decimal; padding-inline-start: 1.5rem; margin: 0.5em 0; }
36244
+ .almadar-rich-text li { margin: 0.25em 0; }
36245
+ .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; }
36246
+ .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; }
36247
+ .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; }
36248
+ .almadar-rich-text pre code { background: transparent; padding: 0; }
36249
+ .almadar-rich-text a { color: var(--color-primary); text-decoration: underline; }
36250
+ .almadar-rich-text hr { border: none; border-top: 1px solid var(--color-border); margin: 1.25em 0; }
36251
+ .almadar-rich-text img { max-width: 100%; border-radius: 0.375rem; }
36252
+ `;
36253
+ IDLE_TOOLBAR = {
36254
+ bold: false,
36255
+ italic: false,
36256
+ underline: false,
36257
+ strikeThrough: false,
36258
+ block: null,
36259
+ bullets: false,
36260
+ numbers: false,
36261
+ link: false
36262
+ };
36263
+ RichTextEditor = ({
36264
+ value,
36265
+ onChange,
36266
+ changeEvent,
36267
+ readOnly = false,
36268
+ placeholder,
36269
+ showToolbar = true,
36270
+ className
36271
+ }) => {
36272
+ const { t } = hooks.useTranslate();
36273
+ const ref = React85.useRef(null);
36274
+ const [toolbar, setToolbar] = React85.useState(IDLE_TOOLBAR);
36275
+ const [empty, setEmpty] = React85.useState(() => !value || !sanitizeRichHtml(value).trim());
36276
+ const eventBus = useEventBus();
36277
+ const onChangeRef = React85.useRef(onChange);
36278
+ const changeEventRef = React85.useRef(changeEvent);
36279
+ React85.useEffect(() => {
36280
+ onChangeRef.current = onChange;
36281
+ changeEventRef.current = changeEvent;
36282
+ }, [onChange, changeEvent]);
36283
+ const emitTimerRef = React85.useRef(null);
36284
+ const emitNow = React85.useCallback(() => {
36285
+ const el = ref.current;
36286
+ if (!el) return;
36287
+ const html = sanitizeRichHtml(el.innerHTML);
36288
+ onChangeRef.current?.(html);
36289
+ const evt = changeEventRef.current;
36290
+ if (evt) eventBus.emit(`UI:${evt}`, { value: html });
36291
+ }, [eventBus]);
36292
+ const scheduleEmit = React85.useCallback(() => {
36293
+ if (emitTimerRef.current) clearTimeout(emitTimerRef.current);
36294
+ emitTimerRef.current = setTimeout(() => {
36295
+ emitTimerRef.current = null;
36296
+ emitNow();
36297
+ }, CHANGE_DEBOUNCE_MS);
36298
+ }, [emitNow]);
36299
+ const flushEmit = React85.useCallback(() => {
36300
+ if (!emitTimerRef.current) return;
36301
+ clearTimeout(emitTimerRef.current);
36302
+ emitTimerRef.current = null;
36303
+ emitNow();
36304
+ }, [emitNow]);
36305
+ React85.useEffect(() => () => {
36306
+ if (emitTimerRef.current) {
36307
+ clearTimeout(emitTimerRef.current);
36308
+ emitTimerRef.current = null;
36309
+ emitNow();
36310
+ }
36311
+ }, [emitNow]);
36312
+ React85.useEffect(() => {
36313
+ if (readOnly) return;
36314
+ const el = ref.current;
36315
+ if (!el) return;
36316
+ if (document.activeElement === el) return;
36317
+ const next = sanitizeRichHtml(value ?? "") || "<p><br></p>";
36318
+ if (el.innerHTML !== next) {
36319
+ el.innerHTML = next;
36320
+ setEmpty(htmlIsEmpty(el));
36321
+ }
36322
+ }, [readOnly, value]);
36323
+ React85.useEffect(() => {
36324
+ if (readOnly) return;
36325
+ document.execCommand("defaultParagraphSeparator", false, "p");
36326
+ }, [readOnly]);
36327
+ React85.useEffect(() => {
36328
+ if (readOnly || !showToolbar) return;
36329
+ const onSelectionChange = () => {
36330
+ const el = ref.current;
36331
+ if (!el) return;
36332
+ setToolbar(readToolbarState(el));
36333
+ };
36334
+ document.addEventListener("selectionchange", onSelectionChange);
36335
+ return () => document.removeEventListener("selectionchange", onSelectionChange);
36336
+ }, [readOnly, showToolbar]);
36337
+ const tryInlineMarkdown = React85.useCallback(() => {
36338
+ const root = ref.current;
36339
+ if (!root) return false;
36340
+ const sel = window.getSelection();
36341
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
36342
+ const node = sel.anchorNode;
36343
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
36344
+ if (node.parentElement?.closest("code, pre")) return false;
36345
+ const text = (node.textContent ?? "").slice(0, sel.anchorOffset);
36346
+ for (const rule of MD_INLINE_RULES) {
36347
+ const m = rule.pattern.exec(text);
36348
+ if (!m) continue;
36349
+ const inner = rule.wrap === "em" ? m[2] : m[1];
36350
+ if (!inner || !inner.trim()) continue;
36351
+ const lead = rule.wrap === "em" ? m[1]?.length ?? 0 : 0;
36352
+ const range = document.createRange();
36353
+ range.setStart(node, m.index + lead);
36354
+ range.setEnd(node, sel.anchorOffset);
36355
+ const el = document.createElement(rule.wrap);
36356
+ el.textContent = inner;
36357
+ range.deleteContents();
36358
+ range.insertNode(el);
36359
+ const after = document.createTextNode("\u200B");
36360
+ el.parentNode?.insertBefore(after, el.nextSibling);
36361
+ const caret = document.createRange();
36362
+ caret.setStart(after, 1);
36363
+ caret.collapse(true);
36364
+ sel.removeAllRanges();
36365
+ sel.addRange(caret);
36366
+ return true;
36367
+ }
36368
+ return false;
36369
+ }, []);
36370
+ const tryBlockMarkdown = React85.useCallback(() => {
36371
+ const root = ref.current;
36372
+ if (!root) return false;
36373
+ const sel = window.getSelection();
36374
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
36375
+ const node = sel.anchorNode;
36376
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
36377
+ const block = closestBlock(node, root);
36378
+ if (block ? block.firstChild !== node : root.firstChild !== node) return false;
36379
+ const prefix = (node.textContent ?? "").slice(0, sel.anchorOffset);
36380
+ const rule = MD_BLOCK_PREFIXES[prefix];
36381
+ if (!rule) return false;
36382
+ if (node.parentElement?.closest("pre, code")) return false;
36383
+ if (rule.kind === "list" && node.parentElement?.closest("li")) return false;
36384
+ node.deleteData(0, prefix.length);
36385
+ const caret = document.createRange();
36386
+ caret.setStart(node, 0);
36387
+ caret.collapse(true);
36388
+ sel.removeAllRanges();
36389
+ sel.addRange(caret);
36390
+ if (rule.kind === "list") {
36391
+ const container = block ?? node.parentElement;
36392
+ const list = document.createElement(rule.command === "insertOrderedList" ? "ol" : "ul");
36393
+ const li = document.createElement("li");
36394
+ if (block) {
36395
+ while (block.firstChild) li.appendChild(block.firstChild);
36396
+ list.appendChild(li);
36397
+ block.replaceWith(list);
36398
+ } else if (container) {
36399
+ li.appendChild(node);
36400
+ list.appendChild(li);
36401
+ container.appendChild(list);
36402
+ }
36403
+ if (!li.firstChild) li.appendChild(document.createElement("br"));
36404
+ const liCaret = document.createRange();
36405
+ liCaret.selectNodeContents(li);
36406
+ liCaret.collapse(true);
36407
+ sel.removeAllRanges();
36408
+ sel.addRange(liCaret);
36409
+ } else {
36410
+ document.execCommand("formatBlock", false, rule.tag);
36411
+ }
36412
+ return true;
36413
+ }, []);
36414
+ const afterEdit = React85.useCallback(() => {
36415
+ const el = ref.current;
36416
+ if (!el) return;
36417
+ tryInlineMarkdown();
36418
+ setEmpty(htmlIsEmpty(el));
36419
+ setToolbar(readToolbarState(el));
36420
+ scheduleEmit();
36421
+ }, [scheduleEmit, tryInlineMarkdown]);
36422
+ const handleKeyDown = React85.useCallback((e) => {
36423
+ if (e.key === " " && tryBlockMarkdown()) {
36424
+ e.preventDefault();
36425
+ afterEdit();
36426
+ }
36427
+ }, [afterEdit, tryBlockMarkdown]);
36428
+ const execInline = React85.useCallback((command) => {
36429
+ document.execCommand(command, false);
36430
+ afterEdit();
36431
+ }, [afterEdit]);
36432
+ const execBlock = React85.useCallback((tag) => {
36433
+ const next = toolbar.block === tag ? "p" : tag;
36434
+ document.execCommand("formatBlock", false, next);
36435
+ afterEdit();
36436
+ }, [afterEdit, toolbar.block]);
36437
+ const execList = React85.useCallback((command) => {
36438
+ document.execCommand(command, false);
36439
+ afterEdit();
36440
+ }, [afterEdit]);
36441
+ const execLink = React85.useCallback(() => {
36442
+ if (toolbar.link) {
36443
+ document.execCommand("unlink", false);
36444
+ afterEdit();
36445
+ return;
36446
+ }
36447
+ const url = window.prompt(t("richTextEditor.linkPrompt"));
36448
+ if (!url) return;
36449
+ const safe = safeUrl(url, ["http://", "https://", "mailto:"]) ?? `https://${url}`;
36450
+ document.execCommand("createLink", false, safe);
36451
+ afterEdit();
36452
+ }, [afterEdit, t, toolbar.link]);
36453
+ const execRule = React85.useCallback(() => {
36454
+ document.execCommand("insertHorizontalRule", false);
36455
+ afterEdit();
36456
+ }, [afterEdit]);
36457
+ if (readOnly) {
36458
+ return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: cn("almadar-rich-text max-w-none", className), children: [
36459
+ /* @__PURE__ */ jsxRuntime.jsx(RichTextStyles, {}),
36460
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { dangerouslySetInnerHTML: { __html: sanitizeRichHtml(value ?? "") } })
36461
+ ] });
36462
+ }
36463
+ return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: cn("flex flex-col gap-2", className), children: [
36464
+ /* @__PURE__ */ jsxRuntime.jsx(RichTextStyles, {}),
36465
+ showToolbar && /* @__PURE__ */ jsxRuntime.jsxs(
36466
+ Box,
36467
+ {
36468
+ role: "toolbar",
36469
+ "aria-label": t("richTextEditor.editorToolbar"),
36470
+ className: cn(
36471
+ "sticky top-0 z-10 flex flex-wrap items-center gap-0.5 self-start",
36472
+ "rounded-md border border-border bg-background/95 px-1 py-0.5 shadow-sm"
36473
+ ),
36474
+ children: [
36475
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Bold, label: t("richTextEditor.bold"), active: toolbar.bold, onExec: () => execInline("bold") }),
36476
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Italic, label: t("richTextEditor.italic"), active: toolbar.italic, onExec: () => execInline("italic") }),
36477
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Underline, label: t("richTextEditor.underline"), active: toolbar.underline, onExec: () => execInline("underline") }),
36478
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Strikethrough, label: t("richTextEditor.strikethrough"), active: toolbar.strikeThrough, onExec: () => execInline("strikeThrough") }),
36479
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
36480
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Heading1, label: t("richTextEditor.heading1"), active: toolbar.block === "h1", onExec: () => execBlock("h1") }),
36481
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Heading2, label: t("richTextEditor.heading2"), active: toolbar.block === "h2", onExec: () => execBlock("h2") }),
36482
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Heading3, label: t("richTextEditor.heading3"), active: toolbar.block === "h3", onExec: () => execBlock("h3") }),
36483
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
36484
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.List, label: t("richTextEditor.bulletList"), active: toolbar.bullets, onExec: () => execList("insertUnorderedList") }),
36485
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.ListOrdered, label: t("richTextEditor.numberedList"), active: toolbar.numbers, onExec: () => execList("insertOrderedList") }),
36486
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
36487
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Quote, label: t("richTextEditor.quote"), active: toolbar.block === "blockquote", onExec: () => execBlock("blockquote") }),
36488
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Code, label: t("richTextEditor.code"), active: toolbar.block === "pre", onExec: () => execBlock("pre") }),
36489
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Link, label: toolbar.link ? t("richTextEditor.removeLink") : t("richTextEditor.link"), active: toolbar.link, onExec: execLink }),
36490
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Minus, label: t("richTextEditor.divider"), onExec: execRule })
36491
+ ]
36492
+ }
36493
+ ),
36494
+ /* @__PURE__ */ jsxRuntime.jsx(
36495
+ Box,
36496
+ {
36497
+ ref,
36498
+ contentEditable: true,
36499
+ suppressContentEditableWarning: true,
36500
+ role: "textbox",
36501
+ "aria-multiline": "true",
36502
+ "aria-label": t("richTextEditor.editorSurface"),
36503
+ "data-placeholder": placeholder ?? t("richTextEditor.placeholder"),
36504
+ "data-empty": empty ? "true" : "false",
36505
+ className: cn(
36506
+ "almadar-rich-text max-w-none min-h-[8rem] outline-none",
36507
+ "data-[empty=true]:before:content-[attr(data-placeholder)]",
36508
+ "data-[empty=true]:before:text-muted-foreground/60",
36509
+ "data-[empty=true]:before:pointer-events-none",
36510
+ "data-[empty=true]:before:absolute",
36511
+ "relative cursor-text"
36512
+ ),
36513
+ onInput: afterEdit,
36514
+ onKeyDown: handleKeyDown,
36515
+ onBlur: flushEmit
36516
+ }
36517
+ )
36518
+ ] });
36189
36519
  };
36520
+ RichTextEditor.displayName = "RichTextEditor";
36190
36521
  }
36191
- const seed = block.children?.[0]?.content ?? block.content ?? "";
36192
- return { id: block.id, type, content: seed };
36522
+ });
36523
+ function renderIconInput4(icon, props) {
36524
+ return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
36193
36525
  }
36194
- function BlockMenu({ block, readOnly, onDelete, onDuplicate, onChangeType }) {
36526
+ function DocumentPanel({
36527
+ entity,
36528
+ title,
36529
+ subtitle,
36530
+ value,
36531
+ actions,
36532
+ maxInlineActions,
36533
+ editing = false,
36534
+ titleCommitEvent,
36535
+ contentChangeEvent,
36536
+ editEvent,
36537
+ doneEvent,
36538
+ editLabel,
36539
+ doneLabel,
36540
+ autosaveHint,
36541
+ placeholder,
36542
+ className
36543
+ }) {
36544
+ const eventBus = useEventBus();
36195
36545
  const { t } = hooks.useTranslate();
36196
- const [open, setOpen] = React85.useState(false);
36197
- const ref = React85.useRef(null);
36198
- React85.useEffect(() => {
36199
- if (!open) return;
36200
- function onDocClick(e) {
36201
- if (ref.current && !ref.current.contains(e.target)) {
36202
- setOpen(false);
36203
- }
36546
+ const [titleDraft, setTitleDraft] = React85.useState(null);
36547
+ const recordId = entity?.id !== void 0 && entity?.id !== null ? String(entity.id) : "";
36548
+ const actionDefs = actions ?? [];
36549
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
36550
+ const inlineActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
36551
+ const menuActions = actionDefs.filter((a) => !inlineActions.includes(a));
36552
+ const fireAction = (action) => {
36553
+ if (!action.event) return;
36554
+ eventBus.emit(`UI:${action.event}`, { id: recordId, row: entity });
36555
+ };
36556
+ const commitTitle = () => {
36557
+ if (!titleCommitEvent) return;
36558
+ const next = (titleDraft ?? "").trim();
36559
+ setTitleDraft(null);
36560
+ if (!next || next === title) return;
36561
+ eventBus.emit(`UI:${titleCommitEvent}`, { title: next, id: recordId });
36562
+ };
36563
+ const untitled = t("documentPanel.untitled") || "Untitled";
36564
+ const titleNode = titleCommitEvent && titleDraft !== null ? /* @__PURE__ */ jsxRuntime.jsx(
36565
+ Input,
36566
+ {
36567
+ value: titleDraft,
36568
+ autoFocus: true,
36569
+ "aria-label": t("common.title"),
36570
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
36571
+ onFocus: (e) => e.currentTarget.select(),
36572
+ onChange: (e) => setTitleDraft(e.target.value),
36573
+ onBlur: commitTitle,
36574
+ onKeyDown: (e) => {
36575
+ if (e.key === "Enter") {
36576
+ e.preventDefault();
36577
+ commitTitle();
36578
+ } else if (e.key === "Escape") {
36579
+ e.preventDefault();
36580
+ setTitleDraft(null);
36581
+ }
36582
+ },
36583
+ "data-testid": "document-title-input"
36204
36584
  }
36205
- document.addEventListener("mousedown", onDocClick);
36206
- return () => document.removeEventListener("mousedown", onDocClick);
36207
- }, [open]);
36208
- if (readOnly) return null;
36209
- return /* @__PURE__ */ jsxRuntime.jsxs(Box, { ref, className: "relative", children: [
36210
- /* @__PURE__ */ jsxRuntime.jsx(
36211
- Button,
36212
- {
36213
- type: "button",
36214
- variant: "ghost",
36215
- "aria-label": t("richBlockEditor.blockActions"),
36216
- className: cn(
36217
- "inline-flex items-center justify-center",
36218
- "h-6 w-6 rounded-sm p-0 gap-0",
36219
- "text-muted-foreground hover:bg-muted hover:text-foreground",
36220
- "opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
36221
- "transition-opacity"
36222
- ),
36223
- onClick: () => setOpen((v) => !v),
36224
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", className: "w-3.5 h-3.5" })
36225
- }
36226
- ),
36227
- open && /* @__PURE__ */ jsxRuntime.jsxs(
36585
+ ) : titleCommitEvent ? /* @__PURE__ */ jsxRuntime.jsx(
36586
+ Box,
36587
+ {
36588
+ role: "button",
36589
+ tabIndex: 0,
36590
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
36591
+ onClick: () => setTitleDraft(title ?? ""),
36592
+ onKeyDown: (e) => {
36593
+ if (e.key === "Enter" || e.key === " ") {
36594
+ e.preventDefault();
36595
+ setTitleDraft(title ?? "");
36596
+ }
36597
+ },
36598
+ "data-testid": "document-title-editable",
36599
+ children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", className: cn(!title && "text-muted-foreground"), children: title || untitled })
36600
+ }
36601
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", children: title || untitled });
36602
+ const emitWithId = (event) => () => {
36603
+ if (event) eventBus.emit(`UI:${event}`, { id: recordId });
36604
+ };
36605
+ 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: [
36606
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", className: "justify-between items-start", children: [
36607
+ /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", className: "flex-1 min-w-0", children: [
36608
+ titleNode,
36609
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: subtitle }) : null
36610
+ ] }),
36611
+ /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", className: "flex-shrink-0 pt-1 items-center", children: editing ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
36612
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", className: "hidden sm:block", children: autosaveHint ?? (t("documentPanel.autosaveHint") || "") }),
36613
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", size: "sm", onClick: emitWithId(doneEvent), "data-testid": "document-done", children: doneLabel ?? (t("documentPanel.done") || "Done") })
36614
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
36615
+ editEvent && /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "ghost", size: "sm", onClick: emitWithId(editEvent), "data-testid": "document-edit", children: [
36616
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "edit", size: "xs", className: "mr-1" }),
36617
+ editLabel ?? (t("documentPanel.edit") || "Edit")
36618
+ ] }),
36619
+ inlineActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
36620
+ Button,
36621
+ {
36622
+ variant: "primary",
36623
+ size: "sm",
36624
+ onClick: () => fireAction(action),
36625
+ "data-testid": `action-${action.event}`,
36626
+ children: [
36627
+ action.icon && renderIconInput4(action.icon, { size: "xs", className: "mr-1" }),
36628
+ action.label
36629
+ ]
36630
+ },
36631
+ idx
36632
+ )),
36633
+ menuActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
36634
+ Menu,
36635
+ {
36636
+ position: "bottom-end",
36637
+ 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" }) }),
36638
+ items: menuActions.map((action) => ({
36639
+ label: action.label,
36640
+ icon: action.icon,
36641
+ variant: action.variant === "danger" ? "danger" : "default",
36642
+ onClick: () => fireAction(action)
36643
+ }))
36644
+ }
36645
+ )
36646
+ ] }) })
36647
+ ] }),
36648
+ editing ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsxRuntime.jsx(
36228
36649
  Box,
36229
36650
  {
36230
- role: "menu",
36651
+ onClick: emitWithId(editEvent),
36231
36652
  className: cn(
36232
- "absolute right-0 z-10 mt-1 w-44",
36233
- "rounded-container border border-border bg-popover shadow-elevation-popover",
36234
- "py-1 text-sm"
36653
+ "rounded-md px-1 -mx-1 min-h-[16rem]",
36654
+ editEvent && "cursor-text transition-colors hover:bg-muted/30"
36235
36655
  ),
36236
- children: [
36237
- /* @__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]) }),
36238
- /* @__PURE__ */ jsxRuntime.jsxs(
36239
- Button,
36240
- {
36241
- type: "button",
36242
- variant: "ghost",
36243
- role: "menuitem",
36244
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
36245
- onClick: () => {
36246
- onDuplicate();
36247
- setOpen(false);
36248
- },
36249
- children: [
36250
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "plus", className: "w-3.5 h-3.5" }),
36251
- " ",
36252
- t("richBlockEditor.duplicate")
36253
- ]
36254
- }
36255
- ),
36256
- /* @__PURE__ */ jsxRuntime.jsxs(
36257
- Button,
36258
- {
36259
- type: "button",
36260
- variant: "ghost",
36261
- role: "menuitem",
36262
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-destructive hover:bg-muted justify-start rounded-none",
36263
- onClick: () => {
36264
- onDelete();
36265
- setOpen(false);
36266
- },
36267
- children: [
36268
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "trash", className: "w-3.5 h-3.5" }),
36269
- " ",
36270
- t("common.delete")
36271
- ]
36272
- }
36273
- ),
36274
- CHANGEABLE_TYPES.includes(block.type) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
36275
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "my-1 border-t border-border" }),
36276
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t("richBlockEditor.turnInto") }),
36277
- CHANGEABLE_TYPES.filter((bt) => bt !== block.type).map((bt) => /* @__PURE__ */ jsxRuntime.jsx(
36278
- Button,
36279
- {
36280
- type: "button",
36281
- variant: "ghost",
36282
- role: "menuitem",
36283
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
36284
- onClick: () => {
36285
- onChangeType(bt);
36286
- setOpen(false);
36287
- },
36288
- children: t(BLOCK_TYPE_LABEL_KEY[bt])
36289
- },
36290
- bt
36291
- ))
36292
- ] })
36293
- ]
36656
+ "data-testid": "document-body",
36657
+ children: value && value !== "" ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, readOnly: true }) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "muted", children: placeholder ?? (t("documentPanel.placeholder") || "") })
36294
36658
  }
36295
36659
  )
36296
- ] });
36660
+ ] }) });
36297
36661
  }
36298
- function Editable({
36299
- tag,
36300
- value,
36301
- readOnly,
36302
- placeholder,
36303
- className,
36304
- ariaLabel,
36305
- onValueChange
36306
- }) {
36307
- const ref = React85.useRef(null);
36308
- React85.useEffect(() => {
36309
- const el = ref.current;
36310
- if (!el) return;
36311
- const isFocused = document.activeElement === el;
36312
- if (!isFocused && el.textContent !== value) {
36313
- el.textContent = value;
36314
- }
36315
- }, [value]);
36316
- const handleInput = React85.useCallback(
36317
- (e) => {
36318
- onValueChange(e.currentTarget.textContent ?? "");
36319
- },
36320
- [onValueChange]
36321
- );
36322
- return /* @__PURE__ */ jsxRuntime.jsx(
36323
- Box,
36324
- {
36325
- as: tag,
36326
- ref,
36327
- contentEditable: !readOnly,
36328
- suppressContentEditableWarning: true,
36329
- role: readOnly ? void 0 : "textbox",
36330
- "aria-label": ariaLabel,
36331
- "aria-multiline": "true",
36332
- "data-placeholder": placeholder,
36333
- className: cn(
36334
- "outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-sm",
36335
- "empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground/60",
36336
- className
36337
- ),
36338
- onInput: handleInput
36339
- }
36340
- );
36662
+ var init_DocumentPanel = __esm({
36663
+ "components/core/molecules/DocumentPanel.tsx"() {
36664
+ "use client";
36665
+ init_cn();
36666
+ init_useEventBus();
36667
+ init_Box();
36668
+ init_Stack();
36669
+ init_Typography();
36670
+ init_Button();
36671
+ init_Card();
36672
+ init_Icon();
36673
+ init_Input();
36674
+ init_Menu();
36675
+ init_RichTextEditor();
36676
+ }
36677
+ });
36678
+ function renderIconInput5(icon, props) {
36679
+ return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
36341
36680
  }
36342
- function BlockRow({
36343
- block,
36344
- readOnly,
36345
- showAffordances,
36346
- placeholder,
36347
- onUpdate,
36348
- onDelete,
36349
- onDuplicate,
36350
- onChangeType
36681
+ function fieldName(field) {
36682
+ return field.name ?? field.key ?? "";
36683
+ }
36684
+ function relationLabel(value) {
36685
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Date) return null;
36686
+ for (const key of ["name", "title", "label"]) {
36687
+ const candidate = value[key];
36688
+ if (typeof candidate === "string" && candidate !== "") return candidate;
36689
+ }
36690
+ const id = value.id;
36691
+ return id !== void 0 && id !== null ? String(id) : null;
36692
+ }
36693
+ function fieldKind(field, value) {
36694
+ if (field.kind) return field.kind;
36695
+ if (field.options && field.options.length > 0) return "select";
36696
+ if (typeof value === "boolean" || field.format === "boolean") return "boolean";
36697
+ if (Array.isArray(value) || relationLabel(value ?? null) !== null) return "readonly";
36698
+ return "text";
36699
+ }
36700
+ function DocumentDetails({
36701
+ entity,
36702
+ fields,
36703
+ metaCommitEvent,
36704
+ title,
36705
+ className
36351
36706
  }) {
36707
+ const eventBus = useEventBus();
36352
36708
  const { t } = hooks.useTranslate();
36353
- const setContent = React85.useCallback(
36354
- (next) => onUpdate((b) => ({ ...b, content: next })),
36355
- [onUpdate]
36356
- );
36357
- const setMetadata = React85.useCallback(
36358
- (key, value) => onUpdate((b) => ({
36359
- ...b,
36360
- metadata: { ...b.metadata ?? {}, [key]: value }
36361
- })),
36362
- [onUpdate]
36363
- );
36364
- const setChildContent = React85.useCallback(
36365
- (childId, next) => onUpdate((b) => ({
36366
- ...b,
36367
- children: (b.children ?? []).map(
36368
- (c) => c.id === childId ? { ...c, content: next } : c
36369
- )
36370
- })),
36371
- [onUpdate]
36372
- );
36373
- const addListItem = React85.useCallback(
36374
- () => onUpdate((b) => ({
36375
- ...b,
36376
- children: [
36377
- ...b.children ?? [],
36378
- { id: nextBlockId("li"), type: "paragraph", content: "" }
36379
- ]
36380
- })),
36381
- [onUpdate]
36382
- );
36383
- const removeListItem = React85.useCallback(
36384
- (childId) => onUpdate((b) => {
36385
- const remaining = (b.children ?? []).filter((c) => c.id !== childId);
36386
- return {
36387
- ...b,
36388
- children: remaining.length === 0 ? [{ id: nextBlockId("li"), type: "paragraph", content: "" }] : remaining
36389
- };
36390
- }),
36391
- [onUpdate]
36392
- );
36393
- const renderBody = () => {
36394
- switch (block.type) {
36395
- case "heading-1":
36396
- return /* @__PURE__ */ jsxRuntime.jsx(
36397
- Editable,
36398
- {
36399
- tag: "h1",
36400
- value: block.content ?? "",
36401
- readOnly,
36402
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading1"),
36403
- ariaLabel: t("richBlockEditor.aria.heading1Block"),
36404
- className: "text-3xl font-bold leading-tight",
36405
- onValueChange: setContent
36406
- }
36407
- );
36408
- case "heading-2":
36409
- return /* @__PURE__ */ jsxRuntime.jsx(
36410
- Editable,
36411
- {
36412
- tag: "h2",
36413
- value: block.content ?? "",
36414
- readOnly,
36415
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading2"),
36416
- ariaLabel: t("richBlockEditor.aria.heading2Block"),
36417
- className: "text-2xl font-semibold leading-tight",
36418
- onValueChange: setContent
36419
- }
36420
- );
36421
- case "heading-3":
36422
- return /* @__PURE__ */ jsxRuntime.jsx(
36423
- Editable,
36424
- {
36425
- tag: "h3",
36426
- value: block.content ?? "",
36427
- readOnly,
36428
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading3"),
36429
- ariaLabel: t("richBlockEditor.aria.heading3Block"),
36430
- className: "text-xl font-semibold leading-tight",
36431
- onValueChange: setContent
36432
- }
36433
- );
36434
- case "quote":
36435
- return /* @__PURE__ */ jsxRuntime.jsx(
36436
- Editable,
36437
- {
36438
- tag: "blockquote",
36439
- value: block.content ?? "",
36440
- readOnly,
36441
- placeholder: placeholder ?? t("richBlockEditor.placeholder.quote"),
36442
- ariaLabel: t("richBlockEditor.aria.quoteBlock"),
36443
- className: "border-l-4 border-primary/60 pl-4 italic text-muted-foreground",
36444
- onValueChange: setContent
36445
- }
36446
- );
36447
- case "code":
36448
- return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "rounded-md border border-border bg-muted/40", children: [
36449
- /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex items-center justify-between border-b border-border px-3 py-1 text-xs text-muted-foreground", children: [
36450
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", variant: "caption", className: "uppercase tracking-wide", children: t("richBlockEditor.blockType.code") }),
36451
- !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
36452
- Input,
36453
- {
36454
- inputType: "text",
36455
- value: String(block.metadata?.language ?? "plaintext"),
36456
- "aria-label": t("richBlockEditor.aria.codeLanguage"),
36457
- className: cn(
36458
- "h-6 w-32 rounded-sm border border-border bg-background",
36459
- "px-2 text-xs outline-none focus:ring-1 focus:ring-ring"
36460
- ),
36461
- onChange: (e) => setMetadata("language", e.target.value)
36462
- }
36463
- ),
36464
- readOnly && /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", variant: "caption", className: "text-xs", children: String(block.metadata?.language ?? "plaintext") })
36465
- ] }),
36466
- /* @__PURE__ */ jsxRuntime.jsx(
36467
- Editable,
36468
- {
36469
- tag: "pre",
36470
- value: block.content ?? "",
36471
- readOnly,
36472
- placeholder: placeholder ?? t("richBlockEditor.placeholder.code"),
36473
- ariaLabel: t("richBlockEditor.aria.codeBlock"),
36474
- className: "block whitespace-pre-wrap p-3 font-mono text-sm leading-relaxed",
36475
- onValueChange: setContent
36476
- }
36477
- )
36478
- ] });
36479
- case "divider":
36480
- return /* @__PURE__ */ jsxRuntime.jsx(Divider, { className: "my-2" });
36481
- case "image": {
36482
- const url = String(block.metadata?.url ?? "");
36483
- const caption = String(block.metadata?.caption ?? "");
36484
- const imgProps = {
36485
- src: url,
36486
- alt: caption || t("richBlockEditor.embeddedImage"),
36487
- className: "max-h-96 w-full rounded-md border border-border object-contain"
36488
- };
36489
- return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "space-y-2", children: [
36490
- url ? /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "img", ...imgProps }) : /* @__PURE__ */ jsxRuntime.jsxs(
36491
- Box,
36492
- {
36493
- className: cn(
36494
- "flex h-32 items-center justify-center",
36495
- "rounded-md border border-dashed border-border",
36496
- "text-sm text-muted-foreground"
36497
- ),
36498
- children: [
36499
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "image", className: "mr-2 w-4 h-4" }),
36500
- " ",
36501
- t("richBlockEditor.noImageUrl")
36502
- ]
36503
- }
36504
- ),
36505
- !readOnly && /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex flex-col gap-2 sm:flex-row", children: [
36506
- /* @__PURE__ */ jsxRuntime.jsx(
36507
- Input,
36508
- {
36509
- inputType: "url",
36510
- value: url,
36511
- placeholder: "https://example.com/image.png",
36512
- "aria-label": t("richBlockEditor.aria.imageUrl"),
36513
- className: cn(
36514
- "h-8 flex-1 rounded-sm border border-border bg-background",
36515
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
36516
- ),
36517
- onChange: (e) => setMetadata("url", e.target.value)
36518
- }
36519
- ),
36520
- /* @__PURE__ */ jsxRuntime.jsx(
36521
- Input,
36522
- {
36523
- inputType: "text",
36524
- value: caption,
36525
- placeholder: t("richBlockEditor.placeholder.caption"),
36526
- "aria-label": t("richBlockEditor.aria.imageCaption"),
36527
- className: cn(
36528
- "h-8 flex-1 rounded-sm border border-border bg-background",
36529
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
36530
- ),
36531
- onChange: (e) => setMetadata("caption", e.target.value)
36532
- }
36533
- )
36534
- ] }),
36535
- readOnly && caption && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-center text-muted-foreground", children: caption })
36536
- ] });
36709
+ const [fieldDraft, setFieldDraft] = React85.useState(null);
36710
+ const recordId = entity?.id !== void 0 && entity?.id !== null ? String(entity.id) : "";
36711
+ const fieldDefs = (fields ?? []).filter((f3) => fieldName(f3) !== "");
36712
+ if (fieldDefs.length === 0) return null;
36713
+ const commitField = (name, next, current) => {
36714
+ setFieldDraft(null);
36715
+ if (!metaCommitEvent || next === current) return;
36716
+ eventBus.emit(`UI:${metaCommitEvent}`, { id: recordId, patch: { id: recordId, [name]: next } });
36717
+ };
36718
+ const renderValue = (field) => {
36719
+ const name = fieldName(field);
36720
+ const raw = getNestedValue(entity ?? {}, name);
36721
+ const kind = fieldKind(field, raw);
36722
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
36723
+ if (kind === "boolean") {
36724
+ return /* @__PURE__ */ jsxRuntime.jsx(
36725
+ Switch,
36726
+ {
36727
+ checked: Boolean(raw),
36728
+ disabled: !metaCommitEvent,
36729
+ "aria-label": label,
36730
+ onChange: (checked) => commitField(name, checked, Boolean(raw))
36731
+ }
36732
+ );
36733
+ }
36734
+ if (kind === "select") {
36735
+ return /* @__PURE__ */ jsxRuntime.jsx(
36736
+ Select,
36737
+ {
36738
+ value: raw !== void 0 && raw !== null ? String(raw) : "",
36739
+ disabled: !metaCommitEvent,
36740
+ "aria-label": label,
36741
+ className: "h-8 w-full",
36742
+ options: (field.options ?? []).map((opt) => ({ value: opt, label: humanizeEnumValue(opt) })),
36743
+ onChange: (e) => commitField(name, e.target.value, raw)
36744
+ }
36745
+ );
36746
+ }
36747
+ if (kind === "readonly" || !metaCommitEvent) {
36748
+ if (Array.isArray(raw)) {
36749
+ if (raw.length === 0) {
36750
+ return /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
36751
+ }
36752
+ 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)) });
36537
36753
  }
36538
- case "bullet-list":
36539
- case "numbered-list": {
36540
- const items = block.children ?? [];
36541
- return /* @__PURE__ */ jsxRuntime.jsxs(
36542
- Box,
36543
- {
36544
- as: block.type === "bullet-list" ? "ul" : "ol",
36545
- className: cn(
36546
- "space-y-1 pl-6",
36547
- block.type === "bullet-list" ? "list-disc" : "list-decimal"
36548
- ),
36549
- children: [
36550
- items.map((child) => /* @__PURE__ */ jsxRuntime.jsxs(Box, { as: "li", className: "group/item flex items-start gap-2", children: [
36551
- /* @__PURE__ */ jsxRuntime.jsx(
36552
- Editable,
36553
- {
36554
- tag: "span",
36555
- value: child.content ?? "",
36556
- readOnly,
36557
- placeholder: t("richBlockEditor.placeholder.listItem"),
36558
- ariaLabel: t("richBlockEditor.aria.listItem"),
36559
- className: "inline-block min-w-[1ch] flex-1",
36560
- onValueChange: (next) => setChildContent(child.id, next)
36561
- }
36562
- ),
36563
- !readOnly && showAffordances && /* @__PURE__ */ jsxRuntime.jsx(
36564
- Button,
36565
- {
36566
- type: "button",
36567
- variant: "ghost",
36568
- "aria-label": t("richBlockEditor.aria.removeListItem"),
36569
- className: cn(
36570
- "h-5 w-5 shrink-0 rounded-sm text-muted-foreground p-0 gap-0",
36571
- "opacity-0 group-hover/item:opacity-100 hover:bg-muted hover:text-foreground"
36572
- ),
36573
- onClick: () => removeListItem(child.id),
36574
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "trash", className: "w-3 h-3" })
36575
- }
36576
- )
36577
- ] }, child.id)),
36578
- !readOnly && showAffordances && /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "li", className: "list-none pl-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
36579
- Button,
36580
- {
36581
- type: "button",
36582
- variant: "ghost",
36583
- className: cn(
36584
- "inline-flex items-center gap-1 text-xs text-muted-foreground",
36585
- "hover:text-foreground p-0 h-auto"
36586
- ),
36587
- onClick: addListItem,
36588
- children: [
36589
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "plus", className: "w-3 h-3" }),
36590
- " ",
36591
- t("richBlockEditor.addItem")
36592
- ]
36593
- }
36594
- ) })
36595
- ]
36754
+ const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
36755
+ return /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", className: "break-words", children: shown2 });
36756
+ }
36757
+ if (fieldDraft?.name === name) {
36758
+ return /* @__PURE__ */ jsxRuntime.jsx(
36759
+ Input,
36760
+ {
36761
+ value: fieldDraft.value,
36762
+ autoFocus: true,
36763
+ "aria-label": label,
36764
+ className: "h-8 w-full",
36765
+ onChange: (e) => setFieldDraft({ name, value: e.target.value }),
36766
+ onBlur: () => commitField(name, fieldDraft.value.trim(), raw),
36767
+ onKeyDown: (e) => {
36768
+ if (e.key === "Enter") {
36769
+ e.preventDefault();
36770
+ commitField(name, fieldDraft.value.trim(), raw);
36771
+ } else if (e.key === "Escape") {
36772
+ e.preventDefault();
36773
+ setFieldDraft(null);
36774
+ }
36775
+ },
36776
+ "data-testid": `document-property-input-${name}`
36777
+ }
36778
+ );
36779
+ }
36780
+ const shown = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
36781
+ return /* @__PURE__ */ jsxRuntime.jsx(
36782
+ Box,
36783
+ {
36784
+ role: "button",
36785
+ tabIndex: 0,
36786
+ className: cn(
36787
+ "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
36788
+ (raw === void 0 || raw === null || raw === "") && "text-muted-foreground"
36789
+ ),
36790
+ onClick: () => setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" }),
36791
+ onKeyDown: (e) => {
36792
+ if (e.key === "Enter" || e.key === " ") {
36793
+ e.preventDefault();
36794
+ setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" });
36596
36795
  }
36597
- );
36796
+ },
36797
+ "data-testid": `document-property-${name}`,
36798
+ children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", className: "break-words", children: shown })
36598
36799
  }
36599
- case "paragraph":
36600
- default:
36601
- return /* @__PURE__ */ jsxRuntime.jsx(
36602
- Editable,
36603
- {
36604
- tag: "p",
36605
- value: block.content ?? "",
36606
- readOnly,
36607
- placeholder: placeholder ?? t("richBlockEditor.placeholder.paragraph"),
36608
- ariaLabel: t("richBlockEditor.aria.paragraphBlock"),
36609
- className: "leading-7",
36610
- onValueChange: setContent
36611
- }
36612
- );
36613
- }
36800
+ );
36614
36801
  };
36615
- return /* @__PURE__ */ jsxRuntime.jsxs(
36616
- Box,
36617
- {
36618
- className: cn(
36619
- "group relative flex items-start gap-2 rounded-sm",
36620
- "px-2 py-1 hover:bg-muted/30"
36621
- ),
36622
- "data-block-id": block.id,
36623
- "data-block-type": block.type,
36624
- children: [
36625
- !readOnly && showAffordances && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex w-8 shrink-0 items-center pt-1", children: /* @__PURE__ */ jsxRuntime.jsx(
36626
- BlockMenu,
36627
- {
36628
- block,
36629
- readOnly,
36630
- onDelete,
36631
- onDuplicate,
36632
- onChangeType
36633
- }
36634
- ) }),
36635
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "min-w-0 flex-1", children: renderBody() })
36636
- ]
36637
- }
36638
- );
36802
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { variant: "bordered", className: cn("w-full", className), children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", className: "p-4", children: [
36803
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "secondary", weight: "medium", className: "uppercase tracking-wide", children: title ?? (t("documentDetails.title") || "Details") }),
36804
+ /* @__PURE__ */ jsxRuntime.jsx(VStack, { gap: "sm", children: fieldDefs.map((field) => {
36805
+ const name = fieldName(field);
36806
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
36807
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", children: [
36808
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "items-center", children: [
36809
+ field.icon && renderIconInput5(field.icon, { size: "xs", className: "text-muted-foreground" }),
36810
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", children: label })
36811
+ ] }),
36812
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "min-w-0", children: renderValue(field) })
36813
+ ] }, name);
36814
+ }) })
36815
+ ] }) });
36639
36816
  }
36640
- var TOOLBAR_ENTRIES, BLOCK_TYPE_LABEL_KEY, CHANGEABLE_TYPES, _idSeq, BLOCK_TYPES, RichBlockEditor;
36641
- var init_RichBlockEditor = __esm({
36642
- "components/core/molecules/RichBlockEditor.tsx"() {
36817
+ var init_DocumentDetails = __esm({
36818
+ "components/core/molecules/DocumentDetails.tsx"() {
36643
36819
  "use client";
36644
36820
  init_cn();
36645
- init_Card();
36646
- init_Typography();
36647
- init_Button();
36821
+ init_format();
36822
+ init_getNestedValue();
36823
+ init_useEventBus();
36648
36824
  init_Box();
36649
- init_Divider();
36650
- init_Input();
36825
+ init_Stack();
36826
+ init_Typography();
36827
+ init_Badge();
36828
+ init_Card();
36651
36829
  init_Icon();
36652
- init_useEventBus();
36653
- TOOLBAR_ENTRIES = [
36654
- { type: "paragraph", labelKey: "richBlockEditor.toolbar.text", icon: LucideIcons2.Type },
36655
- { type: "heading-1", labelKey: "richBlockEditor.toolbar.h1", icon: LucideIcons2.Heading1 },
36656
- { type: "heading-2", labelKey: "richBlockEditor.toolbar.h2", icon: LucideIcons2.Heading2 },
36657
- { type: "heading-3", labelKey: "richBlockEditor.toolbar.h3", icon: LucideIcons2.Heading3 },
36658
- { type: "bullet-list", labelKey: "richBlockEditor.toolbar.bulletList", icon: LucideIcons2.List },
36659
- { type: "numbered-list", labelKey: "richBlockEditor.toolbar.numbered", icon: LucideIcons2.ListOrdered },
36660
- { type: "quote", labelKey: "richBlockEditor.toolbar.quote", icon: LucideIcons2.Quote },
36661
- { type: "code", labelKey: "richBlockEditor.toolbar.code", icon: LucideIcons2.Code },
36662
- { type: "divider", labelKey: "richBlockEditor.toolbar.divider", icon: LucideIcons2.Minus },
36663
- { type: "image", labelKey: "richBlockEditor.toolbar.image", icon: LucideIcons2.Image }
36664
- ];
36665
- BLOCK_TYPE_LABEL_KEY = {
36666
- paragraph: "richBlockEditor.blockType.paragraph",
36667
- "heading-1": "richBlockEditor.blockType.heading1",
36668
- "heading-2": "richBlockEditor.blockType.heading2",
36669
- "heading-3": "richBlockEditor.blockType.heading3",
36670
- "bullet-list": "richBlockEditor.blockType.bulletList",
36671
- "numbered-list": "richBlockEditor.blockType.numberedList",
36672
- quote: "richBlockEditor.blockType.quote",
36673
- code: "richBlockEditor.blockType.code",
36674
- divider: "richBlockEditor.blockType.divider",
36675
- image: "richBlockEditor.blockType.image"
36676
- };
36677
- CHANGEABLE_TYPES = [
36678
- "paragraph",
36679
- "heading-1",
36680
- "heading-2",
36681
- "heading-3",
36682
- "bullet-list",
36683
- "numbered-list",
36684
- "quote",
36685
- "code"
36686
- ];
36687
- _idSeq = 0;
36688
- BLOCK_TYPES = /* @__PURE__ */ new Set([
36689
- "paragraph",
36690
- "heading-1",
36691
- "heading-2",
36692
- "heading-3",
36693
- "bullet-list",
36694
- "numbered-list",
36695
- "quote",
36696
- "code",
36697
- "divider",
36698
- "image"
36699
- ]);
36700
- RichBlockEditor = ({
36701
- initialBlocks,
36702
- onChange,
36703
- changeEvent,
36704
- readOnly = false,
36705
- placeholder,
36706
- enableBlocks = true,
36707
- showToolbar = true,
36708
- className
36709
- }) => {
36710
- const { t } = hooks.useTranslate();
36711
- const [blocks, setBlocks] = React85.useState(
36712
- () => normalizeBlocks(initialBlocks)
36713
- );
36714
- const onChangeRef = React85.useRef(onChange);
36715
- React85.useEffect(() => {
36716
- onChangeRef.current = onChange;
36717
- }, [onChange]);
36718
- const eventBus = useEventBus();
36719
- const changeEventRef = React85.useRef(changeEvent);
36720
- React85.useEffect(() => {
36721
- changeEventRef.current = changeEvent;
36722
- }, [changeEvent]);
36723
- const commit = React85.useCallback((next) => {
36724
- setBlocks(next);
36725
- onChangeRef.current?.(next);
36726
- const evt = changeEventRef.current;
36727
- if (evt) eventBus.emit(`UI:${evt}`, { blocks: next });
36728
- }, [eventBus]);
36729
- const handleAppend = React85.useCallback(
36730
- (type) => {
36731
- if (readOnly) return;
36732
- commit([...blocks, createBlock(type)]);
36733
- },
36734
- [blocks, commit, readOnly]
36735
- );
36736
- const handleUpdate = React85.useCallback(
36737
- (id, updater) => {
36738
- commit(replaceBlock(blocks, id, updater));
36739
- },
36740
- [blocks, commit]
36741
- );
36742
- const handleDelete = React85.useCallback(
36743
- (id) => {
36744
- const next = removeBlock(blocks, id);
36745
- commit(next.length > 0 ? next : [createBlock("paragraph")]);
36746
- },
36747
- [blocks, commit]
36748
- );
36749
- const handleDuplicate = React85.useCallback(
36750
- (id) => {
36751
- const target = blocks.find((b) => b.id === id);
36752
- if (!target) return;
36753
- commit(insertAfter(blocks, id, duplicateBlock(target)));
36754
- },
36755
- [blocks, commit]
36756
- );
36757
- const handleChangeType = React85.useCallback(
36758
- (id, type) => {
36759
- commit(
36760
- replaceBlock(blocks, id, (b) => changeBlockType(b, type))
36761
- );
36762
- },
36763
- [blocks, commit]
36764
- );
36765
- return /* @__PURE__ */ jsxRuntime.jsxs(
36766
- Card,
36767
- {
36768
- variant: "bordered",
36769
- padding: "none",
36770
- className: cn("flex flex-col text-card-foreground", className),
36771
- children: [
36772
- enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
36773
- Box,
36774
- {
36775
- role: "toolbar",
36776
- "aria-label": t("richBlockEditor.editorToolbar"),
36777
- className: cn(
36778
- "flex flex-wrap items-center gap-1",
36779
- "border-b border-border bg-muted/30 px-2 py-2"
36780
- ),
36781
- children: TOOLBAR_ENTRIES.map((entry) => {
36782
- const Icon2 = entry.icon;
36783
- const entryLabel = t(entry.labelKey);
36784
- return /* @__PURE__ */ jsxRuntime.jsxs(
36785
- Button,
36786
- {
36787
- type: "button",
36788
- variant: "ghost",
36789
- size: "sm",
36790
- "aria-label": t("richBlockEditor.insertEntry", { label: entryLabel }),
36791
- title: entryLabel,
36792
- onClick: () => handleAppend(entry.type),
36793
- children: [
36794
- /* @__PURE__ */ jsxRuntime.jsx(Icon2, { size: 14 }),
36795
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", variant: "caption", className: "ml-1 hidden text-xs sm:inline", children: entryLabel })
36796
- ]
36797
- },
36798
- entry.type
36799
- );
36800
- })
36801
- }
36802
- ),
36803
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex flex-col gap-1 px-3 py-3", children: blocks.map((block) => /* @__PURE__ */ jsxRuntime.jsx(
36804
- BlockRow,
36805
- {
36806
- block,
36807
- readOnly,
36808
- showAffordances: enableBlocks,
36809
- placeholder,
36810
- onUpdate: (updater) => handleUpdate(block.id, updater),
36811
- onDelete: () => handleDelete(block.id),
36812
- onDuplicate: () => handleDuplicate(block.id),
36813
- onChangeType: (type) => handleChangeType(block.id, type)
36814
- },
36815
- block.id
36816
- )) })
36817
- ]
36818
- }
36819
- );
36820
- };
36821
- RichBlockEditor.displayName = "RichBlockEditor";
36830
+ init_Input();
36831
+ init_Select();
36832
+ init_Switch();
36822
36833
  }
36823
36834
  });
36824
36835
  function collectInitiallyCollapsed(nodes, acc) {
@@ -41767,8 +41778,8 @@ var init_DataTable = __esm({
41767
41778
  DataTable.displayName = "DataTable";
41768
41779
  }
41769
41780
  });
41770
- function getBadgeVariant(fieldName, value) {
41771
- const name = fieldName.toLowerCase();
41781
+ function getBadgeVariant(fieldName2, value) {
41782
+ const name = fieldName2.toLowerCase();
41772
41783
  const val = String(value).toLowerCase();
41773
41784
  if (name.includes("status")) {
41774
41785
  if (val.includes("complete") || val.includes("done") || val.includes("active"))
@@ -41784,12 +41795,12 @@ function getBadgeVariant(fieldName, value) {
41784
41795
  }
41785
41796
  return "default";
41786
41797
  }
41787
- function formatFieldValue2(value, fieldName) {
41798
+ function formatFieldValue2(value, fieldName2) {
41788
41799
  if (typeof value === "number") {
41789
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
41800
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
41790
41801
  return `${value}%`;
41791
41802
  }
41792
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
41803
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
41793
41804
  return `$${value.toLocaleString()}`;
41794
41805
  }
41795
41806
  return value.toLocaleString();
@@ -41799,7 +41810,7 @@ function formatFieldValue2(value, fieldName) {
41799
41810
  }
41800
41811
  return String(value);
41801
41812
  }
41802
- function renderRichFieldValue(value, fieldName, fieldType, meta) {
41813
+ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
41803
41814
  if (value === void 0 || value === null) return "\u2014";
41804
41815
  const str = String(value);
41805
41816
  switch (fieldType) {
@@ -41810,7 +41821,7 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
41810
41821
  "img",
41811
41822
  {
41812
41823
  src: str,
41813
- alt: formatFieldLabel(fieldName),
41824
+ alt: formatFieldLabel(fieldName2),
41814
41825
  className: "max-w-full max-h-64 rounded-md object-contain",
41815
41826
  loading: "lazy"
41816
41827
  }
@@ -41945,9 +41956,9 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
41945
41956
  return /* @__PURE__ */ jsxRuntime.jsx("a", { href: `tel:${str}`, className: "text-primary hover:underline", children: str });
41946
41957
  default:
41947
41958
  if (meta?.values && meta.values.length > 0 && meta.values.includes(str)) {
41948
- return /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: getBadgeVariant(fieldName, str), children: humanizeEnumValue(str) });
41959
+ return /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: getBadgeVariant(fieldName2, str), children: humanizeEnumValue(str) });
41949
41960
  }
41950
- return formatFieldValue2(value, fieldName);
41961
+ return formatFieldValue2(value, fieldName2);
41951
41962
  }
41952
41963
  }
41953
41964
  function normalizeFieldDefs(fields) {
@@ -42790,7 +42801,21 @@ var init_Form = __esm({
42790
42801
  const normalizedInitialData = React85__namespace.default.useMemo(() => {
42791
42802
  const entityRowAsInitial = isPlainEntityRow(entity) ? entity : void 0;
42792
42803
  const callerInitial = initialData !== null && typeof initialData === "object" && !Array.isArray(initialData) ? initialData : {};
42793
- return entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
42804
+ const merged = entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
42805
+ const toId = (value) => {
42806
+ if (value === null || value === void 0 || typeof value !== "object" || value instanceof Date) return value;
42807
+ if (Array.isArray(value)) {
42808
+ return value.map(
42809
+ (item) => item !== null && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date) && item.id !== void 0 && item.id !== null ? String(item.id) : item
42810
+ );
42811
+ }
42812
+ return value.id !== void 0 && value.id !== null ? String(value.id) : value;
42813
+ };
42814
+ const normalized = {};
42815
+ for (const [key, value] of Object.entries(merged)) {
42816
+ normalized[key] = key === "id" ? value : toId(value);
42817
+ }
42818
+ return normalized;
42794
42819
  }, [entity, initialData]);
42795
42820
  const entityDerivedFields = React85__namespace.default.useMemo(() => {
42796
42821
  if (fields && fields.length > 0) return void 0;
@@ -42919,8 +42944,8 @@ var init_Form = __esm({
42919
42944
  checkViolations(name, newFormData);
42920
42945
  };
42921
42946
  const isFieldVisible = React85__namespace.default.useCallback(
42922
- (fieldName) => {
42923
- const condition = conditionalFields[fieldName];
42947
+ (fieldName2) => {
42948
+ const condition = conditionalFields[fieldName2];
42924
42949
  if (!condition) return true;
42925
42950
  return Boolean(evaluateFormExpression(condition, evalContext));
42926
42951
  },
@@ -42966,9 +42991,9 @@ var init_Form = __esm({
42966
42991
  };
42967
42992
  const handleInvalid = (e) => {
42968
42993
  const target = e.target;
42969
- const fieldName = target.getAttribute("data-field-name") ?? target.name ?? "";
42994
+ const fieldName2 = target.getAttribute("data-field-name") ?? target.name ?? "";
42970
42995
  const fieldMessage = target.validationMessage || "Invalid value";
42971
- debug("forms", "invalid", { mode: formMode, fieldName, fieldMessage });
42996
+ debug("forms", "invalid", { mode: formMode, fieldName: fieldName2, fieldMessage });
42972
42997
  queueMicrotask(() => {
42973
42998
  const form = formRef.current;
42974
42999
  if (!form) return;
@@ -43004,22 +43029,22 @@ var init_Form = __esm({
43004
43029
  };
43005
43030
  const renderField = React85__namespace.default.useCallback(
43006
43031
  (field) => {
43007
- const fieldName = field.name || field.field;
43008
- if (!fieldName) return null;
43009
- if (!isFieldVisible(fieldName)) {
43032
+ const fieldName2 = field.name || field.field;
43033
+ if (!fieldName2) return null;
43034
+ if (!isFieldVisible(fieldName2)) {
43010
43035
  return null;
43011
43036
  }
43012
43037
  const inputType = determineInputType(field);
43013
- const label = field.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/([A-Z])/g, " $1");
43014
- const currentValue = formData[fieldName] ?? field.defaultValue ?? "";
43015
- return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", "data-field": fieldName, children: [
43038
+ const label = field.label || fieldName2.charAt(0).toUpperCase() + fieldName2.slice(1).replace(/([A-Z])/g, " $1");
43039
+ const currentValue = formData[fieldName2] ?? field.defaultValue ?? "";
43040
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", "data-field": fieldName2, children: [
43016
43041
  inputType !== "checkbox" && /* @__PURE__ */ jsxRuntime.jsxs(Typography, { as: "label", variant: "label", weight: "bold", children: [
43017
43042
  label,
43018
43043
  field.required && /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", color: "error", className: "ml-1", children: "*" })
43019
43044
  ] }),
43020
- renderFieldInput(field, fieldName, inputType, currentValue, label),
43045
+ renderFieldInput(field, fieldName2, inputType, currentValue, label),
43021
43046
  field.hint && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", children: field.hint })
43022
- ] }, fieldName);
43047
+ ] }, fieldName2);
43023
43048
  },
43024
43049
  [formData, isFieldVisible, relationsData, relationsLoading, isLoading]
43025
43050
  );
@@ -43048,8 +43073,8 @@ var init_Form = __esm({
43048
43073
  }
43049
43074
  return field;
43050
43075
  }).map((field) => {
43051
- const fieldName = field.name || field.field;
43052
- const override = fieldOverrides?.find((o) => o.name === fieldName);
43076
+ const fieldName2 = field.name || field.field;
43077
+ const override = fieldOverrides?.find((o) => o.name === fieldName2);
43053
43078
  if (!override) return field;
43054
43079
  return {
43055
43080
  ...field,
@@ -43105,11 +43130,11 @@ var init_Form = __esm({
43105
43130
  ] }, section.id);
43106
43131
  }).filter(Boolean);
43107
43132
  }, [sections, isSectionVisible, collapsedSections, renderField, gap]);
43108
- function renderFieldInput(field, fieldName, inputType, currentValue, label) {
43133
+ function renderFieldInput(field, fieldName2, inputType, currentValue, label) {
43109
43134
  const commonProps = {
43110
- id: fieldName,
43111
- name: fieldName,
43112
- "data-field-name": fieldName,
43135
+ id: fieldName2,
43136
+ name: fieldName2,
43137
+ "data-field-name": fieldName2,
43113
43138
  required: field.required,
43114
43139
  disabled: isLoading,
43115
43140
  placeholder: field.placeholder,
@@ -43123,7 +43148,7 @@ var init_Form = __esm({
43123
43148
  ...commonProps,
43124
43149
  label: label + (field.required ? " *" : ""),
43125
43150
  checked: Boolean(currentValue),
43126
- onChange: (e) => handleChange(fieldName, e.target.checked)
43151
+ onChange: (e) => handleChange(fieldName2, e.target.checked)
43127
43152
  }
43128
43153
  );
43129
43154
  case "textarea":
@@ -43132,7 +43157,7 @@ var init_Form = __esm({
43132
43157
  {
43133
43158
  ...commonProps,
43134
43159
  value: String(currentValue),
43135
- onChange: (e) => handleChange(fieldName, e.target.value),
43160
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43136
43161
  minLength: field.min,
43137
43162
  maxLength: field.max
43138
43163
  }
@@ -43145,14 +43170,14 @@ var init_Form = __esm({
43145
43170
  ...commonProps,
43146
43171
  options,
43147
43172
  value: String(currentValue),
43148
- onValueChange: (v) => handleChange(fieldName, v),
43173
+ onValueChange: (v) => handleChange(fieldName2, v),
43149
43174
  placeholder: field.placeholder || `Select ${label}...`
43150
43175
  }
43151
43176
  );
43152
43177
  }
43153
43178
  case "relation": {
43154
- const relationOptions = relationsData[fieldName] || [];
43155
- const relationLoading = relationsLoading[fieldName] || false;
43179
+ const relationOptions = relationsData[fieldName2] || [];
43180
+ const relationLoading = relationsLoading[fieldName2] || false;
43156
43181
  if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
43157
43182
  const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
43158
43183
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -43164,7 +43189,7 @@ var init_Form = __esm({
43164
43189
  clearable: true,
43165
43190
  options: [...relationOptions],
43166
43191
  value: selectedValues,
43167
- onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
43192
+ onValueChange: (value) => handleChange(fieldName2, Array.isArray(value) ? value : [value]),
43168
43193
  placeholder: field.placeholder || `Select ${label}...`
43169
43194
  }
43170
43195
  );
@@ -43174,7 +43199,7 @@ var init_Form = __esm({
43174
43199
  {
43175
43200
  ...commonProps,
43176
43201
  value: currentValue ? String(currentValue) : void 0,
43177
- onChange: (value) => handleChange(fieldName, value),
43202
+ onChange: (value) => handleChange(fieldName2, value),
43178
43203
  options: relationOptions,
43179
43204
  isLoading: relationLoading,
43180
43205
  placeholder: field.placeholder || `Select ${label}...`,
@@ -43191,7 +43216,7 @@ var init_Form = __esm({
43191
43216
  placeholder: field.placeholder,
43192
43217
  disabled: isLoading,
43193
43218
  value: arrayValue,
43194
- onChange: (next) => handleChange(fieldName, [...next])
43219
+ onChange: (next) => handleChange(fieldName2, [...next])
43195
43220
  }
43196
43221
  );
43197
43222
  }
@@ -43203,7 +43228,7 @@ var init_Form = __esm({
43203
43228
  type: "number",
43204
43229
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
43205
43230
  onChange: (e) => handleChange(
43206
- fieldName,
43231
+ fieldName2,
43207
43232
  e.target.value ? Number(e.target.value) : void 0
43208
43233
  ),
43209
43234
  min: field.min,
@@ -43220,7 +43245,7 @@ var init_Form = __esm({
43220
43245
  icon: LucideIcons2.DollarSign,
43221
43246
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
43222
43247
  onChange: (e) => handleChange(
43223
- fieldName,
43248
+ fieldName2,
43224
43249
  e.target.value ? Number(e.target.value) : void 0
43225
43250
  ),
43226
43251
  min: field.min,
@@ -43238,7 +43263,7 @@ var init_Form = __esm({
43238
43263
  const f3 = files[0];
43239
43264
  if (!f3) return;
43240
43265
  const reader = new FileReader();
43241
- reader.onload = () => handleChange(fieldName, {
43266
+ reader.onload = () => handleChange(fieldName2, {
43242
43267
  name: f3.name,
43243
43268
  mimeType: f3.type,
43244
43269
  sizeBytes: f3.size,
@@ -43255,7 +43280,7 @@ var init_Form = __esm({
43255
43280
  ...commonProps,
43256
43281
  type: "date",
43257
43282
  value: formatDateValue(currentValue),
43258
- onChange: (e) => handleChange(fieldName, e.target.value)
43283
+ onChange: (e) => handleChange(fieldName2, e.target.value)
43259
43284
  }
43260
43285
  );
43261
43286
  case "datetime-local":
@@ -43265,7 +43290,7 @@ var init_Form = __esm({
43265
43290
  ...commonProps,
43266
43291
  type: "datetime-local",
43267
43292
  value: formatDateTimeValue(currentValue),
43268
- onChange: (e) => handleChange(fieldName, e.target.value)
43293
+ onChange: (e) => handleChange(fieldName2, e.target.value)
43269
43294
  }
43270
43295
  );
43271
43296
  case "email":
@@ -43275,7 +43300,7 @@ var init_Form = __esm({
43275
43300
  ...commonProps,
43276
43301
  type: "email",
43277
43302
  value: String(currentValue),
43278
- onChange: (e) => handleChange(fieldName, e.target.value),
43303
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43279
43304
  minLength: field.min,
43280
43305
  maxLength: field.max
43281
43306
  }
@@ -43287,7 +43312,7 @@ var init_Form = __esm({
43287
43312
  ...commonProps,
43288
43313
  type: "url",
43289
43314
  value: String(currentValue),
43290
- onChange: (e) => handleChange(fieldName, e.target.value),
43315
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43291
43316
  minLength: field.min,
43292
43317
  maxLength: field.max
43293
43318
  }
@@ -43299,7 +43324,7 @@ var init_Form = __esm({
43299
43324
  ...commonProps,
43300
43325
  type: "password",
43301
43326
  value: String(currentValue),
43302
- onChange: (e) => handleChange(fieldName, e.target.value),
43327
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43303
43328
  minLength: field.min,
43304
43329
  maxLength: field.max
43305
43330
  }
@@ -43312,7 +43337,7 @@ var init_Form = __esm({
43312
43337
  ...commonProps,
43313
43338
  type: "text",
43314
43339
  value: String(currentValue),
43315
- onChange: (e) => handleChange(fieldName, e.target.value),
43340
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43316
43341
  minLength: field.min,
43317
43342
  maxLength: field.max
43318
43343
  }
@@ -43669,7 +43694,7 @@ function entityFieldsFromListItem(item) {
43669
43694
  }
43670
43695
  return result;
43671
43696
  }
43672
- function getStatusStyle(fieldName, value) {
43697
+ function getStatusStyle(fieldName2, value) {
43673
43698
  const val = String(value).toLowerCase();
43674
43699
  if (val.includes("complete") || val.includes("done"))
43675
43700
  return STATUS_STYLES.complete;
@@ -43685,12 +43710,12 @@ function getStatusStyle(fieldName, value) {
43685
43710
  if (val.includes("low")) return STATUS_STYLES.low;
43686
43711
  return STATUS_STYLES.default;
43687
43712
  }
43688
- function formatValue3(value, fieldName) {
43713
+ function formatValue3(value, fieldName2) {
43689
43714
  if (typeof value === "number") {
43690
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
43715
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
43691
43716
  return `${value}%`;
43692
43717
  }
43693
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
43718
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
43694
43719
  return new Intl.NumberFormat("en-US", {
43695
43720
  style: "currency",
43696
43721
  currency: "USD",
@@ -43713,8 +43738,8 @@ function formatValue3(value, fieldName) {
43713
43738
  }
43714
43739
  return String(value);
43715
43740
  }
43716
- function formatFieldLabel2(fieldName) {
43717
- return humanizeFieldName(fieldName).replace(/\sId$/, "").trim();
43741
+ function formatFieldLabel2(fieldName2) {
43742
+ return humanizeFieldName(fieldName2).replace(/\sId$/, "").trim();
43718
43743
  }
43719
43744
  var STATUS_STYLES, StatusBadge, ProgressIndicator, List3;
43720
43745
  var init_List = __esm({
@@ -43782,9 +43807,9 @@ var init_List = __esm({
43782
43807
  };
43783
43808
  StatusBadge = ({
43784
43809
  value,
43785
- fieldName
43810
+ fieldName: fieldName2
43786
43811
  }) => {
43787
- const style = getStatusStyle(fieldName, value);
43812
+ const style = getStatusStyle(fieldName2, value);
43788
43813
  return /* @__PURE__ */ jsxRuntime.jsxs(
43789
43814
  Typography,
43790
43815
  {
@@ -46716,8 +46741,8 @@ var init_StatCard = __esm({
46716
46741
  return items.length;
46717
46742
  }
46718
46743
  if (field.includes(":")) {
46719
- const [fieldName, fieldValue] = field.split(":");
46720
- return items.filter((item) => item[fieldName] === fieldValue).length;
46744
+ const [fieldName2, fieldValue] = field.split(":");
46745
+ return items.filter((item) => item[fieldName2] === fieldValue).length;
46721
46746
  }
46722
46747
  const fieldExistsOnItems = items.some((item) => field in item);
46723
46748
  if (fieldExistsOnItems) {
@@ -48237,6 +48262,8 @@ var init_component_registry_generated = __esm({
48237
48262
  init_DocSearch();
48238
48263
  init_DocSidebar();
48239
48264
  init_DocTOC();
48265
+ init_DocumentDetails();
48266
+ init_DocumentPanel();
48240
48267
  init_DocumentViewer();
48241
48268
  init_DrawFxLayer();
48242
48269
  init_DrawGroup();
@@ -48346,7 +48373,7 @@ var init_component_registry_generated = __esm({
48346
48373
  init_RelationSelect();
48347
48374
  init_RepeatableFormSection();
48348
48375
  init_ReplyTree();
48349
- init_RichBlockEditor();
48376
+ init_RichTextEditor();
48350
48377
  init_RuntimeDebugger2();
48351
48378
  init_ScaledDiagram();
48352
48379
  init_ScoreDisplay();
@@ -48509,6 +48536,8 @@ var init_component_registry_generated = __esm({
48509
48536
  "DocSearch": DocSearch,
48510
48537
  "DocSidebar": DocSidebar,
48511
48538
  "DocTOC": DocTOC,
48539
+ "DocumentDetails": DocumentDetails,
48540
+ "DocumentPanel": DocumentPanel,
48512
48541
  "DocumentViewer": DocumentViewer,
48513
48542
  "DrawFxLayer": DrawFxLayer,
48514
48543
  "DrawGroup": DrawGroup,
@@ -48620,7 +48649,7 @@ var init_component_registry_generated = __esm({
48620
48649
  "RelationSelect": RelationSelect,
48621
48650
  "RepeatableFormSection": RepeatableFormSection,
48622
48651
  "ReplyTree": ReplyTree,
48623
- "RichBlockEditor": RichBlockEditor,
48652
+ "RichTextEditor": RichTextEditor,
48624
48653
  "RuntimeDebugger": RuntimeDebugger,
48625
48654
  "ScaledDiagram": ScaledDiagram,
48626
48655
  "ScoreDisplay": ScoreDisplay,
@@ -48774,10 +48803,10 @@ function enrichFormFields(fields, entityDef) {
48774
48803
  }
48775
48804
  if (field && typeof field === "object" && !Array.isArray(field) && !React85__namespace.default.isValidElement(field) && !(field instanceof Date)) {
48776
48805
  const obj = field;
48777
- const fieldName = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
48778
- if (!fieldName) return field;
48806
+ const fieldName2 = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
48807
+ if (!fieldName2) return field;
48779
48808
  if (obj.type || obj.inputType) return field;
48780
- const entityField = fieldMap.get(fieldName);
48809
+ const entityField = fieldMap.get(fieldName2);
48781
48810
  if (!entityField) return field;
48782
48811
  const enriched = { ...obj, type: entityField.type };
48783
48812
  if (entityField.required && !("required" in obj)) {
@@ -48827,9 +48856,9 @@ function enrichDetailFields(fields, entityDef) {
48827
48856
  }
48828
48857
  if (field && typeof field === "object" && !Array.isArray(field) && !React85__namespace.default.isValidElement(field) && !(field instanceof Date)) {
48829
48858
  const obj = field;
48830
- const fieldName = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
48831
- if (!fieldName || obj.type) return field;
48832
- const meta = metaFor(fieldName);
48859
+ const fieldName2 = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
48860
+ if (!fieldName2 || obj.type) return field;
48861
+ const meta = metaFor(fieldName2);
48833
48862
  return meta ? { ...obj, ...meta } : field;
48834
48863
  }
48835
48864
  return field;
@@ -49668,9 +49697,9 @@ function UISlotRenderer({
49668
49697
  "ui-slot-renderer relative min-h-full",
49669
49698
  className
49670
49699
  ), children: [
49671
- /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex min-h-full", children: [
49672
- /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 shrink-0" }),
49673
- /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-h-[200px]" })
49700
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex min-h-full flex-col lg:flex-row", children: [
49701
+ /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 lg:shrink-0" }),
49702
+ /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-w-0 min-h-[200px]" })
49674
49703
  ] }),
49675
49704
  /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "modal", portal: true }),
49676
49705
  /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "drawer", portal: true }),
@@ -50193,7 +50222,7 @@ function buildTraitRenderConfig(binding, traitConfigsByName, payload) {
50193
50222
  }
50194
50223
  return void 0;
50195
50224
  }
50196
- function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
50225
+ function createSharedEntityWriter(binding, tick, traitStatesRef, emit, traitConfigsByName) {
50197
50226
  return (scratch) => {
50198
50227
  const traitName = binding.trait.name;
50199
50228
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -50202,9 +50231,13 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
50202
50231
  const writes = [];
50203
50232
  const ctx = evaluator.createMinimalContext(scratchEntity, {}, currentState);
50204
50233
  const declaredDefaults = runtime.collectDeclaredConfigDefaults(binding.trait);
50205
- const callSiteConfig = getBindingConfig(binding);
50206
- if (declaredDefaults || callSiteConfig) {
50207
- ctx.config = { ...declaredDefaults ?? {}, ...callSiteConfig ?? {} };
50234
+ const resolvedByName = traitConfigsByName?.[traitName];
50235
+ const callSiteRaw = getBindingConfig(binding);
50236
+ const callSiteConfig = callSiteRaw ? Object.fromEntries(
50237
+ Object.entries(callSiteRaw).filter(([, v]) => !containsConfigForward(v))
50238
+ ) : void 0;
50239
+ if (declaredDefaults || resolvedByName || callSiteConfig) {
50240
+ ctx.config = { ...declaredDefaults ?? {}, ...resolvedByName ?? {}, ...callSiteConfig ?? {} };
50208
50241
  }
50209
50242
  ctx.mutateEntity = (changes) => {
50210
50243
  for (const [field, value] of Object.entries(changes)) {
@@ -50807,7 +50840,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
50807
50840
  const interval = entries[0].tick.interval;
50808
50841
  const onDue = timedTick(`tick:shared:${group.storeKey}@${String(interval)}`, () => {
50809
50842
  const writers = entries.map(
50810
- ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
50843
+ ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter, traitConfigsByName)
50811
50844
  );
50812
50845
  runTickFrame(group.storeKey, writers, sharedEntityStore);
50813
50846
  });