@ai-matrx/vault 0.1.23 → 0.1.24

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/dist/index.d.cts CHANGED
@@ -114,7 +114,7 @@ type VaultSort = "name" | "name-desc" | "website" | "updated" | "created" | "vie
114
114
  type VaultSelection = "all" | "mine" | "shared" | (string & {});
115
115
  interface VaultLocation {
116
116
  vault: VaultSelection;
117
- /** `all`, `favorites`, `authenticator`, `trash`, `category:<id>`, `type:<definitionKey>` or `tag:<tag>`. */
117
+ /** `all`, `favorites`, `authenticator`, `trash`, `category:<id>`, `family:<family>`, `type:<definitionKey>` or `tag:<tag>`. */
118
118
  view: string;
119
119
  q: string;
120
120
  sort: VaultSort;
@@ -134,7 +134,14 @@ declare const DEFAULT_LOCATION: VaultLocation;
134
134
  declare function locationFromParams(params: URLSearchParams, itemId?: string | null): VaultLocation;
135
135
  /** Writes everything that is not a default (and not the item id, which the host places). */
136
136
  declare function locationToParams(loc: VaultLocation): URLSearchParams;
137
- declare function matchesQuery(item: VaultItem, query: string): boolean;
137
+ /**
138
+ * The one search every list shares. It reads what an item SAYS about itself, never a secret value:
139
+ * name, description, type (label and key), the type's family ("AI & model providers"), tags,
140
+ * websites, every field's label, key and env key, and the plaintext custom fields a person wrote
141
+ * (label and value — the server keeps those unencrypted by design). `familyLabel` comes from the
142
+ * catalog the caller holds.
143
+ */
144
+ declare function matchesQuery(item: VaultItem, query: string, familyLabel?: string): boolean;
138
145
  /**
139
146
  * The authenticator's own storage on an item (the seed, digits, period, issuer…): shown only as the
140
147
  * one "One-time password" row, never as loose fields — and always kept when the item is saved.
@@ -423,6 +430,7 @@ interface AuthenticatorEntry {
423
430
  * `mfa_method` so agents know to hand control back at the verification step.
424
431
  */
425
432
  type MfaMethod = "sms" | "push_or_other";
433
+
426
434
  type ItemRef = Pick<VaultItem, "id" | "owner">;
427
435
  /**
428
436
  * One-time passwords: every code, enrol by key / otpauth address / QR image, enable, disable,
package/dist/index.d.ts CHANGED
@@ -114,7 +114,7 @@ type VaultSort = "name" | "name-desc" | "website" | "updated" | "created" | "vie
114
114
  type VaultSelection = "all" | "mine" | "shared" | (string & {});
115
115
  interface VaultLocation {
116
116
  vault: VaultSelection;
117
- /** `all`, `favorites`, `authenticator`, `trash`, `category:<id>`, `type:<definitionKey>` or `tag:<tag>`. */
117
+ /** `all`, `favorites`, `authenticator`, `trash`, `category:<id>`, `family:<family>`, `type:<definitionKey>` or `tag:<tag>`. */
118
118
  view: string;
119
119
  q: string;
120
120
  sort: VaultSort;
@@ -134,7 +134,14 @@ declare const DEFAULT_LOCATION: VaultLocation;
134
134
  declare function locationFromParams(params: URLSearchParams, itemId?: string | null): VaultLocation;
135
135
  /** Writes everything that is not a default (and not the item id, which the host places). */
136
136
  declare function locationToParams(loc: VaultLocation): URLSearchParams;
137
- declare function matchesQuery(item: VaultItem, query: string): boolean;
137
+ /**
138
+ * The one search every list shares. It reads what an item SAYS about itself, never a secret value:
139
+ * name, description, type (label and key), the type's family ("AI & model providers"), tags,
140
+ * websites, every field's label, key and env key, and the plaintext custom fields a person wrote
141
+ * (label and value — the server keeps those unencrypted by design). `familyLabel` comes from the
142
+ * catalog the caller holds.
143
+ */
144
+ declare function matchesQuery(item: VaultItem, query: string, familyLabel?: string): boolean;
138
145
  /**
139
146
  * The authenticator's own storage on an item (the seed, digits, period, issuer…): shown only as the
140
147
  * one "One-time password" row, never as loose fields — and always kept when the item is saved.
@@ -423,6 +430,7 @@ interface AuthenticatorEntry {
423
430
  * `mfa_method` so agents know to hand control back at the verification step.
424
431
  */
425
432
  type MfaMethod = "sms" | "push_or_other";
433
+
426
434
  type ItemRef = Pick<VaultItem, "id" | "owner">;
427
435
  /**
428
436
  * One-time passwords: every code, enrol by key / otpauth address / QR image, enable, disable,
package/dist/index.js CHANGED
@@ -419,6 +419,7 @@ function needsRecentSignIn(e) {
419
419
  // src/auth/IdentityGate.tsx
420
420
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
421
421
  var CANCELLED = "Not confirmed.";
422
+ var NOT_CONFIRMED_NOTICE = "Not confirmed, so nothing was done";
422
423
  var Ctx2 = createContext2((run) => run());
423
424
  function useConfirmed() {
424
425
  return useContext2(Ctx2);
@@ -427,6 +428,7 @@ var PROVIDER_LABEL = { google: "Google", github: "GitHub", azure: "Microsoft", a
427
428
  var providerLabel = (p) => PROVIDER_LABEL[p] ?? p.charAt(0).toUpperCase() + p.slice(1);
428
429
  function IdentityGate({ identity, children }) {
429
430
  const [asking, setAsking] = useState2(false);
431
+ const notify = useNotify();
430
432
  const waiters = useRef2([]);
431
433
  const [returnProblem, setReturnProblem] = useState2(null);
432
434
  useEffect2(() => {
@@ -457,6 +459,7 @@ function IdentityGate({ identity, children }) {
457
459
  waiters.current = [];
458
460
  setAsking(false);
459
461
  for (const x of w) ok ? x.resolve() : x.reject(new Error(CANCELLED));
462
+ if (!ok && w.length > 0) notify(NOT_CONFIRMED_NOTICE, "info");
460
463
  };
461
464
  return /* @__PURE__ */ jsxs3(Ctx2.Provider, { value: confirmed, children: [
462
465
  children,
@@ -569,22 +572,35 @@ function useTransientSecret(given) {
569
572
  const fromHost = useContext3(RevealMs);
570
573
  const ms = given ?? fromHost;
571
574
  const [value, setValue] = useState3(null);
575
+ const [until, setUntil] = useState3(null);
572
576
  const timer2 = useRef3(null);
573
577
  const clear = useCallback2(() => {
574
578
  if (timer2.current) clearTimeout(timer2.current);
575
579
  timer2.current = null;
576
580
  setValue(null);
581
+ setUntil(null);
577
582
  }, []);
578
583
  const show = useCallback2(
579
584
  (next) => {
580
585
  if (timer2.current) clearTimeout(timer2.current);
581
586
  setValue(next);
587
+ setUntil(Date.now() + ms);
582
588
  timer2.current = setTimeout(clear, ms);
583
589
  },
584
590
  [clear, ms]
585
591
  );
586
592
  useEffect3(() => clear, [clear]);
587
- return { value, show, clear };
593
+ return { value, show, clear, until };
594
+ }
595
+ function useSecondsLeft(until) {
596
+ const [now, setNow] = useState3(() => Date.now());
597
+ useEffect3(() => {
598
+ if (until === null) return;
599
+ setNow(Date.now());
600
+ const t = setInterval(() => setNow(Date.now()), 1e3);
601
+ return () => clearInterval(t);
602
+ }, [until]);
603
+ return until === null ? null : Math.max(0, Math.ceil((until - now) / 1e3));
588
604
  }
589
605
  function useElementWidth(ref) {
590
606
  const [width, setWidth] = useState3(0);
@@ -677,6 +693,15 @@ function SecretText({ value, multiline }) {
677
693
  return /* @__PURE__ */ jsx5("span", { className: "break-all font-mono text-sm tracking-wide", children: [...value].map((ch, i) => /* @__PURE__ */ jsx5("span", { className: /[0-9]/.test(ch) ? "text-primary" : /[^A-Za-z0-9]/.test(ch) ? "text-warning" : void 0, children: ch }, i)) });
678
694
  }
679
695
  var quietError = (e, fallback) => wasCancelled(e) ? null : e instanceof Error ? e.message : fallback;
696
+ function HidesIn({ until }) {
697
+ const left = useSecondsLeft(until);
698
+ if (left === null) return null;
699
+ return /* @__PURE__ */ jsxs5("span", { className: "shrink-0 px-1 text-xs tabular-nums text-muted-foreground", "data-hides-in": "", title: "Shown values hide on their own", children: [
700
+ "Hides in ",
701
+ left,
702
+ "s"
703
+ ] });
704
+ }
680
705
  function SecretRow({ port, item, field, extra, onTurnOn }) {
681
706
  const confirmed = useConfirmed();
682
707
  const copyValue2 = useCopy();
@@ -732,6 +757,7 @@ function SecretRow({ port, item, field, extra, onTurnOn }) {
732
757
  actions: /* @__PURE__ */ jsxs5(Fragment2, { children: [
733
758
  extra,
734
759
  off && onTurnOn ? /* @__PURE__ */ jsx5(TurnOnButton, { label: field.label, onTurnOn }) : null,
760
+ !visible && secret.value !== null ? /* @__PURE__ */ jsx5(HidesIn, { until: secret.until }) : null,
735
761
  canSee && !visible ? /* @__PURE__ */ jsx5(Button2, { variant: "quiet", icon: secret.value !== null ? /* @__PURE__ */ jsx5(EyeOffGlyph, {}) : /* @__PURE__ */ jsx5(EyeGlyph, {}), "aria-label": secret.value !== null ? `Hide ${field.label}` : `Show ${field.label}`, disabled: busy, onClick: () => void toggle2() }) : null,
736
762
  canSee ? /* @__PURE__ */ jsx5(Button2, { variant: "quiet", icon: /* @__PURE__ */ jsx5(CopyIcon, {}), "aria-label": `Copy ${field.label}`, disabled: busy, onClick: () => void copy() }) : null
737
763
  ] }),
@@ -906,15 +932,28 @@ function inView(item, view) {
906
932
  return true;
907
933
  }
908
934
  var AUTHENTICATOR_WORDS = ["authenticator", "2fa", "two-factor", "two factor", "totp", "one-time password", "verification code", "2-step"];
909
- function matchesQuery(item, query) {
935
+ function matchesQuery(item, query, familyLabel2) {
910
936
  const q = query.trim().toLowerCase();
911
937
  if (!q) return true;
912
938
  if (item.authenticator && AUTHENTICATOR_WORDS.some((w) => w === q || q.length >= 3 && w.startsWith(q))) return true;
913
- return [item.name, item.definitionLabel, ...item.tags, ...item.loginUrls, ...item.custom.map((c) => `${c.label} ${c.value}`)].some((h) => h.toLowerCase().includes(q));
939
+ return [
940
+ item.name,
941
+ item.description,
942
+ item.definitionLabel,
943
+ item.definitionKey,
944
+ familyLabel2 ?? "",
945
+ ...item.tags,
946
+ ...item.loginUrls,
947
+ ...item.fields.filter((f) => !isAuthenticatorStorage(f.key)).flatMap((f) => [f.label, f.key, f.envKey ?? ""]),
948
+ ...humanCustomFields(item).map((c) => `${c.label} ${c.value}`)
949
+ ].some((h) => h !== "" && h.toLowerCase().includes(q));
914
950
  }
915
951
  var HIDDEN_ITEM_TAG = "matrx:hidden";
916
952
  var isHiddenItem = (item) => (item.tags ?? []).includes(HIDDEN_ITEM_TAG);
917
953
  var isAuthenticatorStorage = (key) => key === "totp_seed" || key.startsWith("totp_") || key === "authenticator_last_time_step";
954
+ var MFA_METHOD_KEY = "mfa_method";
955
+ var isInternalCustomKey = (key) => isAuthenticatorStorage(key) || key === MFA_METHOD_KEY;
956
+ var humanCustomFields = (item) => item.custom.filter((c) => !isInternalCustomKey(c.key));
918
957
  function loginSiteOf(item) {
919
958
  const raw = item.loginUrls[0];
920
959
  if (!raw) return "";
@@ -1532,6 +1571,7 @@ function HistoryRow({ item, port, page, entry, label, onRestored, onConflict, on
1532
1571
  {
1533
1572
  label: when,
1534
1573
  actions: entry.available ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
1574
+ secret.value !== null ? /* @__PURE__ */ jsx7(HidesIn, { until: secret.until }) : null,
1535
1575
  /* @__PURE__ */ jsx7(Button4, { variant: "quiet", icon: secret.value !== null ? /* @__PURE__ */ jsx7(EyeOffGlyph, {}) : /* @__PURE__ */ jsx7(EyeGlyph, {}), "aria-label": secret.value !== null ? "Hide" : "Show", disabled: busy, onClick: () => void toggle2() }),
1536
1576
  /* @__PURE__ */ jsx7(Button4, { variant: "quiet", icon: /* @__PURE__ */ jsx7(CopyIcon2, {}), "aria-label": "Copy", disabled: busy, onClick: () => void copy() }),
1537
1577
  canRestore(page, entry) ? /* @__PURE__ */ jsx7(Button4, { variant: "quiet", icon: /* @__PURE__ */ jsx7(RestoreGlyph, {}), "aria-label": "Restore", disabled: busy, onClick: () => void restore() }) : null
@@ -2067,7 +2107,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSepara
2067
2107
  import { confirm as confirm2 } from "@ai-matrx/kit/confirm-opener";
2068
2108
 
2069
2109
  // src/authenticator/port.ts
2070
- var MFA_METHOD_KEY = "mfa_method";
2071
2110
  var OTP_SEED_KEY = "totp_seed";
2072
2111
  function mfaMethodOf(item) {
2073
2112
  const v = item.custom.find((c) => c.key === MFA_METHOD_KEY)?.value;
@@ -5194,6 +5233,7 @@ function RecoveryCodesRow({ item, field, port, onChanged }) {
5194
5233
  label: "Recovery codes",
5195
5234
  top: codes !== null,
5196
5235
  actions: /* @__PURE__ */ jsxs20(Fragment14, { children: [
5236
+ codes ? /* @__PURE__ */ jsx21(HidesIn, { until: secret.until }) : null,
5197
5237
  canSee ? /* @__PURE__ */ jsx21(Button17, { variant: "quiet", icon: codes ? /* @__PURE__ */ jsx21(EyeOffGlyph, {}) : /* @__PURE__ */ jsx21(EyeGlyph, {}), "aria-label": codes ? "Hide codes" : "Show codes", disabled: busy, onClick: () => void toggle2() }) : null,
5198
5238
  canEdit && !sealed ? /* @__PURE__ */ jsx21(Button17, { variant: "quiet", icon: /* @__PURE__ */ jsx21(PencilGlyph, {}), "aria-label": "Replace all codes", disabled: busy, onClick: () => setDraft("") }) : null
5199
5239
  ] }),
@@ -6129,8 +6169,8 @@ function ItemPane(props) {
6129
6169
  const patchFieldSettings = (key, p) => setDraft((d) => ({ ...d, fieldSettings: { ...d.fieldSettings, [key]: { ...d.fieldSettings?.[key], ...p } } }));
6130
6170
  const extraFields = (item?.fields ?? []).filter((f) => !declaredKeys.has(f.key) && !draft.removed.includes(f.id) && !isAuthenticatorStorage(f.key));
6131
6171
  const metaValue = (key) => draft.values[key] ?? item?.custom.find((c) => c.key === key)?.value ?? "";
6132
- const extraCustom = draft.custom.filter((c) => !declaredKeys.has(c.key) && c.key !== MFA_METHOD_KEY && !isAuthenticatorStorage(c.key));
6133
- const viewCustom = (item?.custom ?? []).filter((c) => !declaredKeys.has(c.key) && c.key !== MFA_METHOD_KEY && !isAuthenticatorStorage(c.key));
6172
+ const extraCustom = draft.custom.filter((c) => !declaredKeys.has(c.key) && !isInternalCustomKey(c.key));
6173
+ const viewCustom = (item?.custom ?? []).filter((c) => !declaredKeys.has(c.key) && !isInternalCustomKey(c.key));
6134
6174
  const isLogin = (item?.definitionKey ?? definition?.key) === "website_login" || (item?.definitionKey ?? definition?.key) === "username_password";
6135
6175
  const typeLabel = definition?.label ?? item?.definitionLabel ?? "";
6136
6176
  const recoveryDeclared = declaredKeys.has(RECOVERY_CODES_KEY);
@@ -6580,7 +6620,7 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6580
6620
  const copyCode = () => code && void run("One-time code", async () => (await port.authenticator.code(item)).code, true);
6581
6621
  const copyName = () => void copy(item.name, "Name", { secret: false });
6582
6622
  const others = otherCopyFields(item, user, secret);
6583
- const custom = canUse ? item.custom.filter((c) => c.value) : [];
6623
+ const custom = canUse ? humanCustomFields(item).filter((c) => c.value) : [];
6584
6624
  const hostEntries = extra?.(item) ?? [];
6585
6625
  const copyEntries = [
6586
6626
  ...user ? [{ id: "copy:username", kind: "copy", label: `Copy ${user.label.toLowerCase()}`, onSelect: copyUser }] : [],
@@ -8180,20 +8220,33 @@ function VaultSelect({ value, choices, onChange }) {
8180
8220
  }
8181
8221
  var CATEGORY_GLYPHS = { logins: GlobeIcon5, passkeys: ShieldGlyph, api: KeyGlyph, oauth: PlugGlyph, infra: DatabaseGlyph, env: TerminalGlyph, files: FileGlyph, other: NoteGlyph };
8182
8222
  var familyOf = (key, definitions) => definitions.find((d) => d.key === key)?.family ?? "generic";
8223
+ function itemFamily(item, definitions) {
8224
+ return familyOf(item.definitionKey, definitions);
8225
+ }
8226
+ var familyLabel = (family) => FAMILY_LABELS[family] ?? family.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
8183
8227
  function itemCategory(item, definitions) {
8184
8228
  return categoryOf(item.definitionKey, familyOf(item.definitionKey, definitions));
8185
8229
  }
8186
8230
  function buildViews(items, definitions, favoriteIds2, recentIds2) {
8187
8231
  const byCat = /* @__PURE__ */ new Map();
8232
+ const byFamily = /* @__PURE__ */ new Map();
8188
8233
  const byTag = /* @__PURE__ */ new Map();
8189
8234
  for (const i of items) {
8190
8235
  const c = itemCategory(i, definitions);
8191
8236
  byCat.set(c, (byCat.get(c) ?? 0) + 1);
8237
+ const fam = itemFamily(i, definitions);
8238
+ const fams = byFamily.get(c) ?? /* @__PURE__ */ new Map();
8239
+ fams.set(fam, (fams.get(fam) ?? 0) + 1);
8240
+ byFamily.set(c, fams);
8192
8241
  for (const t of i.tags) byTag.set(t, (byTag.get(t) ?? 0) + 1);
8193
8242
  }
8194
8243
  const ids = new Set(items.map((i) => i.id));
8195
8244
  return {
8196
- categories: CATEGORIES.filter((c) => byCat.has(c.id)).map((c) => ({ view: `category:${c.id}`, label: c.label, count: byCat.get(c.id) ?? 0, glyph: CATEGORY_GLYPHS[c.id] })),
8245
+ categories: CATEGORIES.filter((c) => byCat.has(c.id)).map((c) => {
8246
+ const fams = [...(byFamily.get(c.id) ?? /* @__PURE__ */ new Map()).entries()];
8247
+ const families = fams.length > 1 ? fams.map(([f, count2]) => ({ view: `family:${f}`, label: familyLabel(f), count: count2, glyph: CATEGORY_GLYPHS[c.id] })).sort((a, b) => a.label.localeCompare(b.label)) : void 0;
8248
+ return { view: `category:${c.id}`, label: c.label, count: byCat.get(c.id) ?? 0, glyph: CATEGORY_GLYPHS[c.id], ...families ? { families } : {} };
8249
+ }),
8197
8250
  tags: [...byTag.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, count2]) => ({ view: `tag:${t}`, label: t, count: count2, glyph: TagGlyph })),
8198
8251
  favorites: favoriteIds2 ? [...favoriteIds2].filter((id) => ids.has(id)).length : void 0,
8199
8252
  authenticator: items.filter((i) => i.authenticator).length,
@@ -8206,21 +8259,27 @@ function viewLabel(view, views) {
8206
8259
  if (view === "authenticator") return AUTHENTICATOR_LABEL;
8207
8260
  if (view === "trash") return "Trash";
8208
8261
  if (view === "page") return "This page";
8262
+ if (view.startsWith("family:")) return familyLabel(view.slice(7));
8209
8263
  return [...views.categories, ...views.tags].find((v) => v.view === view)?.label ?? "All items";
8210
8264
  }
8211
- function viewOptions(views, allCount, deletedCount, hasFavorites, hasRecents, hasPage = false) {
8265
+ function openCategory(view, views) {
8266
+ return views.categories.find((c) => c.view === view || c.families?.some((f) => f.view === view));
8267
+ }
8268
+ function viewOptions(views, allCount, deletedCount, hasFavorites, hasRecents, hasPage = false, current = "all") {
8269
+ const open = openCategory(current, views);
8212
8270
  return [
8213
8271
  ...hasPage ? [{ value: "page", label: "This page" }] : [],
8214
8272
  { value: "all", label: "Every category", meta: String(allCount) },
8215
8273
  ...hasFavorites ? [{ value: "favorites", label: "Favorites", meta: views.favorites === void 0 ? "" : String(views.favorites) }] : [],
8216
8274
  ...hasRecents ? [{ value: "recent", label: "Recently opened", meta: views.recent === void 0 ? "" : String(views.recent) }] : [],
8217
8275
  { value: "authenticator", label: AUTHENTICATOR_LABEL, meta: String(views.authenticator) },
8218
- ...views.categories.map((v) => ({ value: v.view, label: v.label, meta: String(v.count) })),
8276
+ // The open category's families follow it, indented by their sign; other categories stay one line.
8277
+ ...views.categories.flatMap((v) => [{ value: v.view, label: v.label, meta: String(v.count) }, ...v === open ? (v.families ?? []).map((f) => ({ value: f.view, label: `\xB7 ${f.label}`, meta: String(f.count) })) : []]),
8219
8278
  ...views.tags.map((v) => ({ value: v.view, label: `#${v.label}`, meta: String(v.count) })),
8220
8279
  { value: "trash", label: "Trash", meta: String(deletedCount) }
8221
8280
  ];
8222
8281
  }
8223
- function Entry({ on, glyph: Icon, label, count: count2, onClick }) {
8282
+ function Entry({ on, glyph: Icon, label, count: count2, onClick, nested = false }) {
8224
8283
  const desktop = useLook2() === "desktop";
8225
8284
  return /* @__PURE__ */ jsxs37(
8226
8285
  "button",
@@ -8228,9 +8287,10 @@ function Entry({ on, glyph: Icon, label, count: count2, onClick }) {
8228
8287
  type: "button",
8229
8288
  "aria-current": on ? "page" : void 0,
8230
8289
  onClick,
8231
- className: cn15("flex h-8 w-full items-center gap-2.5 rounded-lg px-2.5 text-left text-sm", on ? desktop ? "bg-sidebar-accent text-primary" : "bg-sidebar-accent font-medium text-sidebar-accent-foreground" : "text-sidebar-foreground hover:bg-sidebar-accent"),
8290
+ "data-nested": nested ? "" : void 0,
8291
+ className: cn15("flex h-8 w-full items-center gap-2.5 rounded-lg px-2.5 text-left text-sm", nested && "pl-9", on ? desktop ? "bg-sidebar-accent text-primary" : "bg-sidebar-accent font-medium text-sidebar-accent-foreground" : "text-sidebar-foreground hover:bg-sidebar-accent"),
8232
8292
  children: [
8233
- /* @__PURE__ */ jsx39(Icon, { className: "size-4 shrink-0" }),
8293
+ nested ? null : /* @__PURE__ */ jsx39(Icon, { className: "size-4 shrink-0" }),
8234
8294
  /* @__PURE__ */ jsx39("span", { className: "min-w-0 flex-1 truncate", children: label }),
8235
8295
  count2 !== void 0 ? /* @__PURE__ */ jsx39("span", { className: "text-xs tabular-nums text-muted-foreground", children: count2 }) : null
8236
8296
  ]
@@ -8258,7 +8318,10 @@ function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, v
8258
8318
  hasRecents ? /* @__PURE__ */ jsx39(Entry, { on: view === "recent", glyph: ClockGlyph, label: "Recently opened", count: views.recent, onClick: () => onView("recent") }) : null,
8259
8319
  /* @__PURE__ */ jsx39(Entry, { on: view === "authenticator", glyph: LockGlyph, label: AUTHENTICATOR_LABEL, count: views.authenticator, onClick: () => onView("authenticator") })
8260
8320
  ] }),
8261
- views.categories.length > 0 ? /* @__PURE__ */ jsx39(Group, { title: "Categories", children: views.categories.map((e) => /* @__PURE__ */ jsx39(Entry, { on: view === e.view, glyph: e.glyph, label: e.label, count: e.count, onClick: () => onView(e.view) }, e.view)) }) : null,
8321
+ views.categories.length > 0 ? /* @__PURE__ */ jsx39(Group, { title: "Categories", children: views.categories.map((e) => /* @__PURE__ */ jsxs37("div", { className: "flex flex-col gap-0.5", children: [
8322
+ /* @__PURE__ */ jsx39(Entry, { on: view === e.view, glyph: e.glyph, label: e.label, count: e.count, onClick: () => onView(e.view) }),
8323
+ e === openCategory(view, views) ? e.families?.map((f) => /* @__PURE__ */ jsx39(Entry, { nested: true, on: view === f.view, glyph: f.glyph, label: f.label, count: f.count, onClick: () => onView(f.view) }, f.view)) : null
8324
+ ] }, e.view)) }) : null,
8262
8325
  views.tags.length > 0 ? /* @__PURE__ */ jsxs37("div", { className: "flex flex-col gap-0.5", children: [
8263
8326
  /* @__PURE__ */ jsxs37("button", { type: "button", "aria-expanded": tagsOpen, onClick: () => setTagsOpen((o) => !o), className: "flex h-7 items-center gap-1 rounded-md px-2.5 text-left text-xs font-medium text-muted-foreground hover:text-foreground", children: [
8264
8327
  tagsOpen ? /* @__PURE__ */ jsx39(ChevronDownGlyph, { className: "size-3.5" }) : /* @__PURE__ */ jsx39(ChevronRightGlyph, { className: "size-3.5" }),
@@ -8523,10 +8586,11 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8523
8586
  const views = useMemo6(() => buildViews(inThisVault, definitions, favorites, recents), [inThisVault, definitions, favorites, recents]);
8524
8587
  const visible = useMemo6(() => {
8525
8588
  const filtered = inThisVault.filter((i) => {
8526
- if (!matchesQuery(i, loc.q)) return false;
8589
+ if (!matchesQuery(i, loc.q, familyLabel(itemFamily(i, definitions)))) return false;
8527
8590
  if (loc.view === "favorites") return favorites?.has(i.id) ?? false;
8528
8591
  if (loc.view === "recent") return recents?.includes(i.id) ?? false;
8529
8592
  if (loc.view.startsWith("category:")) return itemCategory(i, definitions) === loc.view.slice(9);
8593
+ if (loc.view.startsWith("family:")) return itemFamily(i, definitions) === loc.view.slice(7);
8530
8594
  return inView(i, loc.view);
8531
8595
  });
8532
8596
  if (loc.view === "recent" && recents) return [...filtered].sort((a, b) => recents.indexOf(a.id) - recents.indexOf(b.id));
@@ -8580,7 +8644,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8580
8644
  const title = loc.vault === "all" && loc.view === "all" ? "All items" : viewLabel(loc.view, views);
8581
8645
  const compactHeader = mode === "full" ? void 0 : /* @__PURE__ */ jsxs38("div", { className: "grid grid-cols-[repeat(auto-fit,minmax(11rem,1fr))] gap-2", "data-vault-compact-header": "", children: [
8582
8646
  /* @__PURE__ */ jsx40(VaultSelect, { value: loc.vault, choices, onChange: (vault) => go({ vault, itemId: null, panel: null }) }),
8583
- /* @__PURE__ */ jsx40(Select12, { value: loc.view, onValueChange: (view) => go({ view, itemId: null, panel: null }), options: viewOptions(views, inThisVault.length, deleted.length, hasFavorites, port.lists.recents !== void 0, host.page !== void 0), "aria-label": "Show" })
8647
+ /* @__PURE__ */ jsx40(Select12, { value: loc.view, onValueChange: (view) => go({ view, itemId: null, panel: null }), options: viewOptions(views, inThisVault.length, deleted.length, hasFavorites, port.lists.recents !== void 0, host.page !== void 0, loc.view), "aria-label": "Show" })
8584
8648
  ] });
8585
8649
  const compactActions = mode === "full" ? void 0 : /* @__PURE__ */ jsxs38("div", { className: "flex shrink-0 items-center gap-2", "data-vault-compact-actions": "", children: [
8586
8650
  /* @__PURE__ */ jsx40(