cursedbelt 4.7.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +36 -0
  2. package/dist/react/chat/index.d.ts +1 -0
  3. package/dist/react/chat/index.d.ts.map +1 -1
  4. package/dist/react/chat/index.js +1 -0
  5. package/dist/react/chat/index.js.map +1 -1
  6. package/dist/react/chat/usePresence.d.ts +75 -0
  7. package/dist/react/chat/usePresence.d.ts.map +1 -0
  8. package/dist/react/chat/usePresence.js +109 -0
  9. package/dist/react/chat/usePresence.js.map +1 -0
  10. package/dist/react/components/SecretInput.d.ts +7 -2
  11. package/dist/react/components/SecretInput.d.ts.map +1 -1
  12. package/dist/react/components/SecretInput.js +5 -2
  13. package/dist/react/components/SecretInput.js.map +1 -1
  14. package/dist/react/components/TooltipImpl.d.ts.map +1 -1
  15. package/dist/react/components/TooltipImpl.js +52 -3
  16. package/dist/react/components/TooltipImpl.js.map +1 -1
  17. package/dist/react/index.d.ts +1 -0
  18. package/dist/react/index.d.ts.map +1 -1
  19. package/dist/react/index.js +1 -0
  20. package/dist/react/index.js.map +1 -1
  21. package/dist/react/master-lock/MasterLockDialog.d.ts.map +1 -1
  22. package/dist/react/master-lock/MasterLockDialog.js +2 -1
  23. package/dist/react/master-lock/MasterLockDialog.js.map +1 -1
  24. package/dist/react/master-lock/MasterLockProvider.d.ts +12 -18
  25. package/dist/react/master-lock/MasterLockProvider.d.ts.map +1 -1
  26. package/dist/react/master-lock/MasterLockProvider.js +14 -20
  27. package/dist/react/master-lock/MasterLockProvider.js.map +1 -1
  28. package/dist/react/media/MediaUploadControl.d.ts +3 -7
  29. package/dist/react/media/MediaUploadControl.d.ts.map +1 -1
  30. package/dist/react/media/MediaUploadControl.js +4 -13
  31. package/dist/react/media/MediaUploadControl.js.map +1 -1
  32. package/dist/react/media/fileUploadClient.d.ts +0 -8
  33. package/dist/react/media/fileUploadClient.d.ts.map +1 -1
  34. package/dist/react/media/fileUploadClient.js +0 -8
  35. package/dist/react/media/fileUploadClient.js.map +1 -1
  36. package/dist/react/media/hlsSource.d.ts.map +1 -1
  37. package/dist/react/media/hlsSource.js +4 -2
  38. package/dist/react/media/hlsSource.js.map +1 -1
  39. package/dist/react/media/index.d.ts +0 -4
  40. package/dist/react/media/index.d.ts.map +1 -1
  41. package/dist/react/media/index.js +0 -4
  42. package/dist/react/media/index.js.map +1 -1
  43. package/dist/react/sessionAuth.d.ts +1 -2
  44. package/dist/react/sessionAuth.d.ts.map +1 -1
  45. package/dist/react/sessionAuth.js +2 -3
  46. package/dist/react/sessionAuth.js.map +1 -1
  47. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  48. package/dist/styles-areas/activity-bar.css +1 -1
  49. package/dist/styles-areas/auth.css +1 -1
  50. package/dist/styles-areas/charts.css +1 -1
  51. package/dist/styles-areas/chat.css +1 -1
  52. package/dist/styles-areas/core.css +1 -1
  53. package/dist/styles-areas/data-table.css +1 -1
  54. package/dist/styles-areas/disk-usage.css +1 -1
  55. package/dist/styles-areas/fields.css +1 -1
  56. package/dist/styles-areas/master-detail.css +1 -1
  57. package/dist/styles-areas/master-lock.css +108 -2
  58. package/dist/styles-areas/media-gallery.css +1 -1
  59. package/dist/styles-areas/media.css +1 -17
  60. package/dist/styles-areas/nav.css +1 -1
  61. package/dist/styles-areas/notifications.css +1 -1
  62. package/dist/styles-areas/spreadsheet.css +1 -1
  63. package/dist/styles-areas/stats.css +1 -1
  64. package/dist/styles-areas/wizard.css +1 -1
  65. package/dist/styles-areas/workbook-viewer.css +1 -1
  66. package/dist/styles-utilities.css +0 -16
  67. package/dist/testing/_noBun.d.ts +2 -0
  68. package/dist/testing/_noBun.d.ts.map +1 -0
  69. package/dist/testing/_noBun.js +10 -0
  70. package/dist/testing/_noBun.js.map +1 -0
  71. package/package.json +42 -19
  72. package/scripts/checkAreaStyles.spec.ts +71 -0
  73. package/scripts/checkAreaStyles.ts +150 -0
  74. package/scripts/guardrailsEnforce.spec.ts +43 -0
  75. package/scripts/guardrailsEnforce.ts +46 -1
  76. package/scripts/styleAreas.ts +1 -2
  77. package/src/barrelsReachNoOptionalPeer.spec.ts +6 -3
  78. package/src/bundlerOnlyEntries.spec.ts +101 -0
  79. package/src/declaredImports.spec.ts +4 -0
  80. package/src/optionalPeersTable.spec.ts +127 -0
  81. package/src/react/chat/index.ts +8 -0
  82. package/src/react/chat/usePresence.spec.tsx +138 -0
  83. package/src/react/chat/usePresence.ts +185 -0
  84. package/src/react/components/SecretInput.tsx +12 -1
  85. package/src/react/components/TooltipImpl.tsx +54 -5
  86. package/src/react/index.ts +8 -0
  87. package/src/react/master-lock/MasterLockDialog.tsx +13 -9
  88. package/src/react/master-lock/MasterLockProvider.spec.tsx +17 -0
  89. package/src/react/master-lock/MasterLockProvider.tsx +16 -19
  90. package/src/react/media/MediaUploadControl.tsx +8 -29
  91. package/src/react/media/fileUploadClient.ts +7 -17
  92. package/src/react/media/hlsSource.ts +4 -1
  93. package/src/react/media/index.ts +0 -16
  94. package/src/react/sessionAuth.ts +2 -3
  95. package/src/scriptsResolve.spec.ts +66 -0
  96. package/src/styles-areas/activity-bar.css +1 -1
  97. package/src/styles-areas/auth.css +1 -1
  98. package/src/styles-areas/charts.css +1 -1
  99. package/src/styles-areas/chat.css +1 -1
  100. package/src/styles-areas/core.css +1 -1
  101. package/src/styles-areas/data-table.css +1 -1
  102. package/src/styles-areas/disk-usage.css +1 -1
  103. package/src/styles-areas/fields.css +1 -1
  104. package/src/styles-areas/master-detail.css +1 -1
  105. package/src/styles-areas/master-lock.css +108 -2
  106. package/src/styles-areas/media-gallery.css +1 -1
  107. package/src/styles-areas/media.css +1 -17
  108. package/src/styles-areas/nav.css +1 -1
  109. package/src/styles-areas/notifications.css +1 -1
  110. package/src/styles-areas/spreadsheet.css +1 -1
  111. package/src/styles-areas/stats.css +1 -1
  112. package/src/styles-areas/wizard.css +1 -1
  113. package/src/styles-areas/workbook-viewer.css +1 -1
  114. package/src/styles-utilities.css +0 -16
  115. package/src/testing/_noBun.ts +12 -0
  116. package/src/verifyGraph.spec.ts +11 -0
  117. package/dist/react/media/PrivateIndexView.d.ts +0 -6
  118. package/dist/react/media/PrivateIndexView.d.ts.map +0 -1
  119. package/dist/react/media/PrivateIndexView.js +0 -44
  120. package/dist/react/media/PrivateIndexView.js.map +0 -1
  121. package/dist/react/media/PrivateModeBadge.d.ts +0 -17
  122. package/dist/react/media/PrivateModeBadge.d.ts.map +0 -1
  123. package/dist/react/media/PrivateModeBadge.js +0 -24
  124. package/dist/react/media/PrivateModeBadge.js.map +0 -1
  125. package/dist/react/media/privateClient.d.ts +0 -34
  126. package/dist/react/media/privateClient.d.ts.map +0 -1
  127. package/dist/react/media/privateClient.js +0 -36
  128. package/dist/react/media/privateClient.js.map +0 -1
  129. package/dist/react/media/usePrivateUnlock.d.ts +0 -25
  130. package/dist/react/media/usePrivateUnlock.d.ts.map +0 -1
  131. package/dist/react/media/usePrivateUnlock.js +0 -68
  132. package/dist/react/media/usePrivateUnlock.js.map +0 -1
  133. package/src/react/media/PrivateIndexView.tsx +0 -113
  134. package/src/react/media/PrivateModeBadge.tsx +0 -50
  135. package/src/react/media/privateClient.spec.ts +0 -113
  136. package/src/react/media/privateClient.ts +0 -82
  137. package/src/react/media/usePrivateUnlock.ts +0 -132
@@ -0,0 +1,12 @@
1
+ // Loaded only on non-Bun runtimes (task 2087). The `cursedbelt/testing` export map routes the
2
+ // real module to Bun (the `bun` condition) and everything else here, so importing these helpers
3
+ // from Node fails with a sentence instead of `Named export 'waitFor' not found` — which is what
4
+ // Node said about `@testing-library/react`, a CommonJS module its ESM loader cannot name-import.
5
+ // Same shape as `cwip/testing`'s `_noBun`, and `tools/proveInstall.ts` excuses exactly this: a
6
+ // `bun` condition plus a plain `Error` of the package's own making.
7
+ throw new Error(
8
+ 'cursedbelt/testing requires the Bun runtime — it drives @testing-library/react under bun:test + happy-dom. ' +
9
+ 'Import it only from specs run with `bun test`.',
10
+ );
11
+
12
+ export {};
@@ -67,6 +67,17 @@ const MUST_RUN = [
67
67
  // added, removing it argues with this list.
68
68
  'typecheck:scripts',
69
69
  'demo:check',
70
+ // 🔴 Added 2026-09-23 (task 102). `biome lint` was in no command at all — `done:serial`
71
+ // called a `lint` script that did not exist, so the 31 errors it would have shown sat
72
+ // unseen. They were cleared in the same landing (29 were in Tailwind's generated output,
73
+ // now excluded in `biome.jsonc`), so the stage landed green.
74
+ 'lint',
75
+ // 🔴 Added 2026-09-23 with `no-raw-password` (task 077). This repo's own guardrails scan was
76
+ // in no command a gate ran, and it had been RED since task 269 gave every relative specifier
77
+ // its `.js`: `no-native-dialog` stopped recognising DeleteButton's `'../confirm.js'` import
78
+ // as the belt's own `confirm`. The rules this package ships to five apps were not being
79
+ // applied to the package itself.
80
+ 'guardrails:report',
70
81
  'styles',
71
82
  'build',
72
83
  'test',
@@ -1,6 +0,0 @@
1
- import React from 'react';
2
- import type { PrivateUnlock } from './usePrivateUnlock.js';
3
- export declare function PrivateIndexView({ unlock }: {
4
- unlock: PrivateUnlock;
5
- }): React.JSX.Element;
6
- //# sourceMappingURL=PrivateIndexView.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PrivateIndexView.d.ts","sourceRoot":"","sources":["../../../src/react/media/PrivateIndexView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAS5E,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,qBAmGrE"}
@@ -1,44 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { formatBytes as fmtBytes } from 'cwip/format';
3
- import React from 'react';
4
- import { Button } from '../components/Button.js';
5
- import { Input } from '../components/Input.js';
6
- // The locked-index cleanup view: every private item's SIZE + created-date (never its title,
7
- // thumbnail, or notes), so a user can prune media whose password they've lost. A password field
8
- // runs "try-a-password" — items it decrypts light up with their revealed title; the rest stay
9
- // opaque. Themed off cursedbelt tokens (with fallbacks so it renders anywhere).
10
- const fmtDate = (ts) => new Date(ts).toISOString().slice(0, 10);
11
- export function PrivateIndexView({ unlock }) {
12
- const [rows, setRows] = React.useState([]);
13
- const [revealed, setRevealed] = React.useState(new Map());
14
- const [password, setPassword] = React.useState('');
15
- const [trying, setTrying] = React.useState(false);
16
- React.useEffect(() => {
17
- unlock.loadIndex().then(setRows);
18
- }, [unlock]);
19
- async function tryPassword() {
20
- if (!password)
21
- return;
22
- setTrying(true);
23
- try {
24
- const items = await unlock.reveal(password);
25
- setRevealed(new Map(items.map((i) => [i.id, i.meta])));
26
- }
27
- finally {
28
- setTrying(false);
29
- }
30
- }
31
- const totalBytes = rows.reduce((s, r) => s + r.sizeBytes, 0);
32
- const matchCount = revealed.size;
33
- return (_jsxs("div", { className: 'flex flex-col gap-3 text-sm', children: [_jsxs("div", { className: 'flex items-center justify-between gap-3', children: [_jsxs("div", { className: 'text-[color:var(--muted-foreground)]', children: [rows.length, " locked ", rows.length === 1 ? 'file' : 'files', " \u00B7 ", fmtBytes(totalBytes)] }), _jsxs("div", { className: 'flex items-center gap-2', children: [_jsx(Input, { type: 'password', value: password, onChange: (e) => setPassword(e.target.value), onEnter: () => tryPassword(), placeholder: 'Try a password\u2026', "aria-label": 'Try a password', className: 'h-auto w-auto bg-transparent px-2 py-1' }), _jsx(Button, { size: 'sm', onClick: tryPassword, disabled: trying || !password, loading: trying, tooltip: 'Try this password against every locked file', children: trying ? 'Trying…' : 'Reveal' })] })] }), matchCount > 0 ? (_jsxs("div", { className: 'text-[color:var(--accent)]', children: ["This password unlocks ", matchCount, " ", matchCount === 1 ? 'file' : 'files', "."] })) : null, _jsxs("div", { className: 'overflow-hidden rounded-lg border border-[color:var(--border-strong)]', children: [rows.map((r) => {
34
- const meta = revealed.get(r.id);
35
- const title = typeof meta?.title === 'string' ? meta.title : null;
36
- return (_jsxs("div", { className: `flex items-center justify-between gap-3 border-b border-[color:var(--border-strong)] px-3 py-2 last:border-b-0 ${
37
- // `--card-overlay` (white/navy per theme.css) isn't the tint this row wants —
38
- // the original rgba(16,185,129,0.08) fallback was an emerald-accent highlight,
39
- // not the neutral card-overlay token, so build the same tint from the real
40
- // accent token via color-mix instead of a literal fallback color.
41
- title ? 'bg-[color:color-mix(in_srgb,var(--accent-solid)_8%,transparent)]' : ''}`, children: [_jsx("div", { className: 'min-w-0 flex-1 truncate', children: title ? (_jsx("span", { className: 'font-medium text-[color:var(--accent)]', children: title })) : (_jsxs("span", { className: 'font-mono text-[color:var(--muted-foreground)]', children: ["\uD83D\uDD12 ", r.id.slice(0, 8), "\u2026"] })) }), _jsx("div", { className: 'shrink-0 tabular-nums text-[color:var(--muted-foreground)]', children: fmtBytes(r.sizeBytes) }), _jsx("div", { className: 'shrink-0 tabular-nums text-[color:var(--muted-foreground)]', children: fmtDate(r.createdAt) })] }, r.id));
42
- }), rows.length === 0 ? (_jsx("div", { className: 'px-3 py-6 text-center text-[color:var(--muted-foreground)]', children: "No private files." })) : null] })] }));
43
- }
44
- //# sourceMappingURL=PrivateIndexView.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PrivateIndexView.js","sourceRoot":"","sources":["../../../src/react/media/PrivateIndexView.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,IAAI,QAAQ,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAG/C,4FAA4F;AAC5F,gGAAgG;AAChG,8FAA8F;AAC9F,gFAAgF;AAEhF,MAAM,OAAO,GAAG,CAAC,EAAU,EAAU,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAEhF,MAAM,UAAU,gBAAgB,CAAC,EAAE,MAAM,EAA6B;IACpE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAoB,EAAE,CAAC,CAAC;IAC9D,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAuC,IAAI,GAAG,EAAE,CAAC,CAAC;IAChG,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACnD,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAElD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,KAAK,UAAU,WAAW;QACxB,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,SAAS,CAAC,IAAI,CAAC,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC5C,WAAW,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACzD,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAC7D,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC;IAEjC,OAAO,CACL,eAAK,SAAS,EAAC,6BAA6B,aAC1C,eAAK,SAAS,EAAC,yCAAyC,aACtD,eAAK,SAAS,EAAC,sCAAsC,aAClD,IAAI,CAAC,MAAM,cAAU,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,cAAK,QAAQ,CAAC,UAAU,CAAC,IAC/E,EACN,eAAK,SAAS,EAAC,yBAAyB,aACtC,KAAC,KAAK,IACJ,IAAI,EAAC,UAAU,EACf,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAC5C,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,EAAE,EAC5B,WAAW,EAAC,sBAAiB,gBAClB,gBAAgB,EAC3B,SAAS,EAAC,wCAAwC,GAClD,EACF,KAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,MAAM,IAAI,CAAC,QAAQ,EAC7B,OAAO,EAAE,MAAM,EACf,OAAO,EAAC,6CAA6C,YAEpD,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,GACvB,IACL,IACF,EAEL,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAChB,eAAK,SAAS,EAAC,4BAA4B,uCAClB,UAAU,OAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,SACnE,CACP,CAAC,CAAC,CAAC,IAAI,EAER,eAAK,SAAS,EAAC,uEAAuE,aACnF,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;wBACd,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;wBAChC,MAAM,KAAK,GAAG,OAAO,IAAI,EAAE,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;wBAClE,OAAO,CACL,eAEE,SAAS,EAAE,kHAAkH;4BAC3H,8EAA8E;4BAC9E,+EAA+E;4BAC/E,2EAA2E;4BAC3E,kEAAkE;4BAClE,KAAK,CAAC,CAAC,CAAC,kEAAkE,CAAC,CAAC,CAAC,EAC/E,EAAE,aAEF,cAAK,SAAS,EAAC,yBAAyB,YACrC,KAAK,CAAC,CAAC,CAAC,CACP,eAAM,SAAS,EAAC,wCAAwC,YAAE,KAAK,GAAQ,CACxE,CAAC,CAAC,CAAC,CACF,gBAAM,SAAS,EAAC,gDAAgD,8BAC1D,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,cACf,CACR,GACG,EACN,cAAK,SAAS,EAAC,4DAA4D,YACxE,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,GAClB,EACN,cAAK,SAAS,EAAC,4DAA4D,YACxE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,GACjB,KAvBD,CAAC,CAAC,EAAE,CAwBL,CACP,CAAC;oBACJ,CAAC,CAAC,EACD,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACnB,cAAK,SAAS,EAAC,4DAA4D,kCAErE,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,IACF,CACP,CAAC;AACJ,CAAC"}
@@ -1,17 +0,0 @@
1
- export interface PrivateModeBadgeProps {
2
- /** How many items are private (shown in the label). */
3
- hiddenCount: number;
4
- /** Whether the feed is currently filtered to private-only. */
5
- active: boolean;
6
- /** Toggle the private-only filter. */
7
- onToggle: () => void;
8
- className?: string;
9
- }
10
- /**
11
- * The "🔓 N private" toggle pill shown in a media gallery's header once private
12
- * mode is unlocked — click filters the feed to private-only items. One shared
13
- * implementation for every gallery (video/photo/media); it was previously
14
- * copy-pasted per plugin with per-plugin CSS classes.
15
- */
16
- export declare function PrivateModeBadge({ hiddenCount, active, onToggle, className }: PrivateModeBadgeProps): import("react").JSX.Element;
17
- //# sourceMappingURL=PrivateModeBadge.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PrivateModeBadge.d.ts","sourceRoot":"","sources":["../../../src/react/media/PrivateModeBadge.tsx"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IACpC,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,MAAM,EAAE,OAAO,CAAC;IAChB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,qBAAqB,+BA8BnG"}
@@ -1,24 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { cn } from '../lib/cn.js';
3
- import { Tooltip } from '../components/Tooltip.js';
4
- /**
5
- * The "🔓 N private" toggle pill shown in a media gallery's header once private
6
- * mode is unlocked — click filters the feed to private-only items. One shared
7
- * implementation for every gallery (video/photo/media); it was previously
8
- * copy-pasted per plugin with per-plugin CSS classes.
9
- */
10
- export function PrivateModeBadge({ hiddenCount, active, onToggle, className }) {
11
- return (_jsx(Tooltip, { content: 'Private mode unlocked \u2014 click to show only private items', children: _jsxs("button", {
12
- // guardrails-ignore no-raw-action-button: compact header pill with pressed
13
- // state — Button's control-height/variant chrome doesn't fit a badge-sized
14
- // toggle; this keeps the exact badge geometry the galleries share.
15
- type: 'button', "data-cb-private-badge": '', "aria-pressed": active, onClick: onToggle, className: cn('inline-flex shrink-0 cursor-pointer items-center gap-1 rounded-full border border-border bg-surface px-2.5 py-0.5 text-xs font-medium text-muted-foreground transition-colors', 'hover:border-accent hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
16
- // `text-accent-strong`, not plain `text-accent` — the /15 tint only clears
17
- // 4.43:1 with the base accent hue, under AA (cbux-101). (Previously
18
- // `text-accent-foreground`, a token this library never defines — an
19
- // undefined Tailwind color utility compiles to no rule at all, so the
20
- // pill silently fell back to inherited text color instead of an
21
- // intentional one.)
22
- active && 'border-accent bg-accent/15 text-accent-strong', className), children: [_jsx("span", { "aria-hidden": true, children: "\uD83D\uDD13" }), hiddenCount, " private"] }) }));
23
- }
24
- //# sourceMappingURL=PrivateModeBadge.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PrivateModeBadge.js","sourceRoot":"","sources":["../../../src/react/media/PrivateModeBadge.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAYnD;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAyB;IAClG,OAAO,CACL,KAAC,OAAO,IAAC,OAAO,EAAC,+DAA0D,YACzE;YACE,2EAA2E;YAC3E,2EAA2E;YAC3E,mEAAmE;YACnE,IAAI,EAAC,QAAQ,2BACS,EAAE,kBACV,MAAM,EACpB,OAAO,EAAE,QAAQ,EACjB,SAAS,EAAE,EAAE,CACX,+KAA+K,EAC/K,2CAA2C,EAC3C,yEAAyE;YACzE,2EAA2E;YAC3E,oEAAoE;YACpE,oEAAoE;YACpE,sEAAsE;YACtE,gEAAgE;YAChE,oBAAoB;YACpB,MAAM,IAAI,+CAA+C,EACzD,SAAS,CACV,aAED,+DAA2B,EAC1B,WAAW,gBACL,GACD,CACX,CAAC;AACJ,CAAC"}
@@ -1,34 +0,0 @@
1
- /** A clear index row — non-sensitive facts only (matches the server's PrivateIndexRow). */
2
- export type PrivateIndexRow = {
3
- id: string;
4
- sizeBytes: number;
5
- createdAt: number;
6
- status: 'pending' | 'processing' | 'ready' | 'failed';
7
- };
8
- export type RevealedItem = {
9
- id: string;
10
- meta: Record<string, unknown>;
11
- };
12
- export interface PrivateClientConfig {
13
- /** Origin+prefix for the private routes. '' = same-origin; an absolute API origin for a
14
- * split-origin deploy. */
15
- apiBase?: string;
16
- /** Route mount prefix (must match mountPrivateRoutes basePath). Default '/_private'. */
17
- basePath?: string;
18
- /** Extra headers for the GUARDED calls (unlock/index/reveal) — e.g. the app's Bearer. The
19
- * token-embedded byte/key URLs need no header. */
20
- authHeaders?: () => Record<string, string>;
21
- }
22
- export declare const privateBaseUrl: (cfg: PrivateClientConfig) => string;
23
- /** Try `password` against every item; returns the ones it decrypts (with their metadata). */
24
- export declare const revealPrivateWithPassword: (cfg: PrivateClientConfig, password: string) => Promise<RevealedItem[]>;
25
- /** Reveal the items an UNLOCK TOKEN opens — no password re-entry. The token must belong to the
26
- * session caller (the server enforces it). */
27
- export declare const revealPrivateWithToken: (cfg: PrivateClientConfig, token: string) => Promise<RevealedItem[]>;
28
- /** The token-embedded HLS master URL for a private video — same shape usePrivateUnlock.hlsUrl
29
- * builds. Needs no auth header (the token IS the authorization). */
30
- export declare const privateHlsUrlWithToken: (cfg: PrivateClientConfig, token: string, sourceId: string, videoId: string) => string;
31
- /** The token-embedded URL for a private NON-video file (photo/doc) stored as an encrypted
32
- * blob — served decrypted by the gated blob route. Needs no auth header. */
33
- export declare const privateFileUrlWithToken: (cfg: PrivateClientConfig, token: string, fileId: string) => string;
34
- //# sourceMappingURL=privateClient.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"privateClient.d.ts","sourceRoot":"","sources":["../../../src/react/media/privateClient.ts"],"names":[],"mappings":"AAOA,2FAA2F;AAC3F,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAEzE,MAAM,WAAW,mBAAmB;IAClC;8BAC0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;sDACkD;IAClD,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAED,eAAO,MAAM,cAAc,GAAI,KAAK,mBAAmB,KAAG,MACJ,CAAC;AAwBvD,6FAA6F;AAC7F,eAAO,MAAM,yBAAyB,GACpC,KAAK,mBAAmB,EACxB,UAAU,MAAM,KACf,OAAO,CAAC,YAAY,EAAE,CAA+C,CAAC;AAEzE;8CAC8C;AAC9C,eAAO,MAAM,sBAAsB,GACjC,KAAK,mBAAmB,EACxB,OAAO,MAAM,KACZ,OAAO,CAAC,YAAY,EAAE,CAAqD,CAAC;AAE/E;oEACoE;AACpE,eAAO,MAAM,sBAAsB,GACjC,KAAK,mBAAmB,EACxB,OAAO,MAAM,EACb,UAAU,MAAM,EAChB,SAAS,MAAM,KACd,MAAoF,CAAC;AAExF;4EAC4E;AAC5E,eAAO,MAAM,uBAAuB,GAClC,KAAK,mBAAmB,EACxB,OAAO,MAAM,EACb,QAAQ,MAAM,KACb,MAA4D,CAAC"}
@@ -1,36 +0,0 @@
1
- // ── Private-media client helpers (hook-free) ────────────────────────────────────
2
- // The fetch/URL logic for the private-media routes, usable OUTSIDE React state — the unlock
3
- // token is held by one component (an unlock plugin) and handed to others (files/gallery
4
- // plugins) via a cross-plugin port, so consumers of a passed-in token need these standalone
5
- // helpers rather than their own usePrivateUnlock state. usePrivateUnlock delegates here:
6
- // one source of truth for the endpoints' shapes.
7
- export const privateBaseUrl = (cfg) => `${cfg.apiBase ?? ''}${cfg.basePath ?? '/_private'}`;
8
- const guardedHeaders = (cfg) => ({
9
- 'content-type': 'application/json',
10
- ...(cfg.authHeaders?.() ?? {}),
11
- });
12
- const postForItems = async (cfg, path, body) => {
13
- const res = await fetch(`${privateBaseUrl(cfg)}${path}`, {
14
- method: 'POST',
15
- headers: guardedHeaders(cfg),
16
- body: JSON.stringify(body),
17
- // Split-origin hosts (SPA on the apex, API on a subdomain) authenticate these
18
- // guarded calls with the app's httpOnly session cookie.
19
- credentials: 'include',
20
- });
21
- if (!res.ok)
22
- return [];
23
- return (await res.json()).items ?? [];
24
- };
25
- /** Try `password` against every item; returns the ones it decrypts (with their metadata). */
26
- export const revealPrivateWithPassword = (cfg, password) => postForItems(cfg, '/reveal', { password });
27
- /** Reveal the items an UNLOCK TOKEN opens — no password re-entry. The token must belong to the
28
- * session caller (the server enforces it). */
29
- export const revealPrivateWithToken = (cfg, token) => postForItems(cfg, '/reveal-unlocked', { token });
30
- /** The token-embedded HLS master URL for a private video — same shape usePrivateUnlock.hlsUrl
31
- * builds. Needs no auth header (the token IS the authorization). */
32
- export const privateHlsUrlWithToken = (cfg, token, sourceId, videoId) => `${privateBaseUrl(cfg)}/u/${token}/hls/${sourceId}/${videoId}/master.m3u8`;
33
- /** The token-embedded URL for a private NON-video file (photo/doc) stored as an encrypted
34
- * blob — served decrypted by the gated blob route. Needs no auth header. */
35
- export const privateFileUrlWithToken = (cfg, token, fileId) => `${privateBaseUrl(cfg)}/u/${token}/file/${fileId}`;
36
- //# sourceMappingURL=privateClient.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"privateClient.js","sourceRoot":"","sources":["../../../src/react/media/privateClient.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,4FAA4F;AAC5F,wFAAwF;AACxF,4FAA4F;AAC5F,yFAAyF;AACzF,iDAAiD;AAuBjD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAwB,EAAU,EAAE,CACjE,GAAG,GAAG,CAAC,OAAO,IAAI,EAAE,GAAG,GAAG,CAAC,QAAQ,IAAI,WAAW,EAAE,CAAC;AAEvD,MAAM,cAAc,GAAG,CAAC,GAAwB,EAA0B,EAAE,CAAC,CAAC;IAC5E,cAAc,EAAE,kBAAkB;IAClC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC;CAC/B,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,KAAK,EACxB,GAAwB,EACxB,IAAY,EACZ,IAA6B,EACJ,EAAE;IAC3B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,EAAE;QACvD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,cAAc,CAAC,GAAG,CAAC;QAC5B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAC1B,8EAA8E;QAC9E,wDAAwD;QACxD,WAAW,EAAE,SAAS;KACvB,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,CAAC;IACvB,OAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA+B,CAAC,KAAK,IAAI,EAAE,CAAC;AACvE,CAAC,CAAC;AAEF,6FAA6F;AAC7F,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,GAAwB,EACxB,QAAgB,EACS,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;AAEzE;8CAC8C;AAC9C,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,GAAwB,EACxB,KAAa,EACY,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,kBAAkB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;AAE/E;oEACoE;AACpE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,GAAwB,EACxB,KAAa,EACb,QAAgB,EAChB,OAAe,EACP,EAAE,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,MAAM,KAAK,QAAQ,QAAQ,IAAI,OAAO,cAAc,CAAC;AAExF;4EAC4E;AAC5E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,GAAwB,EACxB,KAAa,EACb,MAAc,EACN,EAAE,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,MAAM,KAAK,SAAS,MAAM,EAAE,CAAC"}
@@ -1,25 +0,0 @@
1
- import { type PrivateClientConfig, type PrivateIndexRow, type RevealedItem } from './privateClient.js';
2
- export type { PrivateIndexRow, RevealedItem } from './privateClient.js';
3
- export type UsePrivateUnlockOptions = PrivateClientConfig;
4
- export type PrivateUnlock = {
5
- token: string | null;
6
- mode: 'standard' | 'deniable' | null;
7
- busy: boolean;
8
- error: string | null;
9
- /** Unlock: pass a password for a deniable set, or omit for the standard (server-key) set.
10
- * Returns true on success. */
11
- unlock(password?: string): Promise<boolean>;
12
- /** Drop the token (re-lock). */
13
- lock(): void;
14
- /** Fetch the clear index (sizes + created-at) for the cleanup view. */
15
- loadIndex(): Promise<PrivateIndexRow[]>;
16
- /** Try a password against every item; returns the ones it decrypts (with their metadata). */
17
- reveal(password: string): Promise<RevealedItem[]>;
18
- /** Reveal the items the CURRENT unlock token opens — no password re-entry. Throws when
19
- * locked (no token). */
20
- revealUnlocked(): Promise<RevealedItem[]>;
21
- /** Build the token-embedded HLS master URL for a private video (null when locked). */
22
- hlsUrl(sourceId: string, videoId: string): string | null;
23
- };
24
- export declare function usePrivateUnlock(opts?: UsePrivateUnlockOptions): PrivateUnlock;
25
- //# sourceMappingURL=usePrivateUnlock.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"usePrivateUnlock.d.ts","sourceRoot":"","sources":["../../../src/react/media/usePrivateUnlock.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAGpB,KAAK,YAAY,EAGlB,MAAM,oBAAoB,CAAC;AAU5B,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAExE,MAAM,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE1D,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;kCAC8B;IAC9B,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,gCAAgC;IAChC,IAAI,IAAI,IAAI,CAAC;IACb,uEAAuE;IACvE,SAAS,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IACxC,6FAA6F;IAC7F,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAClD;4BACwB;IACxB,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC1C,sFAAsF;IACtF,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAC1D,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,uBAA4B,GAAG,aAAa,CAuFlF"}
@@ -1,68 +0,0 @@
1
- import React from 'react';
2
- import { privateBaseUrl, privateHlsUrlWithToken, revealPrivateWithPassword, revealPrivateWithToken, } from './privateClient.js';
3
- export function usePrivateUnlock(opts = {}) {
4
- const { apiBase, basePath, authHeaders } = opts;
5
- const cfg = React.useMemo(() => ({ apiBase, basePath, authHeaders }), [apiBase, basePath, authHeaders]);
6
- const base = privateBaseUrl(cfg);
7
- const [token, setToken] = React.useState(null);
8
- const [mode, setMode] = React.useState(null);
9
- const [busy, setBusy] = React.useState(false);
10
- const [error, setError] = React.useState(null);
11
- const guarded = React.useCallback((extra) => ({
12
- 'content-type': 'application/json',
13
- ...(authHeaders?.() ?? {}),
14
- ...extra,
15
- }), [authHeaders]);
16
- const unlock = React.useCallback(async (password) => {
17
- setBusy(true);
18
- setError(null);
19
- try {
20
- const res = await fetch(`${base}/unlock`, {
21
- method: 'POST',
22
- headers: guarded(),
23
- body: JSON.stringify(password ? { password } : {}),
24
- // Split-origin hosts authenticate via the app's httpOnly session cookie —
25
- // matches postForItems in ./privateClient and createServiceClient.
26
- credentials: 'include',
27
- });
28
- if (!res.ok) {
29
- setError('Unlock failed');
30
- return false;
31
- }
32
- const data = (await res.json());
33
- setToken(data.token);
34
- setMode(data.mode);
35
- return true;
36
- }
37
- catch {
38
- setError('Unlock failed');
39
- return false;
40
- }
41
- finally {
42
- setBusy(false);
43
- }
44
- }, [base, guarded]);
45
- const lock = React.useCallback(() => {
46
- setToken(null);
47
- setMode(null);
48
- }, []);
49
- const loadIndex = React.useCallback(async () => {
50
- const res = await fetch(`${base}/index`, { headers: guarded(), credentials: 'include' });
51
- if (!res.ok)
52
- return [];
53
- return (await res.json()).items ?? [];
54
- }, [base, guarded]);
55
- const reveal = React.useCallback((password) => revealPrivateWithPassword(cfg, password), [cfg]);
56
- const revealUnlocked = React.useCallback(() => {
57
- if (!token)
58
- return Promise.reject(new Error('locked — unlock first'));
59
- return revealPrivateWithToken(cfg, token);
60
- }, [cfg, token]);
61
- const hlsUrl = React.useCallback((sourceId, videoId) => token ? privateHlsUrlWithToken(cfg, token, sourceId, videoId) : null, [cfg, token]);
62
- // Memoise the returned object so it's a STABLE reference across renders (the callbacks are
63
- // already useCallback-stable; only real state changes flip it). Without this, consumers doing
64
- // `useEffect(() => { unlock.loadIndex()… }, [unlock])` would loop — a new object every render
65
- // re-runs the effect → setState → re-render → "Maximum update depth exceeded".
66
- return React.useMemo(() => ({ token, mode, busy, error, unlock, lock, loadIndex, reveal, revealUnlocked, hlsUrl }), [token, mode, busy, error, unlock, lock, loadIndex, reveal, revealUnlocked, hlsUrl]);
67
- }
68
- //# sourceMappingURL=usePrivateUnlock.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"usePrivateUnlock.js","sourceRoot":"","sources":["../../../src/react/media/usePrivateUnlock.ts"],"names":[],"mappings":"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"}
@@ -1,113 +0,0 @@
1
- import { formatBytes as fmtBytes } from 'cwip/format';
2
- import React from 'react';
3
- import { Button } from '../components/Button.js';
4
- import { Input } from '../components/Input.js';
5
- import type { PrivateIndexRow, PrivateUnlock } from './usePrivateUnlock.js';
6
-
7
- // The locked-index cleanup view: every private item's SIZE + created-date (never its title,
8
- // thumbnail, or notes), so a user can prune media whose password they've lost. A password field
9
- // runs "try-a-password" — items it decrypts light up with their revealed title; the rest stay
10
- // opaque. Themed off cursedbelt tokens (with fallbacks so it renders anywhere).
11
-
12
- const fmtDate = (ts: number): string => new Date(ts).toISOString().slice(0, 10);
13
-
14
- export function PrivateIndexView({ unlock }: { unlock: PrivateUnlock }) {
15
- const [rows, setRows] = React.useState<PrivateIndexRow[]>([]);
16
- const [revealed, setRevealed] = React.useState<Map<string, Record<string, unknown>>>(new Map());
17
- const [password, setPassword] = React.useState('');
18
- const [trying, setTrying] = React.useState(false);
19
-
20
- React.useEffect(() => {
21
- unlock.loadIndex().then(setRows);
22
- }, [unlock]);
23
-
24
- async function tryPassword() {
25
- if (!password) return;
26
- setTrying(true);
27
- try {
28
- const items = await unlock.reveal(password);
29
- setRevealed(new Map(items.map((i) => [i.id, i.meta])));
30
- } finally {
31
- setTrying(false);
32
- }
33
- }
34
-
35
- const totalBytes = rows.reduce((s, r) => s + r.sizeBytes, 0);
36
- const matchCount = revealed.size;
37
-
38
- return (
39
- <div className='flex flex-col gap-3 text-sm'>
40
- <div className='flex items-center justify-between gap-3'>
41
- <div className='text-[color:var(--muted-foreground)]'>
42
- {rows.length} locked {rows.length === 1 ? 'file' : 'files'} · {fmtBytes(totalBytes)}
43
- </div>
44
- <div className='flex items-center gap-2'>
45
- <Input
46
- type='password'
47
- value={password}
48
- onChange={(e) => setPassword(e.target.value)}
49
- onEnter={() => tryPassword()}
50
- placeholder='Try a password…'
51
- aria-label='Try a password'
52
- className='h-auto w-auto bg-transparent px-2 py-1'
53
- />
54
- <Button
55
- size='sm'
56
- onClick={tryPassword}
57
- disabled={trying || !password}
58
- loading={trying}
59
- tooltip='Try this password against every locked file'
60
- >
61
- {trying ? 'Trying…' : 'Reveal'}
62
- </Button>
63
- </div>
64
- </div>
65
-
66
- {matchCount > 0 ? (
67
- <div className='text-[color:var(--accent)]'>
68
- This password unlocks {matchCount} {matchCount === 1 ? 'file' : 'files'}.
69
- </div>
70
- ) : null}
71
-
72
- <div className='overflow-hidden rounded-lg border border-[color:var(--border-strong)]'>
73
- {rows.map((r) => {
74
- const meta = revealed.get(r.id);
75
- const title = typeof meta?.title === 'string' ? meta.title : null;
76
- return (
77
- <div
78
- key={r.id}
79
- className={`flex items-center justify-between gap-3 border-b border-[color:var(--border-strong)] px-3 py-2 last:border-b-0 ${
80
- // `--card-overlay` (white/navy per theme.css) isn't the tint this row wants —
81
- // the original rgba(16,185,129,0.08) fallback was an emerald-accent highlight,
82
- // not the neutral card-overlay token, so build the same tint from the real
83
- // accent token via color-mix instead of a literal fallback color.
84
- title ? 'bg-[color:color-mix(in_srgb,var(--accent-solid)_8%,transparent)]' : ''
85
- }`}
86
- >
87
- <div className='min-w-0 flex-1 truncate'>
88
- {title ? (
89
- <span className='font-medium text-[color:var(--accent)]'>{title}</span>
90
- ) : (
91
- <span className='font-mono text-[color:var(--muted-foreground)]'>
92
- 🔒 {r.id.slice(0, 8)}…
93
- </span>
94
- )}
95
- </div>
96
- <div className='shrink-0 tabular-nums text-[color:var(--muted-foreground)]'>
97
- {fmtBytes(r.sizeBytes)}
98
- </div>
99
- <div className='shrink-0 tabular-nums text-[color:var(--muted-foreground)]'>
100
- {fmtDate(r.createdAt)}
101
- </div>
102
- </div>
103
- );
104
- })}
105
- {rows.length === 0 ? (
106
- <div className='px-3 py-6 text-center text-[color:var(--muted-foreground)]'>
107
- No private files.
108
- </div>
109
- ) : null}
110
- </div>
111
- </div>
112
- );
113
- }
@@ -1,50 +0,0 @@
1
- import { cn } from '../lib/cn.js';
2
- import { Tooltip } from '../components/Tooltip.js';
3
-
4
- export interface PrivateModeBadgeProps {
5
- /** How many items are private (shown in the label). */
6
- hiddenCount: number;
7
- /** Whether the feed is currently filtered to private-only. */
8
- active: boolean;
9
- /** Toggle the private-only filter. */
10
- onToggle: () => void;
11
- className?: string;
12
- }
13
-
14
- /**
15
- * The "🔓 N private" toggle pill shown in a media gallery's header once private
16
- * mode is unlocked — click filters the feed to private-only items. One shared
17
- * implementation for every gallery (video/photo/media); it was previously
18
- * copy-pasted per plugin with per-plugin CSS classes.
19
- */
20
- export function PrivateModeBadge({ hiddenCount, active, onToggle, className }: PrivateModeBadgeProps) {
21
- return (
22
- <Tooltip content='Private mode unlocked — click to show only private items'>
23
- <button
24
- // guardrails-ignore no-raw-action-button: compact header pill with pressed
25
- // state — Button's control-height/variant chrome doesn't fit a badge-sized
26
- // toggle; this keeps the exact badge geometry the galleries share.
27
- type='button'
28
- data-cb-private-badge=''
29
- aria-pressed={active}
30
- onClick={onToggle}
31
- className={cn(
32
- 'inline-flex shrink-0 cursor-pointer items-center gap-1 rounded-full border border-border bg-surface px-2.5 py-0.5 text-xs font-medium text-muted-foreground transition-colors',
33
- 'hover:border-accent hover:text-foreground',
34
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
35
- // `text-accent-strong`, not plain `text-accent` — the /15 tint only clears
36
- // 4.43:1 with the base accent hue, under AA (cbux-101). (Previously
37
- // `text-accent-foreground`, a token this library never defines — an
38
- // undefined Tailwind color utility compiles to no rule at all, so the
39
- // pill silently fell back to inherited text color instead of an
40
- // intentional one.)
41
- active && 'border-accent bg-accent/15 text-accent-strong',
42
- className,
43
- )}
44
- >
45
- <span aria-hidden>🔓</span>
46
- {hiddenCount} private
47
- </button>
48
- </Tooltip>
49
- );
50
- }
@@ -1,113 +0,0 @@
1
- import { afterEach, describe, expect, mock, test } from 'bun:test';
2
- import { act, renderHook } from '@testing-library/react';
3
- import {
4
- privateHlsUrlWithToken,
5
- revealPrivateWithPassword,
6
- revealPrivateWithToken,
7
- } from './privateClient.js';
8
- import { usePrivateUnlock } from './usePrivateUnlock.js';
9
-
10
- // Mock fetch — the helpers are pure client plumbing; assert the request they build and the
11
- // response shape they return.
12
- const realFetch = globalThis.fetch;
13
- type FetchCall = { url: string; init?: RequestInit };
14
- const mockFetch = (status: number, body: unknown) => {
15
- const calls: FetchCall[] = [];
16
- globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
17
- calls.push({ url: String(url), init });
18
- return new Response(JSON.stringify(body), {
19
- status,
20
- headers: { 'content-type': 'application/json' },
21
- });
22
- }) as unknown as typeof fetch;
23
- return calls;
24
- };
25
-
26
- afterEach(() => {
27
- globalThis.fetch = realFetch;
28
- });
29
-
30
- describe('private client helpers (hook-free)', () => {
31
- test('revealPrivateWithToken POSTs the token to /reveal-unlocked and returns items', async () => {
32
- const items = [{ id: 'a1', meta: { title: 'Alpha' } }];
33
- const calls = mockFetch(200, { items });
34
-
35
- const out = await revealPrivateWithToken(
36
- { apiBase: 'https://api.example', authHeaders: () => ({ authorization: 'Bearer T' }) },
37
- 'tok123',
38
- );
39
- expect(out).toEqual(items);
40
- expect(calls[0].url).toBe('https://api.example/_private/reveal-unlocked');
41
- expect(calls[0].init?.method).toBe('POST');
42
- expect(JSON.parse(calls[0].init?.body as string)).toEqual({ token: 'tok123' });
43
- const headers = calls[0].init?.headers as Record<string, string>;
44
- expect(headers.authorization).toBe('Bearer T');
45
- });
46
-
47
- test('revealPrivateWithPassword POSTs the password to /reveal', async () => {
48
- const calls = mockFetch(200, { items: [] });
49
- await revealPrivateWithPassword({ basePath: '/_pm' }, 'pw');
50
- expect(calls[0].url).toBe('/_pm/reveal');
51
- expect(JSON.parse(calls[0].init?.body as string)).toEqual({ password: 'pw' });
52
- });
53
-
54
- test('reveal helpers return [] on a non-OK response', async () => {
55
- mockFetch(401, { error: 'unauthorized' });
56
- expect(await revealPrivateWithToken({}, 'bad')).toEqual([]);
57
- expect(await revealPrivateWithPassword({}, 'bad')).toEqual([]);
58
- });
59
-
60
- test('privateHlsUrlWithToken builds the token-embedded master URL', () => {
61
- expect(privateHlsUrlWithToken({}, 'tok', 'gallery', 'v1')).toBe(
62
- '/_private/u/tok/hls/gallery/v1/master.m3u8',
63
- );
64
- expect(
65
- privateHlsUrlWithToken({ apiBase: 'https://api.example', basePath: '/_pm' }, 'tok', 'g', 'v'),
66
- ).toBe('https://api.example/_pm/u/tok/hls/g/v/master.m3u8');
67
- });
68
- });
69
-
70
- describe('usePrivateUnlock — revealUnlocked', () => {
71
- test('rejects while locked (no token)', async () => {
72
- const { result } = renderHook(() => usePrivateUnlock());
73
- expect(result.current.token).toBeNull();
74
- await expect(result.current.revealUnlocked()).rejects.toThrow('locked');
75
- });
76
-
77
- test('uses the stored token after unlock and hits /reveal-unlocked', async () => {
78
- const items = [{ id: 'x', meta: { title: 'X' } }];
79
- const calls: FetchCall[] = [];
80
- globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
81
- calls.push({ url: String(url), init });
82
- const u = String(url);
83
- if (u.endsWith('/unlock')) {
84
- return Response.json({ token: 'tok-777', mode: 'deniable' });
85
- }
86
- return Response.json({ items });
87
- }) as unknown as typeof fetch;
88
-
89
- const { result } = renderHook(() => usePrivateUnlock());
90
- await act(async () => {
91
- expect(await result.current.unlock('pw')).toBe(true);
92
- });
93
- expect(result.current.token).toBe('tok-777');
94
-
95
- const out = await result.current.revealUnlocked();
96
- expect(out).toEqual(items);
97
- const revealCall = calls.find((c) => c.url.endsWith('/reveal-unlocked'));
98
- expect(revealCall).toBeDefined();
99
- expect(JSON.parse(revealCall?.init?.body as string)).toEqual({ token: 'tok-777' });
100
- });
101
-
102
- test('hlsUrl delegates to privateHlsUrlWithToken (same URL shape)', async () => {
103
- globalThis.fetch = mock(async () =>
104
- Response.json({ token: 'tok-9', mode: 'standard' }),
105
- ) as unknown as typeof fetch;
106
- const { result } = renderHook(() => usePrivateUnlock());
107
- expect(result.current.hlsUrl('g', 'v')).toBeNull(); // locked
108
- await act(async () => {
109
- await result.current.unlock();
110
- });
111
- expect(result.current.hlsUrl('g', 'v')).toBe(privateHlsUrlWithToken({}, 'tok-9', 'g', 'v'));
112
- });
113
- });