@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.
@@ -24954,6 +24954,7 @@ var init_DashboardLayout = __esm({
24954
24954
  onNotificationClick,
24955
24955
  showThemeToggle = true,
24956
24956
  sidebarFooter,
24957
+ sidebarContent,
24957
24958
  onSignOut: onSignOutProp,
24958
24959
  currentPath,
24959
24960
  layoutMode = "sidebar",
@@ -25079,7 +25080,11 @@ var init_DashboardLayout = __esm({
25079
25080
  {
25080
25081
  as: "nav",
25081
25082
  gap: "none",
25082
- className: cn("flex-1 py-4 space-y-1 overflow-y-auto", isRail ? "px-2" : "px-3"),
25083
+ className: cn(
25084
+ "py-4 space-y-1 overflow-y-auto",
25085
+ sidebarContent && !isRail ? "shrink-0" : "flex-1",
25086
+ isRail ? "px-2" : "px-3"
25087
+ ),
25083
25088
  children: navItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
25084
25089
  NavLink,
25085
25090
  {
@@ -25091,6 +25096,7 @@ var init_DashboardLayout = __esm({
25091
25096
  ))
25092
25097
  }
25093
25098
  ),
25099
+ sidebarContent && !isRail && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex-1 overflow-y-auto border-t border-border dark:border-border px-2 py-3", children: sidebarContent }),
25094
25100
  sidebarFooter && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "p-4 border-t border-border dark:border-border", children: sidebarFooter })
25095
25101
  ]
25096
25102
  }
@@ -27500,7 +27506,17 @@ function fileIcon(name) {
27500
27506
  return "file";
27501
27507
  }
27502
27508
  }
27503
- var TreeNodeItem, FlatTreeNodeItem, FileTree;
27509
+ function ancestorsOf(id, byId) {
27510
+ const out = /* @__PURE__ */ new Set();
27511
+ if (!id) return out;
27512
+ let cur = byId.get(id)?.parentId;
27513
+ while (cur && byId.has(cur) && !out.has(cur)) {
27514
+ out.add(cur);
27515
+ cur = byId.get(cur)?.parentId;
27516
+ }
27517
+ return out;
27518
+ }
27519
+ var TreeNodeItem, FlatTreeNodeItem, FlatFileTree, FileTree;
27504
27520
  var init_FileTree = __esm({
27505
27521
  "components/core/molecules/FileTree.tsx"() {
27506
27522
  "use client";
@@ -27591,42 +27607,77 @@ var init_FileTree = __esm({
27591
27607
  indent,
27592
27608
  childrenByParent,
27593
27609
  onNodeSelect,
27594
- defaultExpanded = false
27610
+ onNodeAction,
27611
+ nodeActionIcon,
27612
+ nodeActionLabel,
27613
+ selectedId,
27614
+ look,
27615
+ isExpanded,
27616
+ onToggle
27595
27617
  }) => {
27596
- const [expanded, setExpanded] = React87.useState(defaultExpanded || depth < 1);
27597
27618
  const children = childrenByParent.get(item.id);
27598
27619
  const hasChildren = !!children && children.length > 0;
27620
+ const expanded = hasChildren && isExpanded(item.id, depth);
27621
+ const isSelected = selectedId !== void 0 && selectedId !== "" && item.id === selectedId;
27622
+ const nav = look === "nav";
27599
27623
  const handleClick = React87.useCallback(() => {
27600
- if (hasChildren) setExpanded((prev) => !prev);
27624
+ if (hasChildren && !onNodeSelect) onToggle(item.id, expanded);
27601
27625
  onNodeSelect?.(item.id);
27602
- }, [hasChildren, item.id, onNodeSelect]);
27626
+ }, [hasChildren, expanded, item.id, onNodeSelect, onToggle]);
27627
+ const handleChevron = React87.useCallback((e) => {
27628
+ e.stopPropagation();
27629
+ onToggle(item.id, expanded);
27630
+ }, [item.id, expanded, onToggle]);
27631
+ const handleAction = React87.useCallback((e) => {
27632
+ e.stopPropagation();
27633
+ onNodeAction?.(item.id);
27634
+ }, [item.id, onNodeAction]);
27603
27635
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
27604
27636
  /* @__PURE__ */ jsxRuntime.jsxs(
27605
27637
  Box,
27606
27638
  {
27607
- className: "flex items-center gap-1.5 py-0.5 px-2 cursor-pointer rounded-sm transition-colors hover:bg-muted",
27639
+ 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"}`,
27608
27640
  style: { paddingLeft: depth * indent + 8 },
27609
27641
  onClick: handleClick,
27610
27642
  role: "treeitem",
27643
+ "aria-selected": isSelected,
27611
27644
  "aria-expanded": hasChildren ? expanded : void 0,
27612
27645
  children: [
27613
- hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(
27646
+ hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(Box, { onClick: handleChevron, className: "flex items-center flex-shrink-0", role: "button", "aria-label": expanded ? "Collapse" : "Expand", children: /* @__PURE__ */ jsxRuntime.jsx(
27614
27647
  Icon,
27615
27648
  {
27616
27649
  name: expanded ? "chevron-down" : "chevron-right",
27617
27650
  size: "xs",
27618
- className: "text-[var(--color-muted-foreground)] flex-shrink-0"
27651
+ className: isSelected ? "text-inherit" : "text-[var(--color-muted-foreground)]"
27619
27652
  }
27620
- ) : /* @__PURE__ */ jsxRuntime.jsx(Box, { style: { width: 12, flexShrink: 0 } }),
27653
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx(Box, { style: { width: 12, flexShrink: 0 } }),
27621
27654
  /* @__PURE__ */ jsxRuntime.jsx(
27622
27655
  Icon,
27623
27656
  {
27624
- name: item.icon ?? (hasChildren ? expanded ? "folder-open" : "folder" : "file"),
27657
+ name: item.icon || (nav ? "file-text" : hasChildren ? expanded ? "folder-open" : "folder" : "file"),
27625
27658
  size: "xs",
27626
- className: hasChildren ? "text-[var(--color-warning)]" : "text-[var(--color-muted-foreground)]"
27659
+ className: isSelected ? "text-inherit" : nav || !hasChildren ? "text-[var(--color-muted-foreground)]" : "text-[var(--color-warning)]"
27627
27660
  }
27628
27661
  ),
27629
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "truncate font-mono text-xs", children: item.label })
27662
+ /* @__PURE__ */ jsxRuntime.jsx(
27663
+ Typography,
27664
+ {
27665
+ variant: "caption",
27666
+ className: `truncate ${nav ? "text-sm" : "font-mono text-xs"} !text-inherit ${isSelected ? "font-semibold" : ""}`,
27667
+ children: item.label
27668
+ }
27669
+ ),
27670
+ onNodeAction && /* @__PURE__ */ jsxRuntime.jsx(
27671
+ Box,
27672
+ {
27673
+ onClick: handleAction,
27674
+ role: "button",
27675
+ "aria-label": nodeActionLabel ?? "Node action",
27676
+ title: nodeActionLabel,
27677
+ 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"}`,
27678
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: nodeActionIcon ?? "plus", size: "xs", className: "text-inherit" })
27679
+ }
27680
+ )
27630
27681
  ]
27631
27682
  }
27632
27683
  ),
@@ -27637,47 +27688,116 @@ var init_FileTree = __esm({
27637
27688
  depth: depth + 1,
27638
27689
  indent,
27639
27690
  childrenByParent,
27640
- onNodeSelect
27691
+ onNodeSelect,
27692
+ onNodeAction,
27693
+ nodeActionIcon,
27694
+ nodeActionLabel,
27695
+ selectedId,
27696
+ look,
27697
+ isExpanded,
27698
+ onToggle
27641
27699
  },
27642
27700
  child.id
27643
27701
  )) })
27644
27702
  ] });
27645
27703
  };
27704
+ FlatFileTree = ({
27705
+ items,
27706
+ selectedId,
27707
+ look = "files",
27708
+ onNodeSelect,
27709
+ onNodeAction,
27710
+ nodeActionIcon,
27711
+ nodeActionLabel,
27712
+ className,
27713
+ indent = 16
27714
+ }) => {
27715
+ const [overrides, setOverrides] = React87.useState(() => /* @__PURE__ */ new Map());
27716
+ const byId = React87__namespace.default.useMemo(() => new Map(items.map((node) => [node.id, node])), [items]);
27717
+ const selectedAncestors = React87__namespace.default.useMemo(() => ancestorsOf(selectedId, byId), [selectedId, byId]);
27718
+ const lastSelectedRef = React87__namespace.default.useRef(selectedId);
27719
+ React87__namespace.default.useEffect(() => {
27720
+ if (selectedId === lastSelectedRef.current) return;
27721
+ lastSelectedRef.current = selectedId;
27722
+ setOverrides((prev) => {
27723
+ if (![...selectedAncestors].some((a) => prev.get(a) === false)) return prev;
27724
+ const next = new Map(prev);
27725
+ for (const a of selectedAncestors) if (next.get(a) === false) next.delete(a);
27726
+ return next;
27727
+ });
27728
+ }, [selectedId, selectedAncestors]);
27729
+ const isExpanded = React87.useCallback(
27730
+ (id, depth) => overrides.get(id) ?? (depth < 1 || selectedAncestors.has(id)),
27731
+ [overrides, selectedAncestors]
27732
+ );
27733
+ const onToggle = React87.useCallback((id, effective) => {
27734
+ setOverrides((prev) => {
27735
+ const next = new Map(prev);
27736
+ next.set(id, !effective);
27737
+ return next;
27738
+ });
27739
+ }, []);
27740
+ if (items.length === 0) return null;
27741
+ const ids = new Set(items.map((node) => node.id));
27742
+ const childrenByParent = /* @__PURE__ */ new Map();
27743
+ const roots = [];
27744
+ for (const item of items) {
27745
+ if (item.parentId && ids.has(item.parentId)) {
27746
+ const siblings = childrenByParent.get(item.parentId);
27747
+ if (siblings) siblings.push(item);
27748
+ else childrenByParent.set(item.parentId, [item]);
27749
+ } else {
27750
+ roots.push(item);
27751
+ }
27752
+ }
27753
+ return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
27754
+ FlatTreeNodeItem,
27755
+ {
27756
+ item,
27757
+ depth: 0,
27758
+ indent,
27759
+ childrenByParent,
27760
+ onNodeSelect,
27761
+ onNodeAction,
27762
+ nodeActionIcon,
27763
+ nodeActionLabel,
27764
+ selectedId,
27765
+ look,
27766
+ isExpanded,
27767
+ onToggle
27768
+ },
27769
+ item.id
27770
+ )) });
27771
+ };
27646
27772
  FileTree = ({
27647
27773
  tree,
27648
27774
  items,
27649
27775
  selectedPath,
27776
+ selectedId,
27777
+ look = "files",
27650
27778
  onFileSelect,
27651
27779
  onNodeSelect,
27780
+ onNodeAction,
27781
+ nodeActionIcon,
27782
+ nodeActionLabel,
27652
27783
  className,
27653
27784
  indent = 16
27654
27785
  }) => {
27655
27786
  if (items) {
27656
- if (items.length === 0) return null;
27657
- const ids = new Set(items.map((node) => node.id));
27658
- const childrenByParent = /* @__PURE__ */ new Map();
27659
- const roots = [];
27660
- for (const item of items) {
27661
- if (item.parentId && ids.has(item.parentId)) {
27662
- const siblings = childrenByParent.get(item.parentId);
27663
- if (siblings) siblings.push(item);
27664
- else childrenByParent.set(item.parentId, [item]);
27665
- } else {
27666
- roots.push(item);
27667
- }
27668
- }
27669
- return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
27670
- FlatTreeNodeItem,
27787
+ return /* @__PURE__ */ jsxRuntime.jsx(
27788
+ FlatFileTree,
27671
27789
  {
27672
- item,
27673
- depth: 0,
27674
- indent,
27675
- childrenByParent,
27790
+ items,
27791
+ selectedId,
27792
+ look,
27676
27793
  onNodeSelect,
27677
- defaultExpanded: true
27678
- },
27679
- item.id
27680
- )) });
27794
+ onNodeAction,
27795
+ nodeActionIcon,
27796
+ nodeActionLabel,
27797
+ className,
27798
+ indent
27799
+ }
27800
+ );
27681
27801
  }
27682
27802
  if (!tree || tree.length === 0) return null;
27683
27803
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: tree.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -33142,6 +33262,33 @@ var init_Lightbox = __esm({
33142
33262
  Lightbox.displayName = "Lightbox";
33143
33263
  }
33144
33264
  });
33265
+
33266
+ // lib/relationLabel.ts
33267
+ function relationLabel(value) {
33268
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Date)
33269
+ return null;
33270
+ for (const key of ["name", "title", "label"]) {
33271
+ const candidate = value[key];
33272
+ if (typeof candidate === "string" && candidate !== "") return candidate;
33273
+ }
33274
+ const id = value.id;
33275
+ return id !== void 0 && id !== null ? String(id) : null;
33276
+ }
33277
+ function relationDisplayLabels(value, options) {
33278
+ if (value === void 0 || value === null || value === "") return [];
33279
+ if (Array.isArray(value)) {
33280
+ return value.flatMap((item) => relationDisplayLabels(item, options));
33281
+ }
33282
+ const hydrated = relationLabel(value);
33283
+ if (hydrated !== null) return [hydrated];
33284
+ const raw = String(value);
33285
+ const match = options?.find((opt) => opt.value === raw);
33286
+ return [match ? match.label : raw];
33287
+ }
33288
+ var init_relationLabel = __esm({
33289
+ "lib/relationLabel.ts"() {
33290
+ }
33291
+ });
33145
33292
  function renderIconInput3(icon, props) {
33146
33293
  return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
33147
33294
  }
@@ -33459,6 +33606,7 @@ var init_TableView = __esm({
33459
33606
  "use client";
33460
33607
  init_cn();
33461
33608
  init_format();
33609
+ init_relationLabel();
33462
33610
  init_getNestedValue();
33463
33611
  init_useEventBus();
33464
33612
  init_Box();
@@ -33472,7 +33620,13 @@ var init_TableView = __esm({
33472
33620
  init_Menu();
33473
33621
  init_useDataDnd();
33474
33622
  tableViewLog = logger.createLogger("almadar:ui:table-view");
33475
- formatCell = (value, format) => formatValue(value, format);
33623
+ formatCell = (value, format) => {
33624
+ if (value !== null && value !== void 0 && typeof value === "object" && !(value instanceof Date)) {
33625
+ const labels = relationDisplayLabels(value);
33626
+ if (labels.length > 0) return labels.join(", ");
33627
+ }
33628
+ return formatValue(value, format);
33629
+ };
33476
33630
  MAX_MEASURED_COL_CH = 32;
33477
33631
  BADGE_CHROME_CH = 4;
33478
33632
  alignClass = {
@@ -37087,6 +37241,35 @@ var init_RichTextEditor = __esm({
37087
37241
  document.execCommand("createLink", false, safe);
37088
37242
  afterEdit();
37089
37243
  }, [afterEdit, t, toolbar.link]);
37244
+ const imageInputRef = React87.useRef(null);
37245
+ const savedRangeRef = React87.useRef(null);
37246
+ const execImage = React87.useCallback(() => {
37247
+ const root = ref.current;
37248
+ const sel = window.getSelection();
37249
+ savedRangeRef.current = root && sel && sel.rangeCount > 0 && root.contains(sel.anchorNode) ? sel.getRangeAt(0).cloneRange() : null;
37250
+ imageInputRef.current?.click();
37251
+ }, []);
37252
+ const handleImageFile = React87.useCallback((e) => {
37253
+ const file = e.target.files?.[0];
37254
+ e.target.value = "";
37255
+ if (!file) return;
37256
+ const reader = new FileReader();
37257
+ reader.onload = () => {
37258
+ const url = typeof reader.result === "string" ? reader.result : "";
37259
+ if (!url.startsWith("data:image/")) return;
37260
+ const root = ref.current;
37261
+ if (!root) return;
37262
+ root.focus();
37263
+ const sel = window.getSelection();
37264
+ if (sel && savedRangeRef.current && root.contains(savedRangeRef.current.startContainer)) {
37265
+ sel.removeAllRanges();
37266
+ sel.addRange(savedRangeRef.current);
37267
+ }
37268
+ document.execCommand("insertImage", false, url);
37269
+ afterEdit();
37270
+ };
37271
+ reader.readAsDataURL(file);
37272
+ }, [afterEdit]);
37090
37273
  const execRule = React87.useCallback(() => {
37091
37274
  document.execCommand("insertHorizontalRule", false);
37092
37275
  afterEdit();
@@ -37099,6 +37282,18 @@ var init_RichTextEditor = __esm({
37099
37282
  }
37100
37283
  return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: cn("flex flex-col gap-2", className), children: [
37101
37284
  /* @__PURE__ */ jsxRuntime.jsx(RichTextStyles, {}),
37285
+ /* @__PURE__ */ jsxRuntime.jsx(
37286
+ "input",
37287
+ {
37288
+ ref: imageInputRef,
37289
+ type: "file",
37290
+ accept: "image/*",
37291
+ className: "hidden",
37292
+ "aria-hidden": "true",
37293
+ tabIndex: -1,
37294
+ onChange: handleImageFile
37295
+ }
37296
+ ),
37102
37297
  showToolbar && /* @__PURE__ */ jsxRuntime.jsxs(
37103
37298
  Box,
37104
37299
  {
@@ -37124,6 +37319,7 @@ var init_RichTextEditor = __esm({
37124
37319
  /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Quote, label: t("richTextEditor.quote"), active: toolbar.block === "blockquote", onExec: () => execBlock("blockquote") }),
37125
37320
  /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Code, label: t("richTextEditor.code"), active: toolbar.block === "pre", onExec: () => execBlock("pre") }),
37126
37321
  /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Link, label: toolbar.link ? t("richTextEditor.removeLink") : t("richTextEditor.link"), active: toolbar.link, onExec: execLink }),
37322
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Image, label: t("richTextEditor.image"), onExec: execImage }),
37127
37323
  /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { icon: LucideIcons2.Minus, label: t("richTextEditor.divider"), onExec: execRule })
37128
37324
  ]
37129
37325
  }
@@ -37164,6 +37360,8 @@ function DocumentPanel({
37164
37360
  id,
37165
37361
  title,
37166
37362
  subtitle,
37363
+ coverImage,
37364
+ icon,
37167
37365
  value,
37168
37366
  actions,
37169
37367
  maxInlineActions,
@@ -37239,62 +37437,74 @@ function DocumentPanel({
37239
37437
  const emitWithId = (event) => () => {
37240
37438
  if (event) eventBus.emit(`UI:${event}`, { id: recordId });
37241
37439
  };
37242
- return /* @__PURE__ */ jsxRuntime.jsx(Card, { variant: "elevated", className: cn("w-full", className), children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", className: "p-6 sm:p-8", children: [
37243
- /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", className: "justify-between items-start", children: [
37244
- /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", className: "flex-1 min-w-0", children: [
37245
- titleNode,
37246
- subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: subtitle }) : null
37247
- ] }),
37248
- /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", className: "flex-shrink-0 pt-1 items-center", children: editing ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
37249
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", className: "hidden sm:block", children: autosaveHint ?? (t("documentPanel.autosaveHint") || "") }),
37250
- /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", size: "sm", onClick: emitWithId(doneEvent), "data-testid": "document-done", children: doneLabel ?? (t("documentPanel.done") || "Done") })
37251
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
37252
- editEvent && /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "ghost", size: "sm", onClick: emitWithId(editEvent), "data-testid": "document-edit", children: [
37253
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "edit", size: "xs", className: "mr-1" }),
37254
- editLabel ?? (t("documentPanel.edit") || "Edit")
37255
- ] }),
37256
- inlineActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
37257
- Button,
37258
- {
37259
- variant: "primary",
37260
- size: "sm",
37261
- onClick: () => fireAction(action),
37262
- "data-testid": `action-${action.event}`,
37263
- children: [
37264
- action.icon && renderIconInput4(action.icon, { size: "xs", className: "mr-1" }),
37265
- action.label
37266
- ]
37267
- },
37268
- idx
37269
- )),
37270
- menuActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
37271
- Menu,
37272
- {
37273
- position: "bottom-end",
37274
- trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
37275
- items: menuActions.map((action) => ({
37276
- label: action.label,
37277
- icon: action.icon,
37278
- variant: action.variant === "danger" ? "danger" : "default",
37279
- onClick: () => fireAction(action)
37280
- }))
37281
- }
37282
- )
37283
- ] }) })
37284
- ] }),
37285
- editing ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsxRuntime.jsx(
37286
- Box,
37440
+ return /* @__PURE__ */ jsxRuntime.jsxs(Card, { variant: "elevated", className: cn("w-full overflow-hidden", className), children: [
37441
+ coverImage ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "h-44 w-full sm:h-52", "data-testid": "document-cover", children: /* @__PURE__ */ jsxRuntime.jsx(
37442
+ "img",
37287
37443
  {
37288
- onClick: emitWithId(editEvent),
37289
- className: cn(
37290
- "rounded-md px-1 -mx-1 min-h-[16rem]",
37291
- editEvent && "cursor-text transition-colors hover:bg-muted/30"
37292
- ),
37293
- "data-testid": "document-body",
37294
- children: value && value !== "" ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, readOnly: true }) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "muted", children: placeholder ?? (t("documentPanel.placeholder") || "") })
37295
- }
37296
- )
37297
- ] }) });
37444
+ src: coverImage,
37445
+ alt: "",
37446
+ "aria-hidden": "true",
37447
+ className: "h-full w-full object-cover"
37448
+ }
37449
+ ) }) : null,
37450
+ /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", className: "p-6 sm:p-8", children: [
37451
+ /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "sm", className: "justify-between items-start", children: [
37452
+ /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", className: "flex-1 min-w-0", children: [
37453
+ icon ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-muted-foreground", "data-testid": "document-icon", children: renderIconInput4(icon, { size: "lg" }) }) : null,
37454
+ titleNode,
37455
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: subtitle }) : null
37456
+ ] }),
37457
+ /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", className: "flex-shrink-0 pt-1 items-center", children: editing ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
37458
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", color: "muted", className: "hidden sm:block", children: autosaveHint ?? (t("documentPanel.autosaveHint") || "") }),
37459
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", size: "sm", onClick: emitWithId(doneEvent), "data-testid": "document-done", children: doneLabel ?? (t("documentPanel.done") || "Done") })
37460
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
37461
+ editEvent && /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "ghost", size: "sm", onClick: emitWithId(editEvent), "data-testid": "document-edit", children: [
37462
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "edit", size: "xs", className: "mr-1" }),
37463
+ editLabel ?? (t("documentPanel.edit") || "Edit")
37464
+ ] }),
37465
+ inlineActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
37466
+ Button,
37467
+ {
37468
+ variant: "primary",
37469
+ size: "sm",
37470
+ onClick: () => fireAction(action),
37471
+ "data-testid": `action-${action.event}`,
37472
+ children: [
37473
+ action.icon && renderIconInput4(action.icon, { size: "xs", className: "mr-1" }),
37474
+ action.label
37475
+ ]
37476
+ },
37477
+ idx
37478
+ )),
37479
+ menuActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
37480
+ Menu,
37481
+ {
37482
+ position: "bottom-end",
37483
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
37484
+ items: menuActions.map((action) => ({
37485
+ label: action.label,
37486
+ icon: action.icon,
37487
+ variant: action.variant === "danger" ? "danger" : "default",
37488
+ onClick: () => fireAction(action)
37489
+ }))
37490
+ }
37491
+ )
37492
+ ] }) })
37493
+ ] }),
37494
+ editing ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, changeEvent: contentChangeEvent, placeholder }) : /* @__PURE__ */ jsxRuntime.jsx(
37495
+ Box,
37496
+ {
37497
+ onClick: emitWithId(editEvent),
37498
+ className: cn(
37499
+ "rounded-md px-1 -mx-1 min-h-[16rem]",
37500
+ editEvent && "cursor-text transition-colors hover:bg-muted/30"
37501
+ ),
37502
+ "data-testid": "document-body",
37503
+ children: value && value !== "" ? /* @__PURE__ */ jsxRuntime.jsx(RichTextEditor, { value, readOnly: true }) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "muted", children: placeholder ?? (t("documentPanel.placeholder") || "") })
37504
+ }
37505
+ )
37506
+ ] })
37507
+ ] });
37298
37508
  }
37299
37509
  var init_DocumentPanel = __esm({
37300
37510
  "components/core/molecules/DocumentPanel.tsx"() {
@@ -37318,17 +37528,16 @@ function renderIconInput5(icon, props) {
37318
37528
  function fieldName(field) {
37319
37529
  return field.name ?? field.key ?? "";
37320
37530
  }
37321
- function relationLabel(value) {
37322
- if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Date) return null;
37323
- for (const key of ["name", "title", "label"]) {
37324
- const candidate = value[key];
37325
- if (typeof candidate === "string" && candidate !== "") return candidate;
37531
+ function relationId(value) {
37532
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
37533
+ const id = value.id;
37534
+ if (id !== void 0 && id !== null) return String(id);
37326
37535
  }
37327
- const id = value.id;
37328
- return id !== void 0 && id !== null ? String(id) : null;
37536
+ return String(value);
37329
37537
  }
37330
37538
  function fieldKind(field, value) {
37331
37539
  if (field.kind) return field.kind;
37540
+ if (field.type === "image") return "image";
37332
37541
  if (field.options && field.options.length > 0) return "select";
37333
37542
  if (typeof value === "boolean" || field.format === "boolean") return "boolean";
37334
37543
  if (Array.isArray(value) || relationLabel(value ?? null) !== null) return "readonly";
@@ -37338,6 +37547,7 @@ function DocumentDetails({
37338
37547
  entity,
37339
37548
  fields,
37340
37549
  metaCommitEvent,
37550
+ relationEvent,
37341
37551
  title,
37342
37552
  className
37343
37553
  }) {
@@ -37352,11 +37562,51 @@ function DocumentDetails({
37352
37562
  if (!metaCommitEvent || next === current) return;
37353
37563
  eventBus.emit(`UI:${metaCommitEvent}`, { id: recordId, patch: { id: recordId, [name]: next } });
37354
37564
  };
37565
+ const relationChip = (item, name, key) => {
37566
+ const label = relationLabel(item) ?? humanizeEnumValue(String(item));
37567
+ if (!relationEvent) {
37568
+ return /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "default", children: label }, key);
37569
+ }
37570
+ const emitClick = () => eventBus.emit(`UI:${relationEvent}`, { field: name, id: relationId(item), name: label });
37571
+ return /* @__PURE__ */ jsxRuntime.jsx(
37572
+ Badge,
37573
+ {
37574
+ variant: "default",
37575
+ role: "button",
37576
+ tabIndex: 0,
37577
+ className: "cursor-pointer transition-colors hover:bg-accent",
37578
+ onClick: emitClick,
37579
+ onKeyDown: (e) => {
37580
+ if (e.key === "Enter" || e.key === " ") {
37581
+ e.preventDefault();
37582
+ emitClick();
37583
+ }
37584
+ },
37585
+ "data-testid": `relation-chip-${name}`,
37586
+ children: label
37587
+ },
37588
+ key
37589
+ );
37590
+ };
37355
37591
  const renderValue = (field) => {
37356
37592
  const name = fieldName(field);
37357
37593
  const raw = getNestedValue(entity ?? {}, name);
37358
37594
  const kind = fieldKind(field, raw);
37359
37595
  const label = field.label ?? field.header ?? humanizeFieldName(name);
37596
+ if (kind === "image") {
37597
+ const url = typeof raw === "string" ? raw : "";
37598
+ if (!url) return /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
37599
+ return /* @__PURE__ */ jsxRuntime.jsx(
37600
+ "img",
37601
+ {
37602
+ src: url,
37603
+ alt: label,
37604
+ loading: "lazy",
37605
+ className: "max-h-32 w-full rounded-md border border-border object-cover",
37606
+ "data-testid": `document-property-image-${name}`
37607
+ }
37608
+ );
37609
+ }
37360
37610
  if (kind === "boolean") {
37361
37611
  return /* @__PURE__ */ jsxRuntime.jsx(
37362
37612
  Switch,
@@ -37386,9 +37636,12 @@ function DocumentDetails({
37386
37636
  if (raw.length === 0) {
37387
37637
  return /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "secondary", children: "\u2014" });
37388
37638
  }
37389
- return /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", className: "flex-wrap", children: raw.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "default", children: relationLabel(item) ?? humanizeEnumValue(String(item)) }, i)) });
37639
+ return /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", className: "flex-wrap", children: raw.map((item, i) => relationChip(item, name, i)) });
37390
37640
  }
37391
- const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : relationLabel(raw) ?? formatValue(raw, field.format);
37641
+ if (raw !== void 0 && raw !== null && raw !== "" && relationLabel(raw) !== null) {
37642
+ return relationChip(raw, name);
37643
+ }
37644
+ const shown2 = raw === void 0 || raw === null || raw === "" ? "\u2014" : formatValue(raw, field.format);
37392
37645
  return /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", className: "break-words", children: shown2 });
37393
37646
  }
37394
37647
  if (fieldDraft?.name === name) {
@@ -37457,6 +37710,7 @@ var init_DocumentDetails = __esm({
37457
37710
  init_cn();
37458
37711
  init_format();
37459
37712
  init_getNestedValue();
37713
+ init_relationLabel();
37460
37714
  init_useEventBus();
37461
37715
  init_Box();
37462
37716
  init_Stack();
@@ -42451,7 +42705,18 @@ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
42451
42705
  if (value === void 0 || value === null) return "\u2014";
42452
42706
  const str = String(value);
42453
42707
  switch (fieldType) {
42454
- case "image":
42708
+ case "image": {
42709
+ if (!str) return "\u2014";
42710
+ return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mt-1 max-w-full", children: /* @__PURE__ */ jsxRuntime.jsx(
42711
+ "img",
42712
+ {
42713
+ src: str,
42714
+ alt: formatFieldLabel(fieldName2),
42715
+ className: "max-w-full max-h-64 rounded-md object-contain",
42716
+ loading: "lazy"
42717
+ }
42718
+ ) });
42719
+ }
42455
42720
  case "url": {
42456
42721
  if (str.match(/\.(png|jpe?g|gif|svg|webp|avif)(\?|$)/i) || str.startsWith("data:image/")) {
42457
42722
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "mt-1 max-w-full", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -42464,7 +42729,7 @@ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
42464
42729
  }
42465
42730
  ) });
42466
42731
  }
42467
- if (fieldType === "url" && /^https?:\/\//i.test(str)) {
42732
+ if (/^https?:\/\//i.test(str)) {
42468
42733
  return /* @__PURE__ */ jsxRuntime.jsx(
42469
42734
  "a",
42470
42735
  {
@@ -42550,8 +42815,10 @@ function renderRichFieldValue(value, fieldName2, fieldType, meta) {
42550
42815
  return str;
42551
42816
  }
42552
42817
  case "relation": {
42553
- const match = meta?.options?.find((opt) => opt.value === str);
42554
- return match ? match.label : str;
42818
+ const labels = relationDisplayLabels(value, meta?.options);
42819
+ if (labels.length === 0) return "\u2014";
42820
+ if (labels.length === 1) return labels[0];
42821
+ return /* @__PURE__ */ jsxRuntime.jsx(HStack, { gap: "xs", wrap: true, children: labels.map((label, i) => /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "default", children: label }, i)) });
42555
42822
  }
42556
42823
  case "file": {
42557
42824
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
@@ -42653,6 +42920,7 @@ var init_DetailPanel = __esm({
42653
42920
  init_cn();
42654
42921
  init_format();
42655
42922
  init_getNestedValue();
42923
+ init_relationLabel();
42656
42924
  init_useEventBus();
42657
42925
  init_UploadDropZone();
42658
42926
  formatFieldLabel = humanizeFieldName;
@@ -43311,6 +43579,8 @@ function determineInputType(field) {
43311
43579
  return "url";
43312
43580
  case "file":
43313
43581
  return "file";
43582
+ case "image":
43583
+ return "image";
43314
43584
  case "money":
43315
43585
  return "currency";
43316
43586
  case "number":
@@ -43910,6 +44180,50 @@ var init_Form = __esm({
43910
44180
  }
43911
44181
  }
43912
44182
  );
44183
+ case "image": {
44184
+ const imageUrl = typeof currentValue === "string" ? currentValue : "";
44185
+ return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
44186
+ imageUrl ? /* @__PURE__ */ jsxRuntime.jsx(
44187
+ "img",
44188
+ {
44189
+ src: imageUrl,
44190
+ alt: "",
44191
+ "aria-hidden": "true",
44192
+ className: "h-24 w-full max-w-xs rounded-md border border-border object-cover",
44193
+ "data-testid": `image-preview-${fieldName2}`
44194
+ }
44195
+ ) : null,
44196
+ /* @__PURE__ */ jsxRuntime.jsx(
44197
+ UploadDropZone,
44198
+ {
44199
+ accept: "image/*",
44200
+ maxFiles: 1,
44201
+ maxSize: 2 * 1024 * 1024,
44202
+ disabled: isLoading,
44203
+ onFiles: (files) => {
44204
+ const f3 = files[0];
44205
+ if (!f3) return;
44206
+ const reader = new FileReader();
44207
+ reader.onload = () => handleChange(
44208
+ fieldName2,
44209
+ typeof reader.result === "string" ? reader.result : ""
44210
+ );
44211
+ reader.readAsDataURL(f3);
44212
+ }
44213
+ }
44214
+ ),
44215
+ /* @__PURE__ */ jsxRuntime.jsx(
44216
+ Input,
44217
+ {
44218
+ ...commonProps,
44219
+ type: "url",
44220
+ placeholder: t("form.imageUrlFallback"),
44221
+ value: imageUrl,
44222
+ onChange: (e) => handleChange(fieldName2, e.target.value)
44223
+ }
44224
+ )
44225
+ ] });
44226
+ }
43913
44227
  case "date":
43914
44228
  return /* @__PURE__ */ jsxRuntime.jsx(
43915
44229
  Input,