@almadar/ui 5.153.0 → 5.154.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.
@@ -1,6 +1,6 @@
1
1
  import * as React87 from 'react';
2
2
  import React87__default, { createContext, useContext, useMemo, useRef, useEffect, useCallback, Suspense, useState, useSyncExternalStore, useLayoutEffect, lazy, useId } from 'react';
3
- import { EventBusContext, useTraitScopeChain, useEntitySchemaOptional, useEntityBindingSnapshot, useTraitScope, TraitScopeProvider, useCurrentPagePath, useGameAudioContextOptional } from '@almadar/ui/providers';
3
+ import { EventBusContext, useTraitScopeChain, useEntitySchemaOptional, useEntityBindingSnapshot, useTraitScope, TraitScopeProvider, useNavStack, useCurrentPagePath, useGameAudioContextOptional, matchPathAmong } from '@almadar/ui/providers';
4
4
  import { createLogger, isLogLevelEnabled } from '@almadar/logger';
5
5
  import { createContextFromBindings, interpolateValue } from '@almadar/runtime';
6
6
  import { ANONYMOUS_USER, isRenderBindingMarker, isFileValue, isInlineTrait, ANIMATION_NAMES } from '@almadar/core';
@@ -16,7 +16,7 @@ import { useUISlots, ThemeProvider, useTheme } from '@almadar/ui/context';
16
16
  export { DesignThemeProvider, useDesignTheme } from '@almadar/ui/context';
17
17
  import { evaluate, createMinimalContext } from '@almadar/evaluator';
18
18
  import { wrapCallbackForEvent } from '@almadar/runtime/ui';
19
- import { Link, Outlet, useLocation } from 'react-router-dom';
19
+ import { useLocation, useNavigate, Link, Outlet } from 'react-router-dom';
20
20
  import ELK from 'elkjs/lib/elk.bundled.js';
21
21
  import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light.js';
22
22
  import dark from 'react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus.js';
@@ -21326,17 +21326,27 @@ var init_Breadcrumb = __esm({
21326
21326
  init_useEventBus();
21327
21327
  Breadcrumb = ({
21328
21328
  items,
21329
+ fromNavStack = false,
21330
+ itemEvent,
21329
21331
  separator = "chevron-right",
21330
21332
  maxItems,
21331
21333
  className
21332
21334
  }) => {
21333
21335
  const eventBus = useEventBus();
21334
21336
  const { t } = useTranslate();
21335
- const displayItems = maxItems && items.length > maxItems ? [
21336
- ...items.slice(0, 1),
21337
+ const navStack = useNavStack();
21338
+ const sourceItems = fromNavStack ? navStack.entries.map((entry, i) => ({
21339
+ label: entry.label,
21340
+ path: entry.href,
21341
+ isCurrent: i === navStack.entries.length - 1,
21342
+ event: itemEvent
21343
+ })) : items ?? [];
21344
+ if (fromNavStack && sourceItems.length === 0) return null;
21345
+ const displayItems = maxItems && sourceItems.length > maxItems ? [
21346
+ ...sourceItems.slice(0, 1),
21337
21347
  { label: "...", isCurrent: false },
21338
- ...items.slice(-maxItems + 1)
21339
- ] : items;
21348
+ ...sourceItems.slice(-maxItems + 1)
21349
+ ] : sourceItems;
21340
21350
  return /* @__PURE__ */ jsx(
21341
21351
  "nav",
21342
21352
  {
@@ -21346,7 +21356,7 @@ var init_Breadcrumb = __esm({
21346
21356
  const isLast = index === displayItems.length - 1;
21347
21357
  const isEllipsis = item.label === "...";
21348
21358
  return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2", children: [
21349
- isEllipsis ? /* @__PURE__ */ jsx(Typography, { variant: "small", color: "muted", children: item.label }) : item.href || item.path ? /* @__PURE__ */ jsxs(
21359
+ isEllipsis ? /* @__PURE__ */ jsx(Typography, { variant: "small", color: "muted", children: item.label }) : (item.href || item.path) && !item.event && !fromNavStack ? /* @__PURE__ */ jsxs(
21350
21360
  "a",
21351
21361
  {
21352
21362
  href: item.href || item.path,
@@ -21372,7 +21382,12 @@ var init_Breadcrumb = __esm({
21372
21382
  {
21373
21383
  type: "button",
21374
21384
  onClick: () => {
21375
- if (item.event) eventBus.emit(`UI:${item.event}`, { label: item.label });
21385
+ const href = item.path ?? item.href;
21386
+ if (item.event) {
21387
+ eventBus.emit(`UI:${item.event}`, { label: item.label, href, index });
21388
+ } else if (fromNavStack && href) {
21389
+ navStack.goTo(href);
21390
+ }
21376
21391
  item.onClick?.();
21377
21392
  },
21378
21393
  className: cn(
@@ -31967,13 +31982,13 @@ var init_MapView = __esm({
31967
31982
  shadowSize: [41, 41]
31968
31983
  });
31969
31984
  L.Marker.prototype.options.icon = defaultIcon;
31970
- const { useEffect: useEffect64, useRef: useRef63, useCallback: useCallback96, useState: useState94 } = React87__default;
31985
+ const { useEffect: useEffect65, useRef: useRef64, useCallback: useCallback97, useState: useState95 } = React87__default;
31971
31986
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31972
31987
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31973
31988
  function MapUpdater({ centerLat, centerLng, zoom }) {
31974
31989
  const map = useMap();
31975
- const prevRef = useRef63({ centerLat, centerLng, zoom });
31976
- useEffect64(() => {
31990
+ const prevRef = useRef64({ centerLat, centerLng, zoom });
31991
+ useEffect65(() => {
31977
31992
  const prev = prevRef.current;
31978
31993
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
31979
31994
  map.setView([centerLat, centerLng], zoom);
@@ -31984,7 +31999,7 @@ var init_MapView = __esm({
31984
31999
  }
31985
32000
  function MapClickHandler({ onMapClick }) {
31986
32001
  const map = useMap();
31987
- useEffect64(() => {
32002
+ useEffect65(() => {
31988
32003
  if (!onMapClick) return;
31989
32004
  const handler = (e) => {
31990
32005
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -32012,8 +32027,8 @@ var init_MapView = __esm({
32012
32027
  showAttribution = true
32013
32028
  }) {
32014
32029
  const eventBus = useEventBus2();
32015
- const [clickedPosition, setClickedPosition] = useState94(null);
32016
- const handleMapClick = useCallback96((lat, lng) => {
32030
+ const [clickedPosition, setClickedPosition] = useState95(null);
32031
+ const handleMapClick = useCallback97((lat, lng) => {
32017
32032
  if (showClickedPin) {
32018
32033
  setClickedPosition({ lat, lng });
32019
32034
  }
@@ -32022,7 +32037,7 @@ var init_MapView = __esm({
32022
32037
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
32023
32038
  }
32024
32039
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
32025
- const handleMarkerClick = useCallback96((marker) => {
32040
+ const handleMarkerClick = useCallback97((marker) => {
32026
32041
  onMarkerClick?.(marker);
32027
32042
  if (markerClickEvent) {
32028
32043
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -42254,6 +42269,7 @@ var init_DetailPanel = __esm({
42254
42269
  init_Box();
42255
42270
  init_Stack();
42256
42271
  init_SimpleGrid();
42272
+ init_Menu();
42257
42273
  init_LoadingState();
42258
42274
  init_ErrorState();
42259
42275
  init_EmptyState();
@@ -42271,6 +42287,7 @@ var init_DetailPanel = __esm({
42271
42287
  avatar,
42272
42288
  sections: propSections,
42273
42289
  actions,
42290
+ maxInlineActions,
42274
42291
  backAction,
42275
42292
  footer,
42276
42293
  slideOver = false,
@@ -42505,7 +42522,7 @@ var init_DetailPanel = __esm({
42505
42522
  }
42506
42523
  ) }),
42507
42524
  /* @__PURE__ */ jsxs(HStack, { justify: "end", align: "center", gap: "xs", children: [
42508
- otherActions.map((action, idx) => /* @__PURE__ */ jsx(
42525
+ (maxInlineActions != null ? otherActions.slice(0, maxInlineActions) : otherActions).map((action, idx) => /* @__PURE__ */ jsx(
42509
42526
  Button,
42510
42527
  {
42511
42528
  variant: action.variant || "secondary",
@@ -42520,6 +42537,22 @@ var init_DetailPanel = __esm({
42520
42537
  },
42521
42538
  idx
42522
42539
  )),
42540
+ maxInlineActions != null && otherActions.length > maxInlineActions && /* @__PURE__ */ jsx(
42541
+ Menu,
42542
+ {
42543
+ position: "bottom-end",
42544
+ 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" }) }),
42545
+ items: otherActions.slice(maxInlineActions).map((action) => ({
42546
+ // ONE firing path: onClick → handleActionClick (emits with
42547
+ // the {id, row} payload). Passing `event` too would make
42548
+ // Menu emit a second, payload-less copy of the same event.
42549
+ label: action.label,
42550
+ icon: action.icon,
42551
+ variant: action.variant === "danger" ? "danger" : "default",
42552
+ onClick: () => handleActionClick(action, normalizedData)
42553
+ }))
42554
+ }
42555
+ ),
42523
42556
  /* @__PURE__ */ jsx(
42524
42557
  Button,
42525
42558
  {
@@ -51144,12 +51177,18 @@ function ServerBridgeProvider({
51144
51177
  } else if (effectType === "navigate") {
51145
51178
  const route = effect[1];
51146
51179
  const rawParams = effect[2];
51180
+ const rawOptions = effect[3];
51181
+ const optionsObj = rawOptions !== null && rawOptions !== void 0 && typeof rawOptions === "object" && !Array.isArray(rawOptions) ? rawOptions : void 0;
51182
+ const crumb = typeof optionsObj?.crumb === "string" ? optionsObj.crumb : void 0;
51147
51183
  effects.push({
51148
51184
  type: "navigate",
51149
51185
  route,
51150
51186
  params: rawParams !== null && rawParams !== void 0 && typeof rawParams === "object" && !Array.isArray(rawParams) ? rawParams : void 0,
51187
+ crumb,
51151
51188
  traitName
51152
51189
  });
51190
+ } else if (effectType === "navigate-back") {
51191
+ effects.push({ type: "navigate-back", traitName });
51153
51192
  } else if (effectType === "notify") {
51154
51193
  const message = effect[1];
51155
51194
  effects.push({ type: "notify", message: typeof message === "string" ? message : void 0, traitName });
@@ -51414,5 +51453,169 @@ function GameAudioProvider2({
51414
51453
  return /* @__PURE__ */ jsx(GameAudioContext2.Provider, { value, children });
51415
51454
  }
51416
51455
  GameAudioProvider2.displayName = "GameAudioProvider";
51456
+ function derivePageLabel(name) {
51457
+ const stripped = name.endsWith("Page") && name.length > 4 ? name.slice(0, -4) : name;
51458
+ return stripped.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
51459
+ }
51460
+ function normalizePath(p) {
51461
+ let normalized = p.trim();
51462
+ if (!normalized.startsWith("/")) normalized = "/" + normalized;
51463
+ if (normalized.length > 1 && normalized.endsWith("/")) {
51464
+ normalized = normalized.slice(0, -1);
51465
+ }
51466
+ return normalized;
51467
+ }
51468
+ function matchNavPage(pages, path) {
51469
+ const hit = matchPathAmong(pages, path, (p) => p.path);
51470
+ return hit ? hit.candidate : null;
51471
+ }
51472
+ function seedAncestors(pages, path) {
51473
+ const segments = normalizePath(path).split("/").filter(Boolean);
51474
+ const ancestors = [];
51475
+ for (let depth = 1; depth < segments.length; depth++) {
51476
+ const prefix = "/" + segments.slice(0, depth).join("/");
51477
+ const page = matchNavPage(pages, prefix);
51478
+ if (page) {
51479
+ ancestors.push({ href: prefix, label: derivePageLabel(page.name) });
51480
+ }
51481
+ }
51482
+ return ancestors;
51483
+ }
51484
+ function syncNavStack(state, pages, path, pending) {
51485
+ const normalized = normalizePath(path);
51486
+ const page = matchNavPage(pages, normalized);
51487
+ if (!page) return { state, orbital: null };
51488
+ const label = pending && normalizePath(pending.href) === normalized ? pending.crumb : derivePageLabel(page.name);
51489
+ const prior = state[page.orbital];
51490
+ const stack = prior && prior.length > 0 ? [...prior] : seedAncestors(pages, normalized);
51491
+ const existing = stack.findIndex((e) => e.href === normalized);
51492
+ if (existing >= 0) {
51493
+ stack.length = existing + 1;
51494
+ if (pending && normalizePath(pending.href) === normalized) {
51495
+ stack[existing] = { href: normalized, label };
51496
+ }
51497
+ } else {
51498
+ stack.push({ href: normalized, label });
51499
+ }
51500
+ return { state: { ...state, [page.orbital]: stack }, orbital: page.orbital };
51501
+ }
51502
+ function previousEntry(state, pages, path) {
51503
+ const page = matchNavPage(pages, normalizePath(path));
51504
+ if (!page) return null;
51505
+ const stack = state[page.orbital] ?? [];
51506
+ return stack.length >= 2 ? stack[stack.length - 2] : null;
51507
+ }
51508
+ function entriesFor(state, pages, path) {
51509
+ const page = matchNavPage(pages, normalizePath(path));
51510
+ if (!page) return [];
51511
+ return state[page.orbital] ?? [];
51512
+ }
51513
+ var INERT_API = {
51514
+ entries: [],
51515
+ canGoBack: false,
51516
+ beginNavigate: () => void 0,
51517
+ back: () => void 0,
51518
+ goTo: () => void 0
51519
+ };
51520
+ var NavStackContext = createContext(INERT_API);
51521
+ function useNavStack2() {
51522
+ return useContext(NavStackContext);
51523
+ }
51524
+ function loadStored(storageKey) {
51525
+ if (!storageKey) return {};
51526
+ try {
51527
+ const raw = window.sessionStorage.getItem(storageKey);
51528
+ if (!raw) return {};
51529
+ const parsed = JSON.parse(raw);
51530
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
51531
+ return parsed;
51532
+ }
51533
+ return {};
51534
+ } catch {
51535
+ return {};
51536
+ }
51537
+ }
51538
+ function persistStored(storageKey, state) {
51539
+ if (!storageKey) return;
51540
+ try {
51541
+ window.sessionStorage.setItem(storageKey, JSON.stringify(state));
51542
+ } catch {
51543
+ }
51544
+ }
51545
+ var NavStackProvider = ({
51546
+ pages,
51547
+ currentPath,
51548
+ navigate,
51549
+ storageKey,
51550
+ children
51551
+ }) => {
51552
+ const [state, setState] = useState(() => loadStored(storageKey));
51553
+ const pendingCrumbRef = useRef(null);
51554
+ const stateRef = useRef(state);
51555
+ stateRef.current = state;
51556
+ useEffect(() => {
51557
+ const pending = pendingCrumbRef.current;
51558
+ pendingCrumbRef.current = null;
51559
+ setState((prev) => {
51560
+ const next = syncNavStack(prev, pages, currentPath, pending);
51561
+ if (next.state !== prev) persistStored(storageKey, next.state);
51562
+ return next.state;
51563
+ });
51564
+ }, [currentPath, pages, storageKey]);
51565
+ const beginNavigate = useCallback((href, crumb) => {
51566
+ pendingCrumbRef.current = crumb !== void 0 && crumb !== "" ? { href, crumb } : null;
51567
+ }, []);
51568
+ const back = useCallback(() => {
51569
+ const prev = previousEntry(stateRef.current, pages, currentPath);
51570
+ if (prev) navigate(prev.href);
51571
+ }, [pages, currentPath, navigate]);
51572
+ const goTo = useCallback(
51573
+ (href) => {
51574
+ navigate(href);
51575
+ },
51576
+ [navigate]
51577
+ );
51578
+ const entries = useMemo(
51579
+ () => entriesFor(state, pages, currentPath),
51580
+ [state, pages, currentPath]
51581
+ );
51582
+ const api = useMemo(
51583
+ () => ({
51584
+ entries,
51585
+ canGoBack: previousEntry(state, pages, currentPath) !== null,
51586
+ beginNavigate,
51587
+ back,
51588
+ goTo
51589
+ }),
51590
+ [entries, state, pages, currentPath, beginNavigate, back, goTo]
51591
+ );
51592
+ return /* @__PURE__ */ jsx(NavStackContext.Provider, { value: api, children });
51593
+ };
51594
+ NavStackProvider.displayName = "NavStackProvider";
51595
+ var NavStackRouterBridge = ({
51596
+ pages,
51597
+ storageKey = "almadar:navstack",
51598
+ children
51599
+ }) => {
51600
+ const location = useLocation();
51601
+ const routerNavigate = useNavigate();
51602
+ const navigate = useCallback(
51603
+ (path) => {
51604
+ routerNavigate(path);
51605
+ },
51606
+ [routerNavigate]
51607
+ );
51608
+ return /* @__PURE__ */ jsx(
51609
+ NavStackProvider,
51610
+ {
51611
+ pages,
51612
+ currentPath: location.pathname,
51613
+ navigate,
51614
+ storageKey,
51615
+ children
51616
+ }
51617
+ );
51618
+ };
51619
+ NavStackRouterBridge.displayName = "NavStackRouterBridge";
51417
51620
 
51418
- export { CurrentPagePathContext, CurrentPagePathProvider, EntityBindingContext, EntitySchemaProvider, EventBusContext2 as EventBusContext, EventBusProvider, GameAudioContext2 as GameAudioContext, GameAudioProvider2 as GameAudioProvider, NavigationProvider2 as NavigationProvider, OfflineModeProvider, OrbitalProvider, OrbitalThemeProvider, SelectionContext, SelectionProvider, ServerBridgeProvider, TraitContext, TraitProvider, TraitScopeProvider3 as TraitScopeProvider, UserContext, UserProvider, VerificationProvider, comparePathSpecificity2 as comparePathSpecificity, extractRouteParams2 as extractRouteParams, findPageByName2 as findPageByName, findPageByPath2 as findPageByPath, getAllPages2 as getAllPages, getDefaultPage2 as getDefaultPage, matchPath2 as matchPath, matchPathAmong2 as matchPathAmong, pathMatches2 as pathMatches, useActivePage2 as useActivePage, useCurrentPagePath2 as useCurrentPagePath, useEntityBindingSnapshot2 as useEntityBindingSnapshot, useEntitySchema, useEntitySchemaOptional6 as useEntitySchemaOptional, useGameAudioContext2 as useGameAudioContext, useGameAudioContextOptional2 as useGameAudioContextOptional, useHasPermission, useHasRole, useInitPayload2 as useInitPayload, useNavigateTo2 as useNavigateTo, useNavigation2 as useNavigation, useNavigationId2 as useNavigationId, useNavigationState2 as useNavigationState, useOfflineMode, useOptionalOfflineMode, useSelection, useSelectionOptional, useServerBridge, useTrait, useTraitContext, useTraitScope2 as useTraitScope, useTraitScopeChain2 as useTraitScopeChain, useUser, useUserForEvaluation };
51621
+ export { CurrentPagePathContext, CurrentPagePathProvider, EntityBindingContext, EntitySchemaProvider, EventBusContext2 as EventBusContext, EventBusProvider, GameAudioContext2 as GameAudioContext, GameAudioProvider2 as GameAudioProvider, NavStackProvider, NavStackRouterBridge, NavigationProvider2 as NavigationProvider, OfflineModeProvider, OrbitalProvider, OrbitalThemeProvider, SelectionContext, SelectionProvider, ServerBridgeProvider, TraitContext, TraitProvider, TraitScopeProvider3 as TraitScopeProvider, UserContext, UserProvider, VerificationProvider, comparePathSpecificity2 as comparePathSpecificity, extractRouteParams2 as extractRouteParams, findPageByName2 as findPageByName, findPageByPath2 as findPageByPath, getAllPages2 as getAllPages, getDefaultPage2 as getDefaultPage, matchPath2 as matchPath, matchPathAmong2 as matchPathAmong, pathMatches2 as pathMatches, useActivePage2 as useActivePage, useCurrentPagePath2 as useCurrentPagePath, useEntityBindingSnapshot2 as useEntityBindingSnapshot, useEntitySchema, useEntitySchemaOptional6 as useEntitySchemaOptional, useGameAudioContext2 as useGameAudioContext, useGameAudioContextOptional2 as useGameAudioContextOptional, useHasPermission, useHasRole, useInitPayload2 as useInitPayload, useNavStack2 as useNavStack, useNavigateTo2 as useNavigateTo, useNavigation2 as useNavigation, useNavigationId2 as useNavigationId, useNavigationState2 as useNavigationState, useOfflineMode, useOptionalOfflineMode, useSelection, useSelectionOptional, useServerBridge, useTrait, useTraitContext, useTraitScope2 as useTraitScope, useTraitScopeChain2 as useTraitScopeChain, useUser, useUserForEvaluation };
@@ -21034,17 +21034,27 @@ var init_Breadcrumb = __esm({
21034
21034
  init_useEventBus();
21035
21035
  Breadcrumb = ({
21036
21036
  items,
21037
+ fromNavStack = false,
21038
+ itemEvent,
21037
21039
  separator = "chevron-right",
21038
21040
  maxItems,
21039
21041
  className
21040
21042
  }) => {
21041
21043
  const eventBus = useEventBus();
21042
21044
  const { t } = hooks.useTranslate();
21043
- const displayItems = maxItems && items.length > maxItems ? [
21044
- ...items.slice(0, 1),
21045
+ const navStack = providers.useNavStack();
21046
+ const sourceItems = fromNavStack ? navStack.entries.map((entry, i) => ({
21047
+ label: entry.label,
21048
+ path: entry.href,
21049
+ isCurrent: i === navStack.entries.length - 1,
21050
+ event: itemEvent
21051
+ })) : items ?? [];
21052
+ if (fromNavStack && sourceItems.length === 0) return null;
21053
+ const displayItems = maxItems && sourceItems.length > maxItems ? [
21054
+ ...sourceItems.slice(0, 1),
21045
21055
  { label: "...", isCurrent: false },
21046
- ...items.slice(-maxItems + 1)
21047
- ] : items;
21056
+ ...sourceItems.slice(-maxItems + 1)
21057
+ ] : sourceItems;
21048
21058
  return /* @__PURE__ */ jsxRuntime.jsx(
21049
21059
  "nav",
21050
21060
  {
@@ -21054,7 +21064,7 @@ var init_Breadcrumb = __esm({
21054
21064
  const isLast = index === displayItems.length - 1;
21055
21065
  const isEllipsis = item.label === "...";
21056
21066
  return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-center gap-2", children: [
21057
- isEllipsis ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", children: item.label }) : item.href || item.path ? /* @__PURE__ */ jsxRuntime.jsxs(
21067
+ isEllipsis ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", children: item.label }) : (item.href || item.path) && !item.event && !fromNavStack ? /* @__PURE__ */ jsxRuntime.jsxs(
21058
21068
  "a",
21059
21069
  {
21060
21070
  href: item.href || item.path,
@@ -21080,7 +21090,12 @@ var init_Breadcrumb = __esm({
21080
21090
  {
21081
21091
  type: "button",
21082
21092
  onClick: () => {
21083
- if (item.event) eventBus.emit(`UI:${item.event}`, { label: item.label });
21093
+ const href = item.path ?? item.href;
21094
+ if (item.event) {
21095
+ eventBus.emit(`UI:${item.event}`, { label: item.label, href, index });
21096
+ } else if (fromNavStack && href) {
21097
+ navStack.goTo(href);
21098
+ }
21084
21099
  item.onClick?.();
21085
21100
  },
21086
21101
  className: cn(
@@ -41679,6 +41694,7 @@ var init_DetailPanel = __esm({
41679
41694
  init_Box();
41680
41695
  init_Stack();
41681
41696
  init_SimpleGrid();
41697
+ init_Menu();
41682
41698
  init_LoadingState();
41683
41699
  init_ErrorState();
41684
41700
  init_EmptyState();
@@ -41696,6 +41712,7 @@ var init_DetailPanel = __esm({
41696
41712
  avatar,
41697
41713
  sections: propSections,
41698
41714
  actions,
41715
+ maxInlineActions,
41699
41716
  backAction,
41700
41717
  footer,
41701
41718
  slideOver = false,
@@ -41930,7 +41947,7 @@ var init_DetailPanel = __esm({
41930
41947
  }
41931
41948
  ) }),
41932
41949
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { justify: "end", align: "center", gap: "xs", children: [
41933
- otherActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
41950
+ (maxInlineActions != null ? otherActions.slice(0, maxInlineActions) : otherActions).map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
41934
41951
  Button,
41935
41952
  {
41936
41953
  variant: action.variant || "secondary",
@@ -41945,6 +41962,22 @@ var init_DetailPanel = __esm({
41945
41962
  },
41946
41963
  idx
41947
41964
  )),
41965
+ maxInlineActions != null && otherActions.length > maxInlineActions && /* @__PURE__ */ jsxRuntime.jsx(
41966
+ Menu,
41967
+ {
41968
+ position: "bottom-end",
41969
+ 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" }) }),
41970
+ items: otherActions.slice(maxInlineActions).map((action) => ({
41971
+ // ONE firing path: onClick → handleActionClick (emits with
41972
+ // the {id, row} payload). Passing `event` too would make
41973
+ // Menu emit a second, payload-less copy of the same event.
41974
+ label: action.label,
41975
+ icon: action.icon,
41976
+ variant: action.variant === "danger" ? "danger" : "default",
41977
+ onClick: () => handleActionClick(action, normalizedData)
41978
+ }))
41979
+ }
41980
+ ),
41948
41981
  /* @__PURE__ */ jsxRuntime.jsx(
41949
41982
  Button,
41950
41983
  {
@@ -49389,7 +49422,7 @@ function runTickFrame(entityId, orderedWriters, store) {
49389
49422
  }
49390
49423
  var log3 = logger.createLogger("almadar:ui:effects:client-handlers");
49391
49424
  function createClientEffectHandlers(options) {
49392
- const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
49425
+ const { eventBus, slotSetter, navigate, navigateBack, notify, callService, liveEntity } = options;
49393
49426
  return {
49394
49427
  emit: (event, payload, source) => {
49395
49428
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
@@ -49439,6 +49472,9 @@ function createClientEffectHandlers(options) {
49439
49472
  }
49440
49473
  log3.warn("No navigate handler, ignoring", { path });
49441
49474
  }),
49475
+ navigateBack: navigateBack ?? (() => {
49476
+ log3.warn("No navigate-back handler, ignoring");
49477
+ }),
49442
49478
  notify: notify ?? ((msg, type) => {
49443
49479
  log3.debug("notify", { type, message: msg });
49444
49480
  })
@@ -50078,6 +50114,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
50078
50114
  }
50079
50115
  },
50080
50116
  navigate: optionsRef.current?.navigate,
50117
+ navigateBack: optionsRef.current?.navigateBack,
50081
50118
  notify: optionsRef.current?.notify,
50082
50119
  callService: optionsRef.current?.callService,
50083
50120
  // The canonical client `set` writes `(set @entity.X)` straight into
@@ -50139,6 +50176,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
50139
50176
  emit: clientHandlers.emit,
50140
50177
  renderUI: clientHandlers.renderUI,
50141
50178
  navigate: clientHandlers.navigate,
50179
+ navigateBack: clientHandlers.navigateBack,
50142
50180
  notify: clientHandlers.notify
50143
50181
  };
50144
50182
  }
@@ -50861,7 +50899,7 @@ function normalizeChild(child) {
50861
50899
  props: { ...rest, ...normalizedChildren !== void 0 ? { children: normalizedChildren } : {} }
50862
50900
  };
50863
50901
  }
50864
- function applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraits) {
50902
+ function applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraits, onNavigateBack) {
50865
50903
  for (const eff of effects) {
50866
50904
  if (eff.type === "render-ui" && eff.slot && eff.pattern) {
50867
50905
  if (eff.traitName && activeTraits && !activeTraits.has(eff.traitName)) {
@@ -50907,7 +50945,9 @@ function applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, active
50907
50945
  });
50908
50946
  }
50909
50947
  } else if (eff.type === "navigate" && eff.route && onNavigate) {
50910
- onNavigate(eff.route, eff.params);
50948
+ onNavigate(eff.route, eff.params, eff.crumb);
50949
+ } else if (eff.type === "navigate-back" && onNavigateBack) {
50950
+ onNavigateBack();
50911
50951
  }
50912
50952
  }
50913
50953
  }
@@ -50930,7 +50970,17 @@ function collectServerActiveTraits(ir, allTraits, mountedTraitNames) {
50930
50970
  }
50931
50971
  return active;
50932
50972
  }
50933
- function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNavigate, onLocalFallback, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, serverActiveTraits, children }) {
50973
+ function NavStackRefBridge({ apiRef }) {
50974
+ const api = providers.useNavStack();
50975
+ React85.useEffect(() => {
50976
+ apiRef.current = api;
50977
+ return () => {
50978
+ apiRef.current = null;
50979
+ };
50980
+ }, [api, apiRef]);
50981
+ return null;
50982
+ }
50983
+ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNavigate, onNavigateBack, onLocalFallback, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, serverActiveTraits, children }) {
50934
50984
  const bridge = providers.useServerBridge();
50935
50985
  const activeTraitNames = React85.useMemo(
50936
50986
  () => new Set(traits2.map((b) => b.trait.name).filter((n) => !!n)),
@@ -50956,10 +51006,10 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
50956
51006
  for (const name of targets) {
50957
51007
  const { effects, meta } = await bridge.sendEvent(name, event, withActiveTraits(payload));
50958
51008
  recordServerResponse(name, event, meta);
50959
- applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraitNames);
51009
+ applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraitNames, onNavigateBack);
50960
51010
  }
50961
- }, [bridge.connected, bridge.sendEvent, orbitalNames, uiSlots, onNavigate, embeddedTraits, activeTraitNames, withActiveTraits]);
50962
- const opts = orbitalNames ? { onEventProcessed, navigate: onNavigate, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams } : { navigate: onNavigate, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams };
51011
+ }, [bridge.connected, bridge.sendEvent, orbitalNames, uiSlots, onNavigate, onNavigateBack, embeddedTraits, activeTraitNames, withActiveTraits]);
51012
+ const opts = orbitalNames ? { onEventProcessed, navigate: onNavigate, navigateBack: onNavigateBack, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams } : { navigate: onNavigate, navigateBack: onNavigateBack, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams };
50963
51013
  const { sendEvent, entityBindingSource } = useTraitStateMachine(traits2, uiSlots, opts);
50964
51014
  const initSentRef = React85.useRef(false);
50965
51015
  const prevTraitsRef = React85.useRef(void 0);
@@ -51018,13 +51068,13 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
51018
51068
  effects: effectTraces,
51019
51069
  timestamp: Date.now()
51020
51070
  });
51021
- applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraitNames);
51071
+ applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraitNames, onNavigateBack);
51022
51072
  }
51023
51073
  })();
51024
- }, [bridge.connected, orbitalNames, bridge.sendEvent, uiSlots, onNavigate, embeddedTraits, activeTraitNames, withActiveTraits, routeParams]);
51074
+ }, [bridge.connected, orbitalNames, bridge.sendEvent, uiSlots, onNavigate, onNavigateBack, embeddedTraits, activeTraitNames, withActiveTraits, routeParams]);
51025
51075
  return /* @__PURE__ */ jsxRuntime.jsx(providers.EntityBindingContext.Provider, { value: entityBindingSource, children });
51026
51076
  }
51027
- function SchemaRunner({ schema, serverUrl, transport, mockData, pageName, routeParams, onNavigate, onLocalFallback, persistence }) {
51077
+ function SchemaRunner({ schema, serverUrl, transport, mockData, pageName, routeParams, onNavigate, onNavigateBack, onLocalFallback, persistence }) {
51028
51078
  const { traits: traits2, allEntities, allTraits, ir } = useResolvedSchema(schema, pageName);
51029
51079
  const allPageTraits = React85.useMemo(() => {
51030
51080
  let base;
@@ -51172,6 +51222,7 @@ function SchemaRunner({ schema, serverUrl, transport, mockData, pageName, routeP
51172
51222
  embeddedTraits,
51173
51223
  serverActiveTraits,
51174
51224
  onNavigate,
51225
+ onNavigateBack,
51175
51226
  onLocalFallback,
51176
51227
  persistence,
51177
51228
  children: /* @__PURE__ */ jsxRuntime.jsx(providers.OrbitalThemeProvider, { theme: activeOrbitalTheme, children: /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "h-full min-h-full overflow-auto p-4", children: /* @__PURE__ */ jsxRuntime.jsx(UISlotRenderer, { includeHud: true, hudMode: "inline", includeFloating: true }) }) })
@@ -51264,6 +51315,15 @@ function OrbPreview({
51264
51315
  const resolved = pages.find((p) => p.page.name === activePageName)?.page.path;
51265
51316
  return resolved ?? initialPagePath;
51266
51317
  }, [pages, currentPage, initialPagePath]);
51318
+ const navPages = React85.useMemo(
51319
+ () => pages.filter((p) => typeof p.page.path === "string" && p.page.path.length > 0).map((p) => ({ path: p.page.path, name: p.page.name, orbital: p.orbitalName })),
51320
+ [pages]
51321
+ );
51322
+ const concreteCurrentPath = React85.useMemo(() => {
51323
+ const pattern = currentPagePath ?? "/";
51324
+ return pattern.replace(/:([A-Za-z0-9_]+)/g, (whole, key) => routeParams[key] ?? whole);
51325
+ }, [currentPagePath, routeParams]);
51326
+ const navStackRef = React85.useRef(null);
51267
51327
  const handleNavigate = React85.useCallback((path) => {
51268
51328
  const hit = providers.matchPathAmong(pages, path, (entry) => entry.page.path);
51269
51329
  const match = hit?.candidate;
@@ -51285,6 +51345,16 @@ function OrbPreview({
51285
51345
  }
51286
51346
  }
51287
51347
  }, [pages]);
51348
+ const handleNavigateEffect = React85.useCallback(
51349
+ (path, _params, crumb) => {
51350
+ navStackRef.current?.beginNavigate(path, crumb);
51351
+ handleNavigate(path);
51352
+ },
51353
+ [handleNavigate]
51354
+ );
51355
+ const handleNavigateBack = React85.useCallback(() => {
51356
+ navStackRef.current?.back();
51357
+ }, []);
51288
51358
  React85.useEffect(() => {
51289
51359
  const unsubscribe = eventBus.on("UI:NAVIGATE", (event) => {
51290
51360
  const url = event.payload?.url;
@@ -51337,20 +51407,33 @@ function OrbPreview({
51337
51407
  style: { height },
51338
51408
  children: [
51339
51409
  localFallback && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "px-3 py-2 bg-[var(--color-warning)] bg-opacity-10 border-b border-[var(--color-warning)] flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-[var(--color-warning-foreground)] flex-1", children: "Preview server unreachable \u2014 running locally. Server-side state and persistence are disabled." }) }),
51340
- /* @__PURE__ */ jsxRuntime.jsx(providers.CurrentPagePathProvider, { value: currentPagePath, children: /* @__PURE__ */ jsxRuntime.jsx(providers.OrbitalProvider, { initialData: effectiveMockData, skipTheme: true, verification: true, isolated, children: /* @__PURE__ */ jsxRuntime.jsx(context.UISlotProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(
51341
- SchemaRunner,
51410
+ /* @__PURE__ */ jsxRuntime.jsx(providers.CurrentPagePathProvider, { value: currentPagePath, children: /* @__PURE__ */ jsxRuntime.jsxs(
51411
+ providers.NavStackProvider,
51342
51412
  {
51343
- schema: parseResult.schema,
51344
- serverUrl,
51345
- transport,
51346
- mockData: effectiveMockData,
51347
- pageName: currentPage,
51348
- routeParams,
51349
- onNavigate: handleNavigate,
51350
- onLocalFallback: handleLocalFallback,
51351
- persistence
51352
- }
51353
- ) }) }) })
51413
+ pages: navPages,
51414
+ currentPath: concreteCurrentPath,
51415
+ navigate: handleNavigate,
51416
+ storageKey: `almadar:navstack:${parseResult.schema.name ?? "preview"}`,
51417
+ children: [
51418
+ /* @__PURE__ */ jsxRuntime.jsx(NavStackRefBridge, { apiRef: navStackRef }),
51419
+ /* @__PURE__ */ jsxRuntime.jsx(providers.OrbitalProvider, { initialData: effectiveMockData, skipTheme: true, verification: true, isolated, children: /* @__PURE__ */ jsxRuntime.jsx(context.UISlotProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(
51420
+ SchemaRunner,
51421
+ {
51422
+ schema: parseResult.schema,
51423
+ serverUrl,
51424
+ transport,
51425
+ mockData: effectiveMockData,
51426
+ pageName: currentPage,
51427
+ routeParams,
51428
+ onNavigate: handleNavigateEffect,
51429
+ onNavigateBack: handleNavigateBack,
51430
+ onLocalFallback: handleLocalFallback,
51431
+ persistence
51432
+ }
51433
+ ) }) })
51434
+ ]
51435
+ }
51436
+ ) })
51354
51437
  ]
51355
51438
  }
51356
51439
  );