@almadar/ui 5.159.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.
@@ -8,7 +8,7 @@ export { ANONYMOUS_USER } from '@almadar/core';
8
8
  import { clsx } from 'clsx';
9
9
  import { twMerge } from 'tailwind-merge';
10
10
  import * as LucideIcons2 from 'lucide-react';
11
- import { Loader2, X, Lightbulb, CheckCircle, List, Printer, ChevronRight, ChevronLeft, GitBranch, Pencil, Eye, Plus, ArrowRight, Trash, Code, FileText, WrapText, Check, Copy, RotateCcw, Play, Terminal, XCircle, AlertTriangle, Trash2, Link2, ArrowLeft, ZoomOut, ZoomIn, Download, DollarSign, ChevronDown, Menu as Menu$1, Package, Calendar, MoreHorizontal, Image as Image$1, Upload, HelpCircle, PauseCircle, Search, Type, Heading1, Heading2, Heading3, ListOrdered, Quote, Minus, Eraser, TrendingUp, TrendingDown, AlertCircle, Circle, Clock, CheckCircle2, ChevronUp } from 'lucide-react';
11
+ import { Loader2, X, Lightbulb, CheckCircle, List, Printer, ChevronRight, ChevronLeft, GitBranch, Pencil, Eye, Plus, ArrowRight, Trash, Code, FileText, WrapText, Check, Copy, RotateCcw, Play, Terminal, XCircle, AlertTriangle, Trash2, Link2, ArrowLeft, ZoomOut, ZoomIn, Download, DollarSign, ChevronDown, Menu as Menu$1, Package, Calendar, MoreHorizontal, Image as Image$1, Upload, HelpCircle, PauseCircle, Search, Bold, Italic, Underline, Strikethrough, Heading1, Heading2, Heading3, ListOrdered, Quote, Link as Link$1, Minus, Eraser, TrendingUp, TrendingDown, AlertCircle, Circle, Clock, CheckCircle2, ChevronUp } from 'lucide-react';
12
12
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
13
  import { createPortal } from 'react-dom';
14
14
  import { useTranslate } from '@almadar/ui/hooks';
@@ -26264,8 +26264,9 @@ function DataGrid({
26264
26264
  const titleField = fieldDefs.find((f3) => f3.variant === "h3" || f3.variant === "h4") ?? fieldDefs[0];
26265
26265
  const badgeFields = fieldDefs.filter((f3) => f3.variant === "badge" && f3 !== titleField);
26266
26266
  const bodyFields = fieldDefs.filter((f3) => f3 !== titleField && !badgeFields.includes(f3));
26267
- const primaryActions = actionDefs.filter((a) => a.variant !== "danger");
26268
- const dangerActions = actionDefs.filter((a) => a.variant === "danger");
26267
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
26268
+ const inlineCardActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
26269
+ const menuCardActions = actionDefs.filter((a) => !inlineCardActions.includes(a));
26269
26270
  const fireAction = (action, itemData) => {
26270
26271
  if (action.navigatesTo) {
26271
26272
  const url = action.navigatesTo.replace(
@@ -26285,6 +26286,9 @@ function DataGrid({
26285
26286
  e.stopPropagation();
26286
26287
  fireAction(action, itemData);
26287
26288
  };
26289
+ const cardClickAction = actionDefs.find((a) => a.variant !== "danger");
26290
+ const handleCardClick = cardClickAction ? (itemData) => () => fireAction(cardClickAction, itemData) : void 0;
26291
+ const stopCardClick = handleCardClick ? (e) => e.stopPropagation() : void 0;
26288
26292
  const hasRenderProp = typeof children === "function";
26289
26293
  useEffect(() => {
26290
26294
  if (data.length > 0 && !hasRenderProp && fieldDefs.length === 0) {
@@ -26356,55 +26360,40 @@ function DataGrid({
26356
26360
  {
26357
26361
  "data-entity-row": true,
26358
26362
  "data-entity-id": id,
26359
- className: cn("relative group/rowactions", isSelected && "ring-2 ring-primary rounded-lg"),
26363
+ onClick: handleCardClick?.(itemData),
26364
+ className: cn("relative group/rowactions", handleCardClick && "cursor-pointer", isSelected && "ring-2 ring-primary rounded-lg"),
26360
26365
  children: [
26361
26366
  children(itemData, index),
26362
- actionDefs.length > 0 && /* @__PURE__ */ jsxs(Box, { className: "absolute top-2 right-2 z-10 opacity-0 group-hover/rowactions:opacity-100 focus-within:opacity-100 [@media(pointer:coarse)]:opacity-100 transition-opacity duration-fast", children: [
26363
- /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "rounded-md border border-border bg-card/95 backdrop-blur-sm shadow-sm p-0.5 [@media(pointer:coarse)]:hidden", children: [
26364
- (maxInlineActions != null ? actionDefs.slice(0, maxInlineActions) : actionDefs).map((action, idx) => /* @__PURE__ */ jsxs(
26365
- Button,
26366
- {
26367
- variant: action.variant === "primary" ? "primary" : "ghost",
26368
- size: "sm",
26369
- onClick: handleActionClick(action, itemData),
26370
- "data-testid": `action-${action.event}`,
26371
- "data-row-id": String(itemData.id),
26372
- className: action.variant === "danger" ? "text-error hover:text-error hover:bg-error/10" : void 0,
26373
- children: [
26374
- action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
26375
- action.label
26376
- ]
26377
- },
26378
- idx
26379
- )),
26380
- maxInlineActions != null && actionDefs.length > maxInlineActions && /* @__PURE__ */ jsx(
26381
- Menu,
26382
- {
26383
- position: "bottom-end",
26384
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
26385
- items: actionDefs.slice(maxInlineActions).map((action) => ({
26386
- label: action.label,
26387
- icon: action.icon,
26388
- event: action.event,
26389
- onClick: () => fireAction(action, itemData)
26390
- }))
26391
- }
26392
- )
26393
- ] }),
26394
- /* @__PURE__ */ jsx(Box, { className: "hidden [@media(pointer:coarse)]:block rounded-md border border-border bg-card/95 backdrop-blur-sm shadow-sm p-0.5", children: /* @__PURE__ */ jsx(
26367
+ actionDefs.length > 0 && /* @__PURE__ */ jsx(Box, { onClick: stopCardClick, className: "absolute top-2 right-2 z-10 opacity-0 group-hover/rowactions:opacity-100 focus-within:opacity-100 [@media(pointer:coarse)]:opacity-100 transition-opacity duration-fast", children: /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "rounded-md border border-border bg-card/95 backdrop-blur-sm shadow-sm p-0.5", children: [
26368
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsxs(
26369
+ Button,
26370
+ {
26371
+ variant: "primary",
26372
+ size: "sm",
26373
+ onClick: handleActionClick(action, itemData),
26374
+ "data-testid": `action-${action.event}`,
26375
+ "data-row-id": String(itemData.id),
26376
+ children: [
26377
+ action.icon && renderIconInput(action.icon, { size: "xs", className: "mr-1" }),
26378
+ action.label
26379
+ ]
26380
+ },
26381
+ idx
26382
+ )),
26383
+ menuCardActions.length > 0 && /* @__PURE__ */ jsx(
26395
26384
  Menu,
26396
26385
  {
26397
26386
  position: "bottom-end",
26398
26387
  trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
26399
- items: actionDefs.map((action) => ({
26388
+ items: menuCardActions.map((action) => ({
26400
26389
  label: action.label,
26401
26390
  icon: action.icon,
26402
- event: action.event,
26391
+ variant: action.variant === "danger" ? "danger" : "default",
26403
26392
  onClick: () => fireAction(action, itemData)
26404
26393
  }))
26405
26394
  }
26406
- ) })
26407
- ] })
26395
+ )
26396
+ ] }) })
26408
26397
  ]
26409
26398
  },
26410
26399
  id
@@ -26418,12 +26407,14 @@ function DataGrid({
26418
26407
  {
26419
26408
  "data-entity-row": true,
26420
26409
  "data-entity-id": id,
26410
+ onClick: handleCardClick?.(itemData),
26421
26411
  className: cn(
26422
26412
  "bg-card rounded-lg",
26423
26413
  "border border-border",
26424
26414
  "shadow-elevation-card hover:shadow-elevation-dialog",
26425
26415
  "hover:border-primary transition-all",
26426
26416
  "flex flex-col",
26417
+ handleCardClick && "cursor-pointer",
26427
26418
  isSelected && "ring-2 ring-primary border-primary"
26428
26419
  ),
26429
26420
  children: [
@@ -26473,53 +26464,35 @@ function DataGrid({
26473
26464
  ] }, field.name);
26474
26465
  }) })
26475
26466
  ] }),
26476
- (primaryActions.length > 0 || dangerActions.length > 0) && /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
26477
- (maxInlineActions != null ? primaryActions.slice(0, maxInlineActions) : primaryActions).map((action, idx) => /* @__PURE__ */ jsx(
26467
+ actionDefs.length > 0 && /* @__PURE__ */ jsxs(HStack, { gap: "xs", onClick: stopCardClick, className: "flex-shrink-0", children: [
26468
+ inlineCardActions.map((action, idx) => /* @__PURE__ */ jsx(
26478
26469
  Button,
26479
26470
  {
26480
- variant: action.variant === "primary" ? "primary" : "ghost",
26471
+ variant: "primary",
26481
26472
  size: "sm",
26482
26473
  onClick: handleActionClick(action, itemData),
26483
26474
  "data-testid": `action-${action.event}`,
26484
26475
  "data-row-id": String(itemData.id),
26485
26476
  "aria-label": action.label,
26486
26477
  title: action.label,
26487
- className: cn(
26488
- action.variant === "primary" ? void 0 : "text-muted-foreground hover:text-foreground",
26489
- action.icon && "px-2"
26490
- ),
26478
+ className: cn(action.icon && "px-2"),
26491
26479
  children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
26492
26480
  },
26493
26481
  idx
26494
26482
  )),
26495
- maxInlineActions != null && primaryActions.length > maxInlineActions && /* @__PURE__ */ jsx(
26483
+ menuCardActions.length > 0 && /* @__PURE__ */ jsx(
26496
26484
  Menu,
26497
26485
  {
26498
26486
  position: "bottom-end",
26499
26487
  trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
26500
- items: primaryActions.slice(maxInlineActions).map((action) => ({
26488
+ items: menuCardActions.map((action) => ({
26501
26489
  label: action.label,
26502
26490
  icon: action.icon,
26503
- event: action.event,
26491
+ variant: action.variant === "danger" ? "danger" : "default",
26504
26492
  onClick: () => fireAction(action, itemData)
26505
26493
  }))
26506
26494
  }
26507
- ),
26508
- dangerActions.map((action, idx) => /* @__PURE__ */ jsx(
26509
- Button,
26510
- {
26511
- variant: "ghost",
26512
- size: "sm",
26513
- onClick: handleActionClick(action, itemData),
26514
- "data-testid": `action-${action.event}`,
26515
- "data-row-id": String(itemData.id),
26516
- "aria-label": action.label,
26517
- title: action.label,
26518
- className: "text-error hover:text-error hover:bg-error/10 px-2",
26519
- children: action.icon ? renderIconInput(action.icon, { size: "xs" }) : action.label
26520
- },
26521
- `danger-${idx}`
26522
- ))
26495
+ )
26523
26496
  ] })
26524
26497
  ] }) }),
26525
26498
  bodyFields.length > 0 && /* @__PURE__ */ jsx(Box, { className: "px-4 pt-2 pb-4 flex-1", children: /* @__PURE__ */ jsxs(VStack, { gap: "xs", children: [
@@ -26540,8 +26513,8 @@ function DataGrid({
26540
26513
  }
26541
26514
  return /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "items-center", children: [
26542
26515
  field.icon && renderIconInput(field.icon, { size: "xs", className: "text-muted-foreground" }),
26543
- /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "secondary", children: (field.label ?? fieldLabel2(field.name)) + ":" }),
26544
- /* @__PURE__ */ jsx(Typography, { variant: "small", children: formatValue(value, field.format) })
26516
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "secondary", className: "sr-only", children: (field.label ?? fieldLabel2(field.name)) + ":" }),
26517
+ /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: formatValue(value, field.format) })
26545
26518
  ] }, field.name);
26546
26519
  }) })
26547
26520
  ] }) })
@@ -26761,54 +26734,59 @@ function DataList({
26761
26734
  if (!itemActions || itemActions.length === 0) return null;
26762
26735
  const inline = maxInlineActions != null ? itemActions.slice(0, maxInlineActions) : itemActions;
26763
26736
  const overflow = maxInlineActions != null ? itemActions.slice(maxInlineActions) : [];
26764
- return /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "flex-shrink-0", children: [
26765
- inline.map((action, idx) => /* @__PURE__ */ jsxs(
26766
- Button,
26767
- {
26768
- variant: action.variant ?? "ghost",
26769
- size: "sm",
26770
- onClick: handleActionClick(action, itemData),
26771
- "data-testid": `action-${action.event}`,
26772
- "data-row-id": String(itemData.id),
26773
- className: cn(
26774
- action.variant === "danger" && "text-error hover:bg-error/10",
26775
- // Must sit on the Button itself: the variant's own text colour
26776
- // beats an inherited one from the row wrapper.
26777
- onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
26778
- ),
26779
- children: [
26780
- action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
26781
- action.label
26782
- ]
26783
- },
26784
- idx
26785
- )),
26786
- overflow.length > 0 && /* @__PURE__ */ jsx(
26787
- Menu,
26788
- {
26789
- position: "bottom-end",
26790
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
26791
- items: overflow.map((action) => ({
26792
- label: action.label,
26793
- icon: action.icon,
26794
- event: action.event,
26795
- variant: action.variant === "danger" ? "danger" : "default",
26796
- onClick: () => eventBus.emit(`UI:${action.event}`, {
26797
- id: itemData.id,
26798
- row: itemData
26799
- })
26800
- }))
26801
- }
26802
- )
26803
- ] });
26737
+ return (
26738
+ // stopPropagation at the cluster: the "⋯" trigger opens the overflow
26739
+ // menu without also firing the row's default click (inline buttons
26740
+ // already stop it in handleActionClick; the Menu panel is portaled).
26741
+ /* @__PURE__ */ jsxs(HStack, { gap: "xs", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, className: "flex-shrink-0", children: [
26742
+ inline.map((action, idx) => /* @__PURE__ */ jsxs(
26743
+ Button,
26744
+ {
26745
+ variant: action.variant ?? "ghost",
26746
+ size: "sm",
26747
+ onClick: handleActionClick(action, itemData),
26748
+ "data-testid": `action-${action.event}`,
26749
+ "data-row-id": String(itemData.id),
26750
+ className: cn(
26751
+ action.variant === "danger" && "text-error hover:bg-error/10",
26752
+ // Must sit on the Button itself: the variant's own text colour
26753
+ // beats an inherited one from the row wrapper.
26754
+ onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
26755
+ ),
26756
+ children: [
26757
+ action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
26758
+ action.label
26759
+ ]
26760
+ },
26761
+ idx
26762
+ )),
26763
+ overflow.length > 0 && /* @__PURE__ */ jsx(
26764
+ Menu,
26765
+ {
26766
+ position: "bottom-end",
26767
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
26768
+ items: overflow.map((action) => ({
26769
+ label: action.label,
26770
+ icon: action.icon,
26771
+ variant: action.variant === "danger" ? "danger" : "default",
26772
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
26773
+ id: itemData.id,
26774
+ row: itemData
26775
+ })
26776
+ }))
26777
+ }
26778
+ )
26779
+ ] })
26780
+ );
26804
26781
  };
26782
+ const rowClickEvent = itemClickEvent || itemActions?.find((a) => a.variant !== "danger")?.event;
26805
26783
  const handleRowClick = (itemData) => () => {
26806
- if (!itemClickEvent) return;
26784
+ if (!rowClickEvent) return;
26807
26785
  const payload = {
26808
26786
  id: itemData.id,
26809
26787
  row: itemData
26810
26788
  };
26811
- eventBus.emit(`UI:${itemClickEvent}`, payload);
26789
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
26812
26790
  };
26813
26791
  if (isLoading) {
26814
26792
  return /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) });
@@ -26856,10 +26834,10 @@ function DataList({
26856
26834
  {
26857
26835
  "data-entity-row": true,
26858
26836
  "data-entity-id": id,
26859
- onClick: itemClickEvent ? handleRowClick(itemData) : void 0,
26837
+ onClick: rowClickEvent ? handleRowClick(itemData) : void 0,
26860
26838
  className: cn(
26861
26839
  "flex px-4 group/rowactions",
26862
- itemClickEvent && "cursor-pointer",
26840
+ rowClickEvent && "cursor-pointer",
26863
26841
  isSent ? "justify-end" : "justify-start"
26864
26842
  ),
26865
26843
  children: /* @__PURE__ */ jsxs(
@@ -26935,7 +26913,7 @@ function DataList({
26935
26913
  const id2 = itemData.id || String(index);
26936
26914
  const actions = renderItemActions(itemData);
26937
26915
  return wrapDnd(
26938
- /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id2, onClick: itemClickEvent ? handleRowClick(itemData) : void 0, className: cn("relative group/rowactions", itemClickEvent && "cursor-pointer"), children: [
26916
+ /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id2, onClick: rowClickEvent ? handleRowClick(itemData) : void 0, className: cn("relative group/rowactions", rowClickEvent && "cursor-pointer"), children: [
26939
26917
  children(itemData, index),
26940
26918
  actions && /* @__PURE__ */ jsxs(Box, { className: "absolute top-2 right-2 z-10 opacity-0 group-hover/rowactions:opacity-100 focus-within:opacity-100 [@media(pointer:coarse)]:opacity-100 transition-opacity duration-fast", children: [
26941
26919
  /* @__PURE__ */ jsx(Box, { className: "rounded-md border border-border bg-card/95 backdrop-blur-sm shadow-sm p-0.5 [@media(pointer:coarse)]:hidden", children: actions }),
@@ -26947,7 +26925,6 @@ function DataList({
26947
26925
  items: (itemActions ?? []).map((action) => ({
26948
26926
  label: action.label,
26949
26927
  icon: action.icon,
26950
- event: action.event,
26951
26928
  variant: action.variant === "danger" ? "danger" : "default",
26952
26929
  onClick: () => eventBus.emit(`UI:${action.event}`, {
26953
26930
  id: itemData.id,
@@ -26964,12 +26941,16 @@ function DataList({
26964
26941
  const id = itemData.id || String(index);
26965
26942
  const titleValue = getNestedValue(itemData, titleField?.name ?? "");
26966
26943
  return wrapDnd(
26967
- /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: itemClickEvent ? handleRowClick(itemData) : void 0, className: cn(itemClickEvent && "cursor-pointer"), children: [
26944
+ /* @__PURE__ */ jsxs(Box, { "data-entity-row": true, "data-entity-id": id, onClick: rowClickEvent ? handleRowClick(itemData) : void 0, className: cn(rowClickEvent && "cursor-pointer"), children: [
26968
26945
  /* @__PURE__ */ jsxs(
26969
26946
  Box,
26970
26947
  {
26971
26948
  className: cn(
26972
- "group flex items-center gap-4 transition-all duration-fast",
26949
+ // items-start, not items-center: a multi-line row (title + meta +
26950
+ // progress) centred its action cluster in the vertical middle, so
26951
+ // the buttons floated between the meta fields instead of anchoring
26952
+ // to the title they act on (U-DATALIST-ACTIONS-FLOAT-MID-ROW).
26953
+ "group flex items-start gap-4 transition-all duration-fast",
26973
26954
  isCompact ? "px-4 py-2" : "px-6 py-4",
26974
26955
  "hover:bg-muted/80",
26975
26956
  !isCard && !isCompact && "rounded-lg border border-transparent hover:border-border"
@@ -27000,11 +26981,19 @@ function DataList({
27000
26981
  if (value === void 0 || value === null || value === "") return null;
27001
26982
  return /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "items-center", children: [
27002
26983
  field.icon && renderIconInput2(field.icon, { size: "xs", className: "text-muted-foreground" }),
27003
- /* @__PURE__ */ jsxs(Typography, { variant: "caption", color: "secondary", children: [
27004
- field.label ?? fieldLabel3(field.name),
27005
- ":"
27006
- ] }),
27007
- /* @__PURE__ */ jsx(Typography, { variant: "small", children: formatValue2(value, field.format, { yes: t("common.yes"), no: t("common.no") }) })
26984
+ /* @__PURE__ */ jsxs(
26985
+ Typography,
26986
+ {
26987
+ variant: "caption",
26988
+ color: "secondary",
26989
+ className: cn(field.format !== "boolean" && "sr-only"),
26990
+ children: [
26991
+ field.label ?? fieldLabel3(field.name),
26992
+ ":"
26993
+ ]
26994
+ }
26995
+ ),
26996
+ /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: formatValue2(value, field.format, { yes: t("common.yes"), no: t("common.no") }) })
27008
26997
  ] }, field.name);
27009
26998
  }) }),
27010
26999
  progressFields.map((field) => {
@@ -27023,7 +27012,7 @@ function DataList({
27023
27012
  ]
27024
27013
  }
27025
27014
  ),
27026
- isCard && !isLast && /* @__PURE__ */ jsx(Box, { className: "mx-6 border-b border-border/40" })
27015
+ (isCard || isCompact) && !isLast && /* @__PURE__ */ jsx(Box, { className: cn("border-b border-border/40", isCompact ? "mx-4" : "mx-6") })
27027
27016
  ] }, id)
27028
27017
  );
27029
27018
  };
@@ -27033,7 +27022,13 @@ function DataList({
27033
27022
  {
27034
27023
  className: cn(
27035
27024
  isCard && "bg-card rounded-xl border border-border shadow-elevation-dialog overflow-hidden",
27036
- !isCard && gapClass,
27025
+ // `gap-*` is inert on a block container, and Box only emits a display
27026
+ // class when its `display` prop is set — so every non-card list had
27027
+ // been asking for a gap that CSS silently dropped. flex-col makes it
27028
+ // real. `compact` keeps gap-0 on purpose: it separates with the row
27029
+ // divider above instead, never both (Almadar_UI_Beauty.md 6).
27030
+ !isCard && "flex flex-col",
27031
+ !isCard && !isCompact && gapClass,
27037
27032
  listLookStyles[look],
27038
27033
  className
27039
27034
  ),
@@ -32222,7 +32217,7 @@ var init_MapView = __esm({
32222
32217
  shadowSize: [41, 41]
32223
32218
  });
32224
32219
  L.Marker.prototype.options.icon = defaultIcon;
32225
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState95 } = React87__default;
32220
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState97 } = React87__default;
32226
32221
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
32227
32222
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
32228
32223
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -32267,7 +32262,7 @@ var init_MapView = __esm({
32267
32262
  showAttribution = true
32268
32263
  }) {
32269
32264
  const eventBus = useEventBus2();
32270
- const [clickedPosition, setClickedPosition] = useState95(null);
32265
+ const [clickedPosition, setClickedPosition] = useState97(null);
32271
32266
  const handleMapClick = useCallback97((lat, lng) => {
32272
32267
  if (showClickedPin) {
32273
32268
  setClickedPosition({ lat, lng });
@@ -36629,741 +36624,775 @@ var init_PositionedCanvas = __esm({
36629
36624
  PositionedCanvas.displayName = "PositionedCanvas";
36630
36625
  }
36631
36626
  });
36632
- function nextBlockId(prefix = "blk") {
36633
- _idSeq += 1;
36634
- const random = Math.random().toString(36).slice(2, 8);
36635
- return `${prefix}-${Date.now().toString(36)}-${_idSeq}-${random}`;
36636
- }
36637
- function normalizeBlocks(raw) {
36638
- if (!Array.isArray(raw) || raw.length === 0) return [createBlock("paragraph")];
36639
- return raw.map((row) => {
36640
- const entity = row;
36641
- const rawType = entity.type;
36642
- const type = typeof rawType === "string" && BLOCK_TYPES.has(rawType) ? rawType : "paragraph";
36643
- const id = typeof entity.id === "string" && entity.id ? entity.id : nextBlockId(type);
36644
- return { ...entity, id, type };
36645
- });
36646
- }
36647
- function createBlock(type) {
36648
- switch (type) {
36649
- case "bullet-list":
36650
- case "numbered-list":
36651
- return {
36652
- id: nextBlockId(type),
36653
- type,
36654
- children: [
36655
- { id: nextBlockId("li"), type: "paragraph", content: "" }
36656
- ]
36657
- };
36658
- case "image":
36659
- return {
36660
- id: nextBlockId(type),
36661
- type,
36662
- content: "",
36663
- metadata: { url: "", caption: "" }
36664
- };
36665
- case "code":
36666
- return {
36667
- id: nextBlockId(type),
36668
- type,
36669
- content: "",
36670
- metadata: { language: "plaintext" }
36671
- };
36672
- case "divider":
36673
- return { id: nextBlockId(type), type };
36674
- default:
36675
- return { id: nextBlockId(type), type, content: "" };
36627
+ function safeUrl(raw, kinds) {
36628
+ const url = raw.trim();
36629
+ if (url.startsWith("#") || url.startsWith("/")) return url;
36630
+ const lower = url.toLowerCase();
36631
+ for (const kind of kinds) {
36632
+ if (lower.startsWith(kind)) return url;
36676
36633
  }
36634
+ return null;
36677
36635
  }
36678
- function replaceBlock(blocks, id, updater) {
36679
- return blocks.map((block) => block.id === id ? updater(block) : block);
36680
- }
36681
- function removeBlock(blocks, id) {
36682
- return blocks.filter((block) => block.id !== id);
36683
- }
36684
- function duplicateBlock(block) {
36685
- return {
36686
- ...block,
36687
- id: nextBlockId(block.type),
36688
- children: block.children?.map((child) => ({
36689
- ...child,
36690
- id: nextBlockId("li")
36691
- })),
36692
- metadata: block.metadata ? { ...block.metadata } : void 0
36693
- };
36694
- }
36695
- function insertAfter(blocks, targetId, inserted) {
36696
- const idx = blocks.findIndex((b) => b.id === targetId);
36697
- if (idx === -1) return [...blocks, inserted];
36698
- const next = blocks.slice();
36699
- next.splice(idx + 1, 0, inserted);
36700
- return next;
36701
- }
36702
- function changeBlockType(block, type) {
36703
- if (block.type === type) return block;
36704
- if (type === "bullet-list" || type === "numbered-list") {
36705
- if (block.children && block.children.length > 0) {
36706
- return { ...block, type };
36636
+ function sanitizeNode(el) {
36637
+ const children = Array.from(el.children);
36638
+ for (const child of children) {
36639
+ const tag = child.tagName.toLowerCase();
36640
+ if (DROP_TAGS.has(tag)) {
36641
+ child.remove();
36642
+ continue;
36707
36643
  }
36708
- const seed2 = block.content ?? "";
36709
- return {
36710
- id: block.id,
36711
- type,
36712
- children: [
36713
- { id: nextBlockId("li"), type: "paragraph", content: seed2 }
36714
- ]
36715
- };
36716
- }
36717
- if (type === "divider") {
36718
- return { id: block.id, type };
36644
+ const allowed = ALLOWED_ATTRS[tag];
36645
+ if (!allowed) {
36646
+ sanitizeNode(child);
36647
+ child.replaceWith(...Array.from(child.childNodes));
36648
+ continue;
36649
+ }
36650
+ for (const attr of Array.from(child.attributes)) {
36651
+ const name = attr.name.toLowerCase();
36652
+ if (!allowed.includes(name)) {
36653
+ child.removeAttribute(attr.name);
36654
+ continue;
36655
+ }
36656
+ if (name === "href") {
36657
+ const url = safeUrl(attr.value, ["http://", "https://", "mailto:"]);
36658
+ if (url === null) child.removeAttribute(attr.name);
36659
+ else child.setAttribute("href", url);
36660
+ }
36661
+ if (name === "src") {
36662
+ const url = safeUrl(attr.value, ["http://", "https://", "data:image/"]);
36663
+ if (url === null) child.remove();
36664
+ else child.setAttribute("src", url);
36665
+ }
36666
+ }
36667
+ sanitizeNode(child);
36719
36668
  }
36720
- if (type === "image") {
36721
- return {
36722
- id: block.id,
36723
- type,
36724
- content: "",
36725
- metadata: { url: "", caption: block.content ?? "" }
36726
- };
36669
+ }
36670
+ function sanitizeRichHtml(html) {
36671
+ if (!html || typeof html !== "string") return "";
36672
+ if (typeof window === "undefined" || typeof DOMParser === "undefined") return "";
36673
+ const doc = new DOMParser().parseFromString(html, "text/html");
36674
+ sanitizeNode(doc.body);
36675
+ if (!doc.body.querySelector("img, hr") && (doc.body.textContent ?? "").trim().length === 0) {
36676
+ return "";
36727
36677
  }
36728
- if (type === "code") {
36729
- return {
36730
- id: block.id,
36731
- type,
36732
- content: block.content ?? "",
36733
- metadata: { language: "plaintext" }
36734
- };
36678
+ return doc.body.innerHTML.replace(/​/g, "");
36679
+ }
36680
+ function htmlIsEmpty(el) {
36681
+ if (el.querySelector("img, hr")) return false;
36682
+ return (el.textContent ?? "").trim().length === 0;
36683
+ }
36684
+ function closestBlock(node, root) {
36685
+ let cur = node;
36686
+ while (cur && cur !== root) {
36687
+ if (cur instanceof Element && BLOCK_TAGS.has(cur.tagName)) return cur;
36688
+ cur = cur.parentNode;
36735
36689
  }
36736
- const seed = block.children?.[0]?.content ?? block.content ?? "";
36737
- return { id: block.id, type, content: seed };
36690
+ return null;
36738
36691
  }
36739
- function BlockMenu({ block, readOnly, onDelete, onDuplicate, onChangeType }) {
36740
- const { t } = useTranslate();
36741
- const [open, setOpen] = useState(false);
36742
- const ref = useRef(null);
36743
- useEffect(() => {
36744
- if (!open) return;
36745
- function onDocClick(e) {
36746
- if (ref.current && !ref.current.contains(e.target)) {
36747
- setOpen(false);
36692
+ function RichTextStyles() {
36693
+ return /* @__PURE__ */ jsx("style", { children: RICH_TEXT_CSS });
36694
+ }
36695
+ function readToolbarState(root) {
36696
+ const sel = window.getSelection();
36697
+ if (!sel || sel.rangeCount === 0) return IDLE_TOOLBAR;
36698
+ const anchor = sel.anchorNode;
36699
+ if (!anchor || !root.contains(anchor)) return IDLE_TOOLBAR;
36700
+ let block = null;
36701
+ let link = false;
36702
+ let node = anchor;
36703
+ while (node && node !== root) {
36704
+ if (node instanceof Element) {
36705
+ const tag = node.tagName.toLowerCase();
36706
+ if (!block && (tag === "h1" || tag === "h2" || tag === "h3" || tag === "blockquote" || tag === "pre")) {
36707
+ block = tag;
36748
36708
  }
36709
+ if (tag === "a") link = true;
36749
36710
  }
36750
- document.addEventListener("mousedown", onDocClick);
36751
- return () => document.removeEventListener("mousedown", onDocClick);
36752
- }, [open]);
36753
- if (readOnly) return null;
36754
- return /* @__PURE__ */ jsxs(Box, { ref, className: "relative", children: [
36755
- /* @__PURE__ */ jsx(
36756
- Button,
36757
- {
36758
- type: "button",
36759
- variant: "ghost",
36760
- "aria-label": t("richBlockEditor.blockActions"),
36761
- className: cn(
36762
- "inline-flex items-center justify-center",
36763
- "h-6 w-6 rounded-sm p-0 gap-0",
36764
- "text-muted-foreground hover:bg-muted hover:text-foreground",
36765
- "opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
36766
- "transition-opacity"
36767
- ),
36768
- onClick: () => setOpen((v) => !v),
36769
- children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", className: "w-3.5 h-3.5" })
36770
- }
36771
- ),
36772
- open && /* @__PURE__ */ jsxs(
36773
- Box,
36774
- {
36775
- role: "menu",
36776
- className: cn(
36777
- "absolute right-0 z-10 mt-1 w-44",
36778
- "rounded-container border border-border bg-popover shadow-elevation-popover",
36779
- "py-1 text-sm"
36780
- ),
36781
- children: [
36782
- /* @__PURE__ */ jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t(BLOCK_TYPE_LABEL_KEY[block.type]) }),
36783
- /* @__PURE__ */ jsxs(
36784
- Button,
36785
- {
36786
- type: "button",
36787
- variant: "ghost",
36788
- role: "menuitem",
36789
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
36790
- onClick: () => {
36791
- onDuplicate();
36792
- setOpen(false);
36793
- },
36794
- children: [
36795
- /* @__PURE__ */ jsx(Icon, { name: "plus", className: "w-3.5 h-3.5" }),
36796
- " ",
36797
- t("richBlockEditor.duplicate")
36798
- ]
36799
- }
36800
- ),
36801
- /* @__PURE__ */ jsxs(
36802
- Button,
36803
- {
36804
- type: "button",
36805
- variant: "ghost",
36806
- role: "menuitem",
36807
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-destructive hover:bg-muted justify-start rounded-none",
36808
- onClick: () => {
36809
- onDelete();
36810
- setOpen(false);
36811
- },
36812
- children: [
36813
- /* @__PURE__ */ jsx(Icon, { name: "trash", className: "w-3.5 h-3.5" }),
36814
- " ",
36815
- t("common.delete")
36816
- ]
36817
- }
36818
- ),
36819
- CHANGEABLE_TYPES.includes(block.type) && /* @__PURE__ */ jsxs(Fragment, { children: [
36820
- /* @__PURE__ */ jsx(Box, { className: "my-1 border-t border-border" }),
36821
- /* @__PURE__ */ jsx(Box, { className: "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground", children: t("richBlockEditor.turnInto") }),
36822
- CHANGEABLE_TYPES.filter((bt) => bt !== block.type).map((bt) => /* @__PURE__ */ jsx(
36823
- Button,
36824
- {
36825
- type: "button",
36826
- variant: "ghost",
36827
- role: "menuitem",
36828
- className: "flex w-full items-center gap-2 px-2 py-1.5 text-left justify-start rounded-none",
36829
- onClick: () => {
36830
- onChangeType(bt);
36831
- setOpen(false);
36832
- },
36833
- children: t(BLOCK_TYPE_LABEL_KEY[bt])
36834
- },
36835
- bt
36836
- ))
36837
- ] })
36838
- ]
36839
- }
36840
- )
36841
- ] });
36711
+ node = node.parentNode;
36712
+ }
36713
+ return {
36714
+ bold: document.queryCommandState("bold"),
36715
+ italic: document.queryCommandState("italic"),
36716
+ underline: document.queryCommandState("underline"),
36717
+ strikeThrough: document.queryCommandState("strikeThrough"),
36718
+ block,
36719
+ bullets: document.queryCommandState("insertUnorderedList"),
36720
+ numbers: document.queryCommandState("insertOrderedList"),
36721
+ link
36722
+ };
36842
36723
  }
36843
- function Editable({
36844
- tag,
36845
- value,
36846
- readOnly,
36847
- placeholder,
36848
- className,
36849
- ariaLabel,
36850
- onValueChange
36851
- }) {
36852
- const ref = useRef(null);
36853
- useEffect(() => {
36854
- const el = ref.current;
36855
- if (!el) return;
36856
- const isFocused = document.activeElement === el;
36857
- if (!isFocused && el.textContent !== value) {
36858
- el.textContent = value;
36859
- }
36860
- }, [value]);
36861
- const handleInput = useCallback(
36862
- (e) => {
36863
- onValueChange(e.currentTarget.textContent ?? "");
36864
- },
36865
- [onValueChange]
36866
- );
36724
+ function ToolbarButton({ icon: IconCmp, label, active, onExec }) {
36867
36725
  return /* @__PURE__ */ jsx(
36868
- Box,
36726
+ Button,
36869
36727
  {
36870
- as: tag,
36871
- ref,
36872
- contentEditable: !readOnly,
36873
- suppressContentEditableWarning: true,
36874
- role: readOnly ? void 0 : "textbox",
36875
- "aria-label": ariaLabel,
36876
- "aria-multiline": "true",
36877
- "data-placeholder": placeholder,
36728
+ type: "button",
36729
+ variant: "ghost",
36730
+ size: "sm",
36731
+ "aria-label": label,
36732
+ "aria-pressed": active,
36733
+ title: label,
36878
36734
  className: cn(
36879
- "outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-sm",
36880
- "empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground/60",
36881
- className
36735
+ "h-8 w-8 p-0 gap-0 justify-center",
36736
+ active && "bg-muted text-foreground"
36882
36737
  ),
36883
- onInput: handleInput
36738
+ onMouseDown: (e) => {
36739
+ e.preventDefault();
36740
+ onExec();
36741
+ },
36742
+ children: /* @__PURE__ */ jsx(IconCmp, { size: 15 })
36884
36743
  }
36885
36744
  );
36886
36745
  }
36887
- function BlockRow({
36888
- block,
36889
- readOnly,
36890
- showAffordances,
36891
- placeholder,
36892
- onUpdate,
36893
- onDelete,
36894
- onDuplicate,
36895
- onChangeType
36896
- }) {
36897
- const { t } = useTranslate();
36898
- const setContent = useCallback(
36899
- (next) => onUpdate((b) => ({ ...b, content: next })),
36900
- [onUpdate]
36901
- );
36902
- const setMetadata = useCallback(
36903
- (key, value) => onUpdate((b) => ({
36904
- ...b,
36905
- metadata: { ...b.metadata ?? {}, [key]: value }
36906
- })),
36907
- [onUpdate]
36908
- );
36909
- const setChildContent = useCallback(
36910
- (childId, next) => onUpdate((b) => ({
36911
- ...b,
36912
- children: (b.children ?? []).map(
36913
- (c) => c.id === childId ? { ...c, content: next } : c
36914
- )
36915
- })),
36916
- [onUpdate]
36917
- );
36918
- const addListItem = useCallback(
36919
- () => onUpdate((b) => ({
36920
- ...b,
36921
- children: [
36922
- ...b.children ?? [],
36923
- { id: nextBlockId("li"), type: "paragraph", content: "" }
36924
- ]
36925
- })),
36926
- [onUpdate]
36927
- );
36928
- const removeListItem = useCallback(
36929
- (childId) => onUpdate((b) => {
36930
- const remaining = (b.children ?? []).filter((c) => c.id !== childId);
36931
- return {
36932
- ...b,
36933
- children: remaining.length === 0 ? [{ id: nextBlockId("li"), type: "paragraph", content: "" }] : remaining
36934
- };
36935
- }),
36936
- [onUpdate]
36937
- );
36938
- const renderBody = () => {
36939
- switch (block.type) {
36940
- case "heading-1":
36941
- return /* @__PURE__ */ jsx(
36942
- Editable,
36943
- {
36944
- tag: "h1",
36945
- value: block.content ?? "",
36946
- readOnly,
36947
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading1"),
36948
- ariaLabel: t("richBlockEditor.aria.heading1Block"),
36949
- className: "text-3xl font-bold leading-tight",
36950
- onValueChange: setContent
36951
- }
36952
- );
36953
- case "heading-2":
36954
- return /* @__PURE__ */ jsx(
36955
- Editable,
36956
- {
36957
- tag: "h2",
36958
- value: block.content ?? "",
36959
- readOnly,
36960
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading2"),
36961
- ariaLabel: t("richBlockEditor.aria.heading2Block"),
36962
- className: "text-2xl font-semibold leading-tight",
36963
- onValueChange: setContent
36964
- }
36965
- );
36966
- case "heading-3":
36967
- return /* @__PURE__ */ jsx(
36968
- Editable,
36969
- {
36970
- tag: "h3",
36971
- value: block.content ?? "",
36972
- readOnly,
36973
- placeholder: placeholder ?? t("richBlockEditor.placeholder.heading3"),
36974
- ariaLabel: t("richBlockEditor.aria.heading3Block"),
36975
- className: "text-xl font-semibold leading-tight",
36976
- onValueChange: setContent
36977
- }
36978
- );
36979
- case "quote":
36980
- return /* @__PURE__ */ jsx(
36981
- Editable,
36982
- {
36983
- tag: "blockquote",
36984
- value: block.content ?? "",
36985
- readOnly,
36986
- placeholder: placeholder ?? t("richBlockEditor.placeholder.quote"),
36987
- ariaLabel: t("richBlockEditor.aria.quoteBlock"),
36988
- className: "border-l-4 border-primary/60 pl-4 italic text-muted-foreground",
36989
- onValueChange: setContent
36990
- }
36991
- );
36992
- case "code":
36993
- return /* @__PURE__ */ jsxs(Box, { className: "rounded-md border border-border bg-muted/40", children: [
36994
- /* @__PURE__ */ jsxs(Box, { className: "flex items-center justify-between border-b border-border px-3 py-1 text-xs text-muted-foreground", children: [
36995
- /* @__PURE__ */ jsx(Typography, { as: "span", variant: "caption", className: "uppercase tracking-wide", children: t("richBlockEditor.blockType.code") }),
36996
- !readOnly && /* @__PURE__ */ jsx(
36997
- Input,
36998
- {
36999
- inputType: "text",
37000
- value: String(block.metadata?.language ?? "plaintext"),
37001
- "aria-label": t("richBlockEditor.aria.codeLanguage"),
37002
- className: cn(
37003
- "h-6 w-32 rounded-sm border border-border bg-background",
37004
- "px-2 text-xs outline-none focus:ring-1 focus:ring-ring"
37005
- ),
37006
- onChange: (e) => setMetadata("language", e.target.value)
37007
- }
37008
- ),
37009
- readOnly && /* @__PURE__ */ jsx(Typography, { as: "span", variant: "caption", className: "text-xs", children: String(block.metadata?.language ?? "plaintext") })
37010
- ] }),
37011
- /* @__PURE__ */ jsx(
37012
- Editable,
37013
- {
37014
- tag: "pre",
37015
- value: block.content ?? "",
37016
- readOnly,
37017
- placeholder: placeholder ?? t("richBlockEditor.placeholder.code"),
37018
- ariaLabel: t("richBlockEditor.aria.codeBlock"),
37019
- className: "block whitespace-pre-wrap p-3 font-mono text-sm leading-relaxed",
37020
- onValueChange: setContent
37021
- }
37022
- )
37023
- ] });
37024
- case "divider":
37025
- return /* @__PURE__ */ jsx(Divider, { className: "my-2" });
37026
- case "image": {
37027
- const url = String(block.metadata?.url ?? "");
37028
- const caption = String(block.metadata?.caption ?? "");
37029
- const imgProps = {
37030
- src: url,
37031
- alt: caption || t("richBlockEditor.embeddedImage"),
37032
- className: "max-h-96 w-full rounded-md border border-border object-contain"
36746
+ var CHANGE_DEBOUNCE_MS, ALLOWED_ATTRS, DROP_TAGS, MD_BLOCK_PREFIXES, BLOCK_TAGS, MD_INLINE_RULES, RICH_TEXT_CSS, IDLE_TOOLBAR, RichTextEditor;
36747
+ var init_RichTextEditor = __esm({
36748
+ "components/core/molecules/RichTextEditor.tsx"() {
36749
+ "use client";
36750
+ init_cn();
36751
+ init_Box();
36752
+ init_Button();
36753
+ init_useEventBus();
36754
+ CHANGE_DEBOUNCE_MS = 400;
36755
+ ALLOWED_ATTRS = {
36756
+ p: [],
36757
+ h1: [],
36758
+ h2: [],
36759
+ h3: [],
36760
+ h4: [],
36761
+ ul: [],
36762
+ ol: [],
36763
+ li: [],
36764
+ blockquote: [],
36765
+ pre: [],
36766
+ code: [],
36767
+ b: [],
36768
+ strong: [],
36769
+ i: [],
36770
+ em: [],
36771
+ u: [],
36772
+ s: [],
36773
+ strike: [],
36774
+ br: [],
36775
+ hr: [],
36776
+ div: [],
36777
+ span: [],
36778
+ a: ["href"],
36779
+ img: ["src", "alt"]
36780
+ };
36781
+ DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "link", "meta", "form", "input", "button", "textarea", "select"]);
36782
+ MD_BLOCK_PREFIXES = {
36783
+ "#": { kind: "block", tag: "h1" },
36784
+ "##": { kind: "block", tag: "h2" },
36785
+ "###": { kind: "block", tag: "h3" },
36786
+ ">": { kind: "block", tag: "blockquote" },
36787
+ "```": { kind: "block", tag: "pre" },
36788
+ "-": { kind: "list", command: "insertUnorderedList" },
36789
+ "*": { kind: "list", command: "insertUnorderedList" },
36790
+ "1.": { kind: "list", command: "insertOrderedList" }
36791
+ };
36792
+ BLOCK_TAGS = /* @__PURE__ */ new Set(["P", "DIV", "LI", "H1", "H2", "H3", "H4", "BLOCKQUOTE", "PRE"]);
36793
+ MD_INLINE_RULES = [
36794
+ { pattern: /\*\*([^*\n]+)\*\*$/, wrap: "strong" },
36795
+ { pattern: /(^|[^*])\*([^*\n]+)\*$/, wrap: "em" },
36796
+ { pattern: /`([^`\n]+)`$/, wrap: "code" }
36797
+ ];
36798
+ RICH_TEXT_CSS = `
36799
+ .almadar-rich-text { line-height: 1.7; color: var(--color-foreground); }
36800
+ .almadar-rich-text h1 { font-size: 1.875rem; font-weight: 700; line-height: 1.25; margin: 1.25em 0 0.4em; }
36801
+ .almadar-rich-text h2 { font-size: 1.5rem; font-weight: 650; line-height: 1.3; margin: 1.1em 0 0.4em; }
36802
+ .almadar-rich-text h3 { font-size: 1.25rem; font-weight: 600; line-height: 1.35; margin: 1em 0 0.35em; }
36803
+ .almadar-rich-text h1:first-child, .almadar-rich-text h2:first-child, .almadar-rich-text h3:first-child { margin-top: 0; }
36804
+ .almadar-rich-text p { margin: 0.5em 0; }
36805
+ .almadar-rich-text ul { list-style: disc; padding-inline-start: 1.5rem; margin: 0.5em 0; }
36806
+ .almadar-rich-text ol { list-style: decimal; padding-inline-start: 1.5rem; margin: 0.5em 0; }
36807
+ .almadar-rich-text li { margin: 0.25em 0; }
36808
+ .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; }
36809
+ .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; }
36810
+ .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; }
36811
+ .almadar-rich-text pre code { background: transparent; padding: 0; }
36812
+ .almadar-rich-text a { color: var(--color-primary); text-decoration: underline; }
36813
+ .almadar-rich-text hr { border: none; border-top: 1px solid var(--color-border); margin: 1.25em 0; }
36814
+ .almadar-rich-text img { max-width: 100%; border-radius: 0.375rem; }
36815
+ `;
36816
+ IDLE_TOOLBAR = {
36817
+ bold: false,
36818
+ italic: false,
36819
+ underline: false,
36820
+ strikeThrough: false,
36821
+ block: null,
36822
+ bullets: false,
36823
+ numbers: false,
36824
+ link: false
36825
+ };
36826
+ RichTextEditor = ({
36827
+ value,
36828
+ onChange,
36829
+ changeEvent,
36830
+ readOnly = false,
36831
+ placeholder,
36832
+ showToolbar = true,
36833
+ className
36834
+ }) => {
36835
+ const { t } = useTranslate();
36836
+ const ref = useRef(null);
36837
+ const [toolbar, setToolbar] = useState(IDLE_TOOLBAR);
36838
+ const [empty, setEmpty] = useState(() => !value || !sanitizeRichHtml(value).trim());
36839
+ const eventBus = useEventBus();
36840
+ const onChangeRef = useRef(onChange);
36841
+ const changeEventRef = useRef(changeEvent);
36842
+ useEffect(() => {
36843
+ onChangeRef.current = onChange;
36844
+ changeEventRef.current = changeEvent;
36845
+ }, [onChange, changeEvent]);
36846
+ const emitTimerRef = useRef(null);
36847
+ const emitNow = useCallback(() => {
36848
+ const el = ref.current;
36849
+ if (!el) return;
36850
+ const html = sanitizeRichHtml(el.innerHTML);
36851
+ onChangeRef.current?.(html);
36852
+ const evt = changeEventRef.current;
36853
+ if (evt) eventBus.emit(`UI:${evt}`, { value: html });
36854
+ }, [eventBus]);
36855
+ const scheduleEmit = useCallback(() => {
36856
+ if (emitTimerRef.current) clearTimeout(emitTimerRef.current);
36857
+ emitTimerRef.current = setTimeout(() => {
36858
+ emitTimerRef.current = null;
36859
+ emitNow();
36860
+ }, CHANGE_DEBOUNCE_MS);
36861
+ }, [emitNow]);
36862
+ const flushEmit = useCallback(() => {
36863
+ if (!emitTimerRef.current) return;
36864
+ clearTimeout(emitTimerRef.current);
36865
+ emitTimerRef.current = null;
36866
+ emitNow();
36867
+ }, [emitNow]);
36868
+ useEffect(() => () => {
36869
+ if (emitTimerRef.current) {
36870
+ clearTimeout(emitTimerRef.current);
36871
+ emitTimerRef.current = null;
36872
+ emitNow();
36873
+ }
36874
+ }, [emitNow]);
36875
+ useEffect(() => {
36876
+ if (readOnly) return;
36877
+ const el = ref.current;
36878
+ if (!el) return;
36879
+ if (document.activeElement === el) return;
36880
+ const next = sanitizeRichHtml(value ?? "") || "<p><br></p>";
36881
+ if (el.innerHTML !== next) {
36882
+ el.innerHTML = next;
36883
+ setEmpty(htmlIsEmpty(el));
36884
+ }
36885
+ }, [readOnly, value]);
36886
+ useEffect(() => {
36887
+ if (readOnly) return;
36888
+ document.execCommand("defaultParagraphSeparator", false, "p");
36889
+ }, [readOnly]);
36890
+ useEffect(() => {
36891
+ if (readOnly || !showToolbar) return;
36892
+ const onSelectionChange = () => {
36893
+ const el = ref.current;
36894
+ if (!el) return;
36895
+ setToolbar(readToolbarState(el));
37033
36896
  };
37034
- return /* @__PURE__ */ jsxs(Box, { className: "space-y-2", children: [
37035
- url ? /* @__PURE__ */ jsx(Box, { as: "img", ...imgProps }) : /* @__PURE__ */ jsxs(
37036
- Box,
37037
- {
37038
- className: cn(
37039
- "flex h-32 items-center justify-center",
37040
- "rounded-md border border-dashed border-border",
37041
- "text-sm text-muted-foreground"
37042
- ),
37043
- children: [
37044
- /* @__PURE__ */ jsx(Icon, { name: "image", className: "mr-2 w-4 h-4" }),
37045
- " ",
37046
- t("richBlockEditor.noImageUrl")
37047
- ]
37048
- }
37049
- ),
37050
- !readOnly && /* @__PURE__ */ jsxs(Box, { className: "flex flex-col gap-2 sm:flex-row", children: [
37051
- /* @__PURE__ */ jsx(
37052
- Input,
37053
- {
37054
- inputType: "url",
37055
- value: url,
37056
- placeholder: "https://example.com/image.png",
37057
- "aria-label": t("richBlockEditor.aria.imageUrl"),
37058
- className: cn(
37059
- "h-8 flex-1 rounded-sm border border-border bg-background",
37060
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
37061
- ),
37062
- onChange: (e) => setMetadata("url", e.target.value)
37063
- }
37064
- ),
37065
- /* @__PURE__ */ jsx(
37066
- Input,
37067
- {
37068
- inputType: "text",
37069
- value: caption,
37070
- placeholder: t("richBlockEditor.placeholder.caption"),
37071
- "aria-label": t("richBlockEditor.aria.imageCaption"),
37072
- className: cn(
37073
- "h-8 flex-1 rounded-sm border border-border bg-background",
37074
- "px-2 text-sm outline-none focus:ring-1 focus:ring-ring"
37075
- ),
37076
- onChange: (e) => setMetadata("caption", e.target.value)
37077
- }
37078
- )
37079
- ] }),
37080
- readOnly && caption && /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-center text-muted-foreground", children: caption })
36897
+ document.addEventListener("selectionchange", onSelectionChange);
36898
+ return () => document.removeEventListener("selectionchange", onSelectionChange);
36899
+ }, [readOnly, showToolbar]);
36900
+ const tryInlineMarkdown = useCallback(() => {
36901
+ const root = ref.current;
36902
+ if (!root) return false;
36903
+ const sel = window.getSelection();
36904
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
36905
+ const node = sel.anchorNode;
36906
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
36907
+ if (node.parentElement?.closest("code, pre")) return false;
36908
+ const text = (node.textContent ?? "").slice(0, sel.anchorOffset);
36909
+ for (const rule of MD_INLINE_RULES) {
36910
+ const m = rule.pattern.exec(text);
36911
+ if (!m) continue;
36912
+ const inner = rule.wrap === "em" ? m[2] : m[1];
36913
+ if (!inner || !inner.trim()) continue;
36914
+ const lead = rule.wrap === "em" ? m[1]?.length ?? 0 : 0;
36915
+ const range = document.createRange();
36916
+ range.setStart(node, m.index + lead);
36917
+ range.setEnd(node, sel.anchorOffset);
36918
+ const el = document.createElement(rule.wrap);
36919
+ el.textContent = inner;
36920
+ range.deleteContents();
36921
+ range.insertNode(el);
36922
+ const after = document.createTextNode("\u200B");
36923
+ el.parentNode?.insertBefore(after, el.nextSibling);
36924
+ const caret = document.createRange();
36925
+ caret.setStart(after, 1);
36926
+ caret.collapse(true);
36927
+ sel.removeAllRanges();
36928
+ sel.addRange(caret);
36929
+ return true;
36930
+ }
36931
+ return false;
36932
+ }, []);
36933
+ const tryBlockMarkdown = useCallback(() => {
36934
+ const root = ref.current;
36935
+ if (!root) return false;
36936
+ const sel = window.getSelection();
36937
+ if (!sel || !sel.isCollapsed || sel.rangeCount === 0) return false;
36938
+ const node = sel.anchorNode;
36939
+ if (!node || node.nodeType !== Node.TEXT_NODE || !root.contains(node)) return false;
36940
+ const block = closestBlock(node, root);
36941
+ if (block ? block.firstChild !== node : root.firstChild !== node) return false;
36942
+ const prefix = (node.textContent ?? "").slice(0, sel.anchorOffset);
36943
+ const rule = MD_BLOCK_PREFIXES[prefix];
36944
+ if (!rule) return false;
36945
+ if (node.parentElement?.closest("pre, code")) return false;
36946
+ if (rule.kind === "list" && node.parentElement?.closest("li")) return false;
36947
+ node.deleteData(0, prefix.length);
36948
+ const caret = document.createRange();
36949
+ caret.setStart(node, 0);
36950
+ caret.collapse(true);
36951
+ sel.removeAllRanges();
36952
+ sel.addRange(caret);
36953
+ if (rule.kind === "list") {
36954
+ const container = block ?? node.parentElement;
36955
+ const list = document.createElement(rule.command === "insertOrderedList" ? "ol" : "ul");
36956
+ const li = document.createElement("li");
36957
+ if (block) {
36958
+ while (block.firstChild) li.appendChild(block.firstChild);
36959
+ list.appendChild(li);
36960
+ block.replaceWith(list);
36961
+ } else if (container) {
36962
+ li.appendChild(node);
36963
+ list.appendChild(li);
36964
+ container.appendChild(list);
36965
+ }
36966
+ if (!li.firstChild) li.appendChild(document.createElement("br"));
36967
+ const liCaret = document.createRange();
36968
+ liCaret.selectNodeContents(li);
36969
+ liCaret.collapse(true);
36970
+ sel.removeAllRanges();
36971
+ sel.addRange(liCaret);
36972
+ } else {
36973
+ document.execCommand("formatBlock", false, rule.tag);
36974
+ }
36975
+ return true;
36976
+ }, []);
36977
+ const afterEdit = useCallback(() => {
36978
+ const el = ref.current;
36979
+ if (!el) return;
36980
+ tryInlineMarkdown();
36981
+ setEmpty(htmlIsEmpty(el));
36982
+ setToolbar(readToolbarState(el));
36983
+ scheduleEmit();
36984
+ }, [scheduleEmit, tryInlineMarkdown]);
36985
+ const handleKeyDown = useCallback((e) => {
36986
+ if (e.key === " " && tryBlockMarkdown()) {
36987
+ e.preventDefault();
36988
+ afterEdit();
36989
+ }
36990
+ }, [afterEdit, tryBlockMarkdown]);
36991
+ const execInline = useCallback((command) => {
36992
+ document.execCommand(command, false);
36993
+ afterEdit();
36994
+ }, [afterEdit]);
36995
+ const execBlock = useCallback((tag) => {
36996
+ const next = toolbar.block === tag ? "p" : tag;
36997
+ document.execCommand("formatBlock", false, next);
36998
+ afterEdit();
36999
+ }, [afterEdit, toolbar.block]);
37000
+ const execList = useCallback((command) => {
37001
+ document.execCommand(command, false);
37002
+ afterEdit();
37003
+ }, [afterEdit]);
37004
+ const execLink = useCallback(() => {
37005
+ if (toolbar.link) {
37006
+ document.execCommand("unlink", false);
37007
+ afterEdit();
37008
+ return;
37009
+ }
37010
+ const url = window.prompt(t("richTextEditor.linkPrompt"));
37011
+ if (!url) return;
37012
+ const safe = safeUrl(url, ["http://", "https://", "mailto:"]) ?? `https://${url}`;
37013
+ document.execCommand("createLink", false, safe);
37014
+ afterEdit();
37015
+ }, [afterEdit, t, toolbar.link]);
37016
+ const execRule = useCallback(() => {
37017
+ document.execCommand("insertHorizontalRule", false);
37018
+ afterEdit();
37019
+ }, [afterEdit]);
37020
+ if (readOnly) {
37021
+ return /* @__PURE__ */ jsxs(Box, { className: cn("almadar-rich-text max-w-none", className), children: [
37022
+ /* @__PURE__ */ jsx(RichTextStyles, {}),
37023
+ /* @__PURE__ */ jsx(Box, { dangerouslySetInnerHTML: { __html: sanitizeRichHtml(value ?? "") } })
37081
37024
  ] });
37082
37025
  }
37083
- case "bullet-list":
37084
- case "numbered-list": {
37085
- const items = block.children ?? [];
37086
- return /* @__PURE__ */ jsxs(
37026
+ return /* @__PURE__ */ jsxs(Box, { className: cn("flex flex-col gap-2", className), children: [
37027
+ /* @__PURE__ */ jsx(RichTextStyles, {}),
37028
+ showToolbar && /* @__PURE__ */ jsxs(
37087
37029
  Box,
37088
37030
  {
37089
- as: block.type === "bullet-list" ? "ul" : "ol",
37031
+ role: "toolbar",
37032
+ "aria-label": t("richTextEditor.editorToolbar"),
37090
37033
  className: cn(
37091
- "space-y-1 pl-6",
37092
- block.type === "bullet-list" ? "list-disc" : "list-decimal"
37034
+ "sticky top-0 z-10 flex flex-wrap items-center gap-0.5 self-start",
37035
+ "rounded-md border border-border bg-background/95 px-1 py-0.5 shadow-sm"
37093
37036
  ),
37094
37037
  children: [
37095
- items.map((child) => /* @__PURE__ */ jsxs(Box, { as: "li", className: "group/item flex items-start gap-2", children: [
37096
- /* @__PURE__ */ jsx(
37097
- Editable,
37098
- {
37099
- tag: "span",
37100
- value: child.content ?? "",
37101
- readOnly,
37102
- placeholder: t("richBlockEditor.placeholder.listItem"),
37103
- ariaLabel: t("richBlockEditor.aria.listItem"),
37104
- className: "inline-block min-w-[1ch] flex-1",
37105
- onValueChange: (next) => setChildContent(child.id, next)
37106
- }
37107
- ),
37108
- !readOnly && showAffordances && /* @__PURE__ */ jsx(
37109
- Button,
37110
- {
37111
- type: "button",
37112
- variant: "ghost",
37113
- "aria-label": t("richBlockEditor.aria.removeListItem"),
37114
- className: cn(
37115
- "h-5 w-5 shrink-0 rounded-sm text-muted-foreground p-0 gap-0",
37116
- "opacity-0 group-hover/item:opacity-100 hover:bg-muted hover:text-foreground"
37117
- ),
37118
- onClick: () => removeListItem(child.id),
37119
- children: /* @__PURE__ */ jsx(Icon, { name: "trash", className: "w-3 h-3" })
37120
- }
37121
- )
37122
- ] }, child.id)),
37123
- !readOnly && showAffordances && /* @__PURE__ */ jsx(Box, { as: "li", className: "list-none pl-0", children: /* @__PURE__ */ jsxs(
37124
- Button,
37125
- {
37126
- type: "button",
37127
- variant: "ghost",
37128
- className: cn(
37129
- "inline-flex items-center gap-1 text-xs text-muted-foreground",
37130
- "hover:text-foreground p-0 h-auto"
37131
- ),
37132
- onClick: addListItem,
37133
- children: [
37134
- /* @__PURE__ */ jsx(Icon, { name: "plus", className: "w-3 h-3" }),
37135
- " ",
37136
- t("richBlockEditor.addItem")
37137
- ]
37138
- }
37139
- ) })
37038
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Bold, label: t("richTextEditor.bold"), active: toolbar.bold, onExec: () => execInline("bold") }),
37039
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Italic, label: t("richTextEditor.italic"), active: toolbar.italic, onExec: () => execInline("italic") }),
37040
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Underline, label: t("richTextEditor.underline"), active: toolbar.underline, onExec: () => execInline("underline") }),
37041
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Strikethrough, label: t("richTextEditor.strikethrough"), active: toolbar.strikeThrough, onExec: () => execInline("strikeThrough") }),
37042
+ /* @__PURE__ */ jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
37043
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Heading1, label: t("richTextEditor.heading1"), active: toolbar.block === "h1", onExec: () => execBlock("h1") }),
37044
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Heading2, label: t("richTextEditor.heading2"), active: toolbar.block === "h2", onExec: () => execBlock("h2") }),
37045
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Heading3, label: t("richTextEditor.heading3"), active: toolbar.block === "h3", onExec: () => execBlock("h3") }),
37046
+ /* @__PURE__ */ jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
37047
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: List, label: t("richTextEditor.bulletList"), active: toolbar.bullets, onExec: () => execList("insertUnorderedList") }),
37048
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: ListOrdered, label: t("richTextEditor.numberedList"), active: toolbar.numbers, onExec: () => execList("insertOrderedList") }),
37049
+ /* @__PURE__ */ jsx(Box, { className: "mx-1 h-5 w-px bg-border" }),
37050
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Quote, label: t("richTextEditor.quote"), active: toolbar.block === "blockquote", onExec: () => execBlock("blockquote") }),
37051
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Code, label: t("richTextEditor.code"), active: toolbar.block === "pre", onExec: () => execBlock("pre") }),
37052
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Link$1, label: toolbar.link ? t("richTextEditor.removeLink") : t("richTextEditor.link"), active: toolbar.link, onExec: execLink }),
37053
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Minus, label: t("richTextEditor.divider"), onExec: execRule })
37140
37054
  ]
37141
37055
  }
37142
- );
37143
- }
37144
- case "paragraph":
37145
- default:
37146
- return /* @__PURE__ */ jsx(
37147
- Editable,
37056
+ ),
37057
+ /* @__PURE__ */ jsx(
37058
+ Box,
37148
37059
  {
37149
- tag: "p",
37150
- value: block.content ?? "",
37151
- readOnly,
37152
- placeholder: placeholder ?? t("richBlockEditor.placeholder.paragraph"),
37153
- ariaLabel: t("richBlockEditor.aria.paragraphBlock"),
37154
- className: "leading-7",
37155
- onValueChange: setContent
37060
+ ref,
37061
+ contentEditable: true,
37062
+ suppressContentEditableWarning: true,
37063
+ role: "textbox",
37064
+ "aria-multiline": "true",
37065
+ "aria-label": t("richTextEditor.editorSurface"),
37066
+ "data-placeholder": placeholder ?? t("richTextEditor.placeholder"),
37067
+ "data-empty": empty ? "true" : "false",
37068
+ className: cn(
37069
+ "almadar-rich-text max-w-none min-h-[8rem] outline-none",
37070
+ "data-[empty=true]:before:content-[attr(data-placeholder)]",
37071
+ "data-[empty=true]:before:text-muted-foreground/60",
37072
+ "data-[empty=true]:before:pointer-events-none",
37073
+ "data-[empty=true]:before:absolute",
37074
+ "relative cursor-text"
37075
+ ),
37076
+ onInput: afterEdit,
37077
+ onKeyDown: handleKeyDown,
37078
+ onBlur: flushEmit
37156
37079
  }
37157
- );
37158
- }
37080
+ )
37081
+ ] });
37082
+ };
37083
+ RichTextEditor.displayName = "RichTextEditor";
37084
+ }
37085
+ });
37086
+ function renderIconInput4(icon, props) {
37087
+ return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
37088
+ }
37089
+ function DocumentPanel({
37090
+ entity,
37091
+ title,
37092
+ subtitle,
37093
+ value,
37094
+ actions,
37095
+ maxInlineActions,
37096
+ editing = false,
37097
+ titleCommitEvent,
37098
+ contentChangeEvent,
37099
+ editEvent,
37100
+ doneEvent,
37101
+ editLabel,
37102
+ doneLabel,
37103
+ autosaveHint,
37104
+ placeholder,
37105
+ className
37106
+ }) {
37107
+ const eventBus = useEventBus();
37108
+ const { t } = useTranslate();
37109
+ const [titleDraft, setTitleDraft] = useState(null);
37110
+ const recordId = entity?.id !== void 0 && entity?.id !== null ? String(entity.id) : "";
37111
+ const actionDefs = actions ?? [];
37112
+ const inlineCap = Math.min(maxInlineActions ?? 1, 1);
37113
+ const inlineActions = actionDefs.filter((a) => a.variant === "primary").slice(0, inlineCap);
37114
+ const menuActions = actionDefs.filter((a) => !inlineActions.includes(a));
37115
+ const fireAction = (action) => {
37116
+ if (!action.event) return;
37117
+ eventBus.emit(`UI:${action.event}`, { id: recordId, row: entity });
37159
37118
  };
37160
- return /* @__PURE__ */ jsxs(
37119
+ const commitTitle = () => {
37120
+ if (!titleCommitEvent) return;
37121
+ const next = (titleDraft ?? "").trim();
37122
+ setTitleDraft(null);
37123
+ if (!next || next === title) return;
37124
+ eventBus.emit(`UI:${titleCommitEvent}`, { title: next, id: recordId });
37125
+ };
37126
+ const untitled = t("documentPanel.untitled") || "Untitled";
37127
+ const titleNode = titleCommitEvent && titleDraft !== null ? /* @__PURE__ */ jsx(
37128
+ Input,
37129
+ {
37130
+ value: titleDraft,
37131
+ autoFocus: true,
37132
+ "aria-label": t("common.title"),
37133
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
37134
+ onFocus: (e) => e.currentTarget.select(),
37135
+ onChange: (e) => setTitleDraft(e.target.value),
37136
+ onBlur: commitTitle,
37137
+ onKeyDown: (e) => {
37138
+ if (e.key === "Enter") {
37139
+ e.preventDefault();
37140
+ commitTitle();
37141
+ } else if (e.key === "Escape") {
37142
+ e.preventDefault();
37143
+ setTitleDraft(null);
37144
+ }
37145
+ },
37146
+ "data-testid": "document-title-input"
37147
+ }
37148
+ ) : titleCommitEvent ? /* @__PURE__ */ jsx(
37161
37149
  Box,
37162
37150
  {
37163
- className: cn(
37164
- "group relative flex items-start gap-2 rounded-sm",
37165
- "px-2 py-1 hover:bg-muted/30"
37166
- ),
37167
- "data-block-id": block.id,
37168
- "data-block-type": block.type,
37169
- children: [
37170
- !readOnly && showAffordances && /* @__PURE__ */ jsx(Box, { className: "flex w-8 shrink-0 items-center pt-1", children: /* @__PURE__ */ jsx(
37171
- BlockMenu,
37151
+ role: "button",
37152
+ tabIndex: 0,
37153
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
37154
+ onClick: () => setTitleDraft(title ?? ""),
37155
+ onKeyDown: (e) => {
37156
+ if (e.key === "Enter" || e.key === " ") {
37157
+ e.preventDefault();
37158
+ setTitleDraft(title ?? "");
37159
+ }
37160
+ },
37161
+ "data-testid": "document-title-editable",
37162
+ children: /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", className: cn(!title && "text-muted-foreground"), children: title || untitled })
37163
+ }
37164
+ ) : /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", children: title || untitled });
37165
+ const emitWithId = (event) => () => {
37166
+ if (event) eventBus.emit(`UI:${event}`, { id: recordId });
37167
+ };
37168
+ return /* @__PURE__ */ jsx(Card, { variant: "elevated", className: cn("w-full", className), children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", className: "p-6 sm:p-8", children: [
37169
+ /* @__PURE__ */ jsxs(HStack, { gap: "sm", className: "justify-between items-start", children: [
37170
+ /* @__PURE__ */ jsxs(VStack, { gap: "xs", className: "flex-1 min-w-0", children: [
37171
+ titleNode,
37172
+ subtitle ? /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: subtitle }) : null
37173
+ ] }),
37174
+ /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "flex-shrink-0 pt-1 items-center", children: editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
37175
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", className: "hidden sm:block", children: autosaveHint ?? (t("documentPanel.autosaveHint") || "") }),
37176
+ /* @__PURE__ */ jsx(Button, { variant: "primary", size: "sm", onClick: emitWithId(doneEvent), "data-testid": "document-done", children: doneLabel ?? (t("documentPanel.done") || "Done") })
37177
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
37178
+ editEvent && /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: emitWithId(editEvent), "data-testid": "document-edit", children: [
37179
+ /* @__PURE__ */ jsx(Icon, { name: "edit", size: "xs", className: "mr-1" }),
37180
+ editLabel ?? (t("documentPanel.edit") || "Edit")
37181
+ ] }),
37182
+ inlineActions.map((action, idx) => /* @__PURE__ */ jsxs(
37183
+ Button,
37172
37184
  {
37173
- block,
37174
- readOnly,
37175
- onDelete,
37176
- onDuplicate,
37177
- onChangeType
37185
+ variant: "primary",
37186
+ size: "sm",
37187
+ onClick: () => fireAction(action),
37188
+ "data-testid": `action-${action.event}`,
37189
+ children: [
37190
+ action.icon && renderIconInput4(action.icon, { size: "xs", className: "mr-1" }),
37191
+ action.label
37192
+ ]
37193
+ },
37194
+ idx
37195
+ )),
37196
+ menuActions.length > 0 && /* @__PURE__ */ jsx(
37197
+ Menu,
37198
+ {
37199
+ position: "bottom-end",
37200
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
37201
+ items: menuActions.map((action) => ({
37202
+ label: action.label,
37203
+ icon: action.icon,
37204
+ variant: action.variant === "danger" ? "danger" : "default",
37205
+ onClick: () => fireAction(action)
37206
+ }))
37178
37207
  }
37179
- ) }),
37180
- /* @__PURE__ */ jsx(Box, { className: "min-w-0 flex-1", children: renderBody() })
37181
- ]
37182
- }
37183
- );
37208
+ )
37209
+ ] }) })
37210
+ ] }),
37211
+ editing ? /* @__PURE__ */ jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsx(
37212
+ Box,
37213
+ {
37214
+ onClick: emitWithId(editEvent),
37215
+ className: cn(
37216
+ "rounded-md px-1 -mx-1 min-h-[16rem]",
37217
+ editEvent && "cursor-text transition-colors hover:bg-muted/30"
37218
+ ),
37219
+ "data-testid": "document-body",
37220
+ children: value && value !== "" ? /* @__PURE__ */ jsx(RichTextEditor, { value, readOnly: true }) : /* @__PURE__ */ jsx(Typography, { variant: "body", color: "muted", children: placeholder ?? (t("documentPanel.placeholder") || "") })
37221
+ }
37222
+ )
37223
+ ] }) });
37184
37224
  }
37185
- var TOOLBAR_ENTRIES, BLOCK_TYPE_LABEL_KEY, CHANGEABLE_TYPES, _idSeq, BLOCK_TYPES, RichBlockEditor;
37186
- var init_RichBlockEditor = __esm({
37187
- "components/core/molecules/RichBlockEditor.tsx"() {
37225
+ var init_DocumentPanel = __esm({
37226
+ "components/core/molecules/DocumentPanel.tsx"() {
37188
37227
  "use client";
37189
37228
  init_cn();
37190
- init_Card();
37229
+ init_useEventBus();
37230
+ init_Box();
37231
+ init_Stack();
37191
37232
  init_Typography();
37192
37233
  init_Button();
37193
- init_Box();
37194
- init_Divider();
37195
- init_Input();
37234
+ init_Card();
37196
37235
  init_Icon();
37197
- init_useEventBus();
37198
- TOOLBAR_ENTRIES = [
37199
- { type: "paragraph", labelKey: "richBlockEditor.toolbar.text", icon: Type },
37200
- { type: "heading-1", labelKey: "richBlockEditor.toolbar.h1", icon: Heading1 },
37201
- { type: "heading-2", labelKey: "richBlockEditor.toolbar.h2", icon: Heading2 },
37202
- { type: "heading-3", labelKey: "richBlockEditor.toolbar.h3", icon: Heading3 },
37203
- { type: "bullet-list", labelKey: "richBlockEditor.toolbar.bulletList", icon: List },
37204
- { type: "numbered-list", labelKey: "richBlockEditor.toolbar.numbered", icon: ListOrdered },
37205
- { type: "quote", labelKey: "richBlockEditor.toolbar.quote", icon: Quote },
37206
- { type: "code", labelKey: "richBlockEditor.toolbar.code", icon: Code },
37207
- { type: "divider", labelKey: "richBlockEditor.toolbar.divider", icon: Minus },
37208
- { type: "image", labelKey: "richBlockEditor.toolbar.image", icon: Image$1 }
37209
- ];
37210
- BLOCK_TYPE_LABEL_KEY = {
37211
- paragraph: "richBlockEditor.blockType.paragraph",
37212
- "heading-1": "richBlockEditor.blockType.heading1",
37213
- "heading-2": "richBlockEditor.blockType.heading2",
37214
- "heading-3": "richBlockEditor.blockType.heading3",
37215
- "bullet-list": "richBlockEditor.blockType.bulletList",
37216
- "numbered-list": "richBlockEditor.blockType.numberedList",
37217
- quote: "richBlockEditor.blockType.quote",
37218
- code: "richBlockEditor.blockType.code",
37219
- divider: "richBlockEditor.blockType.divider",
37220
- image: "richBlockEditor.blockType.image"
37221
- };
37222
- CHANGEABLE_TYPES = [
37223
- "paragraph",
37224
- "heading-1",
37225
- "heading-2",
37226
- "heading-3",
37227
- "bullet-list",
37228
- "numbered-list",
37229
- "quote",
37230
- "code"
37231
- ];
37232
- _idSeq = 0;
37233
- BLOCK_TYPES = /* @__PURE__ */ new Set([
37234
- "paragraph",
37235
- "heading-1",
37236
- "heading-2",
37237
- "heading-3",
37238
- "bullet-list",
37239
- "numbered-list",
37240
- "quote",
37241
- "code",
37242
- "divider",
37243
- "image"
37244
- ]);
37245
- RichBlockEditor = ({
37246
- initialBlocks,
37247
- onChange,
37248
- changeEvent,
37249
- readOnly = false,
37250
- placeholder,
37251
- enableBlocks = true,
37252
- showToolbar = true,
37253
- className
37254
- }) => {
37255
- const { t } = useTranslate();
37256
- const [blocks, setBlocks] = useState(
37257
- () => normalizeBlocks(initialBlocks)
37258
- );
37259
- const onChangeRef = useRef(onChange);
37260
- useEffect(() => {
37261
- onChangeRef.current = onChange;
37262
- }, [onChange]);
37263
- const eventBus = useEventBus();
37264
- const changeEventRef = useRef(changeEvent);
37265
- useEffect(() => {
37266
- changeEventRef.current = changeEvent;
37267
- }, [changeEvent]);
37268
- const commit = useCallback((next) => {
37269
- setBlocks(next);
37270
- onChangeRef.current?.(next);
37271
- const evt = changeEventRef.current;
37272
- if (evt) eventBus.emit(`UI:${evt}`, { blocks: next });
37273
- }, [eventBus]);
37274
- const handleAppend = useCallback(
37275
- (type) => {
37276
- if (readOnly) return;
37277
- commit([...blocks, createBlock(type)]);
37278
- },
37279
- [blocks, commit, readOnly]
37280
- );
37281
- const handleUpdate = useCallback(
37282
- (id, updater) => {
37283
- commit(replaceBlock(blocks, id, updater));
37284
- },
37285
- [blocks, commit]
37286
- );
37287
- const handleDelete = useCallback(
37288
- (id) => {
37289
- const next = removeBlock(blocks, id);
37290
- commit(next.length > 0 ? next : [createBlock("paragraph")]);
37291
- },
37292
- [blocks, commit]
37293
- );
37294
- const handleDuplicate = useCallback(
37295
- (id) => {
37296
- const target = blocks.find((b) => b.id === id);
37297
- if (!target) return;
37298
- commit(insertAfter(blocks, id, duplicateBlock(target)));
37299
- },
37300
- [blocks, commit]
37236
+ init_Input();
37237
+ init_Menu();
37238
+ init_RichTextEditor();
37239
+ }
37240
+ });
37241
+ function renderIconInput5(icon, props) {
37242
+ return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
37243
+ }
37244
+ function fieldName(field) {
37245
+ return field.name ?? field.key ?? "";
37246
+ }
37247
+ function relationLabel(value) {
37248
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Date) return null;
37249
+ for (const key of ["name", "title", "label"]) {
37250
+ const candidate = value[key];
37251
+ if (typeof candidate === "string" && candidate !== "") return candidate;
37252
+ }
37253
+ const id = value.id;
37254
+ return id !== void 0 && id !== null ? String(id) : null;
37255
+ }
37256
+ function fieldKind(field, value) {
37257
+ if (field.kind) return field.kind;
37258
+ if (field.options && field.options.length > 0) return "select";
37259
+ if (typeof value === "boolean" || field.format === "boolean") return "boolean";
37260
+ if (Array.isArray(value) || relationLabel(value ?? null) !== null) return "readonly";
37261
+ return "text";
37262
+ }
37263
+ function DocumentDetails({
37264
+ entity,
37265
+ fields,
37266
+ metaCommitEvent,
37267
+ title,
37268
+ className
37269
+ }) {
37270
+ const eventBus = useEventBus();
37271
+ const { t } = useTranslate();
37272
+ const [fieldDraft, setFieldDraft] = useState(null);
37273
+ const recordId = entity?.id !== void 0 && entity?.id !== null ? String(entity.id) : "";
37274
+ const fieldDefs = (fields ?? []).filter((f3) => fieldName(f3) !== "");
37275
+ if (fieldDefs.length === 0) return null;
37276
+ const commitField = (name, next, current) => {
37277
+ setFieldDraft(null);
37278
+ if (!metaCommitEvent || next === current) return;
37279
+ eventBus.emit(`UI:${metaCommitEvent}`, { id: recordId, patch: { id: recordId, [name]: next } });
37280
+ };
37281
+ const renderValue = (field) => {
37282
+ const name = fieldName(field);
37283
+ const raw = getNestedValue(entity ?? {}, name);
37284
+ const kind = fieldKind(field, raw);
37285
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
37286
+ if (kind === "boolean") {
37287
+ return /* @__PURE__ */ jsx(
37288
+ Switch,
37289
+ {
37290
+ checked: Boolean(raw),
37291
+ disabled: !metaCommitEvent,
37292
+ "aria-label": label,
37293
+ onChange: (checked) => commitField(name, checked, Boolean(raw))
37294
+ }
37301
37295
  );
37302
- const handleChangeType = useCallback(
37303
- (id, type) => {
37304
- commit(
37305
- replaceBlock(blocks, id, (b) => changeBlockType(b, type))
37306
- );
37307
- },
37308
- [blocks, commit]
37296
+ }
37297
+ if (kind === "select") {
37298
+ return /* @__PURE__ */ jsx(
37299
+ Select,
37300
+ {
37301
+ value: raw !== void 0 && raw !== null ? String(raw) : "",
37302
+ disabled: !metaCommitEvent,
37303
+ "aria-label": label,
37304
+ className: "h-8 w-full",
37305
+ options: (field.options ?? []).map((opt) => ({ value: opt, label: humanizeEnumValue(opt) })),
37306
+ onChange: (e) => commitField(name, e.target.value, raw)
37307
+ }
37309
37308
  );
37310
- return /* @__PURE__ */ jsxs(
37311
- Card,
37309
+ }
37310
+ if (kind === "readonly" || !metaCommitEvent) {
37311
+ if (Array.isArray(raw)) {
37312
+ if (raw.length === 0) {
37313
+ return /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
37314
+ }
37315
+ return /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "flex-wrap", children: raw.map((item, i) => /* @__PURE__ */ jsx(Badge, { variant: "default", children: relationLabel(item) ?? humanizeEnumValue(String(item)) }, i)) });
37316
+ }
37317
+ const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
37318
+ return /* @__PURE__ */ jsx(Typography, { variant: "small", className: "break-words", children: shown2 });
37319
+ }
37320
+ if (fieldDraft?.name === name) {
37321
+ return /* @__PURE__ */ jsx(
37322
+ Input,
37312
37323
  {
37313
- variant: "bordered",
37314
- padding: "none",
37315
- className: cn("flex flex-col text-card-foreground", className),
37316
- children: [
37317
- enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsx(
37318
- Box,
37319
- {
37320
- role: "toolbar",
37321
- "aria-label": t("richBlockEditor.editorToolbar"),
37322
- className: cn(
37323
- "flex flex-wrap items-center gap-1",
37324
- "border-b border-border bg-muted/30 px-2 py-2"
37325
- ),
37326
- children: TOOLBAR_ENTRIES.map((entry) => {
37327
- const Icon2 = entry.icon;
37328
- const entryLabel = t(entry.labelKey);
37329
- return /* @__PURE__ */ jsxs(
37330
- Button,
37331
- {
37332
- type: "button",
37333
- variant: "ghost",
37334
- size: "sm",
37335
- "aria-label": t("richBlockEditor.insertEntry", { label: entryLabel }),
37336
- title: entryLabel,
37337
- onClick: () => handleAppend(entry.type),
37338
- children: [
37339
- /* @__PURE__ */ jsx(Icon2, { size: 14 }),
37340
- /* @__PURE__ */ jsx(Typography, { as: "span", variant: "caption", className: "ml-1 hidden text-xs sm:inline", children: entryLabel })
37341
- ]
37342
- },
37343
- entry.type
37344
- );
37345
- })
37346
- }
37347
- ),
37348
- /* @__PURE__ */ jsx(Box, { className: "flex flex-col gap-1 px-3 py-3", children: blocks.map((block) => /* @__PURE__ */ jsx(
37349
- BlockRow,
37350
- {
37351
- block,
37352
- readOnly,
37353
- showAffordances: enableBlocks,
37354
- placeholder,
37355
- onUpdate: (updater) => handleUpdate(block.id, updater),
37356
- onDelete: () => handleDelete(block.id),
37357
- onDuplicate: () => handleDuplicate(block.id),
37358
- onChangeType: (type) => handleChangeType(block.id, type)
37359
- },
37360
- block.id
37361
- )) })
37362
- ]
37324
+ value: fieldDraft.value,
37325
+ autoFocus: true,
37326
+ "aria-label": label,
37327
+ className: "h-8 w-full",
37328
+ onChange: (e) => setFieldDraft({ name, value: e.target.value }),
37329
+ onBlur: () => commitField(name, fieldDraft.value.trim(), raw),
37330
+ onKeyDown: (e) => {
37331
+ if (e.key === "Enter") {
37332
+ e.preventDefault();
37333
+ commitField(name, fieldDraft.value.trim(), raw);
37334
+ } else if (e.key === "Escape") {
37335
+ e.preventDefault();
37336
+ setFieldDraft(null);
37337
+ }
37338
+ },
37339
+ "data-testid": `document-property-input-${name}`
37363
37340
  }
37364
37341
  );
37365
- };
37366
- RichBlockEditor.displayName = "RichBlockEditor";
37342
+ }
37343
+ const shown = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
37344
+ return /* @__PURE__ */ jsx(
37345
+ Box,
37346
+ {
37347
+ role: "button",
37348
+ tabIndex: 0,
37349
+ className: cn(
37350
+ "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
37351
+ (raw === void 0 || raw === null || raw === "") && "text-muted-foreground"
37352
+ ),
37353
+ onClick: () => setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" }),
37354
+ onKeyDown: (e) => {
37355
+ if (e.key === "Enter" || e.key === " ") {
37356
+ e.preventDefault();
37357
+ setFieldDraft({ name, value: raw !== void 0 && raw !== null ? String(raw) : "" });
37358
+ }
37359
+ },
37360
+ "data-testid": `document-property-${name}`,
37361
+ children: /* @__PURE__ */ jsx(Typography, { variant: "small", className: "break-words", children: shown })
37362
+ }
37363
+ );
37364
+ };
37365
+ return /* @__PURE__ */ jsx(Card, { variant: "bordered", className: cn("w-full", className), children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", className: "p-4", children: [
37366
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "secondary", weight: "medium", className: "uppercase tracking-wide", children: title ?? (t("documentDetails.title") || "Details") }),
37367
+ /* @__PURE__ */ jsx(VStack, { gap: "sm", children: fieldDefs.map((field) => {
37368
+ const name = fieldName(field);
37369
+ const label = field.label ?? field.header ?? humanizeFieldName(name);
37370
+ return /* @__PURE__ */ jsxs(VStack, { gap: "xs", children: [
37371
+ /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "items-center", children: [
37372
+ field.icon && renderIconInput5(field.icon, { size: "xs", className: "text-muted-foreground" }),
37373
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", children: label })
37374
+ ] }),
37375
+ /* @__PURE__ */ jsx(Box, { className: "min-w-0", children: renderValue(field) })
37376
+ ] }, name);
37377
+ }) })
37378
+ ] }) });
37379
+ }
37380
+ var init_DocumentDetails = __esm({
37381
+ "components/core/molecules/DocumentDetails.tsx"() {
37382
+ "use client";
37383
+ init_cn();
37384
+ init_format();
37385
+ init_getNestedValue();
37386
+ init_useEventBus();
37387
+ init_Box();
37388
+ init_Stack();
37389
+ init_Typography();
37390
+ init_Badge();
37391
+ init_Card();
37392
+ init_Icon();
37393
+ init_Input();
37394
+ init_Select();
37395
+ init_Switch();
37367
37396
  }
37368
37397
  });
37369
37398
  function collectInitiallyCollapsed(nodes, acc) {
@@ -42312,8 +42341,8 @@ var init_DataTable = __esm({
42312
42341
  DataTable.displayName = "DataTable";
42313
42342
  }
42314
42343
  });
42315
- function getBadgeVariant(fieldName, value) {
42316
- const name = fieldName.toLowerCase();
42344
+ function getBadgeVariant(fieldName2, value) {
42345
+ const name = fieldName2.toLowerCase();
42317
42346
  const val = String(value).toLowerCase();
42318
42347
  if (name.includes("status")) {
42319
42348
  if (val.includes("complete") || val.includes("done") || val.includes("active"))
@@ -42329,12 +42358,12 @@ function getBadgeVariant(fieldName, value) {
42329
42358
  }
42330
42359
  return "default";
42331
42360
  }
42332
- function formatFieldValue2(value, fieldName) {
42361
+ function formatFieldValue2(value, fieldName2) {
42333
42362
  if (typeof value === "number") {
42334
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
42363
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
42335
42364
  return `${value}%`;
42336
42365
  }
42337
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
42366
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
42338
42367
  return `$${value.toLocaleString()}`;
42339
42368
  }
42340
42369
  return value.toLocaleString();
@@ -42344,7 +42373,7 @@ function formatFieldValue2(value, fieldName) {
42344
42373
  }
42345
42374
  return String(value);
42346
42375
  }
42347
- function renderRichFieldValue(value, fieldName, fieldType, meta) {
42376
+ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
42348
42377
  if (value === void 0 || value === null) return "\u2014";
42349
42378
  const str = String(value);
42350
42379
  switch (fieldType) {
@@ -42355,7 +42384,7 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
42355
42384
  "img",
42356
42385
  {
42357
42386
  src: str,
42358
- alt: formatFieldLabel(fieldName),
42387
+ alt: formatFieldLabel(fieldName2),
42359
42388
  className: "max-w-full max-h-64 rounded-md object-contain",
42360
42389
  loading: "lazy"
42361
42390
  }
@@ -42490,9 +42519,9 @@ function renderRichFieldValue(value, fieldName, fieldType, meta) {
42490
42519
  return /* @__PURE__ */ jsx("a", { href: `tel:${str}`, className: "text-primary hover:underline", children: str });
42491
42520
  default:
42492
42521
  if (meta?.values && meta.values.length > 0 && meta.values.includes(str)) {
42493
- return /* @__PURE__ */ jsx(Badge, { variant: getBadgeVariant(fieldName, str), children: humanizeEnumValue(str) });
42522
+ return /* @__PURE__ */ jsx(Badge, { variant: getBadgeVariant(fieldName2, str), children: humanizeEnumValue(str) });
42494
42523
  }
42495
- return formatFieldValue2(value, fieldName);
42524
+ return formatFieldValue2(value, fieldName2);
42496
42525
  }
42497
42526
  }
42498
42527
  function normalizeFieldDefs(fields) {
@@ -42962,16 +42991,11 @@ var init_DetailPanel = __esm({
42962
42991
  footer
42963
42992
  ] })
42964
42993
  ] }) });
42965
- return /* @__PURE__ */ jsx(
42966
- Box,
42967
- {
42968
- className: cn(
42969
- slideOver && "fixed inset-y-0 right-0 w-full max-w-2xl bg-card shadow-lg z-50 overflow-y-auto p-6",
42970
- className
42971
- ),
42972
- children: content
42973
- }
42974
- );
42994
+ if (!slideOver) {
42995
+ return /* @__PURE__ */ jsx(Box, { className, children: content });
42996
+ }
42997
+ const panel = /* @__PURE__ */ jsx(Box, { className: cn("fixed inset-y-0 right-0 w-full max-w-2xl bg-card shadow-lg z-50 overflow-y-auto p-6", className), children: content });
42998
+ return typeof document === "undefined" ? panel : createPortal(panel, document.body);
42975
42999
  };
42976
43000
  DetailPanel.displayName = "DetailPanel";
42977
43001
  }
@@ -43340,7 +43364,21 @@ var init_Form = __esm({
43340
43364
  const normalizedInitialData = React87__default.useMemo(() => {
43341
43365
  const entityRowAsInitial = isPlainEntityRow(entity) ? entity : void 0;
43342
43366
  const callerInitial = initialData !== null && typeof initialData === "object" && !Array.isArray(initialData) ? initialData : {};
43343
- return entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
43367
+ const merged = entityRowAsInitial !== void 0 ? { ...entityRowAsInitial, ...callerInitial } : callerInitial;
43368
+ const toId = (value) => {
43369
+ if (value === null || value === void 0 || typeof value !== "object" || value instanceof Date) return value;
43370
+ if (Array.isArray(value)) {
43371
+ return value.map(
43372
+ (item) => item !== null && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date) && item.id !== void 0 && item.id !== null ? String(item.id) : item
43373
+ );
43374
+ }
43375
+ return value.id !== void 0 && value.id !== null ? String(value.id) : value;
43376
+ };
43377
+ const normalized = {};
43378
+ for (const [key, value] of Object.entries(merged)) {
43379
+ normalized[key] = key === "id" ? value : toId(value);
43380
+ }
43381
+ return normalized;
43344
43382
  }, [entity, initialData]);
43345
43383
  const entityDerivedFields = React87__default.useMemo(() => {
43346
43384
  if (fields && fields.length > 0) return void 0;
@@ -43469,8 +43507,8 @@ var init_Form = __esm({
43469
43507
  checkViolations(name, newFormData);
43470
43508
  };
43471
43509
  const isFieldVisible = React87__default.useCallback(
43472
- (fieldName) => {
43473
- const condition = conditionalFields[fieldName];
43510
+ (fieldName2) => {
43511
+ const condition = conditionalFields[fieldName2];
43474
43512
  if (!condition) return true;
43475
43513
  return Boolean(evaluateFormExpression(condition, evalContext));
43476
43514
  },
@@ -43516,9 +43554,9 @@ var init_Form = __esm({
43516
43554
  };
43517
43555
  const handleInvalid = (e) => {
43518
43556
  const target = e.target;
43519
- const fieldName = target.getAttribute("data-field-name") ?? target.name ?? "";
43557
+ const fieldName2 = target.getAttribute("data-field-name") ?? target.name ?? "";
43520
43558
  const fieldMessage = target.validationMessage || "Invalid value";
43521
- debug("forms", "invalid", { mode: formMode, fieldName, fieldMessage });
43559
+ debug("forms", "invalid", { mode: formMode, fieldName: fieldName2, fieldMessage });
43522
43560
  queueMicrotask(() => {
43523
43561
  const form = formRef.current;
43524
43562
  if (!form) return;
@@ -43554,22 +43592,22 @@ var init_Form = __esm({
43554
43592
  };
43555
43593
  const renderField = React87__default.useCallback(
43556
43594
  (field) => {
43557
- const fieldName = field.name || field.field;
43558
- if (!fieldName) return null;
43559
- if (!isFieldVisible(fieldName)) {
43595
+ const fieldName2 = field.name || field.field;
43596
+ if (!fieldName2) return null;
43597
+ if (!isFieldVisible(fieldName2)) {
43560
43598
  return null;
43561
43599
  }
43562
43600
  const inputType = determineInputType(field);
43563
- const label = field.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/([A-Z])/g, " $1");
43564
- const currentValue = formData[fieldName] ?? field.defaultValue ?? "";
43565
- return /* @__PURE__ */ jsxs(VStack, { gap: "xs", "data-field": fieldName, children: [
43601
+ const label = field.label || fieldName2.charAt(0).toUpperCase() + fieldName2.slice(1).replace(/([A-Z])/g, " $1");
43602
+ const currentValue = formData[fieldName2] ?? field.defaultValue ?? "";
43603
+ return /* @__PURE__ */ jsxs(VStack, { gap: "xs", "data-field": fieldName2, children: [
43566
43604
  inputType !== "checkbox" && /* @__PURE__ */ jsxs(Typography, { as: "label", variant: "label", weight: "bold", children: [
43567
43605
  label,
43568
43606
  field.required && /* @__PURE__ */ jsx(Typography, { as: "span", color: "error", className: "ml-1", children: "*" })
43569
43607
  ] }),
43570
- renderFieldInput(field, fieldName, inputType, currentValue, label),
43608
+ renderFieldInput(field, fieldName2, inputType, currentValue, label),
43571
43609
  field.hint && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", children: field.hint })
43572
- ] }, fieldName);
43610
+ ] }, fieldName2);
43573
43611
  },
43574
43612
  [formData, isFieldVisible, relationsData, relationsLoading, isLoading]
43575
43613
  );
@@ -43598,8 +43636,8 @@ var init_Form = __esm({
43598
43636
  }
43599
43637
  return field;
43600
43638
  }).map((field) => {
43601
- const fieldName = field.name || field.field;
43602
- const override = fieldOverrides?.find((o) => o.name === fieldName);
43639
+ const fieldName2 = field.name || field.field;
43640
+ const override = fieldOverrides?.find((o) => o.name === fieldName2);
43603
43641
  if (!override) return field;
43604
43642
  return {
43605
43643
  ...field,
@@ -43655,11 +43693,11 @@ var init_Form = __esm({
43655
43693
  ] }, section.id);
43656
43694
  }).filter(Boolean);
43657
43695
  }, [sections, isSectionVisible, collapsedSections, renderField, gap]);
43658
- function renderFieldInput(field, fieldName, inputType, currentValue, label) {
43696
+ function renderFieldInput(field, fieldName2, inputType, currentValue, label) {
43659
43697
  const commonProps = {
43660
- id: fieldName,
43661
- name: fieldName,
43662
- "data-field-name": fieldName,
43698
+ id: fieldName2,
43699
+ name: fieldName2,
43700
+ "data-field-name": fieldName2,
43663
43701
  required: field.required,
43664
43702
  disabled: isLoading,
43665
43703
  placeholder: field.placeholder,
@@ -43673,7 +43711,7 @@ var init_Form = __esm({
43673
43711
  ...commonProps,
43674
43712
  label: label + (field.required ? " *" : ""),
43675
43713
  checked: Boolean(currentValue),
43676
- onChange: (e) => handleChange(fieldName, e.target.checked)
43714
+ onChange: (e) => handleChange(fieldName2, e.target.checked)
43677
43715
  }
43678
43716
  );
43679
43717
  case "textarea":
@@ -43682,7 +43720,7 @@ var init_Form = __esm({
43682
43720
  {
43683
43721
  ...commonProps,
43684
43722
  value: String(currentValue),
43685
- onChange: (e) => handleChange(fieldName, e.target.value),
43723
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43686
43724
  minLength: field.min,
43687
43725
  maxLength: field.max
43688
43726
  }
@@ -43695,14 +43733,14 @@ var init_Form = __esm({
43695
43733
  ...commonProps,
43696
43734
  options,
43697
43735
  value: String(currentValue),
43698
- onValueChange: (v) => handleChange(fieldName, v),
43736
+ onValueChange: (v) => handleChange(fieldName2, v),
43699
43737
  placeholder: field.placeholder || `Select ${label}...`
43700
43738
  }
43701
43739
  );
43702
43740
  }
43703
43741
  case "relation": {
43704
- const relationOptions = relationsData[fieldName] || [];
43705
- const relationLoading = relationsLoading[fieldName] || false;
43742
+ const relationOptions = relationsData[fieldName2] || [];
43743
+ const relationLoading = relationsLoading[fieldName2] || false;
43706
43744
  if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
43707
43745
  const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
43708
43746
  return /* @__PURE__ */ jsx(
@@ -43714,7 +43752,7 @@ var init_Form = __esm({
43714
43752
  clearable: true,
43715
43753
  options: [...relationOptions],
43716
43754
  value: selectedValues,
43717
- onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
43755
+ onValueChange: (value) => handleChange(fieldName2, Array.isArray(value) ? value : [value]),
43718
43756
  placeholder: field.placeholder || `Select ${label}...`
43719
43757
  }
43720
43758
  );
@@ -43724,7 +43762,7 @@ var init_Form = __esm({
43724
43762
  {
43725
43763
  ...commonProps,
43726
43764
  value: currentValue ? String(currentValue) : void 0,
43727
- onChange: (value) => handleChange(fieldName, value),
43765
+ onChange: (value) => handleChange(fieldName2, value),
43728
43766
  options: relationOptions,
43729
43767
  isLoading: relationLoading,
43730
43768
  placeholder: field.placeholder || `Select ${label}...`,
@@ -43741,7 +43779,7 @@ var init_Form = __esm({
43741
43779
  placeholder: field.placeholder,
43742
43780
  disabled: isLoading,
43743
43781
  value: arrayValue,
43744
- onChange: (next) => handleChange(fieldName, [...next])
43782
+ onChange: (next) => handleChange(fieldName2, [...next])
43745
43783
  }
43746
43784
  );
43747
43785
  }
@@ -43753,7 +43791,7 @@ var init_Form = __esm({
43753
43791
  type: "number",
43754
43792
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
43755
43793
  onChange: (e) => handleChange(
43756
- fieldName,
43794
+ fieldName2,
43757
43795
  e.target.value ? Number(e.target.value) : void 0
43758
43796
  ),
43759
43797
  min: field.min,
@@ -43770,7 +43808,7 @@ var init_Form = __esm({
43770
43808
  icon: DollarSign,
43771
43809
  value: currentValue !== void 0 && currentValue !== "" ? String(currentValue) : "",
43772
43810
  onChange: (e) => handleChange(
43773
- fieldName,
43811
+ fieldName2,
43774
43812
  e.target.value ? Number(e.target.value) : void 0
43775
43813
  ),
43776
43814
  min: field.min,
@@ -43788,7 +43826,7 @@ var init_Form = __esm({
43788
43826
  const f3 = files[0];
43789
43827
  if (!f3) return;
43790
43828
  const reader = new FileReader();
43791
- reader.onload = () => handleChange(fieldName, {
43829
+ reader.onload = () => handleChange(fieldName2, {
43792
43830
  name: f3.name,
43793
43831
  mimeType: f3.type,
43794
43832
  sizeBytes: f3.size,
@@ -43805,7 +43843,7 @@ var init_Form = __esm({
43805
43843
  ...commonProps,
43806
43844
  type: "date",
43807
43845
  value: formatDateValue(currentValue),
43808
- onChange: (e) => handleChange(fieldName, e.target.value)
43846
+ onChange: (e) => handleChange(fieldName2, e.target.value)
43809
43847
  }
43810
43848
  );
43811
43849
  case "datetime-local":
@@ -43815,7 +43853,7 @@ var init_Form = __esm({
43815
43853
  ...commonProps,
43816
43854
  type: "datetime-local",
43817
43855
  value: formatDateTimeValue(currentValue),
43818
- onChange: (e) => handleChange(fieldName, e.target.value)
43856
+ onChange: (e) => handleChange(fieldName2, e.target.value)
43819
43857
  }
43820
43858
  );
43821
43859
  case "email":
@@ -43825,7 +43863,7 @@ var init_Form = __esm({
43825
43863
  ...commonProps,
43826
43864
  type: "email",
43827
43865
  value: String(currentValue),
43828
- onChange: (e) => handleChange(fieldName, e.target.value),
43866
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43829
43867
  minLength: field.min,
43830
43868
  maxLength: field.max
43831
43869
  }
@@ -43837,7 +43875,7 @@ var init_Form = __esm({
43837
43875
  ...commonProps,
43838
43876
  type: "url",
43839
43877
  value: String(currentValue),
43840
- onChange: (e) => handleChange(fieldName, e.target.value),
43878
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43841
43879
  minLength: field.min,
43842
43880
  maxLength: field.max
43843
43881
  }
@@ -43849,7 +43887,7 @@ var init_Form = __esm({
43849
43887
  ...commonProps,
43850
43888
  type: "password",
43851
43889
  value: String(currentValue),
43852
- onChange: (e) => handleChange(fieldName, e.target.value),
43890
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43853
43891
  minLength: field.min,
43854
43892
  maxLength: field.max
43855
43893
  }
@@ -43862,7 +43900,7 @@ var init_Form = __esm({
43862
43900
  ...commonProps,
43863
43901
  type: "text",
43864
43902
  value: String(currentValue),
43865
- onChange: (e) => handleChange(fieldName, e.target.value),
43903
+ onChange: (e) => handleChange(fieldName2, e.target.value),
43866
43904
  minLength: field.min,
43867
43905
  maxLength: field.max
43868
43906
  }
@@ -44219,7 +44257,7 @@ function entityFieldsFromListItem(item) {
44219
44257
  }
44220
44258
  return result;
44221
44259
  }
44222
- function getStatusStyle(fieldName, value) {
44260
+ function getStatusStyle(fieldName2, value) {
44223
44261
  const val = String(value).toLowerCase();
44224
44262
  if (val.includes("complete") || val.includes("done"))
44225
44263
  return STATUS_STYLES.complete;
@@ -44235,12 +44273,12 @@ function getStatusStyle(fieldName, value) {
44235
44273
  if (val.includes("low")) return STATUS_STYLES.low;
44236
44274
  return STATUS_STYLES.default;
44237
44275
  }
44238
- function formatValue3(value, fieldName) {
44276
+ function formatValue3(value, fieldName2) {
44239
44277
  if (typeof value === "number") {
44240
- if (fieldName.toLowerCase().includes("progress") || fieldName.toLowerCase().includes("percent")) {
44278
+ if (fieldName2.toLowerCase().includes("progress") || fieldName2.toLowerCase().includes("percent")) {
44241
44279
  return `${value}%`;
44242
44280
  }
44243
- if (fieldName.toLowerCase().includes("budget") || fieldName.toLowerCase().includes("cost")) {
44281
+ if (fieldName2.toLowerCase().includes("budget") || fieldName2.toLowerCase().includes("cost")) {
44244
44282
  return new Intl.NumberFormat("en-US", {
44245
44283
  style: "currency",
44246
44284
  currency: "USD",
@@ -44263,8 +44301,8 @@ function formatValue3(value, fieldName) {
44263
44301
  }
44264
44302
  return String(value);
44265
44303
  }
44266
- function formatFieldLabel2(fieldName) {
44267
- return humanizeFieldName(fieldName).replace(/\sId$/, "").trim();
44304
+ function formatFieldLabel2(fieldName2) {
44305
+ return humanizeFieldName(fieldName2).replace(/\sId$/, "").trim();
44268
44306
  }
44269
44307
  var STATUS_STYLES, StatusBadge, ProgressIndicator, List3;
44270
44308
  var init_List = __esm({
@@ -44332,9 +44370,9 @@ var init_List = __esm({
44332
44370
  };
44333
44371
  StatusBadge = ({
44334
44372
  value,
44335
- fieldName
44373
+ fieldName: fieldName2
44336
44374
  }) => {
44337
- const style = getStatusStyle(fieldName, value);
44375
+ const style = getStatusStyle(fieldName2, value);
44338
44376
  return /* @__PURE__ */ jsxs(
44339
44377
  Typography,
44340
44378
  {
@@ -47247,8 +47285,8 @@ var init_StatCard = __esm({
47247
47285
  return items.length;
47248
47286
  }
47249
47287
  if (field.includes(":")) {
47250
- const [fieldName, fieldValue] = field.split(":");
47251
- return items.filter((item) => item[fieldName] === fieldValue).length;
47288
+ const [fieldName2, fieldValue] = field.split(":");
47289
+ return items.filter((item) => item[fieldName2] === fieldValue).length;
47252
47290
  }
47253
47291
  const fieldExistsOnItems = items.some((item) => field in item);
47254
47292
  if (fieldExistsOnItems) {
@@ -48768,6 +48806,8 @@ var init_component_registry_generated = __esm({
48768
48806
  init_DocSearch();
48769
48807
  init_DocSidebar();
48770
48808
  init_DocTOC();
48809
+ init_DocumentDetails();
48810
+ init_DocumentPanel();
48771
48811
  init_DocumentViewer();
48772
48812
  init_DrawFxLayer();
48773
48813
  init_DrawGroup();
@@ -48877,7 +48917,7 @@ var init_component_registry_generated = __esm({
48877
48917
  init_RelationSelect();
48878
48918
  init_RepeatableFormSection();
48879
48919
  init_ReplyTree();
48880
- init_RichBlockEditor();
48920
+ init_RichTextEditor();
48881
48921
  init_RuntimeDebugger2();
48882
48922
  init_ScaledDiagram();
48883
48923
  init_ScoreDisplay();
@@ -49040,6 +49080,8 @@ var init_component_registry_generated = __esm({
49040
49080
  "DocSearch": DocSearch,
49041
49081
  "DocSidebar": DocSidebar,
49042
49082
  "DocTOC": DocTOC,
49083
+ "DocumentDetails": DocumentDetails,
49084
+ "DocumentPanel": DocumentPanel,
49043
49085
  "DocumentViewer": DocumentViewer,
49044
49086
  "DrawFxLayer": DrawFxLayer,
49045
49087
  "DrawGroup": DrawGroup,
@@ -49151,7 +49193,7 @@ var init_component_registry_generated = __esm({
49151
49193
  "RelationSelect": RelationSelect,
49152
49194
  "RepeatableFormSection": RepeatableFormSection,
49153
49195
  "ReplyTree": ReplyTree,
49154
- "RichBlockEditor": RichBlockEditor,
49196
+ "RichTextEditor": RichTextEditor,
49155
49197
  "RuntimeDebugger": RuntimeDebugger,
49156
49198
  "ScaledDiagram": ScaledDiagram,
49157
49199
  "ScoreDisplay": ScoreDisplay,
@@ -49305,10 +49347,10 @@ function enrichFormFields(fields, entityDef) {
49305
49347
  }
49306
49348
  if (field && typeof field === "object" && !Array.isArray(field) && !React87__default.isValidElement(field) && !(field instanceof Date)) {
49307
49349
  const obj = field;
49308
- const fieldName = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
49309
- if (!fieldName) return field;
49350
+ const fieldName2 = typeof obj.name === "string" ? obj.name : typeof obj.field === "string" ? obj.field : void 0;
49351
+ if (!fieldName2) return field;
49310
49352
  if (obj.type || obj.inputType) return field;
49311
- const entityField = fieldMap.get(fieldName);
49353
+ const entityField = fieldMap.get(fieldName2);
49312
49354
  if (!entityField) return field;
49313
49355
  const enriched = { ...obj, type: entityField.type };
49314
49356
  if (entityField.required && !("required" in obj)) {
@@ -49358,9 +49400,9 @@ function enrichDetailFields(fields, entityDef) {
49358
49400
  }
49359
49401
  if (field && typeof field === "object" && !Array.isArray(field) && !React87__default.isValidElement(field) && !(field instanceof Date)) {
49360
49402
  const obj = field;
49361
- const fieldName = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
49362
- if (!fieldName || obj.type) return field;
49363
- const meta = metaFor(fieldName);
49403
+ const fieldName2 = typeof obj.key === "string" ? obj.key : typeof obj.name === "string" ? obj.name : void 0;
49404
+ if (!fieldName2 || obj.type) return field;
49405
+ const meta = metaFor(fieldName2);
49364
49406
  return meta ? { ...obj, ...meta } : field;
49365
49407
  }
49366
49408
  return field;
@@ -50199,9 +50241,9 @@ function UISlotRenderer({
50199
50241
  "ui-slot-renderer relative min-h-full",
50200
50242
  className
50201
50243
  ), children: [
50202
- /* @__PURE__ */ jsxs(Box, { className: "flex min-h-full", children: [
50203
- /* @__PURE__ */ jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 shrink-0" }),
50204
- /* @__PURE__ */ jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-h-[200px]" })
50244
+ /* @__PURE__ */ jsxs(Box, { className: "flex min-h-full flex-col lg:flex-row", children: [
50245
+ /* @__PURE__ */ jsx(UISlotComponent, { slot: "sidebar", className: "ui-slot-sidebar min-w-0 lg:shrink-0" }),
50246
+ /* @__PURE__ */ jsx(UISlotComponent, { slot: "main", className: "ui-slot-main flex-1 min-w-0 min-h-[200px]" })
50205
50247
  ] }),
50206
50248
  /* @__PURE__ */ jsx(UISlotComponent, { slot: "modal", portal: true }),
50207
50249
  /* @__PURE__ */ jsx(UISlotComponent, { slot: "drawer", portal: true }),