@likec4/diagram 1.50.0 → 1.52.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.
@@ -36,13 +36,15 @@ const Logo = withContext(LogoButton, "logo");
36
36
  const Label = withContext(m.div, "label", {
37
37
  shouldForwardProp
38
38
  });
39
- withContext(m.div, "dropdown", {
39
+ const Dropdown = withContext(m.div, "dropdown", {
40
40
  shouldForwardProp
41
41
  });
42
42
  const NavigationPanel = {
43
43
  Root,
44
44
  Body,
45
45
  Logo,
46
- Label};
46
+ Label,
47
+ Dropdown
48
+ };
47
49
 
48
50
  export { NavigationPanel };
@@ -0,0 +1,79 @@
1
+ import { css } from '@likec4/styles/css';
2
+
3
+ const root = css({
4
+ height: "30px",
5
+ paddingLeft: "sm",
6
+ paddingRight: "1",
7
+ borderRadius: "sm",
8
+ // TODO
9
+ // color: fallbackVar('var(--search-color)', 'mantine.colors.placeholder)',
10
+ border: "1px solid",
11
+ borderColor: {
12
+ base: "default.border",
13
+ _light: "mantine.colors.gray[4]",
14
+ _dark: "mantine.colors.dark[4]",
15
+ _hover: "default.border"
16
+ },
17
+ cursor: "pointer",
18
+ background: {
19
+ base: "default",
20
+ _light: "white",
21
+ _dark: "mantine.colors.dark[6]",
22
+ _hover: "default.hover"
23
+ },
24
+ width: "100%",
25
+ "& .tabler-icon": {
26
+ color: "text"
27
+ },
28
+ transition: {
29
+ base: "fast",
30
+ _whenPanning: "none !important"
31
+ },
32
+ boxShadow: {
33
+ base: "xs",
34
+ _hover: "sm",
35
+ _whenPanning: "none !important"
36
+ }
37
+ });
38
+ const placeholder = css({
39
+ fontSize: "sm",
40
+ // mantine.fontSizes.sm,
41
+ fontWeight: "medium",
42
+ paddingRight: "2.5",
43
+ // 10px
44
+ color: "text.placeholder",
45
+ flex: "1",
46
+ sm: {
47
+ paddingRight: "[30px]"
48
+ },
49
+ md: {
50
+ paddingRight: "[50px]"
51
+ }
52
+ });
53
+ const shortcut = css({
54
+ fontSize: "11px",
55
+ fontWeight: "bold",
56
+ lineHeight: 1,
57
+ padding: "[4px 7px]",
58
+ borderRadius: "sm",
59
+ border: "1px solid",
60
+ transition: "fast",
61
+ _light: {
62
+ color: "mantine.colors.gray[7]",
63
+ borderColor: "mantine.colors.gray[2]"
64
+ },
65
+ _dark: {
66
+ color: "mantine.colors.dark[0]",
67
+ borderColor: "mantine.colors.dark[7]"
68
+ },
69
+ backgroundColor: {
70
+ _light: "mantine.colors.gray[2]/70",
71
+ _dark: "mantine.colors.dark[8]/70",
72
+ _groupHover: {
73
+ _light: "mantine.colors.gray[2]",
74
+ _dark: "mantine.colors.dark[8]"
75
+ }
76
+ }
77
+ });
78
+
79
+ export { placeholder, root, shortcut };
@@ -0,0 +1,17 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { cx } from '@likec4/styles/css';
3
+ import { UnstyledButton, Group, Text } from '@mantine/core';
4
+ import { isMacOs } from '@xyflow/system';
5
+ import { placeholder, shortcut, root } from './SearchControl.css.js';
6
+ import IconSearch from '../node_modules/.pnpm/@tabler_icons-react@3.36.1_react@19.2.4/node_modules/@tabler/icons-react/dist/esm/icons/IconSearch.js';
7
+
8
+ function SearchControl({ className, ...others }) {
9
+ const isMac = isMacOs();
10
+ return /* @__PURE__ */ jsx(UnstyledButton, { ...others, className: cx("group", root, className), children: /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
11
+ /* @__PURE__ */ jsx(IconSearch, { style: { width: "15px", height: "15px" }, stroke: 2 }),
12
+ /* @__PURE__ */ jsx(Text, { component: "div", className: placeholder, children: "Search" }),
13
+ /* @__PURE__ */ jsx(Text, { component: "div", className: shortcut, children: isMac ? "⌘ + K" : "Ctrl + K" })
14
+ ] }) });
15
+ }
16
+
17
+ export { SearchControl };
@@ -12,3 +12,4 @@ export type { OverlayProps } from '../overlays/overlay/Overlay';
12
12
  export { PortalToContainer } from '../components/PortalToContainer';
13
13
  export { ShadowRoot } from '../shadowroot/ShadowRoot';
14
14
  export { FramerMotionConfig } from '../context/FramerMotionConfig';
15
+ export { NavigationPanel } from '../components/NavigationPanel';
@@ -5,6 +5,7 @@ export { Overlay } from '../overlays/overlay/Overlay.js';
5
5
  export { PortalToContainer } from '../components/PortalToContainer.js';
6
6
  export { ShadowRoot } from '../shadowroot/ShadowRoot.js';
7
7
  export { FramerMotionConfig } from '../context/FramerMotionConfig.js';
8
+ export { NavigationPanel } from '../components/NavigationPanel.js';
8
9
  export { CompoundActionButton } from '../base-primitives/compound/CompoundActionButton.js';
9
10
  export { CompoundActions } from '../likec4diagram/custom/nodes/CompoundActions.js';
10
11
  export { CompoundDeploymentNode, CompoundDetailsButtonWithHandler, CompoundElementNode, DeploymentNode, ElementDetailsButtonWithHandler, ElementNode, ViewGroupNode } from '../likec4diagram/custom/nodes/nodes.js';
package/dist/index.d.ts CHANGED
@@ -19,3 +19,6 @@ export { ReactLikeC4, type ReactLikeC4Props } from './ReactLikeC4';
19
19
  export { useCurrentView, useCurrentViewId, } from './hooks/useCurrentView';
20
20
  export { pickViewBounds } from './utils/view-bounds';
21
21
  export { LikeC4ProjectsOverview, type LikeC4ProjectsOverviewProps, } from './LikeC4ProjectsOverview';
22
+ export { normalizeSearch, SearchContext, type SearchContextValue, useSearchContext, } from './search/SearchContext';
23
+ export { bodyCss as searchBodyCss, dialogCss as searchDialogCss, SearchPanelContent, } from './search/Search';
24
+ export { SearchControl } from './components/SearchControl';
package/dist/index.js CHANGED
@@ -16,5 +16,8 @@ export { ReactLikeC4 } from './ReactLikeC4.js';
16
16
  export { useCurrentView, useCurrentViewId } from './hooks/useCurrentView.js';
17
17
  export { pickViewBounds } from './utils/view-bounds.js';
18
18
  export { LikeC4ProjectsOverview } from './LikeC4ProjectsOverview.js';
19
+ export { SearchContext, normalizeSearch, useSearchContext } from './search/SearchContext.js';
20
+ export { SearchPanelContent, bodyCss as searchBodyCss, dialogCss as searchDialogCss } from './search/Search.js';
21
+ export { SearchControl } from './components/SearchControl.js';
19
22
  export { useDiagram } from './hooks/safeContext.js';
20
23
  export { useOptionalCurrentViewModel } from './context/LikeC4ModelContext.js';
@@ -101,7 +101,7 @@ function DriftsSummary() {
101
101
  if (view.length === 0 && nodes.length === 0 && edges.length === 0) {
102
102
  return null;
103
103
  }
104
- return /* @__PURE__ */ jsx(AnimatePresence, { propagate: true, mode: "wait", children: /* @__PURE__ */ jsxs(
104
+ return /* @__PURE__ */ jsx(AnimatePresence, { children: /* @__PURE__ */ jsxs(
105
105
  m.div,
106
106
  {
107
107
  layout: "size",
@@ -120,16 +120,16 @@ function DriftsSummary() {
120
120
  }),
121
121
  children: [
122
122
  hasAtLeast(view, 1) && /* @__PURE__ */ jsx(ViewDrifts, { drifts: view }),
123
- nodes.length > 0 && /* @__PURE__ */ jsxs(m.div, { layout: "size", children: [
123
+ nodes.length > 0 && /* @__PURE__ */ jsxs(m.div, { layout: true, children: [
124
124
  /* @__PURE__ */ jsx(SectionHeader, { children: "Elements:" }),
125
- /* @__PURE__ */ jsx(m.div, { layout: "size", className: vstack({ mt: "2", gap: "2" }), children: map(
125
+ /* @__PURE__ */ jsx("div", { className: vstack({ mt: "2", gap: "2" }), children: map(
126
126
  nodes,
127
127
  (node) => /* @__PURE__ */ jsx(NodeDrifts, { ...node, ...handlers }, node.id)
128
128
  ) })
129
129
  ] }, `nodes-drifts`),
130
- edges.length > 0 && /* @__PURE__ */ jsxs(m.div, { layout: "size", children: [
130
+ edges.length > 0 && /* @__PURE__ */ jsxs(m.div, { layout: true, children: [
131
131
  /* @__PURE__ */ jsx(SectionHeader, { children: "Relationships:" }),
132
- /* @__PURE__ */ jsx(m.div, { layout: "size", className: vstack({ mt: "2", gap: "2" }), children: map(edges, (edge) => /* @__PURE__ */ jsx(EdgeDrifts, { ...edge, ...handlers }, edge.edgeId)) })
132
+ /* @__PURE__ */ jsx("div", { className: vstack({ mt: "2", gap: "2" }), children: map(edges, (edge) => /* @__PURE__ */ jsx(EdgeDrifts, { ...edge, ...handlers }, edge.edgeId)) })
133
133
  ] }, `edges-drifts`)
134
134
  ]
135
135
  },
@@ -153,6 +153,7 @@ function ViewDrifts({ drifts }) {
153
153
  ) });
154
154
  }
155
155
  function NodeDrifts({ id, name, drifts, ...handlers }) {
156
+ const key = `node-drifts-${id}`;
156
157
  return /* @__PURE__ */ jsxs(
157
158
  DriftsGroup,
158
159
  {
@@ -178,28 +179,29 @@ function NodeDrifts({ id, name, drifts, ...handlers }) {
178
179
  children: name
179
180
  }
180
181
  ),
181
- map(drifts, (drift) => /* @__PURE__ */ jsx(DriftLabel, { children: drift }, id + drift))
182
+ map(drifts, (drift, i) => /* @__PURE__ */ jsx(DriftLabel, { children: drift }, key + drift + i))
182
183
  ]
183
184
  },
184
- `node-drifts-${id}`
185
+ key
185
186
  );
186
187
  }
187
188
  function EdgeDrifts({ edgeId, drifts, ...handlers }) {
189
+ const key = `edge-drifts-${edgeId}`;
188
190
  return /* @__PURE__ */ jsx(
189
191
  DriftsGroup,
190
192
  {
191
193
  "data-drift-type": "edge",
192
194
  "data-drift-id": edgeId,
193
195
  ...handlers,
194
- children: map(drifts, (drift) => /* @__PURE__ */ jsx(
196
+ children: map(drifts, (drift, i) => /* @__PURE__ */ jsx(
195
197
  DriftLabel,
196
198
  {
197
199
  children: drift
198
200
  },
199
- edgeId + drift
201
+ key + drift + i
200
202
  ))
201
203
  },
202
- `edge-drifts-${edgeId}`
204
+ key
203
205
  );
204
206
  }
205
207
  const DriftsGroup = styled(m.div, {
@@ -220,7 +222,7 @@ const DriftsGroup = styled(m.div, {
220
222
  defaultProps: {
221
223
  className: "group",
222
224
  variants,
223
- layout: "size",
225
+ layout: true,
224
226
  initial: "initial",
225
227
  animate: "animate",
226
228
  exit: "exit"
@@ -32,14 +32,14 @@ const TriggerWalkthroughButton = forwardRef((props, ref) => /* @__PURE__ */ jsx(
32
32
  }
33
33
  ));
34
34
  function StartWalkthroughButton() {
35
- const { enableReadOnly, enableCompareWithLatest } = useEnabledFeatures();
35
+ const { enableCompareWithLatest } = useEnabledFeatures();
36
36
  const diagram = useDiagram();
37
37
  const actor = useNavigationActor();
38
38
  let tooltipLabel = "Start Dynamic View Walkthrough";
39
39
  switch (true) {
40
- case !enableReadOnly:
41
- tooltipLabel = "Walkthrough not available in Edit mode";
42
- break;
40
+ // case !enableReadOnly:
41
+ // tooltipLabel = 'Walkthrough not available in Edit mode'
42
+ // break
43
43
  case enableCompareWithLatest:
44
44
  tooltipLabel = "Walkthrough not available when Compare is active";
45
45
  break;
@@ -57,7 +57,7 @@ function StartWalkthroughButton() {
57
57
  exit: { opacity: 0, translateX: -20 },
58
58
  size: "compact-xs",
59
59
  h: 26,
60
- disabled: !enableReadOnly || enableCompareWithLatest,
60
+ disabled: enableCompareWithLatest,
61
61
  classNames: {
62
62
  label: css({
63
63
  display: {
@@ -1,2 +1,10 @@
1
1
  import { MemoExoticComponent } from 'react';
2
+ export declare const dialogCss: string;
3
+ export declare const bodyCss: string;
4
+ export declare const scrollAreaCss: string;
2
5
  export declare const Search: MemoExoticComponent<() => import("react/jsx-runtime").JSX.Element>;
6
+ /**
7
+ * The shared search panel content used by both diagram and overview search.
8
+ * Must be rendered inside a SearchContext.Provider.
9
+ */
10
+ export declare const SearchPanelContent: MemoExoticComponent<() => import("react/jsx-runtime").JSX.Element>;
@@ -1,12 +1,12 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { css } from '@likec4/styles/css';
3
3
  import { Box, VStack } from '@likec4/styles/jsx';
4
4
  import { Group, Grid, GridCol, Title, ScrollArea } from '@mantine/core';
5
5
  import { useHotkeys } from '@mantine/hooks';
6
6
  import { useTimeoutEffect } from '@react-hookz/web';
7
7
  import { useSelector } from '@xstate/react';
8
- import { AnimatePresence, usePresence, LayoutGroup } from 'motion/react';
9
- import { memo, useRef, useMemo, useEffect, Suspense } from 'react';
8
+ import { LayoutGroup, AnimatePresence, usePresence } from 'motion/react';
9
+ import { memo, useRef, Suspense, useMemo, useEffect } from 'react';
10
10
  import { isTruthy } from 'remeda';
11
11
  import { createEmptyActor } from 'xstate';
12
12
  import { useCallbackRef } from '../hooks/useCallbackRef.js';
@@ -16,27 +16,20 @@ import { ElementsColumn } from './components/ElementsColumn.js';
16
16
  import { PickView } from './components/PickView.js';
17
17
  import { SearchByTags } from './components/SearchByTags.js';
18
18
  import { LikeC4SearchInput } from './components/SearchInput.js';
19
- import { focusToFirstFoundElement, moveFocusToSearchInput } from './components/utils.js';
19
+ import { moveFocusToSearchInput, focusToFirstFoundElement } from './components/utils.js';
20
20
  import { ViewsColumn } from './components/ViewsColum.js';
21
+ import { useSearchContext } from './SearchContext.js';
22
+ import { XStateSearchAdapter } from './XStateSearchAdapter.js';
21
23
 
22
- const dialog = css({
24
+ const dialogCss = css({
23
25
  backgroundColor: `[rgb(34 34 34 / var(--_opacity, 95%))]`,
24
26
  _light: {
25
27
  backgroundColor: `[rgb(250 250 250 / var(--_opacity, 95%))]`
26
28
  },
27
29
  backdropFilter: "auto",
28
30
  backdropBlur: "var(--_blur, 10px)"
29
- // base: `[rgb(34 34 34 / var(${backdropOpacity}))]`,
30
- // _light: `[rgb(255 255 255/ var(${backdropOpacity}))]`,
31
- // },
32
31
  });
33
- const body = css({
34
- // containerName: 'likec4-search',
35
- // containerType: 'size',
36
- // position: 'fixed',
37
- // zIndex: 901,
38
- // top: '0',
39
- // left: '0',
32
+ const bodyCss = css({
40
33
  width: "100%",
41
34
  height: "100%",
42
35
  maxHeight: "100vh",
@@ -50,12 +43,8 @@ const body = css({
50
43
  paddingRight: "md",
51
44
  paddingBottom: "sm",
52
45
  background: "transparent"
53
- // backgroundColor: {
54
- // _dark: `[rgb(34 34 34 / 0.95)]`,
55
- // _light: `[rgb(255 255 255/ 0.95)]`,
56
- // },
57
46
  });
58
- const scrollArea = css({
47
+ const scrollAreaCss = css({
59
48
  height: [
60
49
  "100%",
61
50
  "100cqh"
@@ -105,38 +94,20 @@ const Search = memo(() => {
105
94
  opacity: 0.9
106
95
  },
107
96
  classes: {
108
- dialog,
109
- body
97
+ dialog: dialogCss,
98
+ body: bodyCss
110
99
  },
111
100
  openDelay: 0,
112
101
  onClose: close,
113
102
  "data-likec4-search": "true",
114
- children: /* @__PURE__ */ jsx(SearchOverlayBody, { searchActorRef })
103
+ children: /* @__PURE__ */ jsx(XStateSearchAdapter, { searchActorRef, children: /* @__PURE__ */ jsx(SearchOverlayBody, { searchActorRef }) })
115
104
  }
116
105
  ) });
117
106
  });
118
107
  Search.displayName = "Search";
119
- const selectPickViewFor = (s) => s.context.pickViewFor;
120
- const SearchOverlayBody = memo(({ searchActorRef }) => {
108
+ const SearchPanelContent = memo(() => {
121
109
  const ref = useRef(null);
122
- const pickViewFor = useSelector(searchActorRef, selectPickViewFor);
123
- useTimeoutEffect(() => {
124
- if (isTruthy(searchActorRef.getSnapshot().context.openedWithSearch)) {
125
- focusToFirstFoundElement(ref.current);
126
- }
127
- }, 150);
128
- const [isPresent, safeToRemove] = usePresence();
129
- useEffect(() => {
130
- if (isPresent) {
131
- return;
132
- }
133
- safeToRemove();
134
- try {
135
- searchActorRef.send({ type: "animation.presence.end" });
136
- } catch (e) {
137
- console.debug("SearchOverlayBody: animation.presence.end failed", e);
138
- }
139
- }, [isPresent, searchActorRef, safeToRemove]);
110
+ const { pickViewFor } = useSearchContext();
140
111
  return /* @__PURE__ */ jsxs(Box, { ref, display: "contents", children: [
141
112
  /* @__PURE__ */ jsx(
142
113
  Group,
@@ -171,7 +142,7 @@ const SearchOverlayBody = memo(({ searchActorRef }) => {
171
142
  ScrollArea,
172
143
  {
173
144
  type: "scroll",
174
- className: scrollArea,
145
+ className: scrollAreaCss,
175
146
  pr: "xs",
176
147
  scrollbars: "y",
177
148
  children: /* @__PURE__ */ jsx(LayoutGroup, { id: "likec4-search-elements", children: /* @__PURE__ */ jsx(Suspense, { children: /* @__PURE__ */ jsx(ElementsColumn, {}) }) })
@@ -181,7 +152,7 @@ const SearchOverlayBody = memo(({ searchActorRef }) => {
181
152
  ScrollArea,
182
153
  {
183
154
  type: "scroll",
184
- className: scrollArea,
155
+ className: scrollAreaCss,
185
156
  pr: "xs",
186
157
  scrollbars: "y",
187
158
  children: /* @__PURE__ */ jsx(Suspense, { children: /* @__PURE__ */ jsx(LayoutGroup, { id: "likec4-search-views", children: /* @__PURE__ */ jsx(ViewsColumn, {}) }) })
@@ -190,8 +161,30 @@ const SearchOverlayBody = memo(({ searchActorRef }) => {
190
161
  ]
191
162
  }
192
163
  ),
193
- pickViewFor && /* @__PURE__ */ jsx(PickView, { searchActorRef, elementFqn: pickViewFor })
164
+ pickViewFor && /* @__PURE__ */ jsx(PickView, { elementFqn: pickViewFor })
194
165
  ] });
195
166
  });
167
+ SearchPanelContent.displayName = "SearchPanelContent";
168
+ const SearchOverlayBody = memo(function SearchOverlayBody2({ searchActorRef }) {
169
+ const ref = useRef(null);
170
+ useTimeoutEffect(() => {
171
+ if (isTruthy(searchActorRef.getSnapshot().context.openedWithSearch)) {
172
+ focusToFirstFoundElement(ref.current);
173
+ }
174
+ }, 150);
175
+ const [isPresent, safeToRemove] = usePresence();
176
+ useEffect(() => {
177
+ if (isPresent) {
178
+ return;
179
+ }
180
+ safeToRemove();
181
+ try {
182
+ searchActorRef.send({ type: "animation.presence.end" });
183
+ } catch (e) {
184
+ console.debug("SearchOverlayBody: animation.presence.end failed", e);
185
+ }
186
+ }, [isPresent, searchActorRef, safeToRemove]);
187
+ return /* @__PURE__ */ jsx(Box, { ref, display: "contents", children: /* @__PURE__ */ jsx(SearchPanelContent, {}) });
188
+ });
196
189
 
197
- export { Search };
190
+ export { Search, SearchPanelContent, bodyCss, dialogCss, scrollAreaCss };
@@ -0,0 +1,21 @@
1
+ import { Fqn, ViewId } from '@likec4/core/types';
2
+ import { Context } from 'react';
3
+ export interface SearchContextValue {
4
+ searchValue: string;
5
+ setSearchValue: (value: string) => void;
6
+ normalizedSearch: string;
7
+ navigateTo: (viewId: ViewId, focusOnElement?: Fqn) => void;
8
+ openPickView: (elementFqn: Fqn) => void;
9
+ closePickView: () => void;
10
+ pickViewFor: Fqn | null;
11
+ close: () => void;
12
+ currentViewId: string | null;
13
+ openedWithSearch: string | null;
14
+ }
15
+ export declare const SearchContext: Context<SearchContextValue | null>;
16
+ export declare function useSearchContext(): SearchContextValue;
17
+ /**
18
+ * Normalize a raw search string for comparison.
19
+ * Extracted as a pure function so both XState and standalone adapters can reuse it.
20
+ */
21
+ export declare function normalizeSearch(value: string): string;
@@ -0,0 +1,20 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ const SearchContext = createContext(null);
4
+ function useSearchContext() {
5
+ const ctx = useContext(SearchContext);
6
+ if (!ctx) {
7
+ throw new Error("useSearchContext must be used within a SearchContext.Provider");
8
+ }
9
+ return ctx;
10
+ }
11
+ function normalizeSearch(value) {
12
+ if (value === "") return value;
13
+ const v = value.trim().toLowerCase();
14
+ if (v.startsWith("#") && v.length <= 2) {
15
+ return "";
16
+ }
17
+ return v.length > 1 ? v : "";
18
+ }
19
+
20
+ export { SearchContext, normalizeSearch, useSearchContext };
@@ -0,0 +1,5 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { SearchActorRef } from './searchActor';
3
+ export declare function XStateSearchAdapter({ searchActorRef, children, }: PropsWithChildren<{
4
+ searchActorRef: SearchActorRef;
5
+ }>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,64 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useSelector } from '@xstate/react';
3
+ import { useMemo } from 'react';
4
+ import { useCallbackRef } from '../hooks/useCallbackRef.js';
5
+ import { useCurrentViewId } from '../hooks/useCurrentView.js';
6
+ import { normalizeSearch, SearchContext } from './SearchContext.js';
7
+
8
+ const selectSearchValue = (s) => s.context.searchValue;
9
+ const selectPickViewFor = (s) => s.context.pickViewFor;
10
+ const selectOpenedWithSearch = (s) => s.context.openedWithSearch;
11
+ function XStateSearchAdapter({
12
+ searchActorRef,
13
+ children
14
+ }) {
15
+ const searchValue = useSelector(searchActorRef, selectSearchValue);
16
+ const pickViewFor = useSelector(searchActorRef, selectPickViewFor);
17
+ const openedWithSearch = useSelector(searchActorRef, selectOpenedWithSearch);
18
+ const currentViewId = useCurrentViewId();
19
+ const setSearchValue = useCallbackRef((search) => {
20
+ searchActorRef.send({ type: "change.search", search });
21
+ });
22
+ const navigateTo = useCallbackRef((viewId, focusOnElement) => {
23
+ searchActorRef.send({
24
+ type: "navigate.to",
25
+ viewId,
26
+ focusOnElement
27
+ });
28
+ });
29
+ const openPickView = useCallbackRef((elementFqn) => {
30
+ searchActorRef.send({ type: "pickview.open", elementFqn });
31
+ });
32
+ const closePickView = useCallbackRef(() => {
33
+ searchActorRef.send({ type: "pickview.close" });
34
+ });
35
+ const close = useCallbackRef(() => {
36
+ searchActorRef.send({ type: "close" });
37
+ });
38
+ const value = useMemo(() => ({
39
+ searchValue,
40
+ setSearchValue,
41
+ normalizedSearch: normalizeSearch(searchValue),
42
+ navigateTo,
43
+ openPickView,
44
+ closePickView,
45
+ pickViewFor,
46
+ close,
47
+ currentViewId,
48
+ openedWithSearch
49
+ }), [
50
+ searchValue,
51
+ pickViewFor,
52
+ currentViewId,
53
+ openedWithSearch,
54
+ // stable refs from useCallbackRef — listed for exhaustive-deps lint rule
55
+ setSearchValue,
56
+ navigateTo,
57
+ openPickView,
58
+ closePickView,
59
+ close
60
+ ]);
61
+ return /* @__PURE__ */ jsx(SearchContext.Provider, { value, children });
62
+ }
63
+
64
+ export { XStateSearchAdapter };
@@ -9,7 +9,8 @@ import { pipe, filter, hasAtLeast, reduce, only, first, last } from 'remeda';
9
9
  import { IconOrShapeRenderer } from '../../context/IconRenderer.js';
10
10
  import { useCallbackRef } from '../../hooks/useCallbackRef.js';
11
11
  import { useLikeC4Model } from '../../hooks/useLikeC4Model.js';
12
- import { useNormalizedSearch, useSearchActor } from '../hooks.js';
12
+ import { useNormalizedSearch } from '../hooks.js';
13
+ import { useSearchContext } from '../SearchContext.js';
13
14
  import { focusable, buttonsva } from './_shared.css.js';
14
15
  import { treeSubtree, treeLabel, treeRoot, treeNode, elementIcon, elementExpandIcon, elementTitleAndId, elementId, elementViewsCount } from './ElementsColumn.css.js';
15
16
  import { stopAndPrevent, moveFocusToSearchInput, queryAllFocusable, centerY } from './utils.js';
@@ -279,7 +280,7 @@ function ElementTreeNode({ node, elementProps, hasChildren, expanded, handleClic
279
280
  ] });
280
281
  }
281
282
  function useHandleElementSelection() {
282
- const searchActorRef = useSearchActor();
283
+ const ctx = useSearchContext();
283
284
  return useCallbackRef((element) => {
284
285
  const views = [...element.views()];
285
286
  if (views.length === 0) {
@@ -288,14 +289,10 @@ function useHandleElementSelection() {
288
289
  const elementFqn = element.id;
289
290
  const onlyOneViewId = only(views)?.id;
290
291
  if (!onlyOneViewId) {
291
- searchActorRef.send({ type: "pickview.open", elementFqn });
292
+ ctx.openPickView(elementFqn);
292
293
  return;
293
294
  }
294
- searchActorRef.send({
295
- type: "navigate.to",
296
- viewId: onlyOneViewId,
297
- focusOnElement: elementFqn
298
- });
295
+ ctx.navigateTo(onlyOneViewId, elementFqn);
299
296
  });
300
297
  }
301
298
 
@@ -1,6 +1,4 @@
1
1
  import { Fqn } from '@likec4/core/types';
2
- import { SearchActorRef } from '../searchActor';
3
- export declare function PickView({ searchActorRef, elementFqn, }: {
4
- searchActorRef: SearchActorRef;
2
+ export declare function PickView({ elementFqn, }: {
5
3
  elementFqn: Fqn;
6
4
  }): import("react/jsx-runtime").JSX.Element;
@@ -2,17 +2,17 @@ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import { FocusTrap, Group, Title, ActionIcon, ScrollAreaAutosize, Stack } from '@mantine/core';
3
3
  import { useWindowEvent, useCallbackRef } from '@mantine/hooks';
4
4
  import * as m from 'motion/react-m';
5
- import { useCurrentViewId } from '../../hooks/useCurrentView.js';
6
5
  import { useLikeC4Model } from '../../hooks/useLikeC4Model.js';
6
+ import { useSearchContext } from '../SearchContext.js';
7
7
  import { pickviewBackdrop, pickview, pickviewGroup } from './styles.css.js';
8
8
  import { ViewButton } from './ViewsColum.js';
9
9
  import IconX from '../../node_modules/.pnpm/@tabler_icons-react@3.36.1_react@19.2.4/node_modules/@tabler/icons-react/dist/esm/icons/IconX.js';
10
10
 
11
11
  function PickView({
12
- searchActorRef,
13
12
  elementFqn
14
13
  }) {
15
- const currentViewId = useCurrentViewId();
14
+ const ctx = useSearchContext();
15
+ const currentViewId = ctx.currentViewId ?? "";
16
16
  const element = useLikeC4Model().element(elementFqn);
17
17
  const scoped = [];
18
18
  const others = [];
@@ -24,7 +24,7 @@ function PickView({
24
24
  }
25
25
  }
26
26
  const closePickView = () => {
27
- searchActorRef.send({ type: "pickview.close" });
27
+ ctx.closePickView();
28
28
  };
29
29
  useWindowEvent(
30
30
  "keydown",
@@ -4,7 +4,7 @@ import { useFocusWithin, useWindowEvent, useMergedRef } from '@mantine/hooks';
4
4
  import { memo, useRef } from 'react';
5
5
  import { keys } from 'remeda';
6
6
  import { useLikeC4Model } from '../../hooks/useLikeC4Model.js';
7
- import { useSearchActor, useSearch } from '../hooks.js';
7
+ import { useSearchContext } from '../SearchContext.js';
8
8
  import { input } from './styles.css.js';
9
9
  import { moveFocusToSearchInput, stopAndPrevent, focusToFirstFoundElement } from './utils.js';
10
10
  import IconSearch from '../../node_modules/.pnpm/@tabler_icons-react@3.36.1_react@19.2.4/node_modules/@tabler/icons-react/dist/esm/icons/IconSearch.js';
@@ -14,11 +14,11 @@ function startingWithKind(search) {
14
14
  }
15
15
  const SEARCH_PREFIXES = ["#", "kind:"];
16
16
  const LikeC4SearchInput = memo(() => {
17
- const searchActorRef = useSearchActor();
17
+ const searchCtx = useSearchContext();
18
18
  const likec4model = useLikeC4Model();
19
19
  const inputRef = useRef(null);
20
20
  const { ref, focused } = useFocusWithin();
21
- const [search, setSearch] = useSearch();
21
+ const { searchValue: search, setSearchValue: setSearch } = searchCtx;
22
22
  const combobox = useCombobox({
23
23
  scrollBehavior: "smooth",
24
24
  loop: false
@@ -129,9 +129,8 @@ const LikeC4SearchInput = memo(() => {
129
129
  {
130
130
  onClick: (e) => {
131
131
  e.stopPropagation();
132
- const openedWithSearch = searchActorRef.getSnapshot().context.openedWithSearch;
133
- if (search === "" || search === openedWithSearch) {
134
- searchActorRef.send({ type: "close" });
132
+ if (search === "" || search === searchCtx.openedWithSearch) {
133
+ searchCtx.close();
135
134
  } else {
136
135
  setSearch("");
137
136
  }