@almadar/ui 5.162.0 → 5.164.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.
@@ -24880,6 +24880,7 @@ var init_DashboardLayout = __esm({
24880
24880
  onNotificationClick,
24881
24881
  showThemeToggle = true,
24882
24882
  sidebarFooter,
24883
+ sidebarContent,
24883
24884
  onSignOut: onSignOutProp,
24884
24885
  currentPath,
24885
24886
  layoutMode = "sidebar",
@@ -25005,7 +25006,11 @@ var init_DashboardLayout = __esm({
25005
25006
  {
25006
25007
  as: "nav",
25007
25008
  gap: "none",
25008
- className: cn("flex-1 py-4 space-y-1 overflow-y-auto", isRail ? "px-2" : "px-3"),
25009
+ className: cn(
25010
+ "py-4 space-y-1 overflow-y-auto",
25011
+ sidebarContent && !isRail ? "shrink-0" : "flex-1",
25012
+ isRail ? "px-2" : "px-3"
25013
+ ),
25009
25014
  children: navItems.map((item) => /* @__PURE__ */ jsx(
25010
25015
  NavLink,
25011
25016
  {
@@ -25017,6 +25022,7 @@ var init_DashboardLayout = __esm({
25017
25022
  ))
25018
25023
  }
25019
25024
  ),
25025
+ sidebarContent && !isRail && /* @__PURE__ */ jsx(Box, { className: "flex-1 overflow-y-auto border-t border-border dark:border-border px-2 py-3", children: sidebarContent }),
25020
25026
  sidebarFooter && /* @__PURE__ */ jsx(Box, { className: "p-4 border-t border-border dark:border-border", children: sidebarFooter })
25021
25027
  ]
25022
25028
  }
@@ -27426,7 +27432,17 @@ function fileIcon(name) {
27426
27432
  return "file";
27427
27433
  }
27428
27434
  }
27429
- var TreeNodeItem, FlatTreeNodeItem, FileTree;
27435
+ function ancestorsOf(id, byId) {
27436
+ const out = /* @__PURE__ */ new Set();
27437
+ if (!id) return out;
27438
+ let cur = byId.get(id)?.parentId;
27439
+ while (cur && byId.has(cur) && !out.has(cur)) {
27440
+ out.add(cur);
27441
+ cur = byId.get(cur)?.parentId;
27442
+ }
27443
+ return out;
27444
+ }
27445
+ var TreeNodeItem, FlatTreeNodeItem, FlatFileTree, FileTree;
27430
27446
  var init_FileTree = __esm({
27431
27447
  "components/core/molecules/FileTree.tsx"() {
27432
27448
  "use client";
@@ -27517,42 +27533,77 @@ var init_FileTree = __esm({
27517
27533
  indent,
27518
27534
  childrenByParent,
27519
27535
  onNodeSelect,
27520
- defaultExpanded = false
27536
+ onNodeAction,
27537
+ nodeActionIcon,
27538
+ nodeActionLabel,
27539
+ selectedId,
27540
+ look,
27541
+ isExpanded,
27542
+ onToggle
27521
27543
  }) => {
27522
- const [expanded, setExpanded] = useState(defaultExpanded || depth < 1);
27523
27544
  const children = childrenByParent.get(item.id);
27524
27545
  const hasChildren = !!children && children.length > 0;
27546
+ const expanded = hasChildren && isExpanded(item.id, depth);
27547
+ const isSelected = selectedId !== void 0 && selectedId !== "" && item.id === selectedId;
27548
+ const nav = look === "nav";
27525
27549
  const handleClick = useCallback(() => {
27526
- if (hasChildren) setExpanded((prev) => !prev);
27550
+ if (hasChildren && !onNodeSelect) onToggle(item.id, expanded);
27527
27551
  onNodeSelect?.(item.id);
27528
- }, [hasChildren, item.id, onNodeSelect]);
27552
+ }, [hasChildren, expanded, item.id, onNodeSelect, onToggle]);
27553
+ const handleChevron = useCallback((e) => {
27554
+ e.stopPropagation();
27555
+ onToggle(item.id, expanded);
27556
+ }, [item.id, expanded, onToggle]);
27557
+ const handleAction = useCallback((e) => {
27558
+ e.stopPropagation();
27559
+ onNodeAction?.(item.id);
27560
+ }, [item.id, onNodeAction]);
27529
27561
  return /* @__PURE__ */ jsxs(Fragment, { children: [
27530
27562
  /* @__PURE__ */ jsxs(
27531
27563
  Box,
27532
27564
  {
27533
- className: "flex items-center gap-1.5 py-0.5 px-2 cursor-pointer rounded-sm transition-colors hover:bg-muted",
27565
+ className: `group/treerow flex items-center gap-1.5 px-2 cursor-pointer rounded-sm transition-colors ${nav ? "py-1" : "py-0.5"} ${isSelected ? "bg-primary text-primary-foreground" : nav ? "text-foreground hover:bg-muted" : "hover:bg-muted"}`,
27534
27566
  style: { paddingLeft: depth * indent + 8 },
27535
27567
  onClick: handleClick,
27536
27568
  role: "treeitem",
27569
+ "aria-selected": isSelected,
27537
27570
  "aria-expanded": hasChildren ? expanded : void 0,
27538
27571
  children: [
27539
- hasChildren ? /* @__PURE__ */ jsx(
27572
+ hasChildren ? /* @__PURE__ */ jsx(Box, { onClick: handleChevron, className: "flex items-center flex-shrink-0", role: "button", "aria-label": expanded ? "Collapse" : "Expand", children: /* @__PURE__ */ jsx(
27540
27573
  Icon,
27541
27574
  {
27542
27575
  name: expanded ? "chevron-down" : "chevron-right",
27543
27576
  size: "xs",
27544
- className: "text-[var(--color-muted-foreground)] flex-shrink-0"
27577
+ className: isSelected ? "text-inherit" : "text-[var(--color-muted-foreground)]"
27545
27578
  }
27546
- ) : /* @__PURE__ */ jsx(Box, { style: { width: 12, flexShrink: 0 } }),
27579
+ ) }) : /* @__PURE__ */ jsx(Box, { style: { width: 12, flexShrink: 0 } }),
27547
27580
  /* @__PURE__ */ jsx(
27548
27581
  Icon,
27549
27582
  {
27550
- name: item.icon ?? (hasChildren ? expanded ? "folder-open" : "folder" : "file"),
27583
+ name: item.icon || (nav ? "file-text" : hasChildren ? expanded ? "folder-open" : "folder" : "file"),
27551
27584
  size: "xs",
27552
- className: hasChildren ? "text-[var(--color-warning)]" : "text-[var(--color-muted-foreground)]"
27585
+ className: isSelected ? "text-inherit" : nav || !hasChildren ? "text-[var(--color-muted-foreground)]" : "text-[var(--color-warning)]"
27553
27586
  }
27554
27587
  ),
27555
- /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "truncate font-mono text-xs", children: item.label })
27588
+ /* @__PURE__ */ jsx(
27589
+ Typography,
27590
+ {
27591
+ variant: "caption",
27592
+ className: `truncate ${nav ? "text-sm" : "font-mono text-xs"} !text-inherit ${isSelected ? "font-semibold" : ""}`,
27593
+ children: item.label
27594
+ }
27595
+ ),
27596
+ onNodeAction && /* @__PURE__ */ jsx(
27597
+ Box,
27598
+ {
27599
+ onClick: handleAction,
27600
+ role: "button",
27601
+ "aria-label": nodeActionLabel ?? "Node action",
27602
+ title: nodeActionLabel,
27603
+ className: `ml-auto flex-shrink-0 rounded-sm p-0.5 opacity-0 group-hover/treerow:opacity-100 transition-opacity ${isSelected ? "hover:bg-primary-foreground/20" : "hover:bg-border"}`,
27604
+ children: /* @__PURE__ */ jsx(Icon, { name: nodeActionIcon ?? "plus", size: "xs", className: "text-inherit" })
27605
+ }
27606
+ )
27556
27607
  ]
27557
27608
  }
27558
27609
  ),
@@ -27563,47 +27614,116 @@ var init_FileTree = __esm({
27563
27614
  depth: depth + 1,
27564
27615
  indent,
27565
27616
  childrenByParent,
27566
- onNodeSelect
27617
+ onNodeSelect,
27618
+ onNodeAction,
27619
+ nodeActionIcon,
27620
+ nodeActionLabel,
27621
+ selectedId,
27622
+ look,
27623
+ isExpanded,
27624
+ onToggle
27567
27625
  },
27568
27626
  child.id
27569
27627
  )) })
27570
27628
  ] });
27571
27629
  };
27630
+ FlatFileTree = ({
27631
+ items,
27632
+ selectedId,
27633
+ look = "files",
27634
+ onNodeSelect,
27635
+ onNodeAction,
27636
+ nodeActionIcon,
27637
+ nodeActionLabel,
27638
+ className,
27639
+ indent = 16
27640
+ }) => {
27641
+ const [overrides, setOverrides] = useState(() => /* @__PURE__ */ new Map());
27642
+ const byId = React87__default.useMemo(() => new Map(items.map((node) => [node.id, node])), [items]);
27643
+ const selectedAncestors = React87__default.useMemo(() => ancestorsOf(selectedId, byId), [selectedId, byId]);
27644
+ const lastSelectedRef = React87__default.useRef(selectedId);
27645
+ React87__default.useEffect(() => {
27646
+ if (selectedId === lastSelectedRef.current) return;
27647
+ lastSelectedRef.current = selectedId;
27648
+ setOverrides((prev) => {
27649
+ if (![...selectedAncestors].some((a) => prev.get(a) === false)) return prev;
27650
+ const next = new Map(prev);
27651
+ for (const a of selectedAncestors) if (next.get(a) === false) next.delete(a);
27652
+ return next;
27653
+ });
27654
+ }, [selectedId, selectedAncestors]);
27655
+ const isExpanded = useCallback(
27656
+ (id, depth) => overrides.get(id) ?? (depth < 1 || selectedAncestors.has(id)),
27657
+ [overrides, selectedAncestors]
27658
+ );
27659
+ const onToggle = useCallback((id, effective) => {
27660
+ setOverrides((prev) => {
27661
+ const next = new Map(prev);
27662
+ next.set(id, !effective);
27663
+ return next;
27664
+ });
27665
+ }, []);
27666
+ if (items.length === 0) return null;
27667
+ const ids = new Set(items.map((node) => node.id));
27668
+ const childrenByParent = /* @__PURE__ */ new Map();
27669
+ const roots = [];
27670
+ for (const item of items) {
27671
+ if (item.parentId && ids.has(item.parentId)) {
27672
+ const siblings = childrenByParent.get(item.parentId);
27673
+ if (siblings) siblings.push(item);
27674
+ else childrenByParent.set(item.parentId, [item]);
27675
+ } else {
27676
+ roots.push(item);
27677
+ }
27678
+ }
27679
+ return /* @__PURE__ */ jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsx(
27680
+ FlatTreeNodeItem,
27681
+ {
27682
+ item,
27683
+ depth: 0,
27684
+ indent,
27685
+ childrenByParent,
27686
+ onNodeSelect,
27687
+ onNodeAction,
27688
+ nodeActionIcon,
27689
+ nodeActionLabel,
27690
+ selectedId,
27691
+ look,
27692
+ isExpanded,
27693
+ onToggle
27694
+ },
27695
+ item.id
27696
+ )) });
27697
+ };
27572
27698
  FileTree = ({
27573
27699
  tree,
27574
27700
  items,
27575
27701
  selectedPath,
27702
+ selectedId,
27703
+ look = "files",
27576
27704
  onFileSelect,
27577
27705
  onNodeSelect,
27706
+ onNodeAction,
27707
+ nodeActionIcon,
27708
+ nodeActionLabel,
27578
27709
  className,
27579
27710
  indent = 16
27580
27711
  }) => {
27581
27712
  if (items) {
27582
- if (items.length === 0) return null;
27583
- const ids = new Set(items.map((node) => node.id));
27584
- const childrenByParent = /* @__PURE__ */ new Map();
27585
- const roots = [];
27586
- for (const item of items) {
27587
- if (item.parentId && ids.has(item.parentId)) {
27588
- const siblings = childrenByParent.get(item.parentId);
27589
- if (siblings) siblings.push(item);
27590
- else childrenByParent.set(item.parentId, [item]);
27591
- } else {
27592
- roots.push(item);
27593
- }
27594
- }
27595
- return /* @__PURE__ */ jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsx(
27596
- FlatTreeNodeItem,
27713
+ return /* @__PURE__ */ jsx(
27714
+ FlatFileTree,
27597
27715
  {
27598
- item,
27599
- depth: 0,
27600
- indent,
27601
- childrenByParent,
27716
+ items,
27717
+ selectedId,
27718
+ look,
27602
27719
  onNodeSelect,
27603
- defaultExpanded: true
27604
- },
27605
- item.id
27606
- )) });
27720
+ onNodeAction,
27721
+ nodeActionIcon,
27722
+ nodeActionLabel,
27723
+ className,
27724
+ indent
27725
+ }
27726
+ );
27607
27727
  }
27608
27728
  if (!tree || tree.length === 0) return null;
27609
27729
  return /* @__PURE__ */ jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: tree.map((node) => /* @__PURE__ */ jsx(
@@ -33068,6 +33188,33 @@ var init_Lightbox = __esm({
33068
33188
  Lightbox.displayName = "Lightbox";
33069
33189
  }
33070
33190
  });
33191
+
33192
+ // lib/relationLabel.ts
33193
+ function relationLabel(value) {
33194
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Date)
33195
+ return null;
33196
+ for (const key of ["name", "title", "label"]) {
33197
+ const candidate = value[key];
33198
+ if (typeof candidate === "string" && candidate !== "") return candidate;
33199
+ }
33200
+ const id = value.id;
33201
+ return id !== void 0 && id !== null ? String(id) : null;
33202
+ }
33203
+ function relationDisplayLabels(value, options) {
33204
+ if (value === void 0 || value === null || value === "") return [];
33205
+ if (Array.isArray(value)) {
33206
+ return value.flatMap((item) => relationDisplayLabels(item, options));
33207
+ }
33208
+ const hydrated = relationLabel(value);
33209
+ if (hydrated !== null) return [hydrated];
33210
+ const raw = String(value);
33211
+ const match = options?.find((opt) => opt.value === raw);
33212
+ return [match ? match.label : raw];
33213
+ }
33214
+ var init_relationLabel = __esm({
33215
+ "lib/relationLabel.ts"() {
33216
+ }
33217
+ });
33071
33218
  function renderIconInput3(icon, props) {
33072
33219
  return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
33073
33220
  }
@@ -33385,6 +33532,7 @@ var init_TableView = __esm({
33385
33532
  "use client";
33386
33533
  init_cn();
33387
33534
  init_format();
33535
+ init_relationLabel();
33388
33536
  init_getNestedValue();
33389
33537
  init_useEventBus();
33390
33538
  init_Box();
@@ -33398,7 +33546,13 @@ var init_TableView = __esm({
33398
33546
  init_Menu();
33399
33547
  init_useDataDnd();
33400
33548
  tableViewLog = createLogger("almadar:ui:table-view");
33401
- formatCell = (value, format) => formatValue(value, format);
33549
+ formatCell = (value, format) => {
33550
+ if (value !== null && value !== void 0 && typeof value === "object" && !(value instanceof Date)) {
33551
+ const labels = relationDisplayLabels(value);
33552
+ if (labels.length > 0) return labels.join(", ");
33553
+ }
33554
+ return formatValue(value, format);
33555
+ };
33402
33556
  MAX_MEASURED_COL_CH = 32;
33403
33557
  BADGE_CHROME_CH = 4;
33404
33558
  alignClass = {
@@ -37013,6 +37167,35 @@ var init_RichTextEditor = __esm({
37013
37167
  document.execCommand("createLink", false, safe);
37014
37168
  afterEdit();
37015
37169
  }, [afterEdit, t, toolbar.link]);
37170
+ const imageInputRef = useRef(null);
37171
+ const savedRangeRef = useRef(null);
37172
+ const execImage = useCallback(() => {
37173
+ const root = ref.current;
37174
+ const sel = window.getSelection();
37175
+ savedRangeRef.current = root && sel && sel.rangeCount > 0 && root.contains(sel.anchorNode) ? sel.getRangeAt(0).cloneRange() : null;
37176
+ imageInputRef.current?.click();
37177
+ }, []);
37178
+ const handleImageFile = useCallback((e) => {
37179
+ const file = e.target.files?.[0];
37180
+ e.target.value = "";
37181
+ if (!file) return;
37182
+ const reader = new FileReader();
37183
+ reader.onload = () => {
37184
+ const url = typeof reader.result === "string" ? reader.result : "";
37185
+ if (!url.startsWith("data:image/")) return;
37186
+ const root = ref.current;
37187
+ if (!root) return;
37188
+ root.focus();
37189
+ const sel = window.getSelection();
37190
+ if (sel && savedRangeRef.current && root.contains(savedRangeRef.current.startContainer)) {
37191
+ sel.removeAllRanges();
37192
+ sel.addRange(savedRangeRef.current);
37193
+ }
37194
+ document.execCommand("insertImage", false, url);
37195
+ afterEdit();
37196
+ };
37197
+ reader.readAsDataURL(file);
37198
+ }, [afterEdit]);
37016
37199
  const execRule = useCallback(() => {
37017
37200
  document.execCommand("insertHorizontalRule", false);
37018
37201
  afterEdit();
@@ -37025,6 +37208,18 @@ var init_RichTextEditor = __esm({
37025
37208
  }
37026
37209
  return /* @__PURE__ */ jsxs(Box, { className: cn("flex flex-col gap-2", className), children: [
37027
37210
  /* @__PURE__ */ jsx(RichTextStyles, {}),
37211
+ /* @__PURE__ */ jsx(
37212
+ "input",
37213
+ {
37214
+ ref: imageInputRef,
37215
+ type: "file",
37216
+ accept: "image/*",
37217
+ className: "hidden",
37218
+ "aria-hidden": "true",
37219
+ tabIndex: -1,
37220
+ onChange: handleImageFile
37221
+ }
37222
+ ),
37028
37223
  showToolbar && /* @__PURE__ */ jsxs(
37029
37224
  Box,
37030
37225
  {
@@ -37050,6 +37245,7 @@ var init_RichTextEditor = __esm({
37050
37245
  /* @__PURE__ */ jsx(ToolbarButton, { icon: Quote, label: t("richTextEditor.quote"), active: toolbar.block === "blockquote", onExec: () => execBlock("blockquote") }),
37051
37246
  /* @__PURE__ */ jsx(ToolbarButton, { icon: Code, label: t("richTextEditor.code"), active: toolbar.block === "pre", onExec: () => execBlock("pre") }),
37052
37247
  /* @__PURE__ */ jsx(ToolbarButton, { icon: Link$1, label: toolbar.link ? t("richTextEditor.removeLink") : t("richTextEditor.link"), active: toolbar.link, onExec: execLink }),
37248
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Image$1, label: t("richTextEditor.image"), onExec: execImage }),
37053
37249
  /* @__PURE__ */ jsx(ToolbarButton, { icon: Minus, label: t("richTextEditor.divider"), onExec: execRule })
37054
37250
  ]
37055
37251
  }
@@ -37090,6 +37286,8 @@ function DocumentPanel({
37090
37286
  id,
37091
37287
  title,
37092
37288
  subtitle,
37289
+ coverImage,
37290
+ icon,
37093
37291
  value,
37094
37292
  actions,
37095
37293
  maxInlineActions,
@@ -37165,62 +37363,74 @@ function DocumentPanel({
37165
37363
  const emitWithId = (event) => () => {
37166
37364
  if (event) eventBus.emit(`UI:${event}`, { id: recordId });
37167
37365
  };
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,
37184
- {
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
- }))
37207
- }
37208
- )
37209
- ] }) })
37210
- ] }),
37211
- editing ? /* @__PURE__ */ jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsx(
37212
- Box,
37366
+ return /* @__PURE__ */ jsxs(Card, { variant: "elevated", className: cn("w-full overflow-hidden", className), children: [
37367
+ coverImage ? /* @__PURE__ */ jsx(Box, { className: "h-44 w-full sm:h-52", "data-testid": "document-cover", children: /* @__PURE__ */ jsx(
37368
+ "img",
37213
37369
  {
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
- ] }) });
37370
+ src: coverImage,
37371
+ alt: "",
37372
+ "aria-hidden": "true",
37373
+ className: "h-full w-full object-cover"
37374
+ }
37375
+ ) }) : null,
37376
+ /* @__PURE__ */ jsxs(VStack, { gap: "sm", className: "p-6 sm:p-8", children: [
37377
+ /* @__PURE__ */ jsxs(HStack, { gap: "sm", className: "justify-between items-start", children: [
37378
+ /* @__PURE__ */ jsxs(VStack, { gap: "xs", className: "flex-1 min-w-0", children: [
37379
+ icon ? /* @__PURE__ */ jsx(Box, { className: "text-muted-foreground", "data-testid": "document-icon", children: renderIconInput4(icon, { size: "lg" }) }) : null,
37380
+ titleNode,
37381
+ subtitle ? /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: subtitle }) : null
37382
+ ] }),
37383
+ /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "flex-shrink-0 pt-1 items-center", children: editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
37384
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "muted", className: "hidden sm:block", children: autosaveHint ?? (t("documentPanel.autosaveHint") || "") }),
37385
+ /* @__PURE__ */ jsx(Button, { variant: "primary", size: "sm", onClick: emitWithId(doneEvent), "data-testid": "document-done", children: doneLabel ?? (t("documentPanel.done") || "Done") })
37386
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
37387
+ editEvent && /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: emitWithId(editEvent), "data-testid": "document-edit", children: [
37388
+ /* @__PURE__ */ jsx(Icon, { name: "edit", size: "xs", className: "mr-1" }),
37389
+ editLabel ?? (t("documentPanel.edit") || "Edit")
37390
+ ] }),
37391
+ inlineActions.map((action, idx) => /* @__PURE__ */ jsxs(
37392
+ Button,
37393
+ {
37394
+ variant: "primary",
37395
+ size: "sm",
37396
+ onClick: () => fireAction(action),
37397
+ "data-testid": `action-${action.event}`,
37398
+ children: [
37399
+ action.icon && renderIconInput4(action.icon, { size: "xs", className: "mr-1" }),
37400
+ action.label
37401
+ ]
37402
+ },
37403
+ idx
37404
+ )),
37405
+ menuActions.length > 0 && /* @__PURE__ */ jsx(
37406
+ Menu,
37407
+ {
37408
+ position: "bottom-end",
37409
+ 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" }) }),
37410
+ items: menuActions.map((action) => ({
37411
+ label: action.label,
37412
+ icon: action.icon,
37413
+ variant: action.variant === "danger" ? "danger" : "default",
37414
+ onClick: () => fireAction(action)
37415
+ }))
37416
+ }
37417
+ )
37418
+ ] }) })
37419
+ ] }),
37420
+ editing ? /* @__PURE__ */ jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsx(
37421
+ Box,
37422
+ {
37423
+ onClick: emitWithId(editEvent),
37424
+ className: cn(
37425
+ "rounded-md px-1 -mx-1 min-h-[16rem]",
37426
+ editEvent && "cursor-text transition-colors hover:bg-muted/30"
37427
+ ),
37428
+ "data-testid": "document-body",
37429
+ children: value && value !== "" ? /* @__PURE__ */ jsx(RichTextEditor, { value, readOnly: true }) : /* @__PURE__ */ jsx(Typography, { variant: "body", color: "muted", children: placeholder ?? (t("documentPanel.placeholder") || "") })
37430
+ }
37431
+ )
37432
+ ] })
37433
+ ] });
37224
37434
  }
37225
37435
  var init_DocumentPanel = __esm({
37226
37436
  "components/core/molecules/DocumentPanel.tsx"() {
@@ -37244,17 +37454,16 @@ function renderIconInput5(icon, props) {
37244
37454
  function fieldName(field) {
37245
37455
  return field.name ?? field.key ?? "";
37246
37456
  }
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;
37457
+ function relationId(value) {
37458
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
37459
+ const id = value.id;
37460
+ if (id !== void 0 && id !== null) return String(id);
37252
37461
  }
37253
- const id = value.id;
37254
- return id !== void 0 && id !== null ? String(id) : null;
37462
+ return String(value);
37255
37463
  }
37256
37464
  function fieldKind(field, value) {
37257
37465
  if (field.kind) return field.kind;
37466
+ if (field.type === "image") return "image";
37258
37467
  if (field.options && field.options.length > 0) return "select";
37259
37468
  if (typeof value === "boolean" || field.format === "boolean") return "boolean";
37260
37469
  if (Array.isArray(value) || relationLabel(value ?? null) !== null) return "readonly";
@@ -37264,6 +37473,7 @@ function DocumentDetails({
37264
37473
  entity,
37265
37474
  fields,
37266
37475
  metaCommitEvent,
37476
+ relationEvent,
37267
37477
  title,
37268
37478
  className
37269
37479
  }) {
@@ -37278,11 +37488,51 @@ function DocumentDetails({
37278
37488
  if (!metaCommitEvent || next === current) return;
37279
37489
  eventBus.emit(`UI:${metaCommitEvent}`, { id: recordId, patch: { id: recordId, [name]: next } });
37280
37490
  };
37491
+ const relationChip = (item, name, key) => {
37492
+ const label = relationLabel(item) ?? humanizeEnumValue(String(item));
37493
+ if (!relationEvent) {
37494
+ return /* @__PURE__ */ jsx(Badge, { variant: "default", children: label }, key);
37495
+ }
37496
+ const emitClick = () => eventBus.emit(`UI:${relationEvent}`, { field: name, id: relationId(item), name: label });
37497
+ return /* @__PURE__ */ jsx(
37498
+ Badge,
37499
+ {
37500
+ variant: "default",
37501
+ role: "button",
37502
+ tabIndex: 0,
37503
+ className: "cursor-pointer transition-colors hover:bg-accent",
37504
+ onClick: emitClick,
37505
+ onKeyDown: (e) => {
37506
+ if (e.key === "Enter" || e.key === " ") {
37507
+ e.preventDefault();
37508
+ emitClick();
37509
+ }
37510
+ },
37511
+ "data-testid": `relation-chip-${name}`,
37512
+ children: label
37513
+ },
37514
+ key
37515
+ );
37516
+ };
37281
37517
  const renderValue = (field) => {
37282
37518
  const name = fieldName(field);
37283
37519
  const raw = getNestedValue(entity ?? {}, name);
37284
37520
  const kind = fieldKind(field, raw);
37285
37521
  const label = field.label ?? field.header ?? humanizeFieldName(name);
37522
+ if (kind === "image") {
37523
+ const url = typeof raw === "string" ? raw : "";
37524
+ if (!url) return /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
37525
+ return /* @__PURE__ */ jsx(
37526
+ "img",
37527
+ {
37528
+ src: url,
37529
+ alt: label,
37530
+ loading: "lazy",
37531
+ className: "max-h-32 w-full rounded-md border border-border object-cover",
37532
+ "data-testid": `document-property-image-${name}`
37533
+ }
37534
+ );
37535
+ }
37286
37536
  if (kind === "boolean") {
37287
37537
  return /* @__PURE__ */ jsx(
37288
37538
  Switch,
@@ -37312,9 +37562,12 @@ function DocumentDetails({
37312
37562
  if (raw.length === 0) {
37313
37563
  return /* @__PURE__ */ jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
37314
37564
  }
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)) });
37565
+ return /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "flex-wrap", children: raw.map((item, i) => relationChip(item, name, i)) });
37316
37566
  }
37317
- const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
37567
+ if (raw !== void 0 && raw !== null && raw !== "" && relationLabel(raw) !== null) {
37568
+ return relationChip(raw, name);
37569
+ }
37570
+ const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : formatValue(raw, field.format);
37318
37571
  return /* @__PURE__ */ jsx(Typography, { variant: "small", className: "break-words", children: shown2 });
37319
37572
  }
37320
37573
  if (fieldDraft?.name === name) {
@@ -37383,6 +37636,7 @@ var init_DocumentDetails = __esm({
37383
37636
  init_cn();
37384
37637
  init_format();
37385
37638
  init_getNestedValue();
37639
+ init_relationLabel();
37386
37640
  init_useEventBus();
37387
37641
  init_Box();
37388
37642
  init_Stack();
@@ -42377,7 +42631,18 @@ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
42377
42631
  if (value === void 0 || value === null) return "\u2014";
42378
42632
  const str = String(value);
42379
42633
  switch (fieldType) {
42380
- case "image":
42634
+ case "image": {
42635
+ if (!str) return "\u2014";
42636
+ return /* @__PURE__ */ jsx(Box, { className: "mt-1 max-w-full", children: /* @__PURE__ */ jsx(
42637
+ "img",
42638
+ {
42639
+ src: str,
42640
+ alt: formatFieldLabel(fieldName2),
42641
+ className: "max-w-full max-h-64 rounded-md object-contain",
42642
+ loading: "lazy"
42643
+ }
42644
+ ) });
42645
+ }
42381
42646
  case "url": {
42382
42647
  if (str.match(/\.(png|jpe?g|gif|svg|webp|avif)(\?|$)/i) || str.startsWith("data:image/")) {
42383
42648
  return /* @__PURE__ */ jsx(Box, { className: "mt-1 max-w-full", children: /* @__PURE__ */ jsx(
@@ -42390,7 +42655,7 @@ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
42390
42655
  }
42391
42656
  ) });
42392
42657
  }
42393
- if (fieldType === "url" && /^https?:\/\//i.test(str)) {
42658
+ if (/^https?:\/\//i.test(str)) {
42394
42659
  return /* @__PURE__ */ jsx(
42395
42660
  "a",
42396
42661
  {
@@ -42476,8 +42741,10 @@ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
42476
42741
  return str;
42477
42742
  }
42478
42743
  case "relation": {
42479
- const match = meta?.options?.find((opt) => opt.value === str);
42480
- return match ? match.label : str;
42744
+ const labels = relationDisplayLabels(value, meta?.options);
42745
+ if (labels.length === 0) return "\u2014";
42746
+ if (labels.length === 1) return labels[0];
42747
+ return /* @__PURE__ */ jsx(HStack, { gap: "xs", wrap: true, children: labels.map((label, i) => /* @__PURE__ */ jsx(Badge, { variant: "default", children: label }, i)) });
42481
42748
  }
42482
42749
  case "file": {
42483
42750
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
@@ -42579,6 +42846,7 @@ var init_DetailPanel = __esm({
42579
42846
  init_cn();
42580
42847
  init_format();
42581
42848
  init_getNestedValue();
42849
+ init_relationLabel();
42582
42850
  init_useEventBus();
42583
42851
  init_UploadDropZone();
42584
42852
  formatFieldLabel = humanizeFieldName;
@@ -43237,6 +43505,8 @@ function determineInputType(field) {
43237
43505
  return "url";
43238
43506
  case "file":
43239
43507
  return "file";
43508
+ case "image":
43509
+ return "image";
43240
43510
  case "money":
43241
43511
  return "currency";
43242
43512
  case "number":
@@ -43836,6 +44106,50 @@ var init_Form = __esm({
43836
44106
  }
43837
44107
  }
43838
44108
  );
44109
+ case "image": {
44110
+ const imageUrl = typeof currentValue === "string" ? currentValue : "";
44111
+ return /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
44112
+ imageUrl ? /* @__PURE__ */ jsx(
44113
+ "img",
44114
+ {
44115
+ src: imageUrl,
44116
+ alt: "",
44117
+ "aria-hidden": "true",
44118
+ className: "h-24 w-full max-w-xs rounded-md border border-border object-cover",
44119
+ "data-testid": `image-preview-${fieldName2}`
44120
+ }
44121
+ ) : null,
44122
+ /* @__PURE__ */ jsx(
44123
+ UploadDropZone,
44124
+ {
44125
+ accept: "image/*",
44126
+ maxFiles: 1,
44127
+ maxSize: 2 * 1024 * 1024,
44128
+ disabled: isLoading,
44129
+ onFiles: (files) => {
44130
+ const f3 = files[0];
44131
+ if (!f3) return;
44132
+ const reader = new FileReader();
44133
+ reader.onload = () => handleChange(
44134
+ fieldName2,
44135
+ typeof reader.result === "string" ? reader.result : ""
44136
+ );
44137
+ reader.readAsDataURL(f3);
44138
+ }
44139
+ }
44140
+ ),
44141
+ /* @__PURE__ */ jsx(
44142
+ Input,
44143
+ {
44144
+ ...commonProps,
44145
+ type: "url",
44146
+ placeholder: t("form.imageUrlFallback"),
44147
+ value: imageUrl,
44148
+ onChange: (e) => handleChange(fieldName2, e.target.value)
44149
+ }
44150
+ )
44151
+ ] });
44152
+ }
43839
44153
  case "date":
43840
44154
  return /* @__PURE__ */ jsx(
43841
44155
  Input,