@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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.24
4
+
5
+ - **A copy or reveal that needs "Confirm it's you" is never silent.** Closing the prompt says "Not confirmed, so nothing was done" once, from `IdentityGate` (every caller already stays quiet about a cancel via `wasCancelled`), so the row buttons, the row's More menu, a host's right-click menu (`rowMenuEntriesAt`) and the keyboard all end in either the copy or a message. Guard: `src/review-0-1-23.test.tsx` § 1. Consumer action: none (a host that passes `notify` receives the line with kind `info`).
6
+ - **The authenticator's own settings are never copy entries or search hits.** `totp_enabled` / `totp_digits` / `totp_period` / `totp_algorithm` / issuer / label and the `mfa_method` marker are internal (`isInternalCustomKey`, `humanCustomFields`); the row menu no longer offers "Copy authenticator digits…". They are still kept on save. Guard: § 2. Consumer action: none.
7
+ - **Search reads everything an item says about itself, never a secret**: name, description, type label and key, the type's family ("AI & model providers"), tags, websites, every field's label, key and env key, and the person's own plaintext custom fields (`matchesQuery(item, query, familyLabel?)`). Guard: § 3. Consumer action: none (a host calling `matchesQuery` may pass the family label).
8
+ - **The old type families are back, nested under their category.** A category holding more than one family lists them indented beneath it while it is open (sidebar) and after it in the compact Show picker (`family:<family>` view, `ViewEntry.families`, `itemFamily`, `familyLabel`; `viewOptions(..., current)`). Guard: § 4. Consumer action: none.
9
+ - **A shown value says when it hides**: a quiet "Hides in Ns" beside every revealed secret, earlier value and recovery code set (`HidesIn`, `useTransientSecret().until`, `useSecondsLeft`). Guard: § 5. Consumer action: none.
10
+
3
11
  ## 0.1.23
4
12
 
5
13
  - **The Vault and Show pickers read in full at every compact width (640-1119px).** They no longer share a row with the Authenticator and More buttons (which squeezed each to ~90px: "All ite…", long vault names cut): the pickers take the whole row (each at least 11rem; one above the other in the 19rem split column, side by side once there is room), and the Authenticator and More buttons moved onto the search row beside the + (`ItemList` `actions`; Trash, Authenticator and the extension's page view carry them on their own search row). Nothing removed, every button one press away as before. Guard: `src/parity-review-0-1-22.test.tsx` § 2. Consumer action: none.
package/dist/index.cjs CHANGED
@@ -489,6 +489,7 @@ function needsRecentSignIn(e) {
489
489
  // src/auth/IdentityGate.tsx
490
490
  var import_jsx_runtime3 = require("react/jsx-runtime");
491
491
  var CANCELLED = "Not confirmed.";
492
+ var NOT_CONFIRMED_NOTICE = "Not confirmed, so nothing was done";
492
493
  var Ctx2 = (0, import_react2.createContext)((run) => run());
493
494
  function useConfirmed() {
494
495
  return (0, import_react2.useContext)(Ctx2);
@@ -497,6 +498,7 @@ var PROVIDER_LABEL = { google: "Google", github: "GitHub", azure: "Microsoft", a
497
498
  var providerLabel = (p) => PROVIDER_LABEL[p] ?? p.charAt(0).toUpperCase() + p.slice(1);
498
499
  function IdentityGate({ identity, children }) {
499
500
  const [asking, setAsking] = (0, import_react2.useState)(false);
501
+ const notify = useNotify();
500
502
  const waiters = (0, import_react2.useRef)([]);
501
503
  const [returnProblem, setReturnProblem] = (0, import_react2.useState)(null);
502
504
  (0, import_react2.useEffect)(() => {
@@ -527,6 +529,7 @@ function IdentityGate({ identity, children }) {
527
529
  waiters.current = [];
528
530
  setAsking(false);
529
531
  for (const x of w) ok ? x.resolve() : x.reject(new Error(CANCELLED));
532
+ if (!ok && w.length > 0) notify(NOT_CONFIRMED_NOTICE, "info");
530
533
  };
531
534
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Ctx2.Provider, { value: confirmed, children: [
532
535
  children,
@@ -639,22 +642,35 @@ function useTransientSecret(given) {
639
642
  const fromHost = (0, import_react3.useContext)(RevealMs);
640
643
  const ms = given ?? fromHost;
641
644
  const [value, setValue] = (0, import_react3.useState)(null);
645
+ const [until, setUntil] = (0, import_react3.useState)(null);
642
646
  const timer2 = (0, import_react3.useRef)(null);
643
647
  const clear = (0, import_react3.useCallback)(() => {
644
648
  if (timer2.current) clearTimeout(timer2.current);
645
649
  timer2.current = null;
646
650
  setValue(null);
651
+ setUntil(null);
647
652
  }, []);
648
653
  const show = (0, import_react3.useCallback)(
649
654
  (next) => {
650
655
  if (timer2.current) clearTimeout(timer2.current);
651
656
  setValue(next);
657
+ setUntil(Date.now() + ms);
652
658
  timer2.current = setTimeout(clear, ms);
653
659
  },
654
660
  [clear, ms]
655
661
  );
656
662
  (0, import_react3.useEffect)(() => clear, [clear]);
657
- return { value, show, clear };
663
+ return { value, show, clear, until };
664
+ }
665
+ function useSecondsLeft(until) {
666
+ const [now, setNow] = (0, import_react3.useState)(() => Date.now());
667
+ (0, import_react3.useEffect)(() => {
668
+ if (until === null) return;
669
+ setNow(Date.now());
670
+ const t = setInterval(() => setNow(Date.now()), 1e3);
671
+ return () => clearInterval(t);
672
+ }, [until]);
673
+ return until === null ? null : Math.max(0, Math.ceil((until - now) / 1e3));
658
674
  }
659
675
  function useElementWidth(ref) {
660
676
  const [width, setWidth] = (0, import_react3.useState)(0);
@@ -747,6 +763,15 @@ function SecretText({ value, multiline }) {
747
763
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "break-all font-mono text-sm tracking-wide", children: [...value].map((ch, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: /[0-9]/.test(ch) ? "text-primary" : /[^A-Za-z0-9]/.test(ch) ? "text-warning" : void 0, children: ch }, i)) });
748
764
  }
749
765
  var quietError = (e, fallback) => wasCancelled(e) ? null : e instanceof Error ? e.message : fallback;
766
+ function HidesIn({ until }) {
767
+ const left = useSecondsLeft(until);
768
+ if (left === null) return null;
769
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "shrink-0 px-1 text-xs tabular-nums text-muted-foreground", "data-hides-in": "", title: "Shown values hide on their own", children: [
770
+ "Hides in ",
771
+ left,
772
+ "s"
773
+ ] });
774
+ }
750
775
  function SecretRow({ port, item, field, extra, onTurnOn }) {
751
776
  const confirmed = useConfirmed();
752
777
  const copyValue2 = useCopy();
@@ -802,6 +827,7 @@ function SecretRow({ port, item, field, extra, onTurnOn }) {
802
827
  actions: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
803
828
  extra,
804
829
  off && onTurnOn ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TurnOnButton, { label: field.label, onTurnOn }) : null,
830
+ !visible && secret.value !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(HidesIn, { until: secret.until }) : null,
805
831
  canSee && !visible ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", icon: secret.value !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EyeOffGlyph, {}) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EyeGlyph, {}), "aria-label": secret.value !== null ? `Hide ${field.label}` : `Show ${field.label}`, disabled: busy, onClick: () => void toggle2() }) : null,
806
832
  canSee ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls2.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_icons2.CopyIcon, {}), "aria-label": `Copy ${field.label}`, disabled: busy, onClick: () => void copy() }) : null
807
833
  ] }),
@@ -976,15 +1002,28 @@ function inView(item, view) {
976
1002
  return true;
977
1003
  }
978
1004
  var AUTHENTICATOR_WORDS = ["authenticator", "2fa", "two-factor", "two factor", "totp", "one-time password", "verification code", "2-step"];
979
- function matchesQuery(item, query) {
1005
+ function matchesQuery(item, query, familyLabel2) {
980
1006
  const q = query.trim().toLowerCase();
981
1007
  if (!q) return true;
982
1008
  if (item.authenticator && AUTHENTICATOR_WORDS.some((w) => w === q || q.length >= 3 && w.startsWith(q))) return true;
983
- return [item.name, item.definitionLabel, ...item.tags, ...item.loginUrls, ...item.custom.map((c) => `${c.label} ${c.value}`)].some((h) => h.toLowerCase().includes(q));
1009
+ return [
1010
+ item.name,
1011
+ item.description,
1012
+ item.definitionLabel,
1013
+ item.definitionKey,
1014
+ familyLabel2 ?? "",
1015
+ ...item.tags,
1016
+ ...item.loginUrls,
1017
+ ...item.fields.filter((f) => !isAuthenticatorStorage(f.key)).flatMap((f) => [f.label, f.key, f.envKey ?? ""]),
1018
+ ...humanCustomFields(item).map((c) => `${c.label} ${c.value}`)
1019
+ ].some((h) => h !== "" && h.toLowerCase().includes(q));
984
1020
  }
985
1021
  var HIDDEN_ITEM_TAG = "matrx:hidden";
986
1022
  var isHiddenItem = (item) => (item.tags ?? []).includes(HIDDEN_ITEM_TAG);
987
1023
  var isAuthenticatorStorage = (key) => key === "totp_seed" || key.startsWith("totp_") || key === "authenticator_last_time_step";
1024
+ var MFA_METHOD_KEY = "mfa_method";
1025
+ var isInternalCustomKey = (key) => isAuthenticatorStorage(key) || key === MFA_METHOD_KEY;
1026
+ var humanCustomFields = (item) => item.custom.filter((c) => !isInternalCustomKey(c.key));
988
1027
  function loginSiteOf(item) {
989
1028
  const raw = item.loginUrls[0];
990
1029
  if (!raw) return "";
@@ -1602,6 +1641,7 @@ function HistoryRow({ item, port, page, entry, label, onRestored, onConflict, on
1602
1641
  {
1603
1642
  label: when,
1604
1643
  actions: entry.available ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1644
+ secret.value !== null ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(HidesIn, { until: secret.until }) : null,
1605
1645
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_controls4.Button, { variant: "quiet", icon: secret.value !== null ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(EyeOffGlyph, {}) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(EyeGlyph, {}), "aria-label": secret.value !== null ? "Hide" : "Show", disabled: busy, onClick: () => void toggle2() }),
1606
1646
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_controls4.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_icons3.CopyIcon, {}), "aria-label": "Copy", disabled: busy, onClick: () => void copy() }),
1607
1647
  canRestore(page, entry) ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_controls4.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(RestoreGlyph, {}), "aria-label": "Restore", disabled: busy, onClick: () => void restore() }) : null
@@ -2137,7 +2177,6 @@ var import_design_system9 = require("@ai-matrx/design-system");
2137
2177
  var import_confirm_opener2 = require("@ai-matrx/kit/confirm-opener");
2138
2178
 
2139
2179
  // src/authenticator/port.ts
2140
- var MFA_METHOD_KEY = "mfa_method";
2141
2180
  var OTP_SEED_KEY = "totp_seed";
2142
2181
  function mfaMethodOf(item) {
2143
2182
  const v = item.custom.find((c) => c.key === MFA_METHOD_KEY)?.value;
@@ -5261,6 +5300,7 @@ function RecoveryCodesRow({ item, field, port, onChanged }) {
5261
5300
  label: "Recovery codes",
5262
5301
  top: codes !== null,
5263
5302
  actions: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
5303
+ codes ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(HidesIn, { until: secret.until }) : null,
5264
5304
  canSee ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_controls17.Button, { variant: "quiet", icon: codes ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EyeOffGlyph, {}) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(EyeGlyph, {}), "aria-label": codes ? "Hide codes" : "Show codes", disabled: busy, onClick: () => void toggle2() }) : null,
5265
5305
  canEdit && !sealed ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_controls17.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PencilGlyph, {}), "aria-label": "Replace all codes", disabled: busy, onClick: () => setDraft("") }) : null
5266
5306
  ] }),
@@ -6196,8 +6236,8 @@ function ItemPane(props) {
6196
6236
  const patchFieldSettings = (key, p) => setDraft((d) => ({ ...d, fieldSettings: { ...d.fieldSettings, [key]: { ...d.fieldSettings?.[key], ...p } } }));
6197
6237
  const extraFields = (item?.fields ?? []).filter((f) => !declaredKeys.has(f.key) && !draft.removed.includes(f.id) && !isAuthenticatorStorage(f.key));
6198
6238
  const metaValue = (key) => draft.values[key] ?? item?.custom.find((c) => c.key === key)?.value ?? "";
6199
- const extraCustom = draft.custom.filter((c) => !declaredKeys.has(c.key) && c.key !== MFA_METHOD_KEY && !isAuthenticatorStorage(c.key));
6200
- const viewCustom = (item?.custom ?? []).filter((c) => !declaredKeys.has(c.key) && c.key !== MFA_METHOD_KEY && !isAuthenticatorStorage(c.key));
6239
+ const extraCustom = draft.custom.filter((c) => !declaredKeys.has(c.key) && !isInternalCustomKey(c.key));
6240
+ const viewCustom = (item?.custom ?? []).filter((c) => !declaredKeys.has(c.key) && !isInternalCustomKey(c.key));
6201
6241
  const isLogin = (item?.definitionKey ?? definition?.key) === "website_login" || (item?.definitionKey ?? definition?.key) === "username_password";
6202
6242
  const typeLabel = definition?.label ?? item?.definitionLabel ?? "";
6203
6243
  const recoveryDeclared = declaredKeys.has(RECOVERY_CODES_KEY);
@@ -6647,7 +6687,7 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6647
6687
  const copyCode = () => code && void run("One-time code", async () => (await port.authenticator.code(item)).code, true);
6648
6688
  const copyName = () => void copy(item.name, "Name", { secret: false });
6649
6689
  const others = otherCopyFields(item, user, secret);
6650
- const custom = canUse ? item.custom.filter((c) => c.value) : [];
6690
+ const custom = canUse ? humanCustomFields(item).filter((c) => c.value) : [];
6651
6691
  const hostEntries = extra?.(item) ?? [];
6652
6692
  const copyEntries = [
6653
6693
  ...user ? [{ id: "copy:username", kind: "copy", label: `Copy ${user.label.toLowerCase()}`, onSelect: copyUser }] : [],
@@ -8247,20 +8287,33 @@ function VaultSelect({ value, choices, onChange }) {
8247
8287
  }
8248
8288
  var CATEGORY_GLYPHS = { logins: import_icons16.GlobeIcon, passkeys: ShieldGlyph, api: KeyGlyph, oauth: PlugGlyph, infra: DatabaseGlyph, env: TerminalGlyph, files: FileGlyph, other: NoteGlyph };
8249
8289
  var familyOf = (key, definitions) => definitions.find((d) => d.key === key)?.family ?? "generic";
8290
+ function itemFamily(item, definitions) {
8291
+ return familyOf(item.definitionKey, definitions);
8292
+ }
8293
+ var familyLabel = (family) => FAMILY_LABELS[family] ?? family.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
8250
8294
  function itemCategory(item, definitions) {
8251
8295
  return categoryOf(item.definitionKey, familyOf(item.definitionKey, definitions));
8252
8296
  }
8253
8297
  function buildViews(items, definitions, favoriteIds2, recentIds2) {
8254
8298
  const byCat = /* @__PURE__ */ new Map();
8299
+ const byFamily = /* @__PURE__ */ new Map();
8255
8300
  const byTag = /* @__PURE__ */ new Map();
8256
8301
  for (const i of items) {
8257
8302
  const c = itemCategory(i, definitions);
8258
8303
  byCat.set(c, (byCat.get(c) ?? 0) + 1);
8304
+ const fam = itemFamily(i, definitions);
8305
+ const fams = byFamily.get(c) ?? /* @__PURE__ */ new Map();
8306
+ fams.set(fam, (fams.get(fam) ?? 0) + 1);
8307
+ byFamily.set(c, fams);
8259
8308
  for (const t of i.tags) byTag.set(t, (byTag.get(t) ?? 0) + 1);
8260
8309
  }
8261
8310
  const ids = new Set(items.map((i) => i.id));
8262
8311
  return {
8263
- 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] })),
8312
+ categories: CATEGORIES.filter((c) => byCat.has(c.id)).map((c) => {
8313
+ const fams = [...(byFamily.get(c.id) ?? /* @__PURE__ */ new Map()).entries()];
8314
+ 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;
8315
+ return { view: `category:${c.id}`, label: c.label, count: byCat.get(c.id) ?? 0, glyph: CATEGORY_GLYPHS[c.id], ...families ? { families } : {} };
8316
+ }),
8264
8317
  tags: [...byTag.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, count2]) => ({ view: `tag:${t}`, label: t, count: count2, glyph: TagGlyph })),
8265
8318
  favorites: favoriteIds2 ? [...favoriteIds2].filter((id) => ids.has(id)).length : void 0,
8266
8319
  authenticator: items.filter((i) => i.authenticator).length,
@@ -8273,21 +8326,27 @@ function viewLabel(view, views) {
8273
8326
  if (view === "authenticator") return AUTHENTICATOR_LABEL;
8274
8327
  if (view === "trash") return "Trash";
8275
8328
  if (view === "page") return "This page";
8329
+ if (view.startsWith("family:")) return familyLabel(view.slice(7));
8276
8330
  return [...views.categories, ...views.tags].find((v) => v.view === view)?.label ?? "All items";
8277
8331
  }
8278
- function viewOptions(views, allCount, deletedCount, hasFavorites, hasRecents, hasPage = false) {
8332
+ function openCategory(view, views) {
8333
+ return views.categories.find((c) => c.view === view || c.families?.some((f) => f.view === view));
8334
+ }
8335
+ function viewOptions(views, allCount, deletedCount, hasFavorites, hasRecents, hasPage = false, current = "all") {
8336
+ const open = openCategory(current, views);
8279
8337
  return [
8280
8338
  ...hasPage ? [{ value: "page", label: "This page" }] : [],
8281
8339
  { value: "all", label: "Every category", meta: String(allCount) },
8282
8340
  ...hasFavorites ? [{ value: "favorites", label: "Favorites", meta: views.favorites === void 0 ? "" : String(views.favorites) }] : [],
8283
8341
  ...hasRecents ? [{ value: "recent", label: "Recently opened", meta: views.recent === void 0 ? "" : String(views.recent) }] : [],
8284
8342
  { value: "authenticator", label: AUTHENTICATOR_LABEL, meta: String(views.authenticator) },
8285
- ...views.categories.map((v) => ({ value: v.view, label: v.label, meta: String(v.count) })),
8343
+ // The open category's families follow it, indented by their sign; other categories stay one line.
8344
+ ...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) })) : []]),
8286
8345
  ...views.tags.map((v) => ({ value: v.view, label: `#${v.label}`, meta: String(v.count) })),
8287
8346
  { value: "trash", label: "Trash", meta: String(deletedCount) }
8288
8347
  ];
8289
8348
  }
8290
- function Entry({ on, glyph: Icon, label, count: count2, onClick }) {
8349
+ function Entry({ on, glyph: Icon, label, count: count2, onClick, nested = false }) {
8291
8350
  const desktop = (0, import_design_system25.useLook)() === "desktop";
8292
8351
  return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
8293
8352
  "button",
@@ -8295,9 +8354,10 @@ function Entry({ on, glyph: Icon, label, count: count2, onClick }) {
8295
8354
  type: "button",
8296
8355
  "aria-current": on ? "page" : void 0,
8297
8356
  onClick,
8298
- className: (0, import_design_system25.cn)("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"),
8357
+ "data-nested": nested ? "" : void 0,
8358
+ className: (0, import_design_system25.cn)("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"),
8299
8359
  children: [
8300
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Icon, { className: "size-4 shrink-0" }),
8360
+ nested ? null : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Icon, { className: "size-4 shrink-0" }),
8301
8361
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "min-w-0 flex-1 truncate", children: label }),
8302
8362
  count2 !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "text-xs tabular-nums text-muted-foreground", children: count2 }) : null
8303
8363
  ]
@@ -8325,7 +8385,10 @@ function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, v
8325
8385
  hasRecents ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Entry, { on: view === "recent", glyph: ClockGlyph, label: "Recently opened", count: views.recent, onClick: () => onView("recent") }) : null,
8326
8386
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Entry, { on: view === "authenticator", glyph: LockGlyph, label: AUTHENTICATOR_LABEL, count: views.authenticator, onClick: () => onView("authenticator") })
8327
8387
  ] }),
8328
- views.categories.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Group, { title: "Categories", children: views.categories.map((e) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Entry, { on: view === e.view, glyph: e.glyph, label: e.label, count: e.count, onClick: () => onView(e.view) }, e.view)) }) : null,
8388
+ views.categories.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Group, { title: "Categories", children: views.categories.map((e) => /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex flex-col gap-0.5", children: [
8389
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Entry, { on: view === e.view, glyph: e.glyph, label: e.label, count: e.count, onClick: () => onView(e.view) }),
8390
+ e === openCategory(view, views) ? e.families?.map((f) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Entry, { nested: true, on: view === f.view, glyph: f.glyph, label: f.label, count: f.count, onClick: () => onView(f.view) }, f.view)) : null
8391
+ ] }, e.view)) }) : null,
8329
8392
  views.tags.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex flex-col gap-0.5", children: [
8330
8393
  /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("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: [
8331
8394
  tagsOpen ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ChevronDownGlyph, { className: "size-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ChevronRightGlyph, { className: "size-3.5" }),
@@ -8590,10 +8653,11 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8590
8653
  const views = (0, import_react42.useMemo)(() => buildViews(inThisVault, definitions, favorites, recents), [inThisVault, definitions, favorites, recents]);
8591
8654
  const visible = (0, import_react42.useMemo)(() => {
8592
8655
  const filtered = inThisVault.filter((i) => {
8593
- if (!matchesQuery(i, loc.q)) return false;
8656
+ if (!matchesQuery(i, loc.q, familyLabel(itemFamily(i, definitions)))) return false;
8594
8657
  if (loc.view === "favorites") return favorites?.has(i.id) ?? false;
8595
8658
  if (loc.view === "recent") return recents?.includes(i.id) ?? false;
8596
8659
  if (loc.view.startsWith("category:")) return itemCategory(i, definitions) === loc.view.slice(9);
8660
+ if (loc.view.startsWith("family:")) return itemFamily(i, definitions) === loc.view.slice(7);
8597
8661
  return inView(i, loc.view);
8598
8662
  });
8599
8663
  if (loc.view === "recent" && recents) return [...filtered].sort((a, b) => recents.indexOf(a.id) - recents.indexOf(b.id));
@@ -8647,7 +8711,7 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8647
8711
  const title = loc.vault === "all" && loc.view === "all" ? "All items" : viewLabel(loc.view, views);
8648
8712
  const compactHeader = mode === "full" ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "grid grid-cols-[repeat(auto-fit,minmax(11rem,1fr))] gap-2", "data-vault-compact-header": "", children: [
8649
8713
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(VaultSelect, { value: loc.vault, choices, onChange: (vault) => go({ vault, itemId: null, panel: null }) }),
8650
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_controls35.Select, { 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" })
8714
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_controls35.Select, { 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" })
8651
8715
  ] });
8652
8716
  const compactActions = mode === "full" ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "flex shrink-0 items-center gap-2", "data-vault-compact-actions": "", children: [
8653
8717
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(