@ai-matrx/vault 0.1.22 → 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);
@@ -6489,7 +6529,7 @@ function subtitleOf(item, withOwner) {
6489
6529
  const what = loginSiteOf(item) || item.definitionLabel;
6490
6530
  return withOwner ? `${what} \xB7 ${item.owner.label}` : what;
6491
6531
  }
6492
- function ItemList({ title, items, definitions, port, selectedId, onSelect, query, onQuery, sort, onSort, onNew, matchingIds, header, showOwner, canSortViewed = false, onRefresh, refreshing = false, rowContextMenu = "package", rowMenuItems, stillLoading, favorites, onToggleFavorite }) {
6532
+ function ItemList({ title, items, definitions, port, selectedId, onSelect, query, onQuery, sort, onSort, onNew, matchingIds, header, actions, showOwner, canSortViewed = false, onRefresh, refreshing = false, rowContextMenu = "package", rowMenuItems, stillLoading, favorites, onToggleFavorite }) {
6493
6533
  const sorts = SORTS2.filter((s) => canSortViewed || s.value !== "viewed");
6494
6534
  const pinned = matchingIds ? items.filter((i) => matchingIds.has(i.id)) : [];
6495
6535
  const rest = matchingIds ? items.filter((i) => !matchingIds.has(i.id)) : items;
@@ -6517,6 +6557,7 @@ function ItemList({ title, items, definitions, port, selectedId, onSelect, query
6517
6557
  header,
6518
6558
  /* @__PURE__ */ jsxs26("div", { className: "flex items-center gap-2", children: [
6519
6559
  /* @__PURE__ */ jsx28(SearchField, { value: query, onChange: (e) => onQuery(e.target.value), onKeyDown, placeholder: `Search ${title}`, className: "min-w-0 flex-1", "aria-label": "Search the vault" }),
6560
+ actions,
6520
6561
  /* @__PURE__ */ jsx28(Button23, { variant: "primary", icon: /* @__PURE__ */ jsx28(PlusGlyph, {}), "aria-label": "New item", onClick: onNew })
6521
6562
  ] }),
6522
6563
  /* @__PURE__ */ jsxs26("div", { className: "flex items-center justify-between gap-2 px-1", children: [
@@ -6579,7 +6620,7 @@ function ListRow({ item, family, port, on, onSelect, withOwner, ownContextMenu,
6579
6620
  const copyCode = () => code && void run("One-time code", async () => (await port.authenticator.code(item)).code, true);
6580
6621
  const copyName = () => void copy(item.name, "Name", { secret: false });
6581
6622
  const others = otherCopyFields(item, user, secret);
6582
- const custom = canUse ? item.custom.filter((c) => c.value) : [];
6623
+ const custom = canUse ? humanCustomFields(item).filter((c) => c.value) : [];
6583
6624
  const hostEntries = extra?.(item) ?? [];
6584
6625
  const copyEntries = [
6585
6626
  ...user ? [{ id: "copy:username", kind: "copy", label: `Copy ${user.label.toLowerCase()}`, onSelect: copyUser }] : [],
@@ -8179,20 +8220,33 @@ function VaultSelect({ value, choices, onChange }) {
8179
8220
  }
8180
8221
  var CATEGORY_GLYPHS = { logins: GlobeIcon5, passkeys: ShieldGlyph, api: KeyGlyph, oauth: PlugGlyph, infra: DatabaseGlyph, env: TerminalGlyph, files: FileGlyph, other: NoteGlyph };
8181
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());
8182
8227
  function itemCategory(item, definitions) {
8183
8228
  return categoryOf(item.definitionKey, familyOf(item.definitionKey, definitions));
8184
8229
  }
8185
8230
  function buildViews(items, definitions, favoriteIds2, recentIds2) {
8186
8231
  const byCat = /* @__PURE__ */ new Map();
8232
+ const byFamily = /* @__PURE__ */ new Map();
8187
8233
  const byTag = /* @__PURE__ */ new Map();
8188
8234
  for (const i of items) {
8189
8235
  const c = itemCategory(i, definitions);
8190
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);
8191
8241
  for (const t of i.tags) byTag.set(t, (byTag.get(t) ?? 0) + 1);
8192
8242
  }
8193
8243
  const ids = new Set(items.map((i) => i.id));
8194
8244
  return {
8195
- 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
+ }),
8196
8250
  tags: [...byTag.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([t, count2]) => ({ view: `tag:${t}`, label: t, count: count2, glyph: TagGlyph })),
8197
8251
  favorites: favoriteIds2 ? [...favoriteIds2].filter((id) => ids.has(id)).length : void 0,
8198
8252
  authenticator: items.filter((i) => i.authenticator).length,
@@ -8205,21 +8259,27 @@ function viewLabel(view, views) {
8205
8259
  if (view === "authenticator") return AUTHENTICATOR_LABEL;
8206
8260
  if (view === "trash") return "Trash";
8207
8261
  if (view === "page") return "This page";
8262
+ if (view.startsWith("family:")) return familyLabel(view.slice(7));
8208
8263
  return [...views.categories, ...views.tags].find((v) => v.view === view)?.label ?? "All items";
8209
8264
  }
8210
- 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);
8211
8270
  return [
8212
8271
  ...hasPage ? [{ value: "page", label: "This page" }] : [],
8213
8272
  { value: "all", label: "Every category", meta: String(allCount) },
8214
8273
  ...hasFavorites ? [{ value: "favorites", label: "Favorites", meta: views.favorites === void 0 ? "" : String(views.favorites) }] : [],
8215
8274
  ...hasRecents ? [{ value: "recent", label: "Recently opened", meta: views.recent === void 0 ? "" : String(views.recent) }] : [],
8216
8275
  { value: "authenticator", label: AUTHENTICATOR_LABEL, meta: String(views.authenticator) },
8217
- ...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) })) : []]),
8218
8278
  ...views.tags.map((v) => ({ value: v.view, label: `#${v.label}`, meta: String(v.count) })),
8219
8279
  { value: "trash", label: "Trash", meta: String(deletedCount) }
8220
8280
  ];
8221
8281
  }
8222
- function Entry({ on, glyph: Icon, label, count: count2, onClick }) {
8282
+ function Entry({ on, glyph: Icon, label, count: count2, onClick, nested = false }) {
8223
8283
  const desktop = useLook2() === "desktop";
8224
8284
  return /* @__PURE__ */ jsxs37(
8225
8285
  "button",
@@ -8227,9 +8287,10 @@ function Entry({ on, glyph: Icon, label, count: count2, onClick }) {
8227
8287
  type: "button",
8228
8288
  "aria-current": on ? "page" : void 0,
8229
8289
  onClick,
8230
- 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"),
8231
8292
  children: [
8232
- /* @__PURE__ */ jsx39(Icon, { className: "size-4 shrink-0" }),
8293
+ nested ? null : /* @__PURE__ */ jsx39(Icon, { className: "size-4 shrink-0" }),
8233
8294
  /* @__PURE__ */ jsx39("span", { className: "min-w-0 flex-1 truncate", children: label }),
8234
8295
  count2 !== void 0 ? /* @__PURE__ */ jsx39("span", { className: "text-xs tabular-nums text-muted-foreground", children: count2 }) : null
8235
8296
  ]
@@ -8257,7 +8318,10 @@ function VaultSidebar({ vault, choices, onVault, view, allCount, deletedCount, v
8257
8318
  hasRecents ? /* @__PURE__ */ jsx39(Entry, { on: view === "recent", glyph: ClockGlyph, label: "Recently opened", count: views.recent, onClick: () => onView("recent") }) : null,
8258
8319
  /* @__PURE__ */ jsx39(Entry, { on: view === "authenticator", glyph: LockGlyph, label: AUTHENTICATOR_LABEL, count: views.authenticator, onClick: () => onView("authenticator") })
8259
8320
  ] }),
8260
- 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,
8261
8325
  views.tags.length > 0 ? /* @__PURE__ */ jsxs37("div", { className: "flex flex-col gap-0.5", children: [
8262
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: [
8263
8327
  tagsOpen ? /* @__PURE__ */ jsx39(ChevronDownGlyph, { className: "size-3.5" }) : /* @__PURE__ */ jsx39(ChevronRightGlyph, { className: "size-3.5" }),
@@ -8522,10 +8586,11 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8522
8586
  const views = useMemo6(() => buildViews(inThisVault, definitions, favorites, recents), [inThisVault, definitions, favorites, recents]);
8523
8587
  const visible = useMemo6(() => {
8524
8588
  const filtered = inThisVault.filter((i) => {
8525
- if (!matchesQuery(i, loc.q)) return false;
8589
+ if (!matchesQuery(i, loc.q, familyLabel(itemFamily(i, definitions)))) return false;
8526
8590
  if (loc.view === "favorites") return favorites?.has(i.id) ?? false;
8527
8591
  if (loc.view === "recent") return recents?.includes(i.id) ?? false;
8528
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);
8529
8594
  return inView(i, loc.view);
8530
8595
  });
8531
8596
  if (loc.view === "recent" && recents) return [...filtered].sort((a, b) => recents.indexOf(a.id) - recents.indexOf(b.id));
@@ -8577,65 +8642,66 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8577
8642
  }
8578
8643
  const hasFavorites = port.lists.favorites !== void 0;
8579
8644
  const title = loc.vault === "all" && loc.view === "all" ? "All items" : viewLabel(loc.view, views);
8580
- const compactHeader = mode === "full" ? void 0 : (
8581
- // The two pickers keep a real width at every compact size (the list column is 19rem in split
8582
- // mode): the Authenticator button is icon-only beside them, its name in its tooltip.
8583
- /* @__PURE__ */ jsxs38("div", { className: "grid grid-cols-[minmax(6rem,1fr)_minmax(6rem,1fr)_auto_auto] gap-2", "data-vault-compact-header": "", children: [
8584
- /* @__PURE__ */ jsx40(VaultSelect, { value: loc.vault, choices, onChange: (vault) => go({ vault, itemId: null, panel: null }) }),
8585
- /* @__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" }),
8586
- /* @__PURE__ */ jsx40(
8587
- Button34,
8588
- {
8589
- variant: loc.view === "authenticator" ? "outline" : "quiet",
8590
- icon: /* @__PURE__ */ jsx40(LockGlyph, {}),
8591
- "aria-label": AUTHENTICATOR_LABEL,
8592
- "aria-pressed": loc.view === "authenticator",
8593
- title: AUTHENTICATOR_LABEL,
8594
- onClick: () => go({ view: loc.view === "authenticator" ? "all" : "authenticator", itemId: null, mode: "view", panel: null })
8595
- }
8596
- ),
8597
- /* @__PURE__ */ jsxs38(DropdownMenu6, { children: [
8598
- /* @__PURE__ */ jsx40(DropdownMenuTrigger6, { asChild: true, children: /* @__PURE__ */ jsx40(Button34, { variant: "quiet", icon: /* @__PURE__ */ jsx40(MoreGlyph, {}), "aria-label": "Vault actions" }) }),
8599
- /* @__PURE__ */ jsxs38(DropdownMenuContent6, { align: "end", children: [
8600
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "import" }), children: [
8601
- /* @__PURE__ */ jsx40(ImportGlyph, { className: "size-4" }),
8602
- "Import"
8603
- ] }),
8604
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "export" }), children: [
8605
- /* @__PURE__ */ jsx40(ExportGlyph, { className: "size-4" }),
8606
- "Export"
8607
- ] }),
8608
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "backup" }), children: [
8609
- /* @__PURE__ */ jsx40(SafeGlyph, { className: "size-4" }),
8610
- "Backup"
8611
- ] }),
8612
- /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "devices" }), children: [
8613
- /* @__PURE__ */ jsx40(ShieldGlyph, { className: "size-4" }),
8614
- "Browsers that fill"
8615
- ] }),
8616
- host.nativeImport ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "import-passkeys" }), children: [
8617
- /* @__PURE__ */ jsx40(ImportGlyph, { className: "size-4" }),
8618
- "Import passkeys"
8619
- ] }) : null,
8620
- host.nativeProvider && host.nativeProvider.status !== "unavailable" ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: host.nativeProvider.openSetup, children: [
8621
- /* @__PURE__ */ jsx40(ShieldGlyph, { className: "size-4" }),
8622
- "System AutoFill"
8623
- ] }) : null,
8624
- host.lock?.lock ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => void host.lock?.lock?.(), children: [
8625
- /* @__PURE__ */ jsx40(LockGlyph, { className: "size-4" }),
8626
- "Lock"
8627
- ] }) : null,
8628
- host.openWebVault ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: host.openWebVault, children: [
8629
- /* @__PURE__ */ jsx40(ExternalGlyph, { className: "size-4" }),
8630
- "Open on the web"
8631
- ] }) : null
8632
- ] })
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: [
8646
+ /* @__PURE__ */ jsx40(VaultSelect, { value: loc.vault, choices, onChange: (vault) => go({ vault, itemId: null, panel: null }) }),
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" })
8648
+ ] });
8649
+ const compactActions = mode === "full" ? void 0 : /* @__PURE__ */ jsxs38("div", { className: "flex shrink-0 items-center gap-2", "data-vault-compact-actions": "", children: [
8650
+ /* @__PURE__ */ jsx40(
8651
+ Button34,
8652
+ {
8653
+ variant: loc.view === "authenticator" ? "outline" : "quiet",
8654
+ icon: /* @__PURE__ */ jsx40(LockGlyph, {}),
8655
+ "aria-label": AUTHENTICATOR_LABEL,
8656
+ "aria-pressed": loc.view === "authenticator",
8657
+ title: AUTHENTICATOR_LABEL,
8658
+ onClick: () => go({ view: loc.view === "authenticator" ? "all" : "authenticator", itemId: null, mode: "view", panel: null })
8659
+ }
8660
+ ),
8661
+ /* @__PURE__ */ jsxs38(DropdownMenu6, { children: [
8662
+ /* @__PURE__ */ jsx40(DropdownMenuTrigger6, { asChild: true, children: /* @__PURE__ */ jsx40(Button34, { variant: "quiet", icon: /* @__PURE__ */ jsx40(MoreGlyph, {}), "aria-label": "Vault actions" }) }),
8663
+ /* @__PURE__ */ jsxs38(DropdownMenuContent6, { align: "end", children: [
8664
+ /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "import" }), children: [
8665
+ /* @__PURE__ */ jsx40(ImportGlyph, { className: "size-4" }),
8666
+ "Import"
8667
+ ] }),
8668
+ /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "export" }), children: [
8669
+ /* @__PURE__ */ jsx40(ExportGlyph, { className: "size-4" }),
8670
+ "Export"
8671
+ ] }),
8672
+ /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "backup" }), children: [
8673
+ /* @__PURE__ */ jsx40(SafeGlyph, { className: "size-4" }),
8674
+ "Backup"
8675
+ ] }),
8676
+ /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "devices" }), children: [
8677
+ /* @__PURE__ */ jsx40(ShieldGlyph, { className: "size-4" }),
8678
+ "Browsers that fill"
8679
+ ] }),
8680
+ host.nativeImport ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => go({ panel: "import-passkeys" }), children: [
8681
+ /* @__PURE__ */ jsx40(ImportGlyph, { className: "size-4" }),
8682
+ "Import passkeys"
8683
+ ] }) : null,
8684
+ host.nativeProvider && host.nativeProvider.status !== "unavailable" ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: host.nativeProvider.openSetup, children: [
8685
+ /* @__PURE__ */ jsx40(ShieldGlyph, { className: "size-4" }),
8686
+ "System AutoFill"
8687
+ ] }) : null,
8688
+ host.lock?.lock ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: () => void host.lock?.lock?.(), children: [
8689
+ /* @__PURE__ */ jsx40(LockGlyph, { className: "size-4" }),
8690
+ "Lock"
8691
+ ] }) : null,
8692
+ host.openWebVault ? /* @__PURE__ */ jsxs38(DropdownMenuItem6, { onSelect: host.openWebVault, children: [
8693
+ /* @__PURE__ */ jsx40(ExternalGlyph, { className: "size-4" }),
8694
+ "Open on the web"
8695
+ ] }) : null
8633
8696
  ] })
8634
8697
  ] })
8635
- );
8698
+ ] });
8636
8699
  const viewHeader = /* @__PURE__ */ jsxs38("div", { className: "flex flex-col gap-2", children: [
8637
8700
  compactHeader,
8638
- /* @__PURE__ */ jsx40(SearchField4, { value: loc.q, onChange: (e) => go({ q: e.target.value }, "replace"), placeholder: `Search ${viewLabel(loc.view, views)}`, "aria-label": `Search ${viewLabel(loc.view, views)}` })
8701
+ /* @__PURE__ */ jsxs38("div", { className: "flex items-center gap-2", children: [
8702
+ /* @__PURE__ */ jsx40(SearchField4, { value: loc.q, onChange: (e) => go({ q: e.target.value }, "replace"), placeholder: `Search ${viewLabel(loc.view, views)}`, "aria-label": `Search ${viewLabel(loc.view, views)}`, className: "min-w-0 flex-1" }),
8703
+ compactActions
8704
+ ] })
8639
8705
  ] });
8640
8706
  const viewProps = { port, host, items: inThisVault, deleted, definitions, query: loc.q, onOpen: (id) => go({ view: "all", itemId: id, mode: "view", panel: null }), onChanged: load, header: viewHeader };
8641
8707
  const pageView = loc.view === "page" && host.page && !loc.q ? /* @__PURE__ */ jsx40(
@@ -8653,7 +8719,10 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8653
8719
  onNewForPage: (url) => go({ mode: "new", kind: null, url, itemId: null, panel: null }),
8654
8720
  onBrowse: () => go({ view: "all", itemId: null }),
8655
8721
  onChanged: load,
8656
- ...compactHeader ? { header: compactHeader } : {}
8722
+ ...compactHeader ? { header: /* @__PURE__ */ jsxs38("div", { className: "flex flex-col gap-2", children: [
8723
+ compactHeader,
8724
+ /* @__PURE__ */ jsx40("div", { className: "flex justify-end", children: compactActions })
8725
+ ] }) } : {}
8657
8726
  }
8658
8727
  ) : null;
8659
8728
  const specialView = pageView ?? (loc.view === "authenticator" ? /* @__PURE__ */ jsx40(AuthenticatorView, { ...viewProps }) : loc.view === "trash" ? /* @__PURE__ */ jsx40(TrashView, { ...viewProps }) : null);
@@ -8680,7 +8749,8 @@ function Workspace({ port, host = NO_HOST, layout = "auto", location, onLocation
8680
8749
  rowContextMenu: host.rowContextMenu ?? "package",
8681
8750
  ...host.rowMenuItems ? { rowMenuItems: host.rowMenuItems } : {},
8682
8751
  ...matchingIds ? { matchingIds } : {},
8683
- ...compactHeader ? { header: compactHeader } : {}
8752
+ ...compactHeader ? { header: compactHeader } : {},
8753
+ ...compactActions ? { actions: compactActions } : {}
8684
8754
  }
8685
8755
  );
8686
8756
  const showingDetail = loc.mode === "new" || loc.itemId !== null;