@ai-matrx/associations 0.11.4 → 0.12.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog — @ai-matrx/associations
2
2
 
3
+ ## 0.12.1
4
+
5
+ Re-cut of 0.12.0 — same package source. The 0.12.0 tag was cut from a commit whose workspace
6
+ lockfile did not match `shared/messaging/package.json` (ERR_PNPM_OUTDATED_LOCKFILE), so its
7
+ publish could never install; an immutable tag cannot move, so the bytes ship as 0.12.1.
8
+ No consumer action beyond 0.12.0's.
9
+
10
+ ## 0.12.0
11
+
12
+ * `UniversalAssociationPicker`: an empty search box with no recents (favorites / last-viewed) now
13
+ **browses candidates across the offered tokens** instead of saying "No recent items" — it was a
14
+ screen that lied for an account with hundreds of records it had never opened. Results carry a
15
+ "Recent" or "Browse" label.
16
+ * `UniversalAssociationPicker` takes `emptyQueryMode?: "recents" | "candidates"` (default
17
+ `"recents"`, which falls back as above).
18
+ * `useUniversalEntitySearch` returns `source: "search" | "recents" | "candidates"` (new
19
+ `UniversalSearchSource` type); `isRecents` is unchanged in meaning.
20
+ * A chip whose token has a `pickerOverrides` component (Files → the host's window) no longer
21
+ leaves the picker body blank: it says the window is open (or closed), offers "Bring window to
22
+ front" / "Reopen window", and "Back to search". Closing the override window keeps the chip
23
+ selected instead of silently dropping back to search.
24
+
25
+ **Consumer action:** none required. Hosts whose override window renders attached state should
26
+ read `AssociationPickerProps.attachedIds` (it has always been passed; matrx-frontend's file
27
+ override did not read it until this release's adoption).
28
+
29
+ ## 0.11.5
30
+
31
+ Automatic changed-only republish, cut by the release train while the 0.12.0 source below was
32
+ already on `main` (a checkout sweep committed it before its own version bump) — so 0.11.5 DOES
33
+ carry the 0.12.0 picker changes despite its auto text. Adopt 0.12.0.
34
+
3
35
  ## 0.11.4
4
36
 
5
37
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -1634,6 +1634,12 @@ interface UniversalAssociationPickerProps {
1634
1634
  ownerId?: string | null;
1635
1635
  /** Org stamped onto rows created via the per-token "+ New" footer. */
1636
1636
  orgId?: string | null;
1637
+ /**
1638
+ * What an empty search box shows. `"recents"` (default) shows favorites and
1639
+ * last-viewed records and falls back to browsing candidates when there are
1640
+ * none; `"candidates"` always browses the available records.
1641
+ */
1642
+ emptyQueryMode?: "recents" | "candidates";
1637
1643
  className?: string;
1638
1644
  }
1639
1645
  declare function attachedKey(token: string, id: string): string;
@@ -1634,6 +1634,12 @@ interface UniversalAssociationPickerProps {
1634
1634
  ownerId?: string | null;
1635
1635
  /** Org stamped onto rows created via the per-token "+ New" footer. */
1636
1636
  orgId?: string | null;
1637
+ /**
1638
+ * What an empty search box shows. `"recents"` (default) shows favorites and
1639
+ * last-viewed records and falls back to browsing candidates when there are
1640
+ * none; `"candidates"` always browses the available records.
1641
+ */
1642
+ emptyQueryMode?: "recents" | "candidates";
1637
1643
  className?: string;
1638
1644
  }
1639
1645
  declare function attachedKey(token: string, id: string): string;
@@ -1690,7 +1690,9 @@ function useUniversalEntitySearch(args) {
1690
1690
  const store = useAssociationsStore();
1691
1691
  const [results, setResults] = (0, import_react6.useState)([]);
1692
1692
  const [loading, setLoading] = (0, import_react6.useState)(false);
1693
- const [isRecents, setIsRecents] = (0, import_react6.useState)(true);
1693
+ const [source, setSource] = (0, import_react6.useState)(
1694
+ emptyQueryMode === "candidates" ? "candidates" : "recents"
1695
+ );
1694
1696
  const runRef = (0, import_react6.useRef)(0);
1695
1697
  const tokensKey = (tokens ?? []).join(",");
1696
1698
  (0, import_react6.useEffect)(() => {
@@ -1701,25 +1703,36 @@ function useUniversalEntitySearch(args) {
1701
1703
  if (runRef.current !== run) return;
1702
1704
  setLoading(true);
1703
1705
  const activeTokens = tokensKey ? tokensKey.split(",") : store.registry.listableTokens();
1706
+ const browse = () => store.candidates.searchCandidatesAcrossTokens({
1707
+ tokens: activeTokens,
1708
+ search: trimmed,
1709
+ ownerId: ownerId ?? null,
1710
+ perTokenLimit
1711
+ });
1704
1712
  let next = [];
1705
- if (trimmed || emptyQueryMode === "candidates") {
1706
- next = await store.candidates.searchCandidatesAcrossTokens({
1707
- tokens: activeTokens,
1708
- search: trimmed,
1709
- ownerId: ownerId ?? null,
1710
- perTokenLimit
1711
- });
1713
+ let nextSource;
1714
+ if (trimmed) {
1715
+ next = await browse();
1716
+ nextSource = "search";
1717
+ } else if (emptyQueryMode === "candidates") {
1718
+ next = await browse();
1719
+ nextSource = "candidates";
1712
1720
  } else {
1713
1721
  next = await loadRecents(store, activeTokens);
1722
+ nextSource = "recents";
1723
+ if (next.length === 0 && runRef.current === run) {
1724
+ next = await browse();
1725
+ nextSource = "candidates";
1726
+ }
1714
1727
  }
1715
1728
  if (runRef.current !== run) return;
1716
1729
  setResults(next);
1717
- setIsRecents(!trimmed && emptyQueryMode === "recents");
1730
+ setSource(nextSource);
1718
1731
  setLoading(false);
1719
1732
  }, DEBOUNCE_MS);
1720
1733
  return () => clearTimeout(timer);
1721
1734
  }, [query, tokensKey, ownerId, enabled, perTokenLimit, emptyQueryMode, store]);
1722
- return { results, loading, isRecents };
1735
+ return { results, loading, isRecents: source === "recents", source };
1723
1736
  }
1724
1737
  async function loadRecents(store, tokens) {
1725
1738
  const res = await store.favorites.list();
@@ -2782,7 +2795,15 @@ function attachedKey(token, id) {
2782
2795
  return `${token}:${id}`;
2783
2796
  }
2784
2797
  function UniversalAssociationPicker(props) {
2785
- const { attachedKeys, onAttach, onDetach, ownerId, orgId, className } = props;
2798
+ const {
2799
+ attachedKeys,
2800
+ onAttach,
2801
+ onDetach,
2802
+ ownerId,
2803
+ orgId,
2804
+ emptyQueryMode = "recents",
2805
+ className
2806
+ } = props;
2786
2807
  const store = useAssociationsStore();
2787
2808
  const { pickerOverrides } = useAssociationsUiPorts();
2788
2809
  const notifier = useNotifier();
@@ -2790,12 +2811,19 @@ function UniversalAssociationPicker(props) {
2790
2811
  const [query, setQuery] = (0, import_react14.useState)("");
2791
2812
  const [browseToken, setBrowseToken] = (0, import_react14.useState)(null);
2792
2813
  const [busyKey, setBusyKey] = (0, import_react14.useState)(null);
2793
- const { results, loading, isRecents } = useUniversalEntitySearch({
2814
+ const [overrideOpen, setOverrideOpen] = (0, import_react14.useState)(false);
2815
+ const [overrideMount, setOverrideMount] = (0, import_react14.useState)(0);
2816
+ const { results, loading, source } = useUniversalEntitySearch({
2794
2817
  query,
2795
2818
  tokens,
2796
2819
  ownerId: ownerId ?? null,
2797
- enabled: browseToken === null
2820
+ enabled: browseToken === null,
2821
+ emptyQueryMode
2798
2822
  });
2823
+ const selectToken = (next) => {
2824
+ setBrowseToken(next);
2825
+ setOverrideOpen(next !== null && Boolean(pickerOverrides?.[next]));
2826
+ };
2799
2827
  const toggle = async (c) => {
2800
2828
  const key = attachedKey(c.token, c.id);
2801
2829
  if (busyKey) return;
@@ -2828,7 +2856,7 @@ function UniversalAssociationPicker(props) {
2828
2856
  value: query,
2829
2857
  onChange: (e) => {
2830
2858
  setQuery(e.target.value);
2831
- if (browseToken) setBrowseToken(null);
2859
+ if (browseToken) selectToken(null);
2832
2860
  },
2833
2861
  placeholder: "Search everything\u2026",
2834
2862
  className: "pl-8 text-base",
@@ -2851,7 +2879,8 @@ function UniversalAssociationPicker(props) {
2851
2879
  "button",
2852
2880
  {
2853
2881
  type: "button",
2854
- onClick: () => setBrowseToken(active ? null : t),
2882
+ "aria-pressed": active,
2883
+ onClick: () => selectToken(active ? null : t),
2855
2884
  className: (0, import_design_system4.cn)(
2856
2885
  "inline-flex shrink-0 items-center gap-1 rounded-md border px-1.5 py-0.5 text-[11px] transition-colors",
2857
2886
  active ? "border-primary/50 bg-primary/10 text-foreground" : "border-border text-muted-foreground hover:text-foreground hover:bg-accent"
@@ -2869,20 +2898,62 @@ function UniversalAssociationPicker(props) {
2869
2898
  browseToken && BrowseOverride ? (
2870
2899
  // The token's canonical host picker (e.g. the stored-files browser)
2871
2900
  // owns per-token browsing when registered — never a plain list twin.
2872
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2873
- BrowseOverride,
2901
+ // It opens in its own window; this body says where it went.
2902
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2903
+ "div",
2874
2904
  {
2875
- open: true,
2876
- onOpenChange: (open) => {
2877
- if (!open) setBrowseToken(null);
2878
- },
2879
- token: browseToken,
2880
- orgId: orgId ?? null,
2881
- attachedIds: new Set(
2882
- [...attachedKeys].filter((k) => k.startsWith(`${browseToken}:`)).map((k) => k.slice(browseToken.length + 1))
2883
- ),
2884
- onAttach: (id, title) => onAttach(browseToken, id, title),
2885
- onDetach: (id) => onDetach(browseToken, id)
2905
+ role: "region",
2906
+ "aria-label": `${store.registry.getEntityInfo(browseToken).labelPlural} window`,
2907
+ className: "flex flex-1 flex-col items-center justify-center gap-3 px-4 py-8 text-center",
2908
+ children: [
2909
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-sm text-foreground", children: overrideOpen ? `The ${store.registry.getEntityInfo(browseToken).labelPlural.toLowerCase()} window is open.` : `The ${store.registry.getEntityInfo(browseToken).labelPlural.toLowerCase()} window is closed.` }),
2910
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "max-w-xs text-[12px] text-muted-foreground", children: overrideOpen ? "Check items there to attach them; uncheck to detach. Changes save as you go." : "Reopen it to pick more, or go back to search everything." }),
2911
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
2912
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2913
+ import_design_system4.Button,
2914
+ {
2915
+ type: "button",
2916
+ size: "sm",
2917
+ variant: overrideOpen ? "outline" : "default",
2918
+ onClick: () => {
2919
+ setOverrideOpen(true);
2920
+ setOverrideMount((n) => n + 1);
2921
+ },
2922
+ children: [
2923
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MaximizeIcon, { className: "mr-1.5 h-3.5 w-3.5" }),
2924
+ overrideOpen ? "Bring window to front" : "Reopen window"
2925
+ ]
2926
+ }
2927
+ ),
2928
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2929
+ import_design_system4.Button,
2930
+ {
2931
+ type: "button",
2932
+ size: "sm",
2933
+ variant: "ghost",
2934
+ onClick: () => selectToken(null),
2935
+ children: "Back to search"
2936
+ }
2937
+ )
2938
+ ] }),
2939
+ overrideOpen && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2940
+ BrowseOverride,
2941
+ {
2942
+ open: true,
2943
+ onOpenChange: (open) => {
2944
+ if (!open) setOverrideOpen(false);
2945
+ },
2946
+ token: browseToken,
2947
+ orgId: orgId ?? null,
2948
+ attachedIds: new Set(
2949
+ [...attachedKeys].filter((k) => k.startsWith(`${browseToken}:`)).map((k) => k.slice(browseToken.length + 1))
2950
+ ),
2951
+ onAttach: (id, title) => onAttach(browseToken, id, title),
2952
+ onDetach: (id) => onDetach(browseToken, id)
2953
+ },
2954
+ overrideMount
2955
+ )
2956
+ ]
2886
2957
  }
2887
2958
  )
2888
2959
  ) : browseToken ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex min-h-0 flex-1 flex-col", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
@@ -2891,7 +2962,7 @@ function UniversalAssociationPicker(props) {
2891
2962
  token: browseToken,
2892
2963
  enabled: true,
2893
2964
  orgId,
2894
- onClose: () => setBrowseToken(null),
2965
+ onClose: () => selectToken(null),
2895
2966
  attachedIds: new Set(
2896
2967
  [...attachedKeys].filter((k) => k.startsWith(`${browseToken}:`)).map((k) => k.slice(browseToken.length + 1))
2897
2968
  ),
@@ -2904,8 +2975,8 @@ function UniversalAssociationPicker(props) {
2904
2975
  role: "region",
2905
2976
  "aria-label": "Association search results",
2906
2977
  className: "-mx-1 min-h-24 flex-1 overflow-y-auto overscroll-contain px-1",
2907
- children: results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "py-6 text-center text-[12px] text-muted-foreground", children: loading ? "Searching\u2026" : query.trim() ? "No matches \u2014 narrow with a type chip to browse." : "No recent items. Type to search, or pick a type to browse." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "space-y-2", children: [
2908
- isRecents && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "px-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60", children: "Recent" }),
2978
+ children: results.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "py-6 text-center text-[12px] text-muted-foreground", children: loading ? "Searching\u2026" : query.trim() ? "No matches \u2014 narrow with a type chip to browse." : "Nothing to attach yet. Type to search, or pick a type to browse." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "space-y-2", children: [
2979
+ source !== "search" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "px-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60", children: source === "recents" ? "Recent" : "Browse" }),
2909
2980
  [...groups.entries()].map(([token, items]) => {
2910
2981
  const info = store.registry.getEntityInfo(token);
2911
2982
  const GroupIcon = info.Icon ?? DefaultEntityIcon;