@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.
@@ -26338,8 +26338,9 @@ function DataGrid({
26338
26338
  const titleField = fieldDefs.find((f3) => f3.variant === "h3" || f3.variant === "h4") ?? fieldDefs[0];
26339
26339
  const badgeFields = fieldDefs.filter((f3) => f3.variant === "badge" && f3 !== titleField);
26340
26340
  const bodyFields = fieldDefs.filter((f3) => f3 !== titleField && !badgeFields.includes(f3));
26341
- const primaryActions = actionDefs.filter((a) => a.variant !== "danger");
26342
- const dangerActions = actionDefs.filter((a) => a.variant === "danger");
26341
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
26342
+ const inlineCardActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
26343
+ const menuCardActions = actionDefs.filter((a) => !inlineCardActions.includes(a));
26343
26344
  const fireAction = (action, itemData) => {
26344
26345
  if (action.navigatesTo) {
26345
26346
  const url = action.navigatesTo.replace(
@@ -26359,6 +26360,9 @@ function DataGrid({
26359
26360
  e.stopPropagation();
26360
26361
  fireAction(action, itemData);
26361
26362
  };
26363
+ const cardClickAction = actionDefs.find((a) => a.variant !== "danger");
26364
+ const handleCardClick = cardClickAction ? (itemData) => () => fireAction(cardClickAction, itemData) : void 0;
26365
+ const stopCardClick = handleCardClick ? (e) => e.stopPropagation() : void 0;
26362
26366
  const hasRenderProp = typeof children === "function";
26363
26367
  React87.useEffect(() => {
26364
26368
  if (data.length > 0 && !hasRenderProp && fieldDefs.length === 0) {
@@ -26430,55 +26434,40 @@ function DataGrid({
26430
26434
  {
26431
26435
  "data-entity-row": true,
26432
26436
  "data-entity-id": id,
26433
- className: cn("relative group/rowactions", isSelected && "ring-2 ring-primary rounded-lg"),
26437
+ onClick: handleCardClick?.(itemData),
26438
+ className: cn("relative group/rowactions", handleCardClick && "cursor-pointer", isSelected && "ring-2 ring-primary rounded-lg"),
26434
26439
  children: [
26435
26440
  children(itemData, index),
26436
- 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: [
26437
- /* @__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: [
26438
- (maxInlineActions != null ? actionDefs.slice(0, maxInlineActions) : actionDefs).map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
26439
- Button,
26440
- {
26441
- variant: action.variant === "primary" ? "primary" : "ghost",
26442
- size: "sm",
26443
- onClick: handleActionClick(action, itemData),
26444
- "data-testid": `action-${action.event}`,
26445
- "data-row-id": String(itemData.id),
26446
- className: action.variant === "danger" ? "text-error hover:text-error hover:bg-error/10" : void 0,
26447
- children: [
26448
- action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
26449
- action.label
26450
- ]
26451
- },
26452
- idx
26453
- )),
26454
- maxInlineActions != null && actionDefs.length > maxInlineActions && /* @__PURE__ */ jsxRuntime.jsx(
26455
- Menu,
26456
- {
26457
- position: "bottom-end",
26458
- 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" }) }),
26459
- items: actionDefs.slice(maxInlineActions).map((action) => ({
26460
- label: action.label,
26461
- icon: action.icon,
26462
- event: action.event,
26463
- onClick: () => fireAction(action, itemData)
26464
- }))
26465
- }
26466
- )
26467
- ] }),
26468
- /* @__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(
26441
+ 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: [
26442
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
26443
+ Button,
26444
+ {
26445
+ variant: "primary",
26446
+ size: "sm",
26447
+ onClick: handleActionClick(action, itemData),
26448
+ "data-testid": `action-${action.event}`,
26449
+ "data-row-id": String(itemData.id),
26450
+ children: [
26451
+ action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
26452
+ action.label
26453
+ ]
26454
+ },
26455
+ idx
26456
+ )),
26457
+ menuCardActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
26469
26458
  Menu,
26470
26459
  {
26471
26460
  position: "bottom-end",
26472
26461
  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" }) }),
26473
- items: actionDefs.map((action) => ({
26462
+ items: menuCardActions.map((action) => ({
26474
26463
  label: action.label,
26475
26464
  icon: action.icon,
26476
- event: action.event,
26465
+ variant: action.variant === "danger" ? "danger" : "default",
26477
26466
  onClick: () => fireAction(action, itemData)
26478
26467
  }))
26479
26468
  }
26480
- ) })
26481
- ] })
26469
+ )
26470
+ ] }) })
26482
26471
  ]
26483
26472
  },
26484
26473
  id
@@ -26492,12 +26481,14 @@ function DataGrid({
26492
26481
  {
26493
26482
  "data-entity-row": true,
26494
26483
  "data-entity-id": id,
26484
+ onClick: handleCardClick?.(itemData),
26495
26485
  className: cn(
26496
26486
  "bg-card rounded-lg",
26497
26487
  "border border-border",
26498
26488
  "shadow-elevation-card hover:shadow-elevation-dialog",
26499
26489
  "hover:border-primary transition-all",
26500
26490
  "flex flex-col",
26491
+ handleCardClick && "cursor-pointer",
26501
26492
  isSelected && "ring-2 ring-primary border-primary"
26502
26493
  ),
26503
26494
  children: [
@@ -26547,53 +26538,35 @@ function DataGrid({
26547
26538
  ] }, field.name);
26548
26539
  }) })
26549
26540
  ] }),
26550
- (primaryActions.length > 0 || dangerActions.length > 0) && /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
26551
- (maxInlineActions != null ? primaryActions.slice(0, maxInlineActions) : primaryActions).map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
26541
+ actionDefs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", onClick: stopCardClick, className: "flex-shrink-0", children: [
26542
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
26552
26543
  Button,
26553
26544
  {
26554
- variant: action.variant === "primary" ? "primary" : "ghost",
26545
+ variant: "primary",
26555
26546
  size: "sm",
26556
26547
  onClick: handleActionClick(action, itemData),
26557
26548
  "data-testid": `action-${action.event}`,
26558
26549
  "data-row-id": String(itemData.id),
26559
26550
  "aria-label": action.label,
26560
26551
  title: action.label,
26561
- className: cn(
26562
- action.variant === "primary" ? void 0 : "text-muted-foreground hover:text-foreground",
26563
- action.icon && "px-2"
26564
- ),
26552
+ className: cn(action.icon && "px-2"),
26565
26553
  children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
26566
26554
  },
26567
26555
  idx
26568
26556
  )),
26569
- maxInlineActions != null && primaryActions.length > maxInlineActions && /* @__PURE__ */ jsxRuntime.jsx(
26557
+ menuCardActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
26570
26558
  Menu,
26571
26559
  {
26572
26560
  position: "bottom-end",
26573
26561
  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" }) }),
26574
- items: primaryActions.slice(maxInlineActions).map((action) => ({
26562
+ items: menuCardActions.map((action) => ({
26575
26563
  label: action.label,
26576
26564
  icon: action.icon,
26577
- event: action.event,
26565
+ variant: action.variant === "danger" ? "danger" : "default",
26578
26566
  onClick: () => fireAction(action, itemData)
26579
26567
  }))
26580
26568
  }
26581
- ),
26582
- dangerActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
26583
- Button,
26584
- {
26585
- variant: "ghost",
26586
- size: "sm",
26587
- onClick: handleActionClick(action, itemData),
26588
- "data-testid": `action-${action.event}`,
26589
- "data-row-id": String(itemData.id),
26590
- "aria-label": action.label,
26591
- title: action.label,
26592
- className: "text-error hover:text-error hover:bg-error/10 px-2",
26593
- children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
26594
- },
26595
- `danger-${idx}`
26596
- ))
26569
+ )
26597
26570
  ] })
26598
26571
  ] }) }),
26599
26572
  bodyFields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-4 pt-2 pb-4 flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", children: [
@@ -26835,54 +26808,59 @@ function DataList({
26835
26808
  if (!itemActions || itemActions.length === 0) return null;
26836
26809
  const inline = maxInlineActions != null ? itemActions.slice(0, maxInlineActions) : itemActions;
26837
26810
  const overflow = maxInlineActions != null ? itemActions.slice(maxInlineActions) : [];
26838
- return /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
26839
- inline.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
26840
- Button,
26841
- {
26842
- variant: action.variant ?? "ghost",
26843
- size: "sm",
26844
- onClick: handleActionClick(action, itemData),
26845
- "data-testid": `action-${action.event}`,
26846
- "data-row-id": String(itemData.id),
26847
- className: cn(
26848
- action.variant === "danger" && "text-error hover:bg-error/10",
26849
- // Must sit on the Button itself: the variant's own text colour
26850
- // beats an inherited one from the row wrapper.
26851
- onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
26852
- ),
26853
- children: [
26854
- action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
26855
- action.label
26856
- ]
26857
- },
26858
- idx
26859
- )),
26860
- overflow.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
26861
- Menu,
26862
- {
26863
- position: "bottom-end",
26864
- 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" }) }),
26865
- items: overflow.map((action) => ({
26866
- label: action.label,
26867
- icon: action.icon,
26868
- event: action.event,
26869
- variant: action.variant === "danger" ? "danger" : "default",
26870
- onClick: () => eventBus.emit(`UI:${action.event}`, {
26871
- id: itemData.id,
26872
- row: itemData
26873
- })
26874
- }))
26875
- }
26876
- )
26877
- ] });
26811
+ return (
26812
+ // stopPropagation at the cluster: the "⋯" trigger opens the overflow
26813
+ // menu without also firing the row's default click (inline buttons
26814
+ // already stop it in handleActionClick; the Menu panel is portaled).
26815
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, className: "flex-shrink-0", children: [
26816
+ inline.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
26817
+ Button,
26818
+ {
26819
+ variant: action.variant ?? "ghost",
26820
+ size: "sm",
26821
+ onClick: handleActionClick(action, itemData),
26822
+ "data-testid": `action-${action.event}`,
26823
+ "data-row-id": String(itemData.id),
26824
+ className: cn(
26825
+ action.variant === "danger" && "text-error hover:bg-error/10",
26826
+ // Must sit on the Button itself: the variant's own text colour
26827
+ // beats an inherited one from the row wrapper.
26828
+ onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
26829
+ ),
26830
+ children: [
26831
+ action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
26832
+ action.label
26833
+ ]
26834
+ },
26835
+ idx
26836
+ )),
26837
+ overflow.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
26838
+ Menu,
26839
+ {
26840
+ position: "bottom-end",
26841
+ 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" }) }),
26842
+ items: overflow.map((action) => ({
26843
+ label: action.label,
26844
+ icon: action.icon,
26845
+ variant: action.variant === "danger" ? "danger" : "default",
26846
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
26847
+ id: itemData.id,
26848
+ row: itemData
26849
+ })
26850
+ }))
26851
+ }
26852
+ )
26853
+ ] })
26854
+ );
26878
26855
  };
26856
+ const rowClickEvent = itemClickEvent || itemActions?.find((a) => a.variant !== "danger")?.event;
26879
26857
  const handleRowClick = (itemData) => () => {
26880
- if (!itemClickEvent) return;
26858
+ if (!rowClickEvent) return;
26881
26859
  const payload = {
26882
26860
  id: itemData.id,
26883
26861
  row: itemData
26884
26862
  };
26885
- eventBus.emit(`UI:${itemClickEvent}`, payload);
26863
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
26886
26864
  };
26887
26865
  if (isLoading) {
26888
26866
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) });
@@ -26930,10 +26908,10 @@ function DataList({
26930
26908
  {
26931
26909
  "data-entity-row": true,
26932
26910
  "data-entity-id": id,
26933
- onClick: itemClickEvent ? handleRowClick(itemData) : void 0,
26911
+ onClick: rowClickEvent ? handleRowClick(itemData) : void 0,
26934
26912
  className: cn(
26935
26913
  "flex px-4 group/rowactions",
26936
- itemClickEvent && "cursor-pointer",
26914
+ rowClickEvent && "cursor-pointer",
26937
26915
  isSent ? "justify-end" : "justify-start"
26938
26916
  ),
26939
26917
  children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -27009,7 +26987,7 @@ function DataList({
27009
26987
  const id2 = itemData.id || String(index);
27010
26988
  const actions = renderItemActions(itemData);
27011
26989
  return wrapDnd(
27012
- /* @__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: [
26990
+ /* @__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: [
27013
26991
  children(itemData, index),
27014
26992
  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: [
27015
26993
  /* @__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 }),
@@ -27021,7 +26999,6 @@ function DataList({
27021
26999
  items: (itemActions ?? []).map((action) => ({
27022
27000
  label: action.label,
27023
27001
  icon: action.icon,
27024
- event: action.event,
27025
27002
  variant: action.variant === "danger" ? "danger" : "default",
27026
27003
  onClick: () => eventBus.emit(`UI:${action.event}`, {
27027
27004
  id: itemData.id,
@@ -27038,7 +27015,7 @@ function DataList({
27038
27015
  const id = itemData.id || String(index);
27039
27016
  const titleValue = getNestedValue(itemData, titleField?.name ?? "");
27040
27017
  return wrapDnd(
27041
- /* @__PURE__ */ jsxRuntime.jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: itemClickEvent ? handleRowClick(itemData) : void 0, className: cn(itemClickEvent && "cursor-pointer"), children: [
27018
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: rowClickEvent ? handleRowClick(itemData) : void 0, className: cn(rowClickEvent && "cursor-pointer"), children: [
27042
27019
  /* @__PURE__ */ jsxRuntime.jsxs(
27043
27020
  Box,
27044
27021
  {
@@ -32314,7 +32291,7 @@ var init_MapView = __esm({
32314
32291
  shadowSize: [41, 41]
32315
32292
  });
32316
32293
  L.Marker.prototype.options.icon = defaultIcon;
32317
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState95 } = React87__namespace.default;
32294
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState97 } = React87__namespace.default;
32318
32295
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
32319
32296
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
32320
32297
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -32359,7 +32336,7 @@ var init_MapView = __esm({
32359
32336
  showAttribution = true
32360
32337
  }) {
32361
32338
  const eventBus = useEventBus2();
32362
- const [clickedPosition, setClickedPosition] = useState95(null);
32339
+ const [clickedPosition, setClickedPosition] = useState97(null);
32363
32340
  const handleMapClick = useCallback97((lat, lng) => {
32364
32341
  if (showClickedPin) {
32365
32342
  setClickedPosition({ lat, lng });
@@ -36721,741 +36698,775 @@ var init_PositionedCanvas = __esm({
36721
36698
  PositionedCanvas.displayName = "PositionedCanvas";
36722
36699
  }
36723
36700
  });
36724
- function nextBlockId(prefix = "blk") {
36725
- _idSeq += 1;
36726
- const random = Math.random().toString(36).slice(2, 8);
36727
- return `${prefix}-${Date.now().toString(36)}-${_idSeq}-${random}`;
36701
+ function safeUrl(raw, kinds) {
36702
+ const url = raw.trim();
36703
+ if (url.startsWith("#") || url.startsWith("/")) return url;
36704
+ const lower = url.toLowerCase();
36705
+ for (const kind of kinds) {
36706
+ if (lower.startsWith(kind)) return url;
36707
+ }
36708
+ return null;
36728
36709
  }
36729
- function normalizeBlocks(raw) {
36730
- if (!Array.isArray(raw) || raw.length === 0) return [createBlock("paragraph")];
36731
- return raw.map((row) => {
36732
- const entity = row;
36733
- const rawType = entity.type;
36734
- const type = typeof rawType === "string" && BLOCK_TYPES.has(rawType) ? rawType : "paragraph";
36735
- const id = typeof entity.id === "string" && entity.id ? entity.id : nextBlockId(type);
36736
- return { ...entity, id, type };
36737
- });
36710
+ function sanitizeNode(el) {
36711
+ const children = Array.from(el.children);
36712
+ for (const child of children) {
36713
+ const tag = child.tagName.toLowerCase();
36714
+ if (DROP_TAGS.has(tag)) {
36715
+ child.remove();
36716
+ continue;
36717
+ }
36718
+ const allowed = ALLOWED_ATTRS[tag];
36719
+ if (!allowed) {
36720
+ sanitizeNode(child);
36721
+ child.replaceWith(...Array.from(child.childNodes));
36722
+ continue;
36723
+ }
36724
+ for (const attr of Array.from(child.attributes)) {
36725
+ const name = attr.name.toLowerCase();
36726
+ if (!allowed.includes(name)) {
36727
+ child.removeAttribute(attr.name);
36728
+ continue;
36729
+ }
36730
+ if (name === "href") {
36731
+ const url = safeUrl(attr.value, ["http://", "https://", "mailto:"]);
36732
+ if (url === null) child.removeAttribute(attr.name);
36733
+ else child.setAttribute("href", url);
36734
+ }
36735
+ if (name === "src") {
36736
+ const url = safeUrl(attr.value, ["http://", "https://", "data:image/"]);
36737
+ if (url === null) child.remove();
36738
+ else child.setAttribute("src", url);
36739
+ }
36740
+ }
36741
+ sanitizeNode(child);
36742
+ }
36738
36743
  }
36739
- function createBlock(type) {
36740
- switch (type) {
36741
- case "bullet-list":
36742
- case "numbered-list":
36743
- return {
36744
- id: nextBlockId(type),
36745
- type,
36746
- children: [
36747
- { id: nextBlockId("li"), type: "paragraph", content: "" }
36748
- ]
36749
- };
36750
- case "image":
36751
- return {
36752
- id: nextBlockId(type),
36753
- type,
36754
- content: "",
36755
- metadata: { url: "", caption: "" }
36756
- };
36757
- case "code":
36758
- return {
36759
- id: nextBlockId(type),
36760
- type,
36761
- content: "",
36762
- metadata: { language: "plaintext" }
36763
- };
36764
- case "divider":
36765
- return { id: nextBlockId(type), type };
36766
- default:
36767
- return { id: nextBlockId(type), type, content: "" };
36744
+ function sanitizeRichHtml(html) {
36745
+ if (!html || typeof html !== "string") return "";
36746
+ if (typeof window === "undefined" || typeof DOMParser === "undefined") return "";
36747
+ const doc = new DOMParser().parseFromString(html, "text/html");
36748
+ sanitizeNode(doc.body);
36749
+ if (!doc.body.querySelector("img, hr") && (doc.body.textContent ?? "").trim().length === 0) {
36750
+ return "";
36768
36751
  }
36752
+ return doc.body.innerHTML.replace(/​/g, "");
36769
36753
  }
36770
- function replaceBlock(blocks, id, updater) {
36771
- return blocks.map((block) => block.id === id ? updater(block) : block);
36754
+ function htmlIsEmpty(el) {
36755
+ if (el.querySelector("img, hr")) return false;
36756
+ return (el.textContent ?? "").trim().length === 0;
36772
36757
  }
36773
- function removeBlock(blocks, id) {
36774
- return blocks.filter((block) => block.id !== id);
36758
+ function closestBlock(node, root) {
36759
+ let cur = node;
36760
+ while (cur && cur !== root) {
36761
+ if (cur instanceof Element && BLOCK_TAGS.has(cur.tagName)) return cur;
36762
+ cur = cur.parentNode;
36763
+ }
36764
+ return null;
36775
36765
  }
36776
- function duplicateBlock(block) {
36766
+ function RichTextStyles() {
36767
+ return /* @__PURE__ */ jsxRuntime.jsx("style", { children: RICH_TEXT_CSS });
36768
+ }
36769
+ function readToolbarState(root) {
36770
+ const sel = window.getSelection();
36771
+ if (!sel || sel.rangeCount === 0) return IDLE_TOOLBAR;
36772
+ const anchor = sel.anchorNode;
36773
+ if (!anchor || !root.contains(anchor)) return IDLE_TOOLBAR;
36774
+ let block = null;
36775
+ let link = false;
36776
+ let node = anchor;
36777
+ while (node && node !== root) {
36778
+ if (node instanceof Element) {
36779
+ const tag = node.tagName.toLowerCase();
36780
+ if (!block && (tag === "h1" || tag === "h2" || tag === "h3" || tag === "blockquote" || tag === "pre")) {
36781
+ block = tag;
36782
+ }
36783
+ if (tag === "a") link = true;
36784
+ }
36785
+ node = node.parentNode;
36786
+ }
36777
36787
  return {
36778
- ...block,
36779
- id: nextBlockId(block.type),
36780
- children: block.children?.map((child) => ({
36781
- ...child,
36782
- id: nextBlockId("li")
36783
- })),
36784
- metadata: block.metadata ? { ...block.metadata } : void 0
36788
+ bold: document.queryCommandState("bold"),
36789
+ italic: document.queryCommandState("italic"),
36790
+ underline: document.queryCommandState("underline"),
36791
+ strikeThrough: document.queryCommandState("strikeThrough"),
36792
+ block,
36793
+ bullets: document.queryCommandState("insertUnorderedList"),
36794
+ numbers: document.queryCommandState("insertOrderedList"),
36795
+ link
36785
36796
  };
36786
36797
  }
36787
- function insertAfter(blocks, targetId, inserted) {
36788
- const idx = blocks.findIndex((b) => b.id === targetId);
36789
- if (idx === -1) return [...blocks, inserted];
36790
- const next = blocks.slice();
36791
- next.splice(idx + 1, 0, inserted);
36792
- return next;
36793
- }
36794
- function changeBlockType(block, type) {
36795
- if (block.type === type) return block;
36796
- if (type === "bullet-list" || type === "numbered-list") {
36797
- if (block.children && block.children.length > 0) {
36798
- return { ...block, type };
36798
+ function ToolbarButton({ icon: IconCmp, label, active, onExec }) {
36799
+ return /* @__PURE__ */ jsxRuntime.jsx(
36800
+ Button,
36801
+ {
36802
+ type: "button",
36803
+ variant: "ghost",
36804
+ size: "sm",
36805
+ "aria-label": label,
36806
+ "aria-pressed": active,
36807
+ title: label,
36808
+ className: cn(
36809
+ "h-8 w-8 p-0 gap-0 justify-center",
36810
+ active && "bg-muted text-foreground"
36811
+ ),
36812
+ onMouseDown: (e) => {
36813
+ e.preventDefault();
36814
+ onExec();
36815
+ },
36816
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconCmp, { size: 15 })
36799
36817
  }
36800
- const seed2 = block.content ?? "";
36801
- return {
36802
- id: block.id,
36803
- type,
36804
- children: [
36805
- { id: nextBlockId("li"), type: "paragraph", content: seed2 }
36806
- ]
36807
- };
36808
- }
36809
- if (type === "divider") {
36810
- return { id: block.id, type };
36811
- }
36812
- if (type === "image") {
36813
- return {
36814
- id: block.id,
36815
- type,
36816
- content: "",
36817
- metadata: { url: "", caption: block.content ?? "" }
36818
- };
36819
- }
36820
- if (type === "code") {
36821
- return {
36822
- id: block.id,
36823
- type,
36824
- content: block.content ?? "",
36825
- metadata: { language: "plaintext" }
36818
+ );
36819
+ }
36820
+ var CHANGE_DEBOUNCE_MS, ALLOWED_ATTRS, DROP_TAGS, MD_BLOCK_PREFIXES, BLOCK_TAGS, MD_INLINE_RULES, RICH_TEXT_CSS, IDLE_TOOLBAR, RichTextEditor;
36821
+ var init_RichTextEditor = __esm({
36822
+ "components/core/molecules/RichTextEditor.tsx"() {
36823
+ "use client";
36824
+ init_cn();
36825
+ init_Box();
36826
+ init_Button();
36827
+ init_useEventBus();
36828
+ CHANGE_DEBOUNCE_MS = 400;
36829
+ ALLOWED_ATTRS = {
36830
+ p: [],
36831
+ h1: [],
36832
+ h2: [],
36833
+ h3: [],
36834
+ h4: [],
36835
+ ul: [],
36836
+ ol: [],
36837
+ li: [],
36838
+ blockquote: [],
36839
+ pre: [],
36840
+ code: [],
36841
+ b: [],
36842
+ strong: [],
36843
+ i: [],
36844
+ em: [],
36845
+ u: [],
36846
+ s: [],
36847
+ strike: [],
36848
+ br: [],
36849
+ hr: [],
36850
+ div: [],
36851
+ span: [],
36852
+ a: ["href"],
36853
+ img: ["src", "alt"]
36854
+ };
36855
+ DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "link", "meta", "form", "input", "button", "textarea", "select"]);
36856
+ MD_BLOCK_PREFIXES = {
36857
+ "#": { kind: "block", tag: "h1" },
36858
+ "##": { kind: "block", tag: "h2" },
36859
+ "###": { kind: "block", tag: "h3" },
36860
+ ">": { kind: "block", tag: "blockquote" },
36861
+ "```": { kind: "block", tag: "pre" },
36862
+ "-": { kind: "list", command: "insertUnorderedList" },
36863
+ "*": { kind: "list", command: "insertUnorderedList" },
36864
+ "1.": { kind: "list", command: "insertOrderedList" }
36865
+ };
36866
+ BLOCK_TAGS = /* @__PURE__ */ new Set(["P", "DIV", "LI", "H1", "H2", "H3", "H4", "BLOCKQUOTE", "PRE"]);
36867
+ MD_INLINE_RULES = [
36868
+ { pattern: /\*\*([^*\n]+)\*\*$/, wrap: "strong" },
36869
+ { pattern: /(^|[^*])\*([^*\n]+)\*$/, wrap: "em" },
36870
+ { pattern: /`([^`\n]+)`$/, wrap: "code" }
36871
+ ];
36872
+ RICH_TEXT_CSS = `
36873
+ .almadar-rich-text { line-height: 1.7; color: var(--color-foreground); }
36874
+ .almadar-rich-text h1 { font-size: 1.875rem; font-weight: 700; line-height: 1.25; margin: 1.25em 0 0.4em; }
36875
+ .almadar-rich-text h2 { font-size: 1.5rem; font-weight: 650; line-height: 1.3; margin: 1.1em 0 0.4em; }
36876
+ .almadar-rich-text h3 { font-size: 1.25rem; font-weight: 600; line-height: 1.35; margin: 1em 0 0.35em; }
36877
+ .almadar-rich-text h1:first-child, .almadar-rich-text h2:first-child, .almadar-rich-text h3:first-child { margin-top: 0; }
36878
+ .almadar-rich-text p { margin: 0.5em 0; }
36879
+ .almadar-rich-text ul { list-style: disc; padding-inline-start: 1.5rem; margin: 0.5em 0; }
36880
+ .almadar-rich-text ol { list-style: decimal; padding-inline-start: 1.5rem; margin: 0.5em 0; }
36881
+ .almadar-rich-text li { margin: 0.25em 0; }
36882
+ .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; }
36883
+ .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; }
36884
+ .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; }
36885
+ .almadar-rich-text pre code { background: transparent; padding: 0; }
36886
+ .almadar-rich-text a { color: var(--color-primary); text-decoration: underline; }
36887
+ .almadar-rich-text hr { border: none; border-top: 1px solid var(--color-border); margin: 1.25em 0; }
36888
+ .almadar-rich-text img { max-width: 100%; border-radius: 0.375rem; }
36889
+ `;
36890
+ IDLE_TOOLBAR = {
36891
+ bold: false,
36892
+ italic: false,
36893
+ underline: false,
36894
+ strikeThrough: false,
36895
+ block: null,
36896
+ bullets: false,
36897
+ numbers: false,
36898
+ link: false
36899
+ };
36900
+ RichTextEditor = ({
36901
+ value,
36902
+ onChange,
36903
+ changeEvent,
36904
+ readOnly = false,
36905
+ placeholder,
36906
+ showToolbar = true,
36907
+ className
36908
+ }) => {
36909
+ const { t } = hooks.useTranslate();
36910
+ const ref = React87.useRef(null);
36911
+ const [toolbar, setToolbar] = React87.useState(IDLE_TOOLBAR);
36912
+ const [empty, setEmpty] = React87.useState(() => !value || !sanitizeRichHtml(value).trim());
36913
+ const eventBus = useEventBus();
36914
+ const onChangeRef = React87.useRef(onChange);
36915
+ const changeEventRef = React87.useRef(changeEvent);
36916
+ React87.useEffect(() => {
36917
+ onChangeRef.current = onChange;
36918
+ changeEventRef.current = changeEvent;
36919
+ }, [onChange, changeEvent]);
36920
+ const emitTimerRef = React87.useRef(null);
36921
+ const emitNow = React87.useCallback(() => {
36922
+ const el = ref.current;
36923
+ if (!el) return;
36924
+ const html = sanitizeRichHtml(el.innerHTML);
36925
+ onChangeRef.current?.(html);
36926
+ const evt = changeEventRef.current;
36927
+ if (evt) eventBus.emit(`UI:${evt}`, { value: html });
36928
+ }, [eventBus]);
36929
+ const scheduleEmit = React87.useCallback(() => {
36930
+ if (emitTimerRef.current) clearTimeout(emitTimerRef.current);
36931
+ emitTimerRef.current = setTimeout(() => {
36932
+ emitTimerRef.current = null;
36933
+ emitNow();
36934
+ }, CHANGE_DEBOUNCE_MS);
36935
+ }, [emitNow]);
36936
+ const flushEmit = React87.useCallback(() => {
36937
+ if (!emitTimerRef.current) return;
36938
+ clearTimeout(emitTimerRef.current);
36939
+ emitTimerRef.current = null;
36940
+ emitNow();
36941
+ }, [emitNow]);
36942
+ React87.useEffect(() => () => {
36943
+ if (emitTimerRef.current) {
36944
+ clearTimeout(emitTimerRef.current);
36945
+ emitTimerRef.current = null;
36946
+ emitNow();
36947
+ }
36948
+ }, [emitNow]);
36949
+ React87.useEffect(() => {
36950
+ if (readOnly) return;
36951
+ const el = ref.current;
36952
+ if (!el) return;
36953
+ if (document.activeElement === el) return;
36954
+ const next = sanitizeRichHtml(value ?? "") || "<p><br></p>";
36955
+ if (el.innerHTML !== next) {
36956
+ el.innerHTML = next;
36957
+ setEmpty(htmlIsEmpty(el));
36958
+ }
36959
+ }, [readOnly, value]);
36960
+ React87.useEffect(() => {
36961
+ if (readOnly) return;
36962
+ document.execCommand("defaultParagraphSeparator", false, "p");
36963
+ }, [readOnly]);
36964
+ React87.useEffect(() => {
36965
+ if (readOnly || !showToolbar) return;
36966
+ const onSelectionChange = () => {
36967
+ const el = ref.current;
36968
+ if (!el) return;
36969
+ setToolbar(readToolbarState(el));
36970
+ };
36971
+ document.addEventListener("selectionchange", onSelectionChange);
36972
+ return () => document.removeEventListener("selectionchange", onSelectionChange);
36973
+ }, [readOnly, showToolbar]);
36974
+ const tryInlineMarkdown = React87.useCallback(() => {
36975
+ const root = ref.current;
36976
+ if (!root) return false;
36977
+ const sel = window.getSelection();
36978
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
36979
+ const node = sel.anchorNode;
36980
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
36981
+ if (node.parentElement?.closest("code, pre")) return false;
36982
+ const text = (node.textContent ?? "").slice(0, sel.anchorOffset);
36983
+ for (const rule of MD_INLINE_RULES) {
36984
+ const m = rule.pattern.exec(text);
36985
+ if (!m) continue;
36986
+ const inner = rule.wrap === "em" ? m[2] : m[1];
36987
+ if (!inner || !inner.trim()) continue;
36988
+ const lead = rule.wrap === "em" ? m[1]?.length ?? 0 : 0;
36989
+ const range = document.createRange();
36990
+ range.setStart(node, m.index + lead);
36991
+ range.setEnd(node, sel.anchorOffset);
36992
+ const el = document.createElement(rule.wrap);
36993
+ el.textContent = inner;
36994
+ range.deleteContents();
36995
+ range.insertNode(el);
36996
+ const after = document.createTextNode("\u200B");
36997
+ el.parentNode?.insertBefore(after, el.nextSibling);
36998
+ const caret = document.createRange();
36999
+ caret.setStart(after, 1);
37000
+ caret.collapse(true);
37001
+ sel.removeAllRanges();
37002
+ sel.addRange(caret);
37003
+ return true;
37004
+ }
37005
+ return false;
37006
+ }, []);
37007
+ const tryBlockMarkdown = React87.useCallback(() => {
37008
+ const root = ref.current;
37009
+ if (!root) return false;
37010
+ const sel = window.getSelection();
37011
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
37012
+ const node = sel.anchorNode;
37013
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
37014
+ const block = closestBlock(node, root);
37015
+ if (block ? block.firstChild !== node : root.firstChild !== node) return false;
37016
+ const prefix = (node.textContent ?? "").slice(0, sel.anchorOffset);
37017
+ const rule = MD_BLOCK_PREFIXES[prefix];
37018
+ if (!rule) return false;
37019
+ if (node.parentElement?.closest("pre, code")) return false;
37020
+ if (rule.kind === "list" && node.parentElement?.closest("li")) return false;
37021
+ node.deleteData(0, prefix.length);
37022
+ const caret = document.createRange();
37023
+ caret.setStart(node, 0);
37024
+ caret.collapse(true);
37025
+ sel.removeAllRanges();
37026
+ sel.addRange(caret);
37027
+ if (rule.kind === "list") {
37028
+ const container = block ?? node.parentElement;
37029
+ const list = document.createElement(rule.command === "insertOrderedList" ? "ol" : "ul");
37030
+ const li = document.createElement("li");
37031
+ if (block) {
37032
+ while (block.firstChild) li.appendChild(block.firstChild);
37033
+ list.appendChild(li);
37034
+ block.replaceWith(list);
37035
+ } else if (container) {
37036
+ li.appendChild(node);
37037
+ list.appendChild(li);
37038
+ container.appendChild(list);
37039
+ }
37040
+ if (!li.firstChild) li.appendChild(document.createElement("br"));
37041
+ const liCaret = document.createRange();
37042
+ liCaret.selectNodeContents(li);
37043
+ liCaret.collapse(true);
37044
+ sel.removeAllRanges();
37045
+ sel.addRange(liCaret);
37046
+ } else {
37047
+ document.execCommand("formatBlock", false, rule.tag);
37048
+ }
37049
+ return true;
37050
+ }, []);
37051
+ const afterEdit = React87.useCallback(() => {
37052
+ const el = ref.current;
37053
+ if (!el) return;
37054
+ tryInlineMarkdown();
37055
+ setEmpty(htmlIsEmpty(el));
37056
+ setToolbar(readToolbarState(el));
37057
+ scheduleEmit();
37058
+ }, [scheduleEmit, tryInlineMarkdown]);
37059
+ const handleKeyDown = React87.useCallback((e) => {
37060
+ if (e.key === " " && tryBlockMarkdown()) {
37061
+ e.preventDefault();
37062
+ afterEdit();
37063
+ }
37064
+ }, [afterEdit, tryBlockMarkdown]);
37065
+ const execInline = React87.useCallback((command) => {
37066
+ document.execCommand(command, false);
37067
+ afterEdit();
37068
+ }, [afterEdit]);
37069
+ const execBlock = React87.useCallback((tag) => {
37070
+ const next = toolbar.block === tag ? "p" : tag;
37071
+ document.execCommand("formatBlock", false, next);
37072
+ afterEdit();
37073
+ }, [afterEdit, toolbar.block]);
37074
+ const execList = React87.useCallback((command) => {
37075
+ document.execCommand(command, false);
37076
+ afterEdit();
37077
+ }, [afterEdit]);
37078
+ const execLink = React87.useCallback(() => {
37079
+ if (toolbar.link) {
37080
+ document.execCommand("unlink", false);
37081
+ afterEdit();
37082
+ return;
37083
+ }
37084
+ const url = window.prompt(t("richTextEditor.linkPrompt"));
37085
+ if (!url) return;
37086
+ const safe = safeUrl(url, ["http://", "https://", "mailto:"]) ?? `https://${url}`;
37087
+ document.execCommand("createLink", false, safe);
37088
+ afterEdit();
37089
+ }, [afterEdit, t, toolbar.link]);
37090
+ const execRule = React87.useCallback(() => {
37091
+ document.execCommand("insertHorizontalRule", false);
37092
+ afterEdit();
37093
+ }, [afterEdit]);
37094
+ if (readOnly) {
37095
+ return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: cn("almadar-rich-text max-w-none", className), children: [
37096
+ /* @__PURE__ */ jsxRuntime.jsx(RichTextStyles, {}),
37097
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { dangerouslySetInnerHTML: { __html: sanitizeRichHtml(value ?? "") } })
37098
+ ] });
37099
+ }
37100
+ return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: cn("flex flex-col gap-2", className), children: [
37101
+ /* @__PURE__ */ jsxRuntime.jsx(RichTextStyles, {}),
37102
+ showToolbar && /* @__PURE__ */ jsxRuntime.jsxs(
37103
+ Box,
37104
+ {
37105
+ role: "toolbar",
37106
+ "aria-label": t("richTextEditor.editorToolbar"),
37107
+ className: cn(
37108
+ "sticky top-0 z-10 flex flex-wrap items-center gap-0.5 self-start",
37109
+ "rounded-md border border-border bg-background/95 px-1 py-0.5 shadow-sm"
37110
+ ),
37111
+ children: [
37112
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Bold, label: t("richTextEditor.bold"), active: toolbar.bold, onExec: () => execInline("bold") }),
37113
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Italic, label: t("richTextEditor.italic"), active: toolbar.italic, onExec: () => execInline("italic") }),
37114
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Underline, label: t("richTextEditor.underline"), active: toolbar.underline, onExec: () => execInline("underline") }),
37115
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Strikethrough, label: t("richTextEditor.strikethrough"), active: toolbar.strikeThrough, onExec: () => execInline("strikeThrough") }),
37116
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
37117
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Heading1, label: t("richTextEditor.heading1"), active: toolbar.block === "h1", onExec: () => execBlock("h1") }),
37118
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Heading2, label: t("richTextEditor.heading2"), active: toolbar.block === "h2", onExec: () => execBlock("h2") }),
37119
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Heading3, label: t("richTextEditor.heading3"), active: toolbar.block === "h3", onExec: () => execBlock("h3") }),
37120
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
37121
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.List, label: t("richTextEditor.bulletList"), active: toolbar.bullets, onExec: () => execList("insertUnorderedList") }),
37122
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.ListOrdered, label: t("richTextEditor.numberedList"), active: toolbar.numbers, onExec: () => execList("insertOrderedList") }),
37123
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
37124
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Quote, label: t("richTextEditor.quote"), active: toolbar.block === "blockquote", onExec: () => execBlock("blockquote") }),
37125
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Code, label: t("richTextEditor.code"), active: toolbar.block === "pre", onExec: () => execBlock("pre") }),
37126
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Link, label: toolbar.link ? t("richTextEditor.removeLink") : t("richTextEditor.link"), active: toolbar.link, onExec: execLink }),
37127
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Minus, label: t("richTextEditor.divider"), onExec: execRule })
37128
+ ]
37129
+ }
37130
+ ),
37131
+ /* @__PURE__ */ jsxRuntime.jsx(
37132
+ Box,
37133
+ {
37134
+ ref,
37135
+ contentEditable: true,
37136
+ suppressContentEditableWarning: true,
37137
+ role: "textbox",
37138
+ "aria-multiline": "true",
37139
+ "aria-label": t("richTextEditor.editorSurface"),
37140
+ "data-placeholder": placeholder ?? t("richTextEditor.placeholder"),
37141
+ "data-empty": empty ? "true" : "false",
37142
+ className: cn(
37143
+ "almadar-rich-text max-w-none min-h-[8rem] outline-none",
37144
+ "data-[empty=true]:before:content-[attr(data-placeholder)]",
37145
+ "data-[empty=true]:before:text-muted-foreground/60",
37146
+ "data-[empty=true]:before:pointer-events-none",
37147
+ "data-[empty=true]:before:absolute",
37148
+ "relative cursor-text"
37149
+ ),
37150
+ onInput: afterEdit,
37151
+ onKeyDown: handleKeyDown,
37152
+ onBlur: flushEmit
37153
+ }
37154
+ )
37155
+ ] });
36826
37156
  };
37157
+ RichTextEditor.displayName = "RichTextEditor";
36827
37158
  }
36828
- const seed = block.children?.[0]?.content ?? block.content ?? "";
36829
- return { id: block.id, type, content: seed };
37159
+ });
37160
+ function renderIconInput4(icon, props) {
37161
+ return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
36830
37162
  }
36831
- function BlockMenu({ block, readOnly, onDelete, onDuplicate, onChangeType }) {
37163
+ function DocumentPanel({
37164
+ entity,
37165
+ title,
37166
+ subtitle,
37167
+ value,
37168
+ actions,
37169
+ maxInlineActions,
37170
+ editing = false,
37171
+ titleCommitEvent,
37172
+ contentChangeEvent,
37173
+ editEvent,
37174
+ doneEvent,
37175
+ editLabel,
37176
+ doneLabel,
37177
+ autosaveHint,
37178
+ placeholder,
37179
+ className
37180
+ }) {
37181
+ const eventBus = useEventBus();
36832
37182
  const { t } = hooks.useTranslate();
36833
- const [open, setOpen] = React87.useState(false);
36834
- const ref = React87.useRef(null);
36835
- React87.useEffect(() => {
36836
- if (!open) return;
36837
- function onDocClick(e) {
36838
- if (ref.current && !ref.current.contains(e.target)) {
36839
- setOpen(false);
36840
- }
37183
+ const [titleDraft, setTitleDraft] = React87.useState(null);
37184
+ const recordId = entity?.id !== void 0 && entity?.id !== null ? String(entity.id) : "";
37185
+ const actionDefs = actions ?? [];
37186
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
37187
+ const inlineActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
37188
+ const menuActions = actionDefs.filter((a) => !inlineActions.includes(a));
37189
+ const fireAction = (action) => {
37190
+ if (!action.event) return;
37191
+ eventBus.emit(`UI:${action.event}`, { id: recordId, row: entity });
37192
+ };
37193
+ const commitTitle = () => {
37194
+ if (!titleCommitEvent) return;
37195
+ const next = (titleDraft ?? "").trim();
37196
+ setTitleDraft(null);
37197
+ if (!next || next === title) return;
37198
+ eventBus.emit(`UI:${titleCommitEvent}`, { title: next, id: recordId });
37199
+ };
37200
+ const untitled = t("documentPanel.untitled") || "Untitled";
37201
+ const titleNode = titleCommitEvent && titleDraft !== null ? /* @__PURE__ */ jsxRuntime.jsx(
37202
+ Input,
37203
+ {
37204
+ value: titleDraft,
37205
+ autoFocus: true,
37206
+ "aria-label": t("common.title"),
37207
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
37208
+ onFocus: (e) => e.currentTarget.select(),
37209
+ onChange: (e) => setTitleDraft(e.target.value),
37210
+ onBlur: commitTitle,
37211
+ onKeyDown: (e) => {
37212
+ if (e.key === "Enter") {
37213
+ e.preventDefault();
37214
+ commitTitle();
37215
+ } else if (e.key === "Escape") {
37216
+ e.preventDefault();
37217
+ setTitleDraft(null);
37218
+ }
37219
+ },
37220
+ "data-testid": "document-title-input"
36841
37221
  }
36842
- document.addEventListener("mousedown", onDocClick);
36843
- return () => document.removeEventListener("mousedown", onDocClick);
36844
- }, [open]);
36845
- if (readOnly) return null;
36846
- return /* @__PURE__ */ jsxRuntime.jsxs(Box, { ref, className: "relative", children: [
36847
- /* @__PURE__ */ jsxRuntime.jsx(
36848
- Button,
36849
- {
36850
- type: "button",
36851
- variant: "ghost",
36852
- "aria-label": t("richBlockEditor.blockActions"),
36853
- className: cn(
36854
- "inline-flex items-center justify-center",
36855
- "h-6 w-6 rounded-sm p-0 gap-0",
36856
- "text-muted-foreground hover:bg-muted hover:text-foreground",
36857
- "opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
36858
- "transition-opacity"
36859
- ),
36860
- onClick: () => setOpen((v) => !v),
36861
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", className: "w-3.5 h-3.5" })
36862
- }
36863
- ),
36864
- open && /* @__PURE__ */ jsxRuntime.jsxs(
37222
+ ) : titleCommitEvent ? /* @__PURE__ */ jsxRuntime.jsx(
37223
+ Box,
37224
+ {
37225
+ role: "button",
37226
+ tabIndex: 0,
37227
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
37228
+ onClick: () => setTitleDraft(title ?? ""),
37229
+ onKeyDown: (e) => {
37230
+ if (e.key === "Enter" || e.key === " ") {
37231
+ e.preventDefault();
37232
+ setTitleDraft(title ?? "");
37233
+ }
37234
+ },
37235
+ "data-testid": "document-title-editable",
37236
+ children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", className: cn(!title && "text-muted-foreground"), children: title || untitled })
37237
+ }
37238
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", children: title || untitled });
37239
+ const emitWithId = (event) => () => {
37240
+ if (event) eventBus.emit(`UI:${event}`, { id: recordId });
37241
+ };
37242
+ 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: [
37243
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", className: "justify-between items-start", children: [
37244
+ /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", className: "flex-1 min-w-0", children: [
37245
+ titleNode,
37246
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: subtitle }) : null
37247
+ ] }),
37248
+ /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", className: "flex-shrink-0 pt-1 items-center", children: editing ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
37249
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", className: "hidden sm:block", children: autosaveHint ?? (t("documentPanel.autosaveHint") || "") }),
37250
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", size: "sm", onClick: emitWithId(doneEvent), "data-testid": "document-done", children: doneLabel ?? (t("documentPanel.done") || "Done") })
37251
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
37252
+ editEvent && /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "ghost", size: "sm", onClick: emitWithId(editEvent), "data-testid": "document-edit", children: [
37253
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "edit", size: "xs", className: "mr-1" }),
37254
+ editLabel ?? (t("documentPanel.edit") || "Edit")
37255
+ ] }),
37256
+ inlineActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
37257
+ Button,
37258
+ {
37259
+ variant: "primary",
37260
+ size: "sm",
37261
+ onClick: () => fireAction(action),
37262
+ "data-testid": `action-${action.event}`,
37263
+ children: [
37264
+ action.icon && renderIconInput4(action.icon, { size: "xs", className: "mr-1" }),
37265
+ action.label
37266
+ ]
37267
+ },
37268
+ idx
37269
+ )),
37270
+ menuActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
37271
+ Menu,
37272
+ {
37273
+ position: "bottom-end",
37274
+ 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" }) }),
37275
+ items: menuActions.map((action) => ({
37276
+ label: action.label,
37277
+ icon: action.icon,
37278
+ variant: action.variant === "danger" ? "danger" : "default",
37279
+ onClick: () => fireAction(action)
37280
+ }))
37281
+ }
37282
+ )
37283
+ ] }) })
37284
+ ] }),
37285
+ editing ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsxRuntime.jsx(
36865
37286
  Box,
36866
37287
  {
36867
- role: "menu",
37288
+ onClick: emitWithId(editEvent),
36868
37289
  className: cn(
36869
- "absolute right-0 z-10 mt-1 w-44",
36870
- "rounded-container border border-border bg-popover shadow-elevation-popover",
36871
- "py-1 text-sm"
37290
+ "rounded-md px-1 -mx-1 min-h-[16rem]",
37291
+ editEvent && "cursor-text transition-colors hover:bg-muted/30"
36872
37292
  ),
36873
- children: [
36874
- /* @__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]) }),
36875
- /* @__PURE__ */ jsxRuntime.jsxs(
36876
- Button,
36877
- {
36878
- type: "button",
36879
- variant: "ghost",
36880
- role: "menuitem",
36881
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
36882
- onClick: () => {
36883
- onDuplicate();
36884
- setOpen(false);
36885
- },
36886
- children: [
36887
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "plus", className: "w-3.5 h-3.5" }),
36888
- " ",
36889
- t("richBlockEditor.duplicate")
36890
- ]
36891
- }
36892
- ),
36893
- /* @__PURE__ */ jsxRuntime.jsxs(
36894
- Button,
36895
- {
36896
- type: "button",
36897
- variant: "ghost",
36898
- role: "menuitem",
36899
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-destructive hover:bg-muted justify-start rounded-none",
36900
- onClick: () => {
36901
- onDelete();
36902
- setOpen(false);
36903
- },
36904
- children: [
36905
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "trash", className: "w-3.5 h-3.5" }),
36906
- " ",
36907
- t("common.delete")
36908
- ]
36909
- }
36910
- ),
36911
- CHANGEABLE_TYPES.includes(block.type) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
36912
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "my-1 border-t border-border" }),
36913
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t("richBlockEditor.turnInto") }),
36914
- CHANGEABLE_TYPES.filter((bt) => bt !== block.type).map((bt) => /* @__PURE__ */ jsxRuntime.jsx(
36915
- Button,
36916
- {
36917
- type: "button",
36918
- variant: "ghost",
36919
- role: "menuitem",
36920
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
36921
- onClick: () => {
36922
- onChangeType(bt);
36923
- setOpen(false);
36924
- },
36925
- children: t(BLOCK_TYPE_LABEL_KEY[bt])
36926
- },
36927
- bt
36928
- ))
36929
- ] })
36930
- ]
37293
+ "data-testid": "document-body",
37294
+ children: value && value !== "" ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, readOnly: true }) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "muted", children: placeholder ?? (t("documentPanel.placeholder") || "") })
36931
37295
  }
36932
37296
  )
36933
- ] });
37297
+ ] }) });
36934
37298
  }
36935
- function Editable({
36936
- tag,
36937
- value,
36938
- readOnly,
36939
- placeholder,
36940
- className,
36941
- ariaLabel,
36942
- onValueChange
36943
- }) {
36944
- const ref = React87.useRef(null);
36945
- React87.useEffect(() => {
36946
- const el = ref.current;
36947
- if (!el) return;
36948
- const isFocused = document.activeElement === el;
36949
- if (!isFocused && el.textContent !== value) {
36950
- el.textContent = value;
36951
- }
36952
- }, [value]);
36953
- const handleInput = React87.useCallback(
36954
- (e) => {
36955
- onValueChange(e.currentTarget.textContent ?? "");
36956
- },
36957
- [onValueChange]
36958
- );
36959
- return /* @__PURE__ */ jsxRuntime.jsx(
36960
- Box,
36961
- {
36962
- as: tag,
36963
- ref,
36964
- contentEditable: !readOnly,
36965
- suppressContentEditableWarning: true,
36966
- role: readOnly ? void 0 : "textbox",
36967
- "aria-label": ariaLabel,
36968
- "aria-multiline": "true",
36969
- "data-placeholder": placeholder,
36970
- className: cn(
36971
- "outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-sm",
36972
- "empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground/60",
36973
- className
36974
- ),
36975
- onInput: handleInput
36976
- }
36977
- );
37299
+ var init_DocumentPanel = __esm({
37300
+ "components/core/molecules/DocumentPanel.tsx"() {
37301
+ "use client";
37302
+ init_cn();
37303
+ init_useEventBus();
37304
+ init_Box();
37305
+ init_Stack();
37306
+ init_Typography();
37307
+ init_Button();
37308
+ init_Card();
37309
+ init_Icon();
37310
+ init_Input();
37311
+ init_Menu();
37312
+ init_RichTextEditor();
37313
+ }
37314
+ });
37315
+ function renderIconInput5(icon, props) {
37316
+ return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
36978
37317
  }
36979
- function BlockRow({
36980
- block,
36981
- readOnly,
36982
- showAffordances,
36983
- placeholder,
36984
- onUpdate,
36985
- onDelete,
36986
- onDuplicate,
36987
- onChangeType
37318
+ function fieldName(field) {
37319
+ return field.name ?? field.key ?? "";
37320
+ }
37321
+ function relationLabel(value) {
37322
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Date) return null;
37323
+ for (const key of ["name", "title", "label"]) {
37324
+ const candidate = value[key];
37325
+ if (typeof candidate === "string" && candidate !== "") return candidate;
37326
+ }
37327
+ const id = value.id;
37328
+ return id !== void 0 && id !== null ? String(id) : null;
37329
+ }
37330
+ function fieldKind(field, value) {
37331
+ if (field.kind) return field.kind;
37332
+ if (field.options && field.options.length > 0) return "select";
37333
+ if (typeof value === "boolean" || field.format === "boolean") return "boolean";
37334
+ if (Array.isArray(value) || relationLabel(value ?? null) !== null) return "readonly";
37335
+ return "text";
37336
+ }
37337
+ function DocumentDetails({
37338
+ entity,
37339
+ fields,
37340
+ metaCommitEvent,
37341
+ title,
37342
+ className
36988
37343
  }) {
37344
+ const eventBus = useEventBus();
36989
37345
  const { t } = hooks.useTranslate();
36990
- const setContent = React87.useCallback(
36991
- (next) => onUpdate((b) => ({ ...b, content: next })),
36992
- [onUpdate]
36993
- );
36994
- const setMetadata = React87.useCallback(
36995
- (key, value) => onUpdate((b) => ({
36996
- ...b,
36997
- metadata: { ...b.metadata ?? {}, [key]: value }
36998
- })),
36999
- [onUpdate]
37000
- );
37001
- const setChildContent = React87.useCallback(
37002
- (childId, next) => onUpdate((b) => ({
37003
- ...b,
37004
- children: (b.children ?? []).map(
37005
- (c) => c.id === childId ? { ...c, content: next } : c
37006
- )
37007
- })),
37008
- [onUpdate]
37009
- );
37010
- const addListItem = React87.useCallback(
37011
- () => onUpdate((b) => ({
37012
- ...b,
37013
- children: [
37014
- ...b.children ?? [],
37015
- { id: nextBlockId("li"), type: "paragraph", content: "" }
37016
- ]
37017
- })),
37018
- [onUpdate]
37019
- );
37020
- const removeListItem = React87.useCallback(
37021
- (childId) => onUpdate((b) => {
37022
- const remaining = (b.children ?? []).filter((c) => c.id !== childId);
37023
- return {
37024
- ...b,
37025
- children: remaining.length === 0 ? [{ id: nextBlockId("li"), type: "paragraph", content: "" }] : remaining
37026
- };
37027
- }),
37028
- [onUpdate]
37029
- );
37030
- const renderBody = () => {
37031
- switch (block.type) {
37032
- case "heading-1":
37033
- return /* @__PURE__ */ jsxRuntime.jsx(
37034
- Editable,
37035
- {
37036
- tag: "h1",
37037
- value: block.content ?? "",
37038
- readOnly,
37039
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading1"),
37040
- ariaLabel: t("richBlockEditor.aria.heading1Block"),
37041
- className: "text-3xl font-bold leading-tight",
37042
- onValueChange: setContent
37043
- }
37044
- );
37045
- case "heading-2":
37046
- return /* @__PURE__ */ jsxRuntime.jsx(
37047
- Editable,
37048
- {
37049
- tag: "h2",
37050
- value: block.content ?? "",
37051
- readOnly,
37052
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading2"),
37053
- ariaLabel: t("richBlockEditor.aria.heading2Block"),
37054
- className: "text-2xl font-semibold leading-tight",
37055
- onValueChange: setContent
37056
- }
37057
- );
37058
- case "heading-3":
37059
- return /* @__PURE__ */ jsxRuntime.jsx(
37060
- Editable,
37061
- {
37062
- tag: "h3",
37063
- value: block.content ?? "",
37064
- readOnly,
37065
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading3"),
37066
- ariaLabel: t("richBlockEditor.aria.heading3Block"),
37067
- className: "text-xl font-semibold leading-tight",
37068
- onValueChange: setContent
37069
- }
37070
- );
37071
- case "quote":
37072
- return /* @__PURE__ */ jsxRuntime.jsx(
37073
- Editable,
37074
- {
37075
- tag: "blockquote",
37076
- value: block.content ?? "",
37077
- readOnly,
37078
- placeholder: placeholder ?? t("richBlockEditor.placeholder.quote"),
37079
- ariaLabel: t("richBlockEditor.aria.quoteBlock"),
37080
- className: "border-l-4 border-primary/60 pl-4 italic text-muted-foreground",
37081
- onValueChange: setContent
37082
- }
37083
- );
37084
- case "code":
37085
- return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "rounded-md border border-border bg-muted/40", children: [
37086
- /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex items-center justify-between border-b border-border px-3 py-1 text-xs text-muted-foreground", children: [
37087
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", variant: "caption", className: "uppercase tracking-wide", children: t("richBlockEditor.blockType.code") }),
37088
- !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
37089
- Input,
37090
- {
37091
- inputType: "text",
37092
- value: String(block.metadata?.language ?? "plaintext"),
37093
- "aria-label": t("richBlockEditor.aria.codeLanguage"),
37094
- className: cn(
37095
- "h-6 w-32 rounded-sm border border-border bg-background",
37096
- "px-2 text-xs outline-none focus:ring-1 focus:ring-ring"
37097
- ),
37098
- onChange: (e) => setMetadata("language", e.target.value)
37099
- }
37100
- ),
37101
- readOnly && /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", variant: "caption", className: "text-xs", children: String(block.metadata?.language ?? "plaintext") })
37102
- ] }),
37103
- /* @__PURE__ */ jsxRuntime.jsx(
37104
- Editable,
37105
- {
37106
- tag: "pre",
37107
- value: block.content ?? "",
37108
- readOnly,
37109
- placeholder: placeholder ?? t("richBlockEditor.placeholder.code"),
37110
- ariaLabel: t("richBlockEditor.aria.codeBlock"),
37111
- className: "block whitespace-pre-wrap p-3 font-mono text-sm leading-relaxed",
37112
- onValueChange: setContent
37113
- }
37114
- )
37115
- ] });
37116
- case "divider":
37117
- return /* @__PURE__ */ jsxRuntime.jsx(Divider, { className: "my-2" });
37118
- case "image": {
37119
- const url = String(block.metadata?.url ?? "");
37120
- const caption = String(block.metadata?.caption ?? "");
37121
- const imgProps = {
37122
- src: url,
37123
- alt: caption || t("richBlockEditor.embeddedImage"),
37124
- className: "max-h-96 w-full rounded-md border border-border object-contain"
37125
- };
37126
- return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "space-y-2", children: [
37127
- url ? /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "img", ...imgProps }) : /* @__PURE__ */ jsxRuntime.jsxs(
37128
- Box,
37129
- {
37130
- className: cn(
37131
- "flex h-32 items-center justify-center",
37132
- "rounded-md border border-dashed border-border",
37133
- "text-sm text-muted-foreground"
37134
- ),
37135
- children: [
37136
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "image", className: "mr-2 w-4 h-4" }),
37137
- " ",
37138
- t("richBlockEditor.noImageUrl")
37139
- ]
37140
- }
37141
- ),
37142
- !readOnly && /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex flex-col gap-2 sm:flex-row", children: [
37143
- /* @__PURE__ */ jsxRuntime.jsx(
37144
- Input,
37145
- {
37146
- inputType: "url",
37147
- value: url,
37148
- placeholder: "https://example.com/image.png",
37149
- "aria-label": t("richBlockEditor.aria.imageUrl"),
37150
- className: cn(
37151
- "h-8 flex-1 rounded-sm border border-border bg-background",
37152
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
37153
- ),
37154
- onChange: (e) => setMetadata("url", e.target.value)
37155
- }
37156
- ),
37157
- /* @__PURE__ */ jsxRuntime.jsx(
37158
- Input,
37159
- {
37160
- inputType: "text",
37161
- value: caption,
37162
- placeholder: t("richBlockEditor.placeholder.caption"),
37163
- "aria-label": t("richBlockEditor.aria.imageCaption"),
37164
- className: cn(
37165
- "h-8 flex-1 rounded-sm border border-border bg-background",
37166
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
37167
- ),
37168
- onChange: (e) => setMetadata("caption", e.target.value)
37169
- }
37170
- )
37171
- ] }),
37172
- readOnly && caption && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-center text-muted-foreground", children: caption })
37173
- ] });
37346
+ const [fieldDraft, setFieldDraft] = React87.useState(null);
37347
+ const recordId = entity?.id !== void 0 && entity?.id !== null ? String(entity.id) : "";
37348
+ const fieldDefs = (fields ?? []).filter((f3) => fieldName(f3) !== "");
37349
+ if (fieldDefs.length === 0) return null;
37350
+ const commitField = (name, next, current) => {
37351
+ setFieldDraft(null);
37352
+ if (!metaCommitEvent || next === current) return;
37353
+ eventBus.emit(`UI:${metaCommitEvent}`, { id: recordId, patch: { id: recordId, [name]: next } });
37354
+ };
37355
+ const renderValue = (field) => {
37356
+ const name = fieldName(field);
37357
+ const raw = getNestedValue(entity ?? {}, name);
37358
+ const kind = fieldKind(field, raw);
37359
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
37360
+ if (kind === "boolean") {
37361
+ return /* @__PURE__ */ jsxRuntime.jsx(
37362
+ Switch,
37363
+ {
37364
+ checked: Boolean(raw),
37365
+ disabled: !metaCommitEvent,
37366
+ "aria-label": label,
37367
+ onChange: (checked) => commitField(name, checked, Boolean(raw))
37368
+ }
37369
+ );
37370
+ }
37371
+ if (kind === "select") {
37372
+ return /* @__PURE__ */ jsxRuntime.jsx(
37373
+ Select,
37374
+ {
37375
+ value: raw !== void 0 && raw !== null ? String(raw) : "",
37376
+ disabled: !metaCommitEvent,
37377
+ "aria-label": label,
37378
+ className: "h-8 w-full",
37379
+ options: (field.options ?? []).map((opt) => ({ value: opt, label: humanizeEnumValue(opt) })),
37380
+ onChange: (e) => commitField(name, e.target.value, raw)
37381
+ }
37382
+ );
37383
+ }
37384
+ if (kind === "readonly" || !metaCommitEvent) {
37385
+ if (Array.isArray(raw)) {
37386
+ if (raw.length === 0) {
37387
+ return /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
37388
+ }
37389
+ 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)) });
37174
37390
  }
37175
- case "bullet-list":
37176
- case "numbered-list": {
37177
- const items = block.children ?? [];
37178
- return /* @__PURE__ */ jsxRuntime.jsxs(
37179
- Box,
37180
- {
37181
- as: block.type === "bullet-list" ? "ul" : "ol",
37182
- className: cn(
37183
- "space-y-1 pl-6",
37184
- block.type === "bullet-list" ? "list-disc" : "list-decimal"
37185
- ),
37186
- children: [
37187
- items.map((child) => /* @__PURE__ */ jsxRuntime.jsxs(Box, { as: "li", className: "group/item flex items-start gap-2", children: [
37188
- /* @__PURE__ */ jsxRuntime.jsx(
37189
- Editable,
37190
- {
37191
- tag: "span",
37192
- value: child.content ?? "",
37193
- readOnly,
37194
- placeholder: t("richBlockEditor.placeholder.listItem"),
37195
- ariaLabel: t("richBlockEditor.aria.listItem"),
37196
- className: "inline-block min-w-[1ch] flex-1",
37197
- onValueChange: (next) => setChildContent(child.id, next)
37198
- }
37199
- ),
37200
- !readOnly && showAffordances && /* @__PURE__ */ jsxRuntime.jsx(
37201
- Button,
37202
- {
37203
- type: "button",
37204
- variant: "ghost",
37205
- "aria-label": t("richBlockEditor.aria.removeListItem"),
37206
- className: cn(
37207
- "h-5 w-5 shrink-0 rounded-sm text-muted-foreground p-0 gap-0",
37208
- "opacity-0 group-hover/item:opacity-100 hover:bg-muted hover:text-foreground"
37209
- ),
37210
- onClick: () => removeListItem(child.id),
37211
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "trash", className: "w-3 h-3" })
37212
- }
37213
- )
37214
- ] }, child.id)),
37215
- !readOnly && showAffordances && /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "li", className: "list-none pl-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
37216
- Button,
37217
- {
37218
- type: "button",
37219
- variant: "ghost",
37220
- className: cn(
37221
- "inline-flex items-center gap-1 text-xs text-muted-foreground",
37222
- "hover:text-foreground p-0 h-auto"
37223
- ),
37224
- onClick: addListItem,
37225
- children: [
37226
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "plus", className: "w-3 h-3" }),
37227
- " ",
37228
- t("richBlockEditor.addItem")
37229
- ]
37230
- }
37231
- ) })
37232
- ]
37391
+ const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
37392
+ return /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", className: "break-words", children: shown2 });
37393
+ }
37394
+ if (fieldDraft?.name === name) {
37395
+ return /* @__PURE__ */ jsxRuntime.jsx(
37396
+ Input,
37397
+ {
37398
+ value: fieldDraft.value,
37399
+ autoFocus: true,
37400
+ "aria-label": label,
37401
+ className: "h-8 w-full",
37402
+ onChange: (e) => setFieldDraft({ name, value: e.target.value }),
37403
+ onBlur: () => commitField(name, fieldDraft.value.trim(), raw),
37404
+ onKeyDown: (e) => {
37405
+ if (e.key === "Enter") {
37406
+ e.preventDefault();
37407
+ commitField(name, fieldDraft.value.trim(), raw);
37408
+ } else if (e.key === "Escape") {
37409
+ e.preventDefault();
37410
+ setFieldDraft(null);
37411
+ }
37412
+ },
37413
+ "data-testid": `document-property-input-${name}`
37414
+ }
37415
+ );
37416
+ }
37417
+ const shown = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
37418
+ return /* @__PURE__ */ jsxRuntime.jsx(
37419
+ Box,
37420
+ {
37421
+ role: "button",
37422
+ tabIndex: 0,
37423
+ className: cn(
37424
+ "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
37425
+ (raw === void 0 || raw === null || raw === "") && "text-muted-foreground"
37426
+ ),
37427
+ onClick: () => setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" }),
37428
+ onKeyDown: (e) => {
37429
+ if (e.key === "Enter" || e.key === " ") {
37430
+ e.preventDefault();
37431
+ setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" });
37233
37432
  }
37234
- );
37433
+ },
37434
+ "data-testid": `document-property-${name}`,
37435
+ children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", className: "break-words", children: shown })
37235
37436
  }
37236
- case "paragraph":
37237
- default:
37238
- return /* @__PURE__ */ jsxRuntime.jsx(
37239
- Editable,
37240
- {
37241
- tag: "p",
37242
- value: block.content ?? "",
37243
- readOnly,
37244
- placeholder: placeholder ?? t("richBlockEditor.placeholder.paragraph"),
37245
- ariaLabel: t("richBlockEditor.aria.paragraphBlock"),
37246
- className: "leading-7",
37247
- onValueChange: setContent
37248
- }
37249
- );
37250
- }
37437
+ );
37251
37438
  };
37252
- return /* @__PURE__ */ jsxRuntime.jsxs(
37253
- Box,
37254
- {
37255
- className: cn(
37256
- "group relative flex items-start gap-2 rounded-sm",
37257
- "px-2 py-1 hover:bg-muted/30"
37258
- ),
37259
- "data-block-id": block.id,
37260
- "data-block-type": block.type,
37261
- children: [
37262
- !readOnly && showAffordances && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex w-8 shrink-0 items-center pt-1", children: /* @__PURE__ */ jsxRuntime.jsx(
37263
- BlockMenu,
37264
- {
37265
- block,
37266
- readOnly,
37267
- onDelete,
37268
- onDuplicate,
37269
- onChangeType
37270
- }
37271
- ) }),
37272
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "min-w-0 flex-1", children: renderBody() })
37273
- ]
37274
- }
37275
- );
37439
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { variant: "bordered", className: cn("w-full", className), children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", className: "p-4", children: [
37440
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "secondary", weight: "medium", className: "uppercase tracking-wide", children: title ?? (t("documentDetails.title") || "Details") }),
37441
+ /* @__PURE__ */ jsxRuntime.jsx(VStack, { gap: "sm", children: fieldDefs.map((field) => {
37442
+ const name = fieldName(field);
37443
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
37444
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", children: [
37445
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", className: "items-center", children: [
37446
+ field.icon && renderIconInput5(field.icon, { size: "xs", className: "text-muted-foreground" }),
37447
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", children: label })
37448
+ ] }),
37449
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "min-w-0", children: renderValue(field) })
37450
+ ] }, name);
37451
+ }) })
37452
+ ] }) });
37276
37453
  }
37277
- var TOOLBAR_ENTRIES, BLOCK_TYPE_LABEL_KEY, CHANGEABLE_TYPES, _idSeq, BLOCK_TYPES, RichBlockEditor;
37278
- var init_RichBlockEditor = __esm({
37279
- "components/core/molecules/RichBlockEditor.tsx"() {
37454
+ var init_DocumentDetails = __esm({
37455
+ "components/core/molecules/DocumentDetails.tsx"() {
37280
37456
  "use client";
37281
37457
  init_cn();
37282
- init_Card();
37283
- init_Typography();
37284
- init_Button();
37458
+ init_format();
37459
+ init_getNestedValue();
37460
+ init_useEventBus();
37285
37461
  init_Box();
37286
- init_Divider();
37287
- init_Input();
37462
+ init_Stack();
37463
+ init_Typography();
37464
+ init_Badge();
37465
+ init_Card();
37288
37466
  init_Icon();
37289
- init_useEventBus();
37290
- TOOLBAR_ENTRIES = [
37291
- { type: "paragraph", labelKey: "richBlockEditor.toolbar.text", icon: LucideIcons2.Type },
37292
- { type: "heading-1", labelKey: "richBlockEditor.toolbar.h1", icon: LucideIcons2.Heading1 },
37293
- { type: "heading-2", labelKey: "richBlockEditor.toolbar.h2", icon: LucideIcons2.Heading2 },
37294
- { type: "heading-3", labelKey: "richBlockEditor.toolbar.h3", icon: LucideIcons2.Heading3 },
37295
- { type: "bullet-list", labelKey: "richBlockEditor.toolbar.bulletList", icon: LucideIcons2.List },
37296
- { type: "numbered-list", labelKey: "richBlockEditor.toolbar.numbered", icon: LucideIcons2.ListOrdered },
37297
- { type: "quote", labelKey: "richBlockEditor.toolbar.quote", icon: LucideIcons2.Quote },
37298
- { type: "code", labelKey: "richBlockEditor.toolbar.code", icon: LucideIcons2.Code },
37299
- { type: "divider", labelKey: "richBlockEditor.toolbar.divider", icon: LucideIcons2.Minus },
37300
- { type: "image", labelKey: "richBlockEditor.toolbar.image", icon: LucideIcons2.Image }
37301
- ];
37302
- BLOCK_TYPE_LABEL_KEY = {
37303
- paragraph: "richBlockEditor.blockType.paragraph",
37304
- "heading-1": "richBlockEditor.blockType.heading1",
37305
- "heading-2": "richBlockEditor.blockType.heading2",
37306
- "heading-3": "richBlockEditor.blockType.heading3",
37307
- "bullet-list": "richBlockEditor.blockType.bulletList",
37308
- "numbered-list": "richBlockEditor.blockType.numberedList",
37309
- quote: "richBlockEditor.blockType.quote",
37310
- code: "richBlockEditor.blockType.code",
37311
- divider: "richBlockEditor.blockType.divider",
37312
- image: "richBlockEditor.blockType.image"
37313
- };
37314
- CHANGEABLE_TYPES = [
37315
- "paragraph",
37316
- "heading-1",
37317
- "heading-2",
37318
- "heading-3",
37319
- "bullet-list",
37320
- "numbered-list",
37321
- "quote",
37322
- "code"
37323
- ];
37324
- _idSeq = 0;
37325
- BLOCK_TYPES = /* @__PURE__ */ new Set([
37326
- "paragraph",
37327
- "heading-1",
37328
- "heading-2",
37329
- "heading-3",
37330
- "bullet-list",
37331
- "numbered-list",
37332
- "quote",
37333
- "code",
37334
- "divider",
37335
- "image"
37336
- ]);
37337
- RichBlockEditor = ({
37338
- initialBlocks,
37339
- onChange,
37340
- changeEvent,
37341
- readOnly = false,
37342
- placeholder,
37343
- enableBlocks = true,
37344
- showToolbar = true,
37345
- className
37346
- }) => {
37347
- const { t } = hooks.useTranslate();
37348
- const [blocks, setBlocks] = React87.useState(
37349
- () => normalizeBlocks(initialBlocks)
37350
- );
37351
- const onChangeRef = React87.useRef(onChange);
37352
- React87.useEffect(() => {
37353
- onChangeRef.current = onChange;
37354
- }, [onChange]);
37355
- const eventBus = useEventBus();
37356
- const changeEventRef = React87.useRef(changeEvent);
37357
- React87.useEffect(() => {
37358
- changeEventRef.current = changeEvent;
37359
- }, [changeEvent]);
37360
- const commit = React87.useCallback((next) => {
37361
- setBlocks(next);
37362
- onChangeRef.current?.(next);
37363
- const evt = changeEventRef.current;
37364
- if (evt) eventBus.emit(`UI:${evt}`, { blocks: next });
37365
- }, [eventBus]);
37366
- const handleAppend = React87.useCallback(
37367
- (type) => {
37368
- if (readOnly) return;
37369
- commit([...blocks, createBlock(type)]);
37370
- },
37371
- [blocks, commit, readOnly]
37372
- );
37373
- const handleUpdate = React87.useCallback(
37374
- (id, updater) => {
37375
- commit(replaceBlock(blocks, id, updater));
37376
- },
37377
- [blocks, commit]
37378
- );
37379
- const handleDelete = React87.useCallback(
37380
- (id) => {
37381
- const next = removeBlock(blocks, id);
37382
- commit(next.length > 0 ? next : [createBlock("paragraph")]);
37383
- },
37384
- [blocks, commit]
37385
- );
37386
- const handleDuplicate = React87.useCallback(
37387
- (id) => {
37388
- const target = blocks.find((b) => b.id === id);
37389
- if (!target) return;
37390
- commit(insertAfter(blocks, id, duplicateBlock(target)));
37391
- },
37392
- [blocks, commit]
37393
- );
37394
- const handleChangeType = React87.useCallback(
37395
- (id, type) => {
37396
- commit(
37397
- replaceBlock(blocks, id, (b) => changeBlockType(b, type))
37398
- );
37399
- },
37400
- [blocks, commit]
37401
- );
37402
- return /* @__PURE__ */ jsxRuntime.jsxs(
37403
- Card,
37404
- {
37405
- variant: "bordered",
37406
- padding: "none",
37407
- className: cn("flex flex-col text-card-foreground", className),
37408
- children: [
37409
- enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
37410
- Box,
37411
- {
37412
- role: "toolbar",
37413
- "aria-label": t("richBlockEditor.editorToolbar"),
37414
- className: cn(
37415
- "flex flex-wrap items-center gap-1",
37416
- "border-b border-border bg-muted/30 px-2 py-2"
37417
- ),
37418
- children: TOOLBAR_ENTRIES.map((entry) => {
37419
- const Icon2 = entry.icon;
37420
- const entryLabel = t(entry.labelKey);
37421
- return /* @__PURE__ */ jsxRuntime.jsxs(
37422
- Button,
37423
- {
37424
- type: "button",
37425
- variant: "ghost",
37426
- size: "sm",
37427
- "aria-label": t("richBlockEditor.insertEntry", { label: entryLabel }),
37428
- title: entryLabel,
37429
- onClick: () => handleAppend(entry.type),
37430
- children: [
37431
- /* @__PURE__ */ jsxRuntime.jsx(Icon2, { size: 14 }),
37432
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", variant: "caption", className: "ml-1 hidden text-xs sm:inline", children: entryLabel })
37433
- ]
37434
- },
37435
- entry.type
37436
- );
37437
- })
37438
- }
37439
- ),
37440
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex flex-col gap-1 px-3 py-3", children: blocks.map((block) => /* @__PURE__ */ jsxRuntime.jsx(
37441
- BlockRow,
37442
- {
37443
- block,
37444
- readOnly,
37445
- showAffordances: enableBlocks,
37446
- placeholder,
37447
- onUpdate: (updater) => handleUpdate(block.id, updater),
37448
- onDelete: () => handleDelete(block.id),
37449
- onDuplicate: () => handleDuplicate(block.id),
37450
- onChangeType: (type) => handleChangeType(block.id, type)
37451
- },
37452
- block.id
37453
- )) })
37454
- ]
37455
- }
37456
- );
37457
- };
37458
- RichBlockEditor.displayName = "RichBlockEditor";
37467
+ init_Input();
37468
+ init_Select();
37469
+ init_Switch();
37459
37470
  }
37460
37471
  });
37461
37472
  function collectInitiallyCollapsed(nodes, acc) {
@@ -42404,8 +42415,8 @@ var init_DataTable = __esm({
42404
42415
  DataTable.displayName = "DataTable";
42405
42416
  }
42406
42417
  });
42407
- function getBadgeVariant(fieldName, value) {
42408
- const name = fieldName.toLowerCase();
42418
+ function getBadgeVariant(fieldName2, value) {
42419
+ const name = fieldName2.toLowerCase();
42409
42420
  const val = String(value).toLowerCase();
42410
42421
  if (name.includes("status")) {
42411
42422
  if (val.includes("complete") || val.includes("done") || val.includes("active"))
@@ -42421,12 +42432,12 @@ function getBadgeVariant(fieldName, value) {
42421
42432
  }
42422
42433
  return "default";
42423
42434
  }
42424
- function formatFieldValue2(value, fieldName) {
42435
+ function formatFieldValue2(value, fieldName2) {
42425
42436
  if (typeof value === "number") {
42426
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
42437
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
42427
42438
  return `${value}%`;
42428
42439
  }
42429
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
42440
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
42430
42441
  return `$${value.toLocaleString()}`;
42431
42442
  }
42432
42443
  return value.toLocaleString();
@@ -42436,7 +42447,7 @@ function formatFieldValue2(value, fieldName) {
42436
42447
  }
42437
42448
  return String(value);
42438
42449
  }
42439
- function renderRichFieldValue(value, fieldName, fieldType, meta) {
42450
+ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
42440
42451
  if (value === void 0 || value === null) return "\u2014";
42441
42452
  const str = String(value);
42442
42453
  switch (fieldType) {
@@ -42447,7 +42458,7 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
42447
42458
  "img",
42448
42459
  {
42449
42460
  src: str,
42450
- alt: formatFieldLabel(fieldName),
42461
+ alt: formatFieldLabel(fieldName2),
42451
42462
  className: "max-w-full max-h-64 rounded-md object-contain",
42452
42463
  loading: "lazy"
42453
42464
  }
@@ -42582,9 +42593,9 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
42582
42593
  return /* @__PURE__ */ jsxRuntime.jsx("a", { href: `tel:${str}`, className: "text-primary hover:underline", children: str });
42583
42594
  default:
42584
42595
  if (meta?.values && meta.values.length > 0 && meta.values.includes(str)) {
42585
- return /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: getBadgeVariant(fieldName, str), children: humanizeEnumValue(str) });
42596
+ return /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: getBadgeVariant(fieldName2, str), children: humanizeEnumValue(str) });
42586
42597
  }
42587
- return formatFieldValue2(value, fieldName);
42598
+ return formatFieldValue2(value, fieldName2);
42588
42599
  }
42589
42600
  }
42590
42601
  function normalizeFieldDefs(fields) {
@@ -43427,7 +43438,21 @@ var init_Form = __esm({
43427
43438
  const normalizedInitialData = React87__namespace.default.useMemo(() => {
43428
43439
  const entityRowAsInitial = isPlainEntityRow(entity) ? entity : void 0;
43429
43440
  const callerInitial = initialData !== null && typeof initialData === "object" && !Array.isArray(initialData) ? initialData : {};
43430
- return entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
43441
+ const merged = entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
43442
+ const toId = (value) => {
43443
+ if (value === null || value === void 0 || typeof value !== "object" || value instanceof Date) return value;
43444
+ if (Array.isArray(value)) {
43445
+ return value.map(
43446
+ (item) => item !== null && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date) && item.id !== void 0 && item.id !== null ? String(item.id) : item
43447
+ );
43448
+ }
43449
+ return value.id !== void 0 && value.id !== null ? String(value.id) : value;
43450
+ };
43451
+ const normalized = {};
43452
+ for (const [key, value] of Object.entries(merged)) {
43453
+ normalized[key] = key === "id" ? value : toId(value);
43454
+ }
43455
+ return normalized;
43431
43456
  }, [entity, initialData]);
43432
43457
  const entityDerivedFields = React87__namespace.default.useMemo(() => {
43433
43458
  if (fields && fields.length > 0) return void 0;
@@ -43556,8 +43581,8 @@ var init_Form = __esm({
43556
43581
  checkViolations(name, newFormData);
43557
43582
  };
43558
43583
  const isFieldVisible = React87__namespace.default.useCallback(
43559
- (fieldName) => {
43560
- const condition = conditionalFields[fieldName];
43584
+ (fieldName2) => {
43585
+ const condition = conditionalFields[fieldName2];
43561
43586
  if (!condition) return true;
43562
43587
  return Boolean(evaluateFormExpression(condition, evalContext));
43563
43588
  },
@@ -43603,9 +43628,9 @@ var init_Form = __esm({
43603
43628
  };
43604
43629
  const handleInvalid = (e) => {
43605
43630
  const target = e.target;
43606
- const fieldName = target.getAttribute("data-field-name") ?? target.name ?? "";
43631
+ const fieldName2 = target.getAttribute("data-field-name") ?? target.name ?? "";
43607
43632
  const fieldMessage = target.validationMessage || "Invalid value";
43608
- debug("forms", "invalid", { mode: formMode, fieldName, fieldMessage });
43633
+ debug("forms", "invalid", { mode: formMode, fieldName: fieldName2, fieldMessage });
43609
43634
  queueMicrotask(() => {
43610
43635
  const form = formRef.current;
43611
43636
  if (!form) return;
@@ -43641,22 +43666,22 @@ var init_Form = __esm({
43641
43666
  };
43642
43667
  const renderField = React87__namespace.default.useCallback(
43643
43668
  (field) => {
43644
- const fieldName = field.name || field.field;
43645
- if (!fieldName) return null;
43646
- if (!isFieldVisible(fieldName)) {
43669
+ const fieldName2 = field.name || field.field;
43670
+ if (!fieldName2) return null;
43671
+ if (!isFieldVisible(fieldName2)) {
43647
43672
  return null;
43648
43673
  }
43649
43674
  const inputType = determineInputType(field);
43650
- const label = field.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/([A-Z])/g, " $1");
43651
- const currentValue = formData[fieldName] ?? field.defaultValue ?? "";
43652
- return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", "data-field": fieldName, children: [
43675
+ const label = field.label || fieldName2.charAt(0).toUpperCase() + fieldName2.slice(1).replace(/([A-Z])/g, " $1");
43676
+ const currentValue = formData[fieldName2] ?? field.defaultValue ?? "";
43677
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", "data-field": fieldName2, children: [
43653
43678
  inputType !== "checkbox" && /* @__PURE__ */ jsxRuntime.jsxs(Typography, { as: "label", variant: "label", weight: "bold", children: [
43654
43679
  label,
43655
43680
  field.required && /* @__PURE__ */ jsxRuntime.jsx(Typography, { as: "span", color: "error", className: "ml-1", children: "*" })
43656
43681
  ] }),
43657
- renderFieldInput(field, fieldName, inputType, currentValue, label),
43682
+ renderFieldInput(field, fieldName2, inputType, currentValue, label),
43658
43683
  field.hint && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", children: field.hint })
43659
- ] }, fieldName);
43684
+ ] }, fieldName2);
43660
43685
  },
43661
43686
  [formData, isFieldVisible, relationsData, relationsLoading, isLoading]
43662
43687
  );
@@ -43685,8 +43710,8 @@ var init_Form = __esm({
43685
43710
  }
43686
43711
  return field;
43687
43712
  }).map((field) => {
43688
- const fieldName = field.name || field.field;
43689
- const override = fieldOverrides?.find((o) => o.name === fieldName);
43713
+ const fieldName2 = field.name || field.field;
43714
+ const override = fieldOverrides?.find((o) => o.name === fieldName2);
43690
43715
  if (!override) return field;
43691
43716
  return {
43692
43717
  ...field,
@@ -43742,11 +43767,11 @@ var init_Form = __esm({
43742
43767
  ] }, section.id);
43743
43768
  }).filter(Boolean);
43744
43769
  }, [sections, isSectionVisible, collapsedSections, renderField, gap]);
43745
- function renderFieldInput(field, fieldName, inputType, currentValue, label) {
43770
+ function renderFieldInput(field, fieldName2, inputType, currentValue, label) {
43746
43771
  const commonProps = {
43747
- id: fieldName,
43748
- name: fieldName,
43749
- "data-field-name": fieldName,
43772
+ id: fieldName2,
43773
+ name: fieldName2,
43774
+ "data-field-name": fieldName2,
43750
43775
  required: field.required,
43751
43776
  disabled: isLoading,
43752
43777
  placeholder: field.placeholder,
@@ -43760,7 +43785,7 @@ var init_Form = __esm({
43760
43785
  ...commonProps,
43761
43786
  label: label + (field.required ? " *" : ""),
43762
43787
  checked: Boolean(currentValue),
43763
- onChange: (e) => handleChange(fieldName, e.target.checked)
43788
+ onChange: (e) => handleChange(fieldName2, e.target.checked)
43764
43789
  }
43765
43790
  );
43766
43791
  case "textarea":
@@ -43769,7 +43794,7 @@ var init_Form = __esm({
43769
43794
  {
43770
43795
  ...commonProps,
43771
43796
  value: String(currentValue),
43772
- onChange: (e) => handleChange(fieldName, e.target.value),
43797
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43773
43798
  minLength: field.min,
43774
43799
  maxLength: field.max
43775
43800
  }
@@ -43782,14 +43807,14 @@ var init_Form = __esm({
43782
43807
  ...commonProps,
43783
43808
  options,
43784
43809
  value: String(currentValue),
43785
- onValueChange: (v) => handleChange(fieldName, v),
43810
+ onValueChange: (v) => handleChange(fieldName2, v),
43786
43811
  placeholder: field.placeholder || `Select ${label}...`
43787
43812
  }
43788
43813
  );
43789
43814
  }
43790
43815
  case "relation": {
43791
- const relationOptions = relationsData[fieldName] || [];
43792
- const relationLoading = relationsLoading[fieldName] || false;
43816
+ const relationOptions = relationsData[fieldName2] || [];
43817
+ const relationLoading = relationsLoading[fieldName2] || false;
43793
43818
  if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
43794
43819
  const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
43795
43820
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -43801,7 +43826,7 @@ var init_Form = __esm({
43801
43826
  clearable: true,
43802
43827
  options: [...relationOptions],
43803
43828
  value: selectedValues,
43804
- onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
43829
+ onValueChange: (value) => handleChange(fieldName2, Array.isArray(value) ? value : [value]),
43805
43830
  placeholder: field.placeholder || `Select ${label}...`
43806
43831
  }
43807
43832
  );
@@ -43811,7 +43836,7 @@ var init_Form = __esm({
43811
43836
  {
43812
43837
  ...commonProps,
43813
43838
  value: currentValue ? String(currentValue) : void 0,
43814
- onChange: (value) => handleChange(fieldName, value),
43839
+ onChange: (value) => handleChange(fieldName2, value),
43815
43840
  options: relationOptions,
43816
43841
  isLoading: relationLoading,
43817
43842
  placeholder: field.placeholder || `Select ${label}...`,
@@ -43828,7 +43853,7 @@ var init_Form = __esm({
43828
43853
  placeholder: field.placeholder,
43829
43854
  disabled: isLoading,
43830
43855
  value: arrayValue,
43831
- onChange: (next) => handleChange(fieldName, [...next])
43856
+ onChange: (next) => handleChange(fieldName2, [...next])
43832
43857
  }
43833
43858
  );
43834
43859
  }
@@ -43840,7 +43865,7 @@ var init_Form = __esm({
43840
43865
  type: "number",
43841
43866
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
43842
43867
  onChange: (e) => handleChange(
43843
- fieldName,
43868
+ fieldName2,
43844
43869
  e.target.value ? Number(e.target.value) : void 0
43845
43870
  ),
43846
43871
  min: field.min,
@@ -43857,7 +43882,7 @@ var init_Form = __esm({
43857
43882
  icon: LucideIcons2.DollarSign,
43858
43883
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
43859
43884
  onChange: (e) => handleChange(
43860
- fieldName,
43885
+ fieldName2,
43861
43886
  e.target.value ? Number(e.target.value) : void 0
43862
43887
  ),
43863
43888
  min: field.min,
@@ -43875,7 +43900,7 @@ var init_Form = __esm({
43875
43900
  const f3 = files[0];
43876
43901
  if (!f3) return;
43877
43902
  const reader = new FileReader();
43878
- reader.onload = () => handleChange(fieldName, {
43903
+ reader.onload = () => handleChange(fieldName2, {
43879
43904
  name: f3.name,
43880
43905
  mimeType: f3.type,
43881
43906
  sizeBytes: f3.size,
@@ -43892,7 +43917,7 @@ var init_Form = __esm({
43892
43917
  ...commonProps,
43893
43918
  type: "date",
43894
43919
  value: formatDateValue(currentValue),
43895
- onChange: (e) => handleChange(fieldName, e.target.value)
43920
+ onChange: (e) => handleChange(fieldName2, e.target.value)
43896
43921
  }
43897
43922
  );
43898
43923
  case "datetime-local":
@@ -43902,7 +43927,7 @@ var init_Form = __esm({
43902
43927
  ...commonProps,
43903
43928
  type: "datetime-local",
43904
43929
  value: formatDateTimeValue(currentValue),
43905
- onChange: (e) => handleChange(fieldName, e.target.value)
43930
+ onChange: (e) => handleChange(fieldName2, e.target.value)
43906
43931
  }
43907
43932
  );
43908
43933
  case "email":
@@ -43912,7 +43937,7 @@ var init_Form = __esm({
43912
43937
  ...commonProps,
43913
43938
  type: "email",
43914
43939
  value: String(currentValue),
43915
- onChange: (e) => handleChange(fieldName, e.target.value),
43940
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43916
43941
  minLength: field.min,
43917
43942
  maxLength: field.max
43918
43943
  }
@@ -43924,7 +43949,7 @@ var init_Form = __esm({
43924
43949
  ...commonProps,
43925
43950
  type: "url",
43926
43951
  value: String(currentValue),
43927
- onChange: (e) => handleChange(fieldName, e.target.value),
43952
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43928
43953
  minLength: field.min,
43929
43954
  maxLength: field.max
43930
43955
  }
@@ -43936,7 +43961,7 @@ var init_Form = __esm({
43936
43961
  ...commonProps,
43937
43962
  type: "password",
43938
43963
  value: String(currentValue),
43939
- onChange: (e) => handleChange(fieldName, e.target.value),
43964
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43940
43965
  minLength: field.min,
43941
43966
  maxLength: field.max
43942
43967
  }
@@ -43949,7 +43974,7 @@ var init_Form = __esm({
43949
43974
  ...commonProps,
43950
43975
  type: "text",
43951
43976
  value: String(currentValue),
43952
- onChange: (e) => handleChange(fieldName, e.target.value),
43977
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43953
43978
  minLength: field.min,
43954
43979
  maxLength: field.max
43955
43980
  }
@@ -44306,7 +44331,7 @@ function entityFieldsFromListItem(item) {
44306
44331
  }
44307
44332
  return result;
44308
44333
  }
44309
- function getStatusStyle(fieldName, value) {
44334
+ function getStatusStyle(fieldName2, value) {
44310
44335
  const val = String(value).toLowerCase();
44311
44336
  if (val.includes("complete") || val.includes("done"))
44312
44337
  return STATUS_STYLES.complete;
@@ -44322,12 +44347,12 @@ function getStatusStyle(fieldName, value) {
44322
44347
  if (val.includes("low")) return STATUS_STYLES.low;
44323
44348
  return STATUS_STYLES.default;
44324
44349
  }
44325
- function formatValue3(value, fieldName) {
44350
+ function formatValue3(value, fieldName2) {
44326
44351
  if (typeof value === "number") {
44327
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
44352
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
44328
44353
  return `${value}%`;
44329
44354
  }
44330
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
44355
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
44331
44356
  return new Intl.NumberFormat("en-US", {
44332
44357
  style: "currency",
44333
44358
  currency: "USD",
@@ -44350,8 +44375,8 @@ function formatValue3(value, fieldName) {
44350
44375
  }
44351
44376
  return String(value);
44352
44377
  }
44353
- function formatFieldLabel2(fieldName) {
44354
- return humanizeFieldName(fieldName).replace(/\sId$/, "").trim();
44378
+ function formatFieldLabel2(fieldName2) {
44379
+ return humanizeFieldName(fieldName2).replace(/\sId$/, "").trim();
44355
44380
  }
44356
44381
  var STATUS_STYLES, StatusBadge, ProgressIndicator, List3;
44357
44382
  var init_List = __esm({
@@ -44419,9 +44444,9 @@ var init_List = __esm({
44419
44444
  };
44420
44445
  StatusBadge = ({
44421
44446
  value,
44422
- fieldName
44447
+ fieldName: fieldName2
44423
44448
  }) => {
44424
- const style = getStatusStyle(fieldName, value);
44449
+ const style = getStatusStyle(fieldName2, value);
44425
44450
  return /* @__PURE__ */ jsxRuntime.jsxs(
44426
44451
  Typography,
44427
44452
  {
@@ -47334,8 +47359,8 @@ var init_StatCard = __esm({
47334
47359
  return items.length;
47335
47360
  }
47336
47361
  if (field.includes(":")) {
47337
- const [fieldName, fieldValue] = field.split(":");
47338
- return items.filter((item) => item[fieldName] === fieldValue).length;
47362
+ const [fieldName2, fieldValue] = field.split(":");
47363
+ return items.filter((item) => item[fieldName2] === fieldValue).length;
47339
47364
  }
47340
47365
  const fieldExistsOnItems = items.some((item) => field in item);
47341
47366
  if (fieldExistsOnItems) {
@@ -48855,6 +48880,8 @@ var init_component_registry_generated = __esm({
48855
48880
  init_DocSearch();
48856
48881
  init_DocSidebar();
48857
48882
  init_DocTOC();
48883
+ init_DocumentDetails();
48884
+ init_DocumentPanel();
48858
48885
  init_DocumentViewer();
48859
48886
  init_DrawFxLayer();
48860
48887
  init_DrawGroup();
@@ -48964,7 +48991,7 @@ var init_component_registry_generated = __esm({
48964
48991
  init_RelationSelect();
48965
48992
  init_RepeatableFormSection();
48966
48993
  init_ReplyTree();
48967
- init_RichBlockEditor();
48994
+ init_RichTextEditor();
48968
48995
  init_RuntimeDebugger2();
48969
48996
  init_ScaledDiagram();
48970
48997
  init_ScoreDisplay();
@@ -49127,6 +49154,8 @@ var init_component_registry_generated = __esm({
49127
49154
  "DocSearch": DocSearch,
49128
49155
  "DocSidebar": DocSidebar,
49129
49156
  "DocTOC": DocTOC,
49157
+ "DocumentDetails": DocumentDetails,
49158
+ "DocumentPanel": DocumentPanel,
49130
49159
  "DocumentViewer": DocumentViewer,
49131
49160
  "DrawFxLayer": DrawFxLayer,
49132
49161
  "DrawGroup": DrawGroup,
@@ -49238,7 +49267,7 @@ var init_component_registry_generated = __esm({
49238
49267
  "RelationSelect": RelationSelect,
49239
49268
  "RepeatableFormSection": RepeatableFormSection,
49240
49269
  "ReplyTree": ReplyTree,
49241
- "RichBlockEditor": RichBlockEditor,
49270
+ "RichTextEditor": RichTextEditor,
49242
49271
  "RuntimeDebugger": RuntimeDebugger,
49243
49272
  "ScaledDiagram": ScaledDiagram,
49244
49273
  "ScoreDisplay": ScoreDisplay,
@@ -49392,10 +49421,10 @@ function enrichFormFields(fields, entityDef) {
49392
49421
  }
49393
49422
  if (field && typeof field === "object" && !Array.isArray(field) && !React87__namespace.default.isValidElement(field) && !(field instanceof Date)) {
49394
49423
  const obj = field;
49395
- const fieldName = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
49396
- if (!fieldName) return field;
49424
+ const fieldName2 = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
49425
+ if (!fieldName2) return field;
49397
49426
  if (obj.type || obj.inputType) return field;
49398
- const entityField = fieldMap.get(fieldName);
49427
+ const entityField = fieldMap.get(fieldName2);
49399
49428
  if (!entityField) return field;
49400
49429
  const enriched = { ...obj, type: entityField.type };
49401
49430
  if (entityField.required && !("required" in obj)) {
@@ -49445,9 +49474,9 @@ function enrichDetailFields(fields, entityDef) {
49445
49474
  }
49446
49475
  if (field && typeof field === "object" && !Array.isArray(field) && !React87__namespace.default.isValidElement(field) && !(field instanceof Date)) {
49447
49476
  const obj = field;
49448
- const fieldName = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
49449
- if (!fieldName || obj.type) return field;
49450
- const meta = metaFor(fieldName);
49477
+ const fieldName2 = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
49478
+ if (!fieldName2 || obj.type) return field;
49479
+ const meta = metaFor(fieldName2);
49451
49480
  return meta ? { ...obj, ...meta } : field;
49452
49481
  }
49453
49482
  return field;
@@ -50286,9 +50315,9 @@ function UISlotRenderer({
50286
50315
  "ui-slot-renderer relative min-h-full",
50287
50316
  className
50288
50317
  ), children: [
50289
- /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex min-h-full", children: [
50290
- /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 shrink-0" }),
50291
- /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-h-[200px]" })
50318
+ /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "flex min-h-full flex-col lg:flex-row", children: [
50319
+ /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 lg:shrink-0" }),
50320
+ /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-w-0 min-h-[200px]" })
50292
50321
  ] }),
50293
50322
  /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "modal", portal: true }),
50294
50323
  /* @__PURE__ */ jsxRuntime.jsx(UISlotComponent, { slot: "drawer", portal: true }),