nextpluggable 0.0.0-stage → 0.1.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 (71) hide show
  1. package/bin/nextpluggable.js +10 -0
  2. package/dist/api/handler.d.ts +10 -0
  3. package/dist/api/handler.js +188 -0
  4. package/dist/auth/custom.d.ts +13 -0
  5. package/dist/auth/custom.js +12 -0
  6. package/dist/auth/password.d.ts +10 -0
  7. package/dist/auth/password.js +66 -0
  8. package/dist/auth/types.d.ts +23 -0
  9. package/dist/auth/types.js +1 -0
  10. package/dist/cli.d.ts +9 -0
  11. package/dist/cli.js +119 -0
  12. package/dist/client/host-runtime.d.ts +9 -0
  13. package/dist/client/host-runtime.js +71 -0
  14. package/dist/client/island.d.ts +28 -0
  15. package/dist/client/island.js +177 -0
  16. package/dist/client/ssr-guard.d.ts +11 -0
  17. package/dist/client/ssr-guard.js +100 -0
  18. package/dist/config.d.ts +46 -0
  19. package/dist/config.js +55 -0
  20. package/dist/core/installer.d.ts +30 -0
  21. package/dist/core/installer.js +340 -0
  22. package/dist/core/routes.d.ts +20 -0
  23. package/dist/core/routes.js +95 -0
  24. package/dist/core/runtime.d.ts +32 -0
  25. package/dist/core/runtime.js +190 -0
  26. package/dist/core/state.d.ts +13 -0
  27. package/dist/core/state.js +23 -0
  28. package/dist/core/store.d.ts +17 -0
  29. package/dist/core/store.js +58 -0
  30. package/dist/core/types.d.ts +69 -0
  31. package/dist/core/types.js +1 -0
  32. package/dist/crypto.d.ts +14 -0
  33. package/dist/crypto.js +61 -0
  34. package/dist/dashboard/Dashboard.d.ts +11 -0
  35. package/dist/dashboard/Dashboard.js +324 -0
  36. package/dist/dashboard/styles.d.ts +2 -0
  37. package/dist/dashboard/styles.js +129 -0
  38. package/dist/dashboard/ui.d.ts +61 -0
  39. package/dist/dashboard/ui.js +135 -0
  40. package/dist/errors.d.ts +15 -0
  41. package/dist/errors.js +29 -0
  42. package/dist/globals.d.ts +24 -0
  43. package/dist/globals.js +24 -0
  44. package/dist/index.d.ts +40 -0
  45. package/dist/index.js +48 -0
  46. package/dist/next/dashboard-page.d.ts +5 -0
  47. package/dist/next/dashboard-page.js +23 -0
  48. package/dist/next/plugin-page.d.ts +16 -0
  49. package/dist/next/plugin-page.js +102 -0
  50. package/dist/next/slot.d.ts +7 -0
  51. package/dist/next/slot.js +42 -0
  52. package/dist/sources/github.d.ts +19 -0
  53. package/dist/sources/github.js +109 -0
  54. package/dist/sources/local.d.ts +3 -0
  55. package/dist/sources/local.js +22 -0
  56. package/dist/sources/reader.d.ts +17 -0
  57. package/dist/sources/reader.js +78 -0
  58. package/dist/storage/file.d.ts +12 -0
  59. package/dist/storage/file.js +61 -0
  60. package/dist/storage/memory.d.ts +3 -0
  61. package/dist/storage/memory.js +22 -0
  62. package/dist/storage/postgres.d.ts +15 -0
  63. package/dist/storage/postgres.js +39 -0
  64. package/dist/storage/types.d.ts +18 -0
  65. package/dist/storage/types.js +15 -0
  66. package/dist/storage/upstash.d.ts +15 -0
  67. package/dist/storage/upstash.js +61 -0
  68. package/dist/views.d.ts +112 -0
  69. package/dist/views.js +124 -0
  70. package/package.json +48 -3
  71. package/README.md +0 -3
@@ -0,0 +1,2 @@
1
+ /** Dashboard styles, shipped as a string so the package needs no CSS tooling in the host site. Everything is scoped under `.np-root`. */
2
+ export declare const dashboardCss = "\n.np-root{--np-bg:#f5f6f8;--np-surface:#fff;--np-surface-2:#f9fafb;--np-text:#111827;--np-muted:#667085;--np-border:#e4e7ec;--np-primary:#4f46e5;--np-primary-hover:#4338ca;--np-primary-soft:#eef2ff;--np-danger:#d92d20;--np-danger-soft:#fef3f2;--np-success:#067647;--np-success-soft:#ecfdf3;--np-warn:#b54708;--np-warn-soft:#fffaeb;--np-shadow:0 1px 2px rgba(16,24,40,.06),0 1px 3px rgba(16,24,40,.08);--np-radius:12px;--np-accent:var(--np-primary);\nposition:fixed;inset:0;z-index:2147483000;overflow-y:auto;background:var(--np-bg);color:var(--np-text);font:15px/1.5 ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif;-webkit-font-smoothing:antialiased;text-align:left}\n@media (prefers-color-scheme:dark){.np-root{--np-bg:#0c0e13;--np-surface:#161a22;--np-surface-2:#1b202a;--np-text:#eceef2;--np-muted:#98a2b3;--np-border:#2a303c;--np-primary:#818cf8;--np-primary-hover:#a5b4fc;--np-primary-soft:#1e2140;--np-danger:#f97066;--np-danger-soft:#3a1614;--np-success:#47cd89;--np-success-soft:#0d2a1c;--np-warn:#fdb022;--np-warn-soft:#33240a;--np-shadow:0 1px 2px rgba(0,0,0,.4)}}\n.np-root *,.np-root *::before,.np-root *::after{box-sizing:border-box}\n.np-root h1,.np-root h2,.np-root h3,.np-root p,.np-root ul,.np-root ol,.np-root dl,.np-root dd{margin:0;padding:0}\n.np-root ul,.np-root ol{list-style:none}\n.np-root a{color:var(--np-primary);text-decoration:none}\n.np-root a:hover{text-decoration:underline}\n.np-root button,.np-root input,.np-root select,.np-root textarea{font:inherit;color:inherit}\n.np-root :focus-visible{outline:2px solid var(--np-primary);outline-offset:2px}\n.np-top{background:var(--np-surface);border-bottom:1px solid var(--np-border);position:sticky;top:0;z-index:5}\n.np-top-in{max-width:1040px;margin:0 auto;padding:0 20px;display:flex;align-items:center;gap:20px;min-height:60px;flex-wrap:wrap}\n.np-brand{display:flex;align-items:center;gap:10px;font-weight:650;font-size:16px;margin-right:auto;color:var(--np-text)!important;text-decoration:none!important}\n.np-brand-mark{width:28px;height:28px;border-radius:8px;background:var(--np-primary);color:#fff;display:grid;place-items:center;font-size:15px}\n.np-brand small{font-weight:400;color:var(--np-muted);font-size:13px}\n.np-nav{display:flex;gap:4px;order:3;width:100%;margin:0 -8px;overflow-x:auto}\n@media (min-width:720px){.np-nav{order:0;width:auto;margin:0}}\n.np-nav a{padding:8px 12px;border-radius:8px;color:var(--np-muted);font-weight:500;white-space:nowrap;text-decoration:none!important}\n.np-nav a:hover{color:var(--np-text);background:var(--np-surface-2)}\n.np-nav a[aria-current=page]{color:var(--np-primary);background:var(--np-primary-soft)}\n.np-main{max-width:1040px;margin:0 auto;padding:28px 20px 80px}\n.np-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px;flex-wrap:wrap}\n.np-head h1{font-size:24px;line-height:1.25;letter-spacing:-.01em}\n.np-head p{color:var(--np-muted);margin-top:4px;max-width:60ch}\n.np-card{background:var(--np-surface);border:1px solid var(--np-border);border-radius:var(--np-radius);box-shadow:var(--np-shadow)}\n.np-pad{padding:20px}\n.np-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--np-border);background:var(--np-surface);border-radius:8px;padding:8px 14px;font-weight:550;cursor:pointer;white-space:nowrap;text-decoration:none!important;color:var(--np-text)!important;min-height:38px}\n.np-btn:hover{background:var(--np-surface-2)}\n.np-btn:disabled{opacity:.55;cursor:not-allowed}\n.np-btn-primary{background:var(--np-primary);border-color:var(--np-primary);color:#fff!important}\n.np-btn-primary:hover{background:var(--np-primary-hover);border-color:var(--np-primary-hover)}\n@media (prefers-color-scheme:dark){.np-btn-primary{color:#0c0e13!important}}\n.np-btn-danger{color:var(--np-danger)!important}\n.np-btn-danger-solid{background:var(--np-danger);border-color:var(--np-danger);color:#fff!important}\n.np-btn-sm{padding:5px 10px;min-height:32px;font-size:14px}\n.np-btn-link{border:0;background:none;padding:0;min-height:0;color:var(--np-primary)!important;font-weight:500;cursor:pointer}\n.np-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;animation:np-spin .7s linear infinite;flex:none}\n@keyframes np-spin{to{transform:rotate(360deg)}}\n.np-list{display:grid;gap:12px}\n.np-row{display:grid;grid-template-columns:auto minmax(0,1fr);gap:14px 16px;padding:18px 20px;align-items:start}\n@media (min-width:720px){.np-row{grid-template-columns:auto minmax(0,1fr) auto;align-items:center}}\n.np-row-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;grid-column:1/-1}\n@media (min-width:720px){.np-row-actions{grid-column:auto;justify-content:flex-end}}\n.np-icon{width:44px;height:44px;border-radius:10px;background:var(--np-primary-soft);display:grid;place-items:center;font-size:22px;flex:none;overflow:hidden}\n.np-icon img{width:100%;height:100%;object-fit:cover}\n.np-title{font-weight:650;font-size:16px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.np-title a{color:inherit}\n.np-meta{color:var(--np-muted);font-size:14px}\n.np-desc{color:var(--np-muted);margin-top:2px;overflow-wrap:anywhere}\n.np-chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;background:var(--np-surface-2);color:var(--np-muted);border:1px solid var(--np-border);white-space:nowrap}\n.np-chip::before{content:\"\";width:6px;height:6px;border-radius:50%;background:currentColor}\n.np-chip-on{background:var(--np-success-soft);color:var(--np-success);border-color:transparent}\n.np-chip-warn{background:var(--np-warn-soft);color:var(--np-warn);border-color:transparent}\n.np-links{display:flex;gap:6px 14px;flex-wrap:wrap;margin-top:8px;font-size:14px}\n.np-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:14px}\n.np-tile{padding:18px;display:flex;flex-direction:column;gap:12px}\n.np-tile-head{display:flex;gap:12px;align-items:center}\n.np-tile-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:4px}\n.np-adds{font-size:14px;color:var(--np-muted);display:grid;gap:4px}\n.np-adds li{display:flex;gap:8px;overflow-wrap:anywhere}\n.np-adds li::before{content:\"+\";color:var(--np-primary);font-weight:700}\n.np-empty{text-align:center;padding:56px 24px}\n.np-empty-art{font-size:40px;margin-bottom:10px}\n.np-empty h2{font-size:19px;margin-bottom:6px}\n.np-empty p{color:var(--np-muted);max-width:46ch;margin:0 auto 18px}\n.np-switch{position:relative;display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;color:var(--np-muted);user-select:none}\n.np-switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}\n.np-switch i{width:40px;height:24px;border-radius:999px;background:var(--np-border);position:relative;transition:background .15s;flex:none}\n.np-switch i::after{content:\"\";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}\n.np-switch input:checked+i{background:var(--np-success)}\n.np-switch input:checked+i::after{transform:translateX(16px)}\n.np-switch input:focus-visible+i{outline:2px solid var(--np-primary);outline-offset:2px}\n.np-switch input:disabled+i{opacity:.5}\n.np-field{display:grid;gap:6px;margin-bottom:18px}\n.np-field:last-child{margin-bottom:0}\n.np-field label,.np-label{font-weight:600;font-size:14px}\n.np-help{color:var(--np-muted);font-size:13px}\n.np-error{color:var(--np-danger);font-size:13px;font-weight:500}\n.np-input{width:100%;border:1px solid var(--np-border);background:var(--np-surface);border-radius:8px;padding:9px 12px;min-height:40px}\n.np-input:focus{outline:2px solid var(--np-primary);outline-offset:-1px;border-color:transparent}\n.np-input[aria-invalid=true]{border-color:var(--np-danger)}\ntextarea.np-input{min-height:96px;resize:vertical}\n.np-affix{display:flex;align-items:stretch;border:1px solid var(--np-border);border-radius:8px;overflow:hidden;background:var(--np-surface)}\n.np-affix span{padding:9px 10px;background:var(--np-surface-2);color:var(--np-muted);border-right:1px solid var(--np-border);white-space:nowrap;max-width:50%;overflow:hidden;text-overflow:ellipsis;font-size:14px;display:flex;align-items:center}\n.np-affix input{border:0;flex:1;min-width:0;padding:9px 12px;background:transparent}\n.np-affix:focus-within{outline:2px solid var(--np-primary);outline-offset:-1px;border-color:transparent}\n.np-affix input:focus{outline:0}\n.np-color{display:flex;gap:8px;align-items:center}\n.np-color input[type=color]{width:44px;height:40px;padding:2px;border:1px solid var(--np-border);border-radius:8px;background:var(--np-surface);cursor:pointer}\n.np-section{margin-bottom:16px}\n.np-section>h2{font-size:15px;padding:14px 20px;border-bottom:1px solid var(--np-border)}\n.np-tabs{display:flex;gap:2px;border-bottom:1px solid var(--np-border);margin-bottom:20px;overflow-x:auto}\n.np-tabs button{border:0;background:none;padding:10px 14px;font-weight:550;color:var(--np-muted);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap}\n.np-tabs button[aria-selected=true]{color:var(--np-primary);border-bottom-color:var(--np-primary)}\n.np-savebar{position:sticky;bottom:16px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;margin-top:16px}\n.np-note{padding:12px 14px;border-radius:10px;font-size:14px;background:var(--np-primary-soft);margin-bottom:16px}\n.np-note-warn{background:var(--np-warn-soft);color:var(--np-warn)}\n.np-note-danger{background:var(--np-danger-soft);color:var(--np-danger)}\n.np-steps{counter-reset:s;display:grid;gap:8px;font-size:14px;margin:10px 0 0}\n.np-steps li{counter-increment:s;display:flex;gap:10px}\n.np-steps li::before{content:counter(s);flex:none;width:22px;height:22px;border-radius:50%;background:var(--np-primary-soft);color:var(--np-primary);font-weight:700;font-size:12px;display:grid;place-items:center}\n.np-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;background:var(--np-surface-2);border:1px solid var(--np-border);border-radius:6px;padding:1px 6px;overflow-wrap:anywhere}\n.np-overlay{position:fixed;inset:0;background:rgba(10,13,20,.55);display:grid;place-items:center;padding:16px;z-index:20}\n.np-modal{background:var(--np-surface);border-radius:14px;max-width:480px;width:100%;max-height:calc(100vh - 32px);overflow-y:auto;box-shadow:0 20px 50px rgba(0,0,0,.3)}\n.np-modal header{padding:20px 22px 0}\n.np-modal header h2{font-size:18px}\n.np-modal-body{padding:12px 22px 4px;color:var(--np-muted)}\n.np-modal-body strong{color:var(--np-text)}\n.np-modal footer{display:flex;justify-content:flex-end;gap:10px;padding:18px 22px 20px;flex-wrap:wrap}\n.np-check{display:flex;gap:10px;align-items:flex-start;font-size:14px;color:var(--np-text);margin-top:12px;cursor:pointer}\n.np-check input{margin-top:3px;width:16px;height:16px;accent-color:var(--np-primary)}\n.np-toasts{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);display:grid;gap:8px;z-index:30;width:min(440px,calc(100vw - 32px))}\n.np-toast{padding:12px 16px;border-radius:10px;background:#111827;color:#fff;box-shadow:0 8px 24px rgba(0,0,0,.25);font-size:14px;display:flex;gap:10px;align-items:flex-start}\n.np-toast-error{background:#b42318}\n.np-toast button{margin-left:auto;border:0;background:none;color:inherit;cursor:pointer;opacity:.8;font-size:16px;line-height:1}\n.np-center{min-height:100%;display:grid;place-items:center;padding:24px}\n.np-auth{width:100%;max-width:400px;padding:28px}\n.np-auth h1{font-size:21px;margin:14px 0 6px}\n.np-auth p{color:var(--np-muted);margin-bottom:18px}\n.np-search{width:240px;max-width:100%}\n.np-dl{display:grid;grid-template-columns:minmax(110px,auto) 1fr;gap:10px 20px;font-size:14px}\n.np-dl dt{color:var(--np-muted)}\n.np-dl dd{overflow-wrap:anywhere}\n.np-skel{height:84px;border-radius:var(--np-radius);background:linear-gradient(90deg,var(--np-surface) 25%,var(--np-surface-2) 50%,var(--np-surface) 75%);background-size:200% 100%;animation:np-sh 1.2s infinite;border:1px solid var(--np-border)}\n@keyframes np-sh{to{background-position:-200% 0}}\n@media (prefers-reduced-motion:reduce){.np-root *{animation:none!important;transition:none!important}}\n";
@@ -0,0 +1,129 @@
1
+ /** Dashboard styles, shipped as a string so the package needs no CSS tooling in the host site. Everything is scoped under `.np-root`. */
2
+ export const dashboardCss = `
3
+ .np-root{--np-bg:#f5f6f8;--np-surface:#fff;--np-surface-2:#f9fafb;--np-text:#111827;--np-muted:#667085;--np-border:#e4e7ec;--np-primary:#4f46e5;--np-primary-hover:#4338ca;--np-primary-soft:#eef2ff;--np-danger:#d92d20;--np-danger-soft:#fef3f2;--np-success:#067647;--np-success-soft:#ecfdf3;--np-warn:#b54708;--np-warn-soft:#fffaeb;--np-shadow:0 1px 2px rgba(16,24,40,.06),0 1px 3px rgba(16,24,40,.08);--np-radius:12px;--np-accent:var(--np-primary);
4
+ position:fixed;inset:0;z-index:2147483000;overflow-y:auto;background:var(--np-bg);color:var(--np-text);font:15px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased;text-align:left}
5
+ @media (prefers-color-scheme:dark){.np-root{--np-bg:#0c0e13;--np-surface:#161a22;--np-surface-2:#1b202a;--np-text:#eceef2;--np-muted:#98a2b3;--np-border:#2a303c;--np-primary:#818cf8;--np-primary-hover:#a5b4fc;--np-primary-soft:#1e2140;--np-danger:#f97066;--np-danger-soft:#3a1614;--np-success:#47cd89;--np-success-soft:#0d2a1c;--np-warn:#fdb022;--np-warn-soft:#33240a;--np-shadow:0 1px 2px rgba(0,0,0,.4)}}
6
+ .np-root *,.np-root *::before,.np-root *::after{box-sizing:border-box}
7
+ .np-root h1,.np-root h2,.np-root h3,.np-root p,.np-root ul,.np-root ol,.np-root dl,.np-root dd{margin:0;padding:0}
8
+ .np-root ul,.np-root ol{list-style:none}
9
+ .np-root a{color:var(--np-primary);text-decoration:none}
10
+ .np-root a:hover{text-decoration:underline}
11
+ .np-root button,.np-root input,.np-root select,.np-root textarea{font:inherit;color:inherit}
12
+ .np-root :focus-visible{outline:2px solid var(--np-primary);outline-offset:2px}
13
+ .np-top{background:var(--np-surface);border-bottom:1px solid var(--np-border);position:sticky;top:0;z-index:5}
14
+ .np-top-in{max-width:1040px;margin:0 auto;padding:0 20px;display:flex;align-items:center;gap:20px;min-height:60px;flex-wrap:wrap}
15
+ .np-brand{display:flex;align-items:center;gap:10px;font-weight:650;font-size:16px;margin-right:auto;color:var(--np-text)!important;text-decoration:none!important}
16
+ .np-brand-mark{width:28px;height:28px;border-radius:8px;background:var(--np-primary);color:#fff;display:grid;place-items:center;font-size:15px}
17
+ .np-brand small{font-weight:400;color:var(--np-muted);font-size:13px}
18
+ .np-nav{display:flex;gap:4px;order:3;width:100%;margin:0 -8px;overflow-x:auto}
19
+ @media (min-width:720px){.np-nav{order:0;width:auto;margin:0}}
20
+ .np-nav a{padding:8px 12px;border-radius:8px;color:var(--np-muted);font-weight:500;white-space:nowrap;text-decoration:none!important}
21
+ .np-nav a:hover{color:var(--np-text);background:var(--np-surface-2)}
22
+ .np-nav a[aria-current=page]{color:var(--np-primary);background:var(--np-primary-soft)}
23
+ .np-main{max-width:1040px;margin:0 auto;padding:28px 20px 80px}
24
+ .np-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px;flex-wrap:wrap}
25
+ .np-head h1{font-size:24px;line-height:1.25;letter-spacing:-.01em}
26
+ .np-head p{color:var(--np-muted);margin-top:4px;max-width:60ch}
27
+ .np-card{background:var(--np-surface);border:1px solid var(--np-border);border-radius:var(--np-radius);box-shadow:var(--np-shadow)}
28
+ .np-pad{padding:20px}
29
+ .np-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--np-border);background:var(--np-surface);border-radius:8px;padding:8px 14px;font-weight:550;cursor:pointer;white-space:nowrap;text-decoration:none!important;color:var(--np-text)!important;min-height:38px}
30
+ .np-btn:hover{background:var(--np-surface-2)}
31
+ .np-btn:disabled{opacity:.55;cursor:not-allowed}
32
+ .np-btn-primary{background:var(--np-primary);border-color:var(--np-primary);color:#fff!important}
33
+ .np-btn-primary:hover{background:var(--np-primary-hover);border-color:var(--np-primary-hover)}
34
+ @media (prefers-color-scheme:dark){.np-btn-primary{color:#0c0e13!important}}
35
+ .np-btn-danger{color:var(--np-danger)!important}
36
+ .np-btn-danger-solid{background:var(--np-danger);border-color:var(--np-danger);color:#fff!important}
37
+ .np-btn-sm{padding:5px 10px;min-height:32px;font-size:14px}
38
+ .np-btn-link{border:0;background:none;padding:0;min-height:0;color:var(--np-primary)!important;font-weight:500;cursor:pointer}
39
+ .np-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;animation:np-spin .7s linear infinite;flex:none}
40
+ @keyframes np-spin{to{transform:rotate(360deg)}}
41
+ .np-list{display:grid;gap:12px}
42
+ .np-row{display:grid;grid-template-columns:auto minmax(0,1fr);gap:14px 16px;padding:18px 20px;align-items:start}
43
+ @media (min-width:720px){.np-row{grid-template-columns:auto minmax(0,1fr) auto;align-items:center}}
44
+ .np-row-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;grid-column:1/-1}
45
+ @media (min-width:720px){.np-row-actions{grid-column:auto;justify-content:flex-end}}
46
+ .np-icon{width:44px;height:44px;border-radius:10px;background:var(--np-primary-soft);display:grid;place-items:center;font-size:22px;flex:none;overflow:hidden}
47
+ .np-icon img{width:100%;height:100%;object-fit:cover}
48
+ .np-title{font-weight:650;font-size:16px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
49
+ .np-title a{color:inherit}
50
+ .np-meta{color:var(--np-muted);font-size:14px}
51
+ .np-desc{color:var(--np-muted);margin-top:2px;overflow-wrap:anywhere}
52
+ .np-chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;background:var(--np-surface-2);color:var(--np-muted);border:1px solid var(--np-border);white-space:nowrap}
53
+ .np-chip::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
54
+ .np-chip-on{background:var(--np-success-soft);color:var(--np-success);border-color:transparent}
55
+ .np-chip-warn{background:var(--np-warn-soft);color:var(--np-warn);border-color:transparent}
56
+ .np-links{display:flex;gap:6px 14px;flex-wrap:wrap;margin-top:8px;font-size:14px}
57
+ .np-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:14px}
58
+ .np-tile{padding:18px;display:flex;flex-direction:column;gap:12px}
59
+ .np-tile-head{display:flex;gap:12px;align-items:center}
60
+ .np-tile-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:4px}
61
+ .np-adds{font-size:14px;color:var(--np-muted);display:grid;gap:4px}
62
+ .np-adds li{display:flex;gap:8px;overflow-wrap:anywhere}
63
+ .np-adds li::before{content:"+";color:var(--np-primary);font-weight:700}
64
+ .np-empty{text-align:center;padding:56px 24px}
65
+ .np-empty-art{font-size:40px;margin-bottom:10px}
66
+ .np-empty h2{font-size:19px;margin-bottom:6px}
67
+ .np-empty p{color:var(--np-muted);max-width:46ch;margin:0 auto 18px}
68
+ .np-switch{position:relative;display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;color:var(--np-muted);user-select:none}
69
+ .np-switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}
70
+ .np-switch i{width:40px;height:24px;border-radius:999px;background:var(--np-border);position:relative;transition:background .15s;flex:none}
71
+ .np-switch i::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
72
+ .np-switch input:checked+i{background:var(--np-success)}
73
+ .np-switch input:checked+i::after{transform:translateX(16px)}
74
+ .np-switch input:focus-visible+i{outline:2px solid var(--np-primary);outline-offset:2px}
75
+ .np-switch input:disabled+i{opacity:.5}
76
+ .np-field{display:grid;gap:6px;margin-bottom:18px}
77
+ .np-field:last-child{margin-bottom:0}
78
+ .np-field label,.np-label{font-weight:600;font-size:14px}
79
+ .np-help{color:var(--np-muted);font-size:13px}
80
+ .np-error{color:var(--np-danger);font-size:13px;font-weight:500}
81
+ .np-input{width:100%;border:1px solid var(--np-border);background:var(--np-surface);border-radius:8px;padding:9px 12px;min-height:40px}
82
+ .np-input:focus{outline:2px solid var(--np-primary);outline-offset:-1px;border-color:transparent}
83
+ .np-input[aria-invalid=true]{border-color:var(--np-danger)}
84
+ textarea.np-input{min-height:96px;resize:vertical}
85
+ .np-affix{display:flex;align-items:stretch;border:1px solid var(--np-border);border-radius:8px;overflow:hidden;background:var(--np-surface)}
86
+ .np-affix span{padding:9px 10px;background:var(--np-surface-2);color:var(--np-muted);border-right:1px solid var(--np-border);white-space:nowrap;max-width:50%;overflow:hidden;text-overflow:ellipsis;font-size:14px;display:flex;align-items:center}
87
+ .np-affix input{border:0;flex:1;min-width:0;padding:9px 12px;background:transparent}
88
+ .np-affix:focus-within{outline:2px solid var(--np-primary);outline-offset:-1px;border-color:transparent}
89
+ .np-affix input:focus{outline:0}
90
+ .np-color{display:flex;gap:8px;align-items:center}
91
+ .np-color input[type=color]{width:44px;height:40px;padding:2px;border:1px solid var(--np-border);border-radius:8px;background:var(--np-surface);cursor:pointer}
92
+ .np-section{margin-bottom:16px}
93
+ .np-section>h2{font-size:15px;padding:14px 20px;border-bottom:1px solid var(--np-border)}
94
+ .np-tabs{display:flex;gap:2px;border-bottom:1px solid var(--np-border);margin-bottom:20px;overflow-x:auto}
95
+ .np-tabs button{border:0;background:none;padding:10px 14px;font-weight:550;color:var(--np-muted);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap}
96
+ .np-tabs button[aria-selected=true]{color:var(--np-primary);border-bottom-color:var(--np-primary)}
97
+ .np-savebar{position:sticky;bottom:16px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;margin-top:16px}
98
+ .np-note{padding:12px 14px;border-radius:10px;font-size:14px;background:var(--np-primary-soft);margin-bottom:16px}
99
+ .np-note-warn{background:var(--np-warn-soft);color:var(--np-warn)}
100
+ .np-note-danger{background:var(--np-danger-soft);color:var(--np-danger)}
101
+ .np-steps{counter-reset:s;display:grid;gap:8px;font-size:14px;margin:10px 0 0}
102
+ .np-steps li{counter-increment:s;display:flex;gap:10px}
103
+ .np-steps li::before{content:counter(s);flex:none;width:22px;height:22px;border-radius:50%;background:var(--np-primary-soft);color:var(--np-primary);font-weight:700;font-size:12px;display:grid;place-items:center}
104
+ .np-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;background:var(--np-surface-2);border:1px solid var(--np-border);border-radius:6px;padding:1px 6px;overflow-wrap:anywhere}
105
+ .np-overlay{position:fixed;inset:0;background:rgba(10,13,20,.55);display:grid;place-items:center;padding:16px;z-index:20}
106
+ .np-modal{background:var(--np-surface);border-radius:14px;max-width:480px;width:100%;max-height:calc(100vh - 32px);overflow-y:auto;box-shadow:0 20px 50px rgba(0,0,0,.3)}
107
+ .np-modal header{padding:20px 22px 0}
108
+ .np-modal header h2{font-size:18px}
109
+ .np-modal-body{padding:12px 22px 4px;color:var(--np-muted)}
110
+ .np-modal-body strong{color:var(--np-text)}
111
+ .np-modal footer{display:flex;justify-content:flex-end;gap:10px;padding:18px 22px 20px;flex-wrap:wrap}
112
+ .np-check{display:flex;gap:10px;align-items:flex-start;font-size:14px;color:var(--np-text);margin-top:12px;cursor:pointer}
113
+ .np-check input{margin-top:3px;width:16px;height:16px;accent-color:var(--np-primary)}
114
+ .np-toasts{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);display:grid;gap:8px;z-index:30;width:min(440px,calc(100vw - 32px))}
115
+ .np-toast{padding:12px 16px;border-radius:10px;background:#111827;color:#fff;box-shadow:0 8px 24px rgba(0,0,0,.25);font-size:14px;display:flex;gap:10px;align-items:flex-start}
116
+ .np-toast-error{background:#b42318}
117
+ .np-toast button{margin-left:auto;border:0;background:none;color:inherit;cursor:pointer;opacity:.8;font-size:16px;line-height:1}
118
+ .np-center{min-height:100%;display:grid;place-items:center;padding:24px}
119
+ .np-auth{width:100%;max-width:400px;padding:28px}
120
+ .np-auth h1{font-size:21px;margin:14px 0 6px}
121
+ .np-auth p{color:var(--np-muted);margin-bottom:18px}
122
+ .np-search{width:240px;max-width:100%}
123
+ .np-dl{display:grid;grid-template-columns:minmax(110px,auto) 1fr;gap:10px 20px;font-size:14px}
124
+ .np-dl dt{color:var(--np-muted)}
125
+ .np-dl dd{overflow-wrap:anywhere}
126
+ .np-skel{height:84px;border-radius:var(--np-radius);background:linear-gradient(90deg,var(--np-surface) 25%,var(--np-surface-2) 50%,var(--np-surface) 75%);background-size:200% 100%;animation:np-sh 1.2s infinite;border:1px solid var(--np-border)}
127
+ @keyframes np-sh{to{background-position:-200% 0}}
128
+ @media (prefers-reduced-motion:reduce){.np-root *{animation:none!important;transition:none!important}}
129
+ `;
@@ -0,0 +1,61 @@
1
+ import * as React from 'react';
2
+ import type { SettingField } from '@nextpluggable/plugin/manifest';
3
+ export declare class ApiError extends Error {
4
+ status: number;
5
+ fields?: Record<string, string> | undefined;
6
+ constructor(message: string, status: number, fields?: Record<string, string> | undefined);
7
+ }
8
+ export type ApiCall = <T = unknown>(method: string, path: string, body?: unknown) => Promise<T>;
9
+ export declare function createApi(apiPath: string, onUnauthorized: () => void): ApiCall;
10
+ export declare function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement> & {
11
+ variant?: 'primary' | 'danger' | 'danger-solid' | 'link';
12
+ small?: boolean;
13
+ busy?: boolean;
14
+ }): React.JSX.Element;
15
+ export declare function Switch(props: {
16
+ checked: boolean;
17
+ onChange: (v: boolean) => void;
18
+ label: string;
19
+ disabled?: boolean;
20
+ showLabel?: boolean;
21
+ }): React.JSX.Element;
22
+ export declare function PluginIcon({ icon, name }: {
23
+ icon?: string;
24
+ name: string;
25
+ }): React.JSX.Element;
26
+ export declare function Modal(props: {
27
+ title: string;
28
+ onClose: () => void;
29
+ children: React.ReactNode;
30
+ footer: React.ReactNode;
31
+ }): React.JSX.Element;
32
+ export interface Toast {
33
+ id: number;
34
+ kind: 'ok' | 'error';
35
+ text: string;
36
+ }
37
+ export declare function Toasts({ toasts, dismiss }: {
38
+ toasts: Toast[];
39
+ dismiss: (id: number) => void;
40
+ }): React.JSX.Element;
41
+ export type FieldValue = string | number | boolean | undefined;
42
+ /** One auto-generated form control for a plugin setting. */
43
+ export declare function SettingInput(props: {
44
+ field: SettingField;
45
+ value: FieldValue;
46
+ secretSet?: boolean;
47
+ error?: string;
48
+ onChange: (value: FieldValue) => void;
49
+ }): React.JSX.Element;
50
+ /** What a plugin adds and is allowed to do, in plain language. */
51
+ export declare function describePlugin(p: {
52
+ pages: {
53
+ title: string;
54
+ path: string;
55
+ }[];
56
+ slots: string[];
57
+ adminPages: string[];
58
+ permissions: string[];
59
+ hasServer: boolean;
60
+ }): string[];
61
+ export declare function timeAgo(iso: string | undefined): string;
@@ -0,0 +1,135 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from 'react';
4
+ export class ApiError extends Error {
5
+ status;
6
+ fields;
7
+ constructor(message, status, fields) {
8
+ super(message);
9
+ this.status = status;
10
+ this.fields = fields;
11
+ }
12
+ }
13
+ export function createApi(apiPath, onUnauthorized) {
14
+ return async (method, path, body) => {
15
+ let res;
16
+ try {
17
+ res = await fetch(`${apiPath}${path}`, {
18
+ method,
19
+ credentials: 'same-origin',
20
+ headers: { 'x-nextpluggable': '1', ...(body !== undefined ? { 'content-type': 'application/json' } : {}) },
21
+ body: body !== undefined ? JSON.stringify(body) : undefined,
22
+ cache: 'no-store',
23
+ });
24
+ }
25
+ catch {
26
+ throw new ApiError('Could not reach the site. Check your connection and try again.', 0);
27
+ }
28
+ const data = (await res.json().catch(() => null));
29
+ if (!res.ok) {
30
+ if (res.status === 401 && path.startsWith('/admin'))
31
+ onUnauthorized();
32
+ throw new ApiError(data?.error ?? 'Something went wrong. Please try again.', res.status, data?.fields);
33
+ }
34
+ return data;
35
+ };
36
+ }
37
+ export function Button(props) {
38
+ const { variant, small, busy, className, children, disabled, ...rest } = props;
39
+ const cls = ['np-btn', variant ? `np-btn-${variant}` : '', small ? 'np-btn-sm' : '', className ?? ''].filter(Boolean).join(' ');
40
+ return (_jsxs("button", { type: "button", ...rest, className: cls, disabled: disabled || busy, "aria-busy": busy || undefined, children: [busy ? _jsx("span", { className: "np-spin", "aria-hidden": "true" }) : null, children] }));
41
+ }
42
+ export function Switch(props) {
43
+ return (_jsxs("label", { className: "np-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: props.checked, disabled: props.disabled, "aria-label": props.label, onChange: (e) => props.onChange(e.target.checked) }), _jsx("i", { "aria-hidden": "true" }), props.showLabel ? _jsx("span", { children: props.label }) : null] }));
44
+ }
45
+ export function PluginIcon({ icon, name }) {
46
+ return (_jsx("span", { className: "np-icon", "aria-hidden": "true", children: icon?.startsWith('data:') ? _jsx("img", { src: icon, alt: "" }) : (icon ?? name.slice(0, 1).toUpperCase()) }));
47
+ }
48
+ export function Modal(props) {
49
+ const ref = React.useRef(null);
50
+ const titleId = React.useId();
51
+ const { onClose } = props;
52
+ React.useEffect(() => {
53
+ const previous = document.activeElement;
54
+ ref.current?.focus();
55
+ const onKey = (e) => e.key === 'Escape' && onClose();
56
+ document.addEventListener('keydown', onKey);
57
+ return () => {
58
+ document.removeEventListener('keydown', onKey);
59
+ previous?.focus?.();
60
+ };
61
+ }, [onClose]);
62
+ return (_jsx("div", { className: "np-overlay", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: _jsxs("div", { className: "np-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, tabIndex: -1, ref: ref, children: [_jsx("header", { children: _jsx("h2", { id: titleId, children: props.title }) }), _jsx("div", { className: "np-modal-body", children: props.children }), _jsx("footer", { children: props.footer })] }) }));
63
+ }
64
+ export function Toasts({ toasts, dismiss }) {
65
+ return (_jsx("div", { className: "np-toasts", role: "status", "aria-live": "polite", children: toasts.map((t) => (_jsxs("div", { className: `np-toast${t.kind === 'error' ? ' np-toast-error' : ''}`, children: [_jsx("span", { children: t.text }), _jsx("button", { type: "button", "aria-label": "Dismiss", onClick: () => dismiss(t.id), children: "\u00D7" })] }, t.id))) }));
66
+ }
67
+ /** One auto-generated form control for a plugin setting. */
68
+ export function SettingInput(props) {
69
+ const { field, value, error, onChange } = props;
70
+ const id = React.useId();
71
+ const describedBy = [field.help ? `${id}-help` : '', error ? `${id}-err` : ''].filter(Boolean).join(' ') || undefined;
72
+ const common = { id, 'aria-invalid': error ? true : undefined, 'aria-describedby': describedBy, className: 'np-input' };
73
+ let control;
74
+ switch (field.type) {
75
+ case 'toggle':
76
+ return (_jsxs("div", { className: "np-field", children: [_jsx(Switch, { checked: Boolean(value), onChange: onChange, label: field.label, showLabel: true }), field.help ? _jsx("span", { className: "np-help", children: field.help }) : null, error ? _jsx("span", { className: "np-error", children: error }) : null] }));
77
+ case 'textarea':
78
+ control = _jsx("textarea", { ...common, value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(e.target.value) });
79
+ break;
80
+ case 'number':
81
+ control = (_jsx("input", { ...common, type: "number", inputMode: "decimal", value: value === undefined ? '' : String(value), min: field.min, max: field.max, step: field.step ?? 'any', placeholder: field.placeholder, onChange: (e) => onChange(e.target.value === '' ? undefined : Number(e.target.value)) }));
82
+ break;
83
+ case 'select':
84
+ control = (_jsxs("select", { ...common, value: value ?? '', onChange: (e) => onChange(e.target.value || undefined), children: [!field.required || value === undefined ? _jsx("option", { value: "", children: "Choose\u2026" }) : null, field.options?.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value)))] }));
85
+ break;
86
+ case 'color':
87
+ control = (_jsxs("span", { className: "np-color", children: [_jsx("input", { type: "color", "aria-label": `${field.label} picker`, value: /^#[0-9a-f]{6}$/i.test(String(value)) ? String(value) : '#000000', onChange: (e) => onChange(e.target.value) }), _jsx("input", { ...common, style: { maxWidth: 140 }, value: value ?? '', placeholder: "#2563eb", onChange: (e) => onChange(e.target.value) })] }));
88
+ break;
89
+ case 'secret':
90
+ control = (_jsx("input", { ...common, type: "password", autoComplete: "new-password", value: value ?? '', placeholder: props.secretSet ? 'Saved. Type to replace it' : field.placeholder, onChange: (e) => onChange(e.target.value === '' ? undefined : e.target.value) }));
91
+ break;
92
+ default:
93
+ control = (_jsx("input", { ...common, type: field.type === 'url' ? 'url' : field.type === 'email' ? 'email' : 'text', value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(e.target.value) }));
94
+ }
95
+ return (_jsxs("div", { className: "np-field", children: [_jsxs("label", { htmlFor: id, children: [field.label, field.required ? _jsx("span", { "aria-hidden": "true", children: " *" }) : null] }), control, field.help ? (_jsx("span", { className: "np-help", id: `${id}-help`, children: field.help })) : null, field.type === 'secret' ? _jsx("span", { className: "np-help", children: "Kept private on the server. It is never shown again after saving." }) : null, error ? (_jsx("span", { className: "np-error", id: `${id}-err`, children: error })) : null] }));
96
+ }
97
+ const SLOT_NAMES = {
98
+ 'header.actions': 'your site header',
99
+ header: 'your site header',
100
+ footer: 'your site footer',
101
+ 'body.end': 'every page',
102
+ sidebar: 'your sidebar',
103
+ };
104
+ /** What a plugin adds and is allowed to do, in plain language. */
105
+ export function describePlugin(p) {
106
+ const out = [];
107
+ for (const page of p.pages)
108
+ out.push(`Adds the page “${page.title}” at ${page.path}`);
109
+ for (const slot of p.slots)
110
+ out.push(`Shows something in ${SLOT_NAMES[slot] ?? `the “${slot}” area of your site`}`);
111
+ for (const title of p.adminPages)
112
+ out.push(`Adds a “${title}” screen to this dashboard`);
113
+ if (p.permissions.includes('storage'))
114
+ out.push('Saves its own data on your site');
115
+ if (p.permissions.includes('email'))
116
+ out.push('Sends email from your site');
117
+ for (const perm of p.permissions)
118
+ if (perm.startsWith('fetch:'))
119
+ out.push(perm === 'fetch:*' ? 'Connects to other services on the internet' : `Connects to ${perm.slice(6)}`);
120
+ if (p.hasServer)
121
+ out.push('Runs its own code on your site’s server');
122
+ return out;
123
+ }
124
+ export function timeAgo(iso) {
125
+ if (!iso)
126
+ return 'never';
127
+ const s = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
128
+ if (s < 90)
129
+ return 'just now';
130
+ if (s < 3600)
131
+ return `${Math.round(s / 60)} minutes ago`;
132
+ if (s < 86_400)
133
+ return `${Math.round(s / 3600)} hour${Math.round(s / 3600) === 1 ? '' : 's'} ago`;
134
+ return `${Math.round(s / 86_400)} day${Math.round(s / 86_400) === 1 ? '' : 's'} ago`;
135
+ }
@@ -0,0 +1,15 @@
1
+ /** An error whose message is safe and useful to show to a non-technical dashboard user. */
2
+ export declare class HttpError extends Error {
3
+ readonly status: number;
4
+ readonly fields?: Record<string, string> | undefined;
5
+ constructor(status: number, message: string, fields?: Record<string, string> | undefined);
6
+ }
7
+ export declare function jsonResponse(body: unknown, status?: number, headers?: Record<string, string>): Response;
8
+ /**
9
+ * A plugin's stored files are gone. Normally this means it was removed (or
10
+ * updated) on another server instance and this instance's cached list is stale.
11
+ */
12
+ export declare class PluginGoneError extends Error {
13
+ readonly pluginId: string;
14
+ constructor(pluginId: string);
15
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,29 @@
1
+ /** An error whose message is safe and useful to show to a non-technical dashboard user. */
2
+ export class HttpError extends Error {
3
+ status;
4
+ fields;
5
+ constructor(status, message, fields) {
6
+ super(message);
7
+ this.status = status;
8
+ this.fields = fields;
9
+ this.name = 'HttpError';
10
+ }
11
+ }
12
+ export function jsonResponse(body, status = 200, headers) {
13
+ return new Response(JSON.stringify(body), {
14
+ status,
15
+ headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', ...headers },
16
+ });
17
+ }
18
+ /**
19
+ * A plugin's stored files are gone. Normally this means it was removed (or
20
+ * updated) on another server instance and this instance's cached list is stale.
21
+ */
22
+ export class PluginGoneError extends Error {
23
+ pluginId;
24
+ constructor(pluginId) {
25
+ super(`The files of plugin "${pluginId}" are missing from storage. If it should be installed, remove it and install it again.`);
26
+ this.pluginId = pluginId;
27
+ this.name = 'PluginGoneError';
28
+ }
29
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Process-wide state. Next.js compiles server components, route handlers and
3
+ * the SSR pass of client components as separate module graphs, so module-level
4
+ * variables are not shared between them — `globalThis` is.
5
+ */
6
+ export declare const GLOBAL_KEY: unique symbol;
7
+ export interface Globals {
8
+ /** Client bundle source keyed by module key, handed from the server layer to the SSR layer. */
9
+ ssrCode: Map<string, string>;
10
+ /** Evaluated plugin server modules keyed by content hash. */
11
+ serverModules: Map<string, unknown>;
12
+ /** Immutable stored files keyed by storage key. */
13
+ files: Map<string, string>;
14
+ /** Cached list of installed plugins, keyed by the instance's API path. */
15
+ state: Map<string, {
16
+ at: number;
17
+ value: unknown;
18
+ }>;
19
+ /** Failed sign-in timestamps keyed by client address. */
20
+ loginAttempts: Map<string, number[]>;
21
+ warned: Set<string>;
22
+ }
23
+ export declare function globals(): Globals;
24
+ export declare function warnOnce(key: string, message: string): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Process-wide state. Next.js compiles server components, route handlers and
3
+ * the SSR pass of client components as separate module graphs, so module-level
4
+ * variables are not shared between them — `globalThis` is.
5
+ */
6
+ export const GLOBAL_KEY = Symbol.for('nextpluggable.state');
7
+ export function globals() {
8
+ const g = globalThis;
9
+ return (g[GLOBAL_KEY] ??= {
10
+ ssrCode: new Map(),
11
+ serverModules: new Map(),
12
+ files: new Map(),
13
+ state: new Map(),
14
+ loginAttempts: new Map(),
15
+ warned: new Set(),
16
+ });
17
+ }
18
+ export function warnOnce(key, message) {
19
+ const g = globals();
20
+ if (g.warned.has(key))
21
+ return;
22
+ g.warned.add(key);
23
+ console.warn(`[nextpluggable] ${message}`);
24
+ }
@@ -0,0 +1,40 @@
1
+ import { type NextPluggableConfig } from './config.js';
2
+ export type { NextPluggableConfig } from './config.js';
3
+ export type { AdminUser, AuthAdapter } from './auth/types.js';
4
+ export type { StorageAdapter } from './storage/types.js';
5
+ export type { PluginPageProps } from './next/plugin-page.js';
6
+ export type { SlotProps } from './next/slot.js';
7
+ export { passwordAuth, type PasswordAuthOptions } from './auth/password.js';
8
+ export { customAuth, type CustomAuthOptions } from './auth/custom.js';
9
+ export { fileStorage, type FileStorageOptions } from './storage/file.js';
10
+ export { memoryStorage } from './storage/memory.js';
11
+ export { upstashStorage, type UpstashStorageOptions } from './storage/upstash.js';
12
+ export { postgresStorage, type PostgresStorageOptions } from './storage/postgres.js';
13
+ /**
14
+ * Set up NextPluggable for a Next.js site. Call once (e.g. in `lib/nextpluggable.ts`)
15
+ * and wire the returned pieces into three route files — see the README.
16
+ */
17
+ export declare function createNextPluggable(config?: NextPluggableConfig): {
18
+ /** Route handlers for `app/api/nextpluggable/[...np]/route.ts`. */
19
+ handlers: {
20
+ GET: (request: Request) => Promise<Response>;
21
+ POST: (request: Request) => Promise<Response>;
22
+ PUT: (request: Request) => Promise<Response>;
23
+ PATCH: (request: Request) => Promise<Response>;
24
+ DELETE: (request: Request) => Promise<Response>;
25
+ };
26
+ /** Page for `app/admin/plugins/[[...np]]/page.tsx`. */
27
+ DashboardPage: () => Promise<import("react").ReactNode>;
28
+ dashboardMetadata: import("next").Metadata;
29
+ /** Page for the root catch-all `app/[...slug]/page.tsx`: serves pages added by plugins. */
30
+ PluginPage: (props: import("./index.js").PluginPageProps) => Promise<import("react").ReactNode>;
31
+ generatePluginMetadata: (props: import("./index.js").PluginPageProps) => Promise<import("next").Metadata>;
32
+ /**
33
+ * `await np.hasPluginPage('/some/address')`: does a plugin serve a page there? Use it in a
34
+ * catch-all route of your own to decide whether to render `PluginPage` or your own content.
35
+ */
36
+ hasPluginPage: (pathname: string) => Promise<boolean>;
37
+ /** `<Slot name="…" />` — a place in your layout that plugins can fill. */
38
+ Slot: ({ name }: import("./index.js").SlotProps) => Promise<import("react").ReactNode>;
39
+ };
40
+ export type NextPluggable = ReturnType<typeof createNextPluggable>;
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import { revalidatePath } from 'next/cache';
2
+ import { createHandler } from './api/handler.js';
3
+ import { resolveConfig } from './config.js';
4
+ import { createDashboardPage, dashboardMetadata } from './next/dashboard-page.js';
5
+ import { createPluginPage } from './next/plugin-page.js';
6
+ import { createSlot } from './next/slot.js';
7
+ export { passwordAuth } from './auth/password.js';
8
+ export { customAuth } from './auth/custom.js';
9
+ export { fileStorage } from './storage/file.js';
10
+ export { memoryStorage } from './storage/memory.js';
11
+ export { upstashStorage } from './storage/upstash.js';
12
+ export { postgresStorage } from './storage/postgres.js';
13
+ /**
14
+ * Set up NextPluggable for a Next.js site. Call once (e.g. in `lib/nextpluggable.ts`)
15
+ * and wire the returned pieces into three route files — see the README.
16
+ */
17
+ export function createNextPluggable(config = {}) {
18
+ const getConfig = resolveConfig(config);
19
+ const handle = createHandler(getConfig, {
20
+ // Anything cached by Next.js (static pages, ISR) is refreshed when plugins change.
21
+ onSiteChange() {
22
+ try {
23
+ revalidatePath('/', 'layout');
24
+ }
25
+ catch (e) {
26
+ console.warn('[nextpluggable] Could not refresh cached pages:', e);
27
+ }
28
+ },
29
+ });
30
+ const { PluginPage, generatePluginMetadata, hasPluginPage } = createPluginPage(getConfig);
31
+ return {
32
+ /** Route handlers for `app/api/nextpluggable/[...np]/route.ts`. */
33
+ handlers: { GET: handle, POST: handle, PUT: handle, PATCH: handle, DELETE: handle },
34
+ /** Page for `app/admin/plugins/[[...np]]/page.tsx`. */
35
+ DashboardPage: createDashboardPage(getConfig),
36
+ dashboardMetadata,
37
+ /** Page for the root catch-all `app/[...slug]/page.tsx`: serves pages added by plugins. */
38
+ PluginPage,
39
+ generatePluginMetadata,
40
+ /**
41
+ * `await np.hasPluginPage('/some/address')`: does a plugin serve a page there? Use it in a
42
+ * catch-all route of your own to decide whether to render `PluginPage` or your own content.
43
+ */
44
+ hasPluginPage,
45
+ /** `<Slot name="…" />` — a place in your layout that plugins can fill. */
46
+ Slot: createSlot(getConfig),
47
+ };
48
+ }
@@ -0,0 +1,5 @@
1
+ import type { Metadata } from 'next';
2
+ import type { ReactNode } from 'react';
3
+ import type { ResolvedConfig } from '../config.js';
4
+ export declare const dashboardMetadata: Metadata;
5
+ export declare function createDashboardPage(getConfig: () => ResolvedConfig): () => Promise<ReactNode>;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { headers } from 'next/headers';
3
+ import { sessionView } from '../api/handler.js';
4
+ import { Dashboard } from '../dashboard/Dashboard.js';
5
+ export const dashboardMetadata = {
6
+ title: 'Plugins',
7
+ robots: { index: false, follow: false },
8
+ };
9
+ export function createDashboardPage(getConfig) {
10
+ /** Server component for the dashboard route. Checks who is signed in, then hands over to the client UI. */
11
+ return async function DashboardPage() {
12
+ let cfg;
13
+ try {
14
+ cfg = getConfig();
15
+ }
16
+ catch (e) {
17
+ return _jsx(Dashboard, { apiPath: "", dashboardPath: "", siteName: "", session: { user: null, mode: 'password', configured: false }, setupError: e instanceof Error ? e.message.replace('[nextpluggable] ', '') : 'Setup is incomplete.' });
18
+ }
19
+ const incoming = await headers();
20
+ const request = new Request(new URL(cfg.dashboardPath, cfg.siteUrl), { headers: new Headers([...incoming.entries()]) });
21
+ return _jsx(Dashboard, { apiPath: cfg.apiPath, dashboardPath: cfg.dashboardPath, siteName: cfg.siteName, session: await sessionView(cfg, request) });
22
+ };
23
+ }
@@ -0,0 +1,16 @@
1
+ import type { Metadata } from 'next';
2
+ import { type ReactNode } from 'react';
3
+ import type { PluginInfo } from '@nextpluggable/plugin';
4
+ import type { ResolvedConfig } from '../config.js';
5
+ import type { InstallRecord } from '../core/types.js';
6
+ type RouteParams = Promise<Record<string, string | string[] | undefined>>;
7
+ export interface PluginPageProps {
8
+ params: RouteParams;
9
+ }
10
+ export declare function pluginInfo(cfg: ResolvedConfig, install: InstallRecord, page?: PluginInfo['page']): PluginInfo;
11
+ export declare function createPluginPage(getConfig: () => ResolvedConfig): {
12
+ PluginPage: (props: PluginPageProps) => Promise<ReactNode>;
13
+ generatePluginMetadata: (props: PluginPageProps) => Promise<Metadata>;
14
+ hasPluginPage: (pathname: string) => Promise<boolean>;
15
+ };
16
+ export {};