@almadar/ui 5.163.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.
@@ -22637,6 +22637,7 @@ var init_DashboardLayout = __esm({
22637
22637
  onNotificationClick,
22638
22638
  showThemeToggle = true,
22639
22639
  sidebarFooter,
22640
+ sidebarContent,
22640
22641
  onSignOut: onSignOutProp,
22641
22642
  currentPath,
22642
22643
  layoutMode = "sidebar",
@@ -22766,7 +22767,11 @@ var init_DashboardLayout = __esm({
22766
22767
  {
22767
22768
  as: "nav",
22768
22769
  gap: "none",
22769
- className: cn("flex-1 py-4 space-y-1 overflow-y-auto", isRail ? "px-2" : "px-3"),
22770
+ className: cn(
22771
+ "py-4 space-y-1 overflow-y-auto",
22772
+ sidebarContent && !isRail ? "shrink-0" : "flex-1",
22773
+ isRail ? "px-2" : "px-3"
22774
+ ),
22770
22775
  children: navItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
22771
22776
  NavLink,
22772
22777
  {
@@ -22778,6 +22783,7 @@ var init_DashboardLayout = __esm({
22778
22783
  ))
22779
22784
  }
22780
22785
  ),
22786
+ sidebarContent && !isRail && /* @__PURE__ */ jsxRuntime.jsx(exports.Box, { className: "flex-1 overflow-y-auto border-t border-border dark:border-border px-2 py-3", children: sidebarContent }),
22781
22787
  sidebarFooter && /* @__PURE__ */ jsxRuntime.jsx(exports.Box, { className: "p-4 border-t border-border dark:border-border", children: sidebarFooter })
22782
22788
  ]
22783
22789
  }
@@ -25848,7 +25854,17 @@ function fileIcon(name) {
25848
25854
  return "file";
25849
25855
  }
25850
25856
  }
25851
- var TreeNodeItem, FlatTreeNodeItem; exports.FileTree = void 0;
25857
+ function ancestorsOf(id, byId) {
25858
+ const out = /* @__PURE__ */ new Set();
25859
+ if (!id) return out;
25860
+ let cur = byId.get(id)?.parentId;
25861
+ while (cur && byId.has(cur) && !out.has(cur)) {
25862
+ out.add(cur);
25863
+ cur = byId.get(cur)?.parentId;
25864
+ }
25865
+ return out;
25866
+ }
25867
+ var TreeNodeItem, FlatTreeNodeItem, FlatFileTree; exports.FileTree = void 0;
25852
25868
  var init_FileTree = __esm({
25853
25869
  "components/core/molecules/FileTree.tsx"() {
25854
25870
  "use client";
@@ -25939,42 +25955,77 @@ var init_FileTree = __esm({
25939
25955
  indent,
25940
25956
  childrenByParent,
25941
25957
  onNodeSelect,
25942
- defaultExpanded = false
25958
+ onNodeAction,
25959
+ nodeActionIcon,
25960
+ nodeActionLabel,
25961
+ selectedId,
25962
+ look,
25963
+ isExpanded,
25964
+ onToggle
25943
25965
  }) => {
25944
- const [expanded, setExpanded] = React77.useState(defaultExpanded || depth < 1);
25945
25966
  const children = childrenByParent.get(item.id);
25946
25967
  const hasChildren = !!children && children.length > 0;
25968
+ const expanded = hasChildren && isExpanded(item.id, depth);
25969
+ const isSelected = selectedId !== void 0 && selectedId !== "" && item.id === selectedId;
25970
+ const nav = look === "nav";
25947
25971
  const handleClick = React77.useCallback(() => {
25948
- if (hasChildren) setExpanded((prev) => !prev);
25972
+ if (hasChildren && !onNodeSelect) onToggle(item.id, expanded);
25949
25973
  onNodeSelect?.(item.id);
25950
- }, [hasChildren, item.id, onNodeSelect]);
25974
+ }, [hasChildren, expanded, item.id, onNodeSelect, onToggle]);
25975
+ const handleChevron = React77.useCallback((e) => {
25976
+ e.stopPropagation();
25977
+ onToggle(item.id, expanded);
25978
+ }, [item.id, expanded, onToggle]);
25979
+ const handleAction = React77.useCallback((e) => {
25980
+ e.stopPropagation();
25981
+ onNodeAction?.(item.id);
25982
+ }, [item.id, onNodeAction]);
25951
25983
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
25952
25984
  /* @__PURE__ */ jsxRuntime.jsxs(
25953
25985
  exports.Box,
25954
25986
  {
25955
- className: "flex items-center gap-1.5 py-0.5 px-2 cursor-pointer rounded-sm transition-colors hover:bg-muted",
25987
+ 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"}`,
25956
25988
  style: { paddingLeft: depth * indent + 8 },
25957
25989
  onClick: handleClick,
25958
25990
  role: "treeitem",
25991
+ "aria-selected": isSelected,
25959
25992
  "aria-expanded": hasChildren ? expanded : void 0,
25960
25993
  children: [
25961
- hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(
25994
+ hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(exports.Box, { onClick: handleChevron, className: "flex items-center flex-shrink-0", role: "button", "aria-label": expanded ? "Collapse" : "Expand", children: /* @__PURE__ */ jsxRuntime.jsx(
25962
25995
  exports.Icon,
25963
25996
  {
25964
25997
  name: expanded ? "chevron-down" : "chevron-right",
25965
25998
  size: "xs",
25966
- className: "text-[var(--color-muted-foreground)] flex-shrink-0"
25999
+ className: isSelected ? "text-inherit" : "text-[var(--color-muted-foreground)]"
25967
26000
  }
25968
- ) : /* @__PURE__ */ jsxRuntime.jsx(exports.Box, { style: { width: 12, flexShrink: 0 } }),
26001
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx(exports.Box, { style: { width: 12, flexShrink: 0 } }),
25969
26002
  /* @__PURE__ */ jsxRuntime.jsx(
25970
26003
  exports.Icon,
25971
26004
  {
25972
- name: item.icon ?? (hasChildren ? expanded ? "folder-open" : "folder" : "file"),
26005
+ name: item.icon || (nav ? "file-text" : hasChildren ? expanded ? "folder-open" : "folder" : "file"),
25973
26006
  size: "xs",
25974
- className: hasChildren ? "text-[var(--color-warning)]" : "text-[var(--color-muted-foreground)]"
26007
+ className: isSelected ? "text-inherit" : nav || !hasChildren ? "text-[var(--color-muted-foreground)]" : "text-[var(--color-warning)]"
26008
+ }
26009
+ ),
26010
+ /* @__PURE__ */ jsxRuntime.jsx(
26011
+ exports.Typography,
26012
+ {
26013
+ variant: "caption",
26014
+ className: `truncate ${nav ? "text-sm" : "font-mono text-xs"} !text-inherit ${isSelected ? "font-semibold" : ""}`,
26015
+ children: item.label
25975
26016
  }
25976
26017
  ),
25977
- /* @__PURE__ */ jsxRuntime.jsx(exports.Typography, { variant: "caption", className: "truncate font-mono text-xs", children: item.label })
26018
+ onNodeAction && /* @__PURE__ */ jsxRuntime.jsx(
26019
+ exports.Box,
26020
+ {
26021
+ onClick: handleAction,
26022
+ role: "button",
26023
+ "aria-label": nodeActionLabel ?? "Node action",
26024
+ title: nodeActionLabel,
26025
+ 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"}`,
26026
+ children: /* @__PURE__ */ jsxRuntime.jsx(exports.Icon, { name: nodeActionIcon ?? "plus", size: "xs", className: "text-inherit" })
26027
+ }
26028
+ )
25978
26029
  ]
25979
26030
  }
25980
26031
  ),
@@ -25985,47 +26036,116 @@ var init_FileTree = __esm({
25985
26036
  depth: depth + 1,
25986
26037
  indent,
25987
26038
  childrenByParent,
25988
- onNodeSelect
26039
+ onNodeSelect,
26040
+ onNodeAction,
26041
+ nodeActionIcon,
26042
+ nodeActionLabel,
26043
+ selectedId,
26044
+ look,
26045
+ isExpanded,
26046
+ onToggle
25989
26047
  },
25990
26048
  child.id
25991
26049
  )) })
25992
26050
  ] });
25993
26051
  };
26052
+ FlatFileTree = ({
26053
+ items,
26054
+ selectedId,
26055
+ look = "files",
26056
+ onNodeSelect,
26057
+ onNodeAction,
26058
+ nodeActionIcon,
26059
+ nodeActionLabel,
26060
+ className,
26061
+ indent = 16
26062
+ }) => {
26063
+ const [overrides, setOverrides] = React77.useState(() => /* @__PURE__ */ new Map());
26064
+ const byId = React77__namespace.default.useMemo(() => new Map(items.map((node) => [node.id, node])), [items]);
26065
+ const selectedAncestors = React77__namespace.default.useMemo(() => ancestorsOf(selectedId, byId), [selectedId, byId]);
26066
+ const lastSelectedRef = React77__namespace.default.useRef(selectedId);
26067
+ React77__namespace.default.useEffect(() => {
26068
+ if (selectedId === lastSelectedRef.current) return;
26069
+ lastSelectedRef.current = selectedId;
26070
+ setOverrides((prev) => {
26071
+ if (![...selectedAncestors].some((a) => prev.get(a) === false)) return prev;
26072
+ const next = new Map(prev);
26073
+ for (const a of selectedAncestors) if (next.get(a) === false) next.delete(a);
26074
+ return next;
26075
+ });
26076
+ }, [selectedId, selectedAncestors]);
26077
+ const isExpanded = React77.useCallback(
26078
+ (id, depth) => overrides.get(id) ?? (depth < 1 || selectedAncestors.has(id)),
26079
+ [overrides, selectedAncestors]
26080
+ );
26081
+ const onToggle = React77.useCallback((id, effective) => {
26082
+ setOverrides((prev) => {
26083
+ const next = new Map(prev);
26084
+ next.set(id, !effective);
26085
+ return next;
26086
+ });
26087
+ }, []);
26088
+ if (items.length === 0) return null;
26089
+ const ids = new Set(items.map((node) => node.id));
26090
+ const childrenByParent = /* @__PURE__ */ new Map();
26091
+ const roots = [];
26092
+ for (const item of items) {
26093
+ if (item.parentId && ids.has(item.parentId)) {
26094
+ const siblings = childrenByParent.get(item.parentId);
26095
+ if (siblings) siblings.push(item);
26096
+ else childrenByParent.set(item.parentId, [item]);
26097
+ } else {
26098
+ roots.push(item);
26099
+ }
26100
+ }
26101
+ return /* @__PURE__ */ jsxRuntime.jsx(exports.Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
26102
+ FlatTreeNodeItem,
26103
+ {
26104
+ item,
26105
+ depth: 0,
26106
+ indent,
26107
+ childrenByParent,
26108
+ onNodeSelect,
26109
+ onNodeAction,
26110
+ nodeActionIcon,
26111
+ nodeActionLabel,
26112
+ selectedId,
26113
+ look,
26114
+ isExpanded,
26115
+ onToggle
26116
+ },
26117
+ item.id
26118
+ )) });
26119
+ };
25994
26120
  exports.FileTree = ({
25995
26121
  tree,
25996
26122
  items,
25997
26123
  selectedPath,
26124
+ selectedId,
26125
+ look = "files",
25998
26126
  onFileSelect,
25999
26127
  onNodeSelect,
26128
+ onNodeAction,
26129
+ nodeActionIcon,
26130
+ nodeActionLabel,
26000
26131
  className,
26001
26132
  indent = 16
26002
26133
  }) => {
26003
26134
  if (items) {
26004
- if (items.length === 0) return null;
26005
- const ids = new Set(items.map((node) => node.id));
26006
- const childrenByParent = /* @__PURE__ */ new Map();
26007
- const roots = [];
26008
- for (const item of items) {
26009
- if (item.parentId && ids.has(item.parentId)) {
26010
- const siblings = childrenByParent.get(item.parentId);
26011
- if (siblings) siblings.push(item);
26012
- else childrenByParent.set(item.parentId, [item]);
26013
- } else {
26014
- roots.push(item);
26015
- }
26016
- }
26017
- return /* @__PURE__ */ jsxRuntime.jsx(exports.Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
26018
- FlatTreeNodeItem,
26135
+ return /* @__PURE__ */ jsxRuntime.jsx(
26136
+ FlatFileTree,
26019
26137
  {
26020
- item,
26021
- depth: 0,
26022
- indent,
26023
- childrenByParent,
26138
+ items,
26139
+ selectedId,
26140
+ look,
26024
26141
  onNodeSelect,
26025
- defaultExpanded: true
26026
- },
26027
- item.id
26028
- )) });
26142
+ onNodeAction,
26143
+ nodeActionIcon,
26144
+ nodeActionLabel,
26145
+ className,
26146
+ indent
26147
+ }
26148
+ );
26029
26149
  }
26030
26150
  if (!tree || tree.length === 0) return null;
26031
26151
  return /* @__PURE__ */ jsxRuntime.jsx(exports.Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: tree.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -38159,13 +38279,35 @@ var init_RichTextEditor = __esm({
38159
38279
  document.execCommand("createLink", false, safe);
38160
38280
  afterEdit();
38161
38281
  }, [afterEdit, t, toolbar.link]);
38282
+ const imageInputRef = React77.useRef(null);
38283
+ const savedRangeRef = React77.useRef(null);
38162
38284
  const execImage = React77.useCallback(() => {
38163
- const url = window.prompt(t("richTextEditor.imagePrompt"));
38164
- if (!url) return;
38165
- const safe = safeUrl(url, ["http://", "https://", "data:image/"]) ?? `https://${url}`;
38166
- document.execCommand("insertImage", false, safe);
38167
- afterEdit();
38168
- }, [afterEdit, t]);
38285
+ const root = ref.current;
38286
+ const sel = window.getSelection();
38287
+ savedRangeRef.current = root && sel && sel.rangeCount > 0 && root.contains(sel.anchorNode) ? sel.getRangeAt(0).cloneRange() : null;
38288
+ imageInputRef.current?.click();
38289
+ }, []);
38290
+ const handleImageFile = React77.useCallback((e) => {
38291
+ const file = e.target.files?.[0];
38292
+ e.target.value = "";
38293
+ if (!file) return;
38294
+ const reader = new FileReader();
38295
+ reader.onload = () => {
38296
+ const url = typeof reader.result === "string" ? reader.result : "";
38297
+ if (!url.startsWith("data:image/")) return;
38298
+ const root = ref.current;
38299
+ if (!root) return;
38300
+ root.focus();
38301
+ const sel = window.getSelection();
38302
+ if (sel && savedRangeRef.current && root.contains(savedRangeRef.current.startContainer)) {
38303
+ sel.removeAllRanges();
38304
+ sel.addRange(savedRangeRef.current);
38305
+ }
38306
+ document.execCommand("insertImage", false, url);
38307
+ afterEdit();
38308
+ };
38309
+ reader.readAsDataURL(file);
38310
+ }, [afterEdit]);
38169
38311
  const execRule = React77.useCallback(() => {
38170
38312
  document.execCommand("insertHorizontalRule", false);
38171
38313
  afterEdit();
@@ -38178,6 +38320,18 @@ var init_RichTextEditor = __esm({
38178
38320
  }
38179
38321
  return /* @__PURE__ */ jsxRuntime.jsxs(exports.Box, { className: cn("flex flex-col gap-2", className), children: [
38180
38322
  /* @__PURE__ */ jsxRuntime.jsx(RichTextStyles, {}),
38323
+ /* @__PURE__ */ jsxRuntime.jsx(
38324
+ "input",
38325
+ {
38326
+ ref: imageInputRef,
38327
+ type: "file",
38328
+ accept: "image/*",
38329
+ className: "hidden",
38330
+ "aria-hidden": "true",
38331
+ tabIndex: -1,
38332
+ onChange: handleImageFile
38333
+ }
38334
+ ),
38181
38335
  showToolbar && /* @__PURE__ */ jsxRuntime.jsxs(
38182
38336
  exports.Box,
38183
38337
  {
@@ -45304,15 +45458,6 @@ var init_Form = __esm({
45304
45458
  "data-testid": `image-preview-${fieldName2}`
45305
45459
  }
45306
45460
  ) : null,
45307
- /* @__PURE__ */ jsxRuntime.jsx(
45308
- exports.Input,
45309
- {
45310
- ...commonProps,
45311
- type: "url",
45312
- value: imageUrl,
45313
- onChange: (e) => handleChange(fieldName2, e.target.value)
45314
- }
45315
- ),
45316
45461
  /* @__PURE__ */ jsxRuntime.jsx(
45317
45462
  exports.UploadDropZone,
45318
45463
  {
@@ -45331,6 +45476,16 @@ var init_Form = __esm({
45331
45476
  reader.readAsDataURL(f3);
45332
45477
  }
45333
45478
  }
45479
+ ),
45480
+ /* @__PURE__ */ jsxRuntime.jsx(
45481
+ exports.Input,
45482
+ {
45483
+ ...commonProps,
45484
+ type: "url",
45485
+ placeholder: t("form.imageUrlFallback"),
45486
+ value: imageUrl,
45487
+ onChange: (e) => handleChange(fieldName2, e.target.value)
45488
+ }
45334
45489
  )
45335
45490
  ] });
45336
45491
  }
@@ -54102,7 +54257,8 @@ var en_default = {
54102
54257
  "richTextEditor.linkPrompt": "Link address",
54103
54258
  "richTextEditor.image": "Insert image",
54104
54259
  "richTextEditor.imagePrompt": "Image URL",
54105
- "richTextEditor.placeholder": "Start writing\u2026"
54260
+ "richTextEditor.placeholder": "Start writing\u2026",
54261
+ "form.imageUrlFallback": "\u2026or paste an image address"
54106
54262
  };
54107
54263
 
54108
54264
  // hooks/useTranslate.ts
@@ -3755,6 +3755,14 @@ interface FileTreeProps {
3755
3755
  items?: FileTreeItem[];
3756
3756
  /** Currently selected file path */
3757
3757
  selectedPath?: string;
3758
+ /** Currently selected node id, in flat `items` mode: the row highlights
3759
+ * and every ancestor auto-expands so the selection is always visible —
3760
+ * the "you are here" a navigation tree needs. */
3761
+ selectedId?: string;
3762
+ /** Visual treatment for flat `items` mode: `files` (default) keeps the
3763
+ * monospace file-browser look; `nav` renders a sidebar navigation tree —
3764
+ * regular font, document icons, roomier hit areas. */
3765
+ look?: 'files' | 'nav';
3758
3766
  /** Called when a file is clicked */
3759
3767
  onFileSelect?: (path: string) => void;
3760
3768
  /** Called when a node is clicked, in flat `items` mode; carries the
@@ -3762,6 +3770,14 @@ interface FileTreeProps {
3762
3770
  * the click-to-select affordance, mirroring Badge's `onRemove`.
3763
3771
  * @offByDefault */
3764
3772
  onNodeSelect?: (id: string) => void;
3773
+ /** Called from the row's hover action button, in flat `items` mode;
3774
+ * carries that node's id. Presence-gated: binding it renders the action.
3775
+ * @offByDefault */
3776
+ onNodeAction?: (id: string) => void;
3777
+ /** Icon for the row hover action (default: plus). */
3778
+ nodeActionIcon?: string;
3779
+ /** Accessible label/tooltip for the row hover action. */
3780
+ nodeActionLabel?: string;
3765
3781
  /** CSS class */
3766
3782
  className?: string;
3767
3783
  /** Indent size per level in px (default: 16) */
@@ -12650,6 +12666,10 @@ interface DashboardLayoutProps {
12650
12666
  /** Custom sidebar footer (optional). When omitted, the sidebar has
12651
12667
  * no footer — apps that need Settings/etc. add them via navItems. */
12652
12668
  sidebarFooter?: React__default.ReactNode;
12669
+ /** Optional content region rendered between the nav links and the footer —
12670
+ * a page tree or any consumer navigation surface; scrolls independently
12671
+ * and takes the sidebar's remaining height. Hidden in the collapsed rail. */
12672
+ sidebarContent?: React__default.ReactNode;
12653
12673
  /** Active path used to highlight the matching nav item. Falls back
12654
12674
  * to `useCurrentPagePath()` (set by `CurrentPagePathProvider`), then
12655
12675
  * to `useLocation().pathname`. Production deploys can omit it; the
@@ -3755,6 +3755,14 @@ interface FileTreeProps {
3755
3755
  items?: FileTreeItem[];
3756
3756
  /** Currently selected file path */
3757
3757
  selectedPath?: string;
3758
+ /** Currently selected node id, in flat `items` mode: the row highlights
3759
+ * and every ancestor auto-expands so the selection is always visible —
3760
+ * the "you are here" a navigation tree needs. */
3761
+ selectedId?: string;
3762
+ /** Visual treatment for flat `items` mode: `files` (default) keeps the
3763
+ * monospace file-browser look; `nav` renders a sidebar navigation tree —
3764
+ * regular font, document icons, roomier hit areas. */
3765
+ look?: 'files' | 'nav';
3758
3766
  /** Called when a file is clicked */
3759
3767
  onFileSelect?: (path: string) => void;
3760
3768
  /** Called when a node is clicked, in flat `items` mode; carries the
@@ -3762,6 +3770,14 @@ interface FileTreeProps {
3762
3770
  * the click-to-select affordance, mirroring Badge's `onRemove`.
3763
3771
  * @offByDefault */
3764
3772
  onNodeSelect?: (id: string) => void;
3773
+ /** Called from the row's hover action button, in flat `items` mode;
3774
+ * carries that node's id. Presence-gated: binding it renders the action.
3775
+ * @offByDefault */
3776
+ onNodeAction?: (id: string) => void;
3777
+ /** Icon for the row hover action (default: plus). */
3778
+ nodeActionIcon?: string;
3779
+ /** Accessible label/tooltip for the row hover action. */
3780
+ nodeActionLabel?: string;
3765
3781
  /** CSS class */
3766
3782
  className?: string;
3767
3783
  /** Indent size per level in px (default: 16) */
@@ -12650,6 +12666,10 @@ interface DashboardLayoutProps {
12650
12666
  /** Custom sidebar footer (optional). When omitted, the sidebar has
12651
12667
  * no footer — apps that need Settings/etc. add them via navItems. */
12652
12668
  sidebarFooter?: React__default.ReactNode;
12669
+ /** Optional content region rendered between the nav links and the footer —
12670
+ * a page tree or any consumer navigation surface; scrolls independently
12671
+ * and takes the sidebar's remaining height. Hidden in the collapsed rail. */
12672
+ sidebarContent?: React__default.ReactNode;
12653
12673
  /** Active path used to highlight the matching nav item. Falls back
12654
12674
  * to `useCurrentPagePath()` (set by `CurrentPagePathProvider`), then
12655
12675
  * to `useLocation().pathname`. Production deploys can omit it; the