@korso/shepherd-ui 0.1.1 → 0.2.1

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 (55) hide show
  1. package/dist/ShepherdRoot.d.ts +9 -0
  2. package/dist/ShepherdRoot.js +46 -0
  3. package/dist/client.d.ts +107 -0
  4. package/dist/client.js +238 -0
  5. package/dist/components/ActiveList.d.ts +22 -0
  6. package/dist/components/ActiveList.js +59 -0
  7. package/dist/components/Chat.d.ts +30 -0
  8. package/dist/components/Chat.js +61 -0
  9. package/dist/components/Composer.d.ts +37 -0
  10. package/dist/components/Composer.js +146 -0
  11. package/dist/components/Crew.d.ts +23 -0
  12. package/dist/components/Crew.js +31 -0
  13. package/dist/components/Dashboard.d.ts +48 -0
  14. package/dist/components/Dashboard.js +280 -0
  15. package/dist/components/DoneList.d.ts +29 -0
  16. package/dist/components/DoneList.js +50 -0
  17. package/dist/components/RepoSelect.d.ts +38 -0
  18. package/dist/components/RepoSelect.js +56 -0
  19. package/dist/components/Territory.d.ts +21 -0
  20. package/dist/components/Territory.js +21 -0
  21. package/dist/config/ConnectAgent.d.ts +10 -0
  22. package/dist/config/ConnectAgent.js +119 -0
  23. package/dist/config/EmptyState.d.ts +12 -0
  24. package/dist/config/EmptyState.js +8 -0
  25. package/dist/config/Members.d.ts +10 -0
  26. package/dist/config/Members.js +83 -0
  27. package/dist/config/Workspaces.d.ts +12 -0
  28. package/dist/config/Workspaces.js +96 -0
  29. package/dist/config/index.d.ts +8 -0
  30. package/dist/config/index.js +6 -0
  31. package/dist/context.d.ts +28 -0
  32. package/dist/context.js +36 -0
  33. package/dist/index.d.ts +18 -0
  34. package/dist/index.js +15 -0
  35. package/dist/lib/Dashboard-B7nioe5N.js +1286 -0
  36. package/dist/lib/index.d.ts +452 -16
  37. package/dist/lib/index.js +372 -6
  38. package/dist/lib/selfhost.js +9 -9
  39. package/dist/lib/styles.css +35 -0
  40. package/dist/logic.d.ts +208 -0
  41. package/dist/logic.js +372 -0
  42. package/dist/main.d.ts +1 -0
  43. package/dist/main.js +11 -0
  44. package/dist/selfhost/assets/index-D6rTsbVM.js +40 -0
  45. package/dist/selfhost/assets/{index-tSyzirZa.css → index-DnhlP_lc.css} +1 -1
  46. package/dist/selfhost/index.html +2 -2
  47. package/dist/selfhost.d.ts +22 -0
  48. package/dist/selfhost.js +97 -0
  49. package/dist/test/mockClient.d.ts +6 -0
  50. package/dist/test/mockClient.js +45 -0
  51. package/dist/useLandscapePolling.d.ts +70 -0
  52. package/dist/useLandscapePolling.js +134 -0
  53. package/package.json +1 -1
  54. package/dist/lib/Dashboard-CB6SL-J2.js +0 -1064
  55. package/dist/selfhost/assets/index-BBx3vo2Y.js +0 -40
@@ -1 +1 @@
1
- :root{--bg:#ece0c6;--ink:#322c20;--ink2:#6f6450;--ink3:#9c917a;--line:#d7c9ac;--line2:#e0d4b9;--live:#3d8b58;--drop:#bd4f3a;--chip:#e3d6ba;--terr:#7a6a48;--card:#f5ecd9}*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;-webkit-font-smoothing:antialiased}.wrap{max-width:1100px;margin:0 auto;padding:40px 44px}header{display:flex;align-items:center;gap:12px;padding-bottom:18px;border-bottom:1px solid var(--line)}#board:has(#panel-chat:not([hidden])) header{border-bottom:0}.brand{font-size:17px;font-weight:600}.vitals{color:var(--ink3);font-size:12.5px}.grow{flex:1}.status{font-size:12px;color:var(--ink3)}.status--ok{color:var(--live)}.status--warn{color:var(--terr)}.status--error{color:var(--drop)}.freshness{font-size:12px;color:var(--ink3)}.repo{position:relative}.repo-trig{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-family:inherit;color:var(--ink);-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--line);border-radius:8px;padding:4px 10px;background:#fffaee66;cursor:pointer}.repo-trig .slash{color:var(--ink3)}.repo-menu{position:absolute;top:36px;left:0;min-width:200px;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:5px;box-shadow:0 8px 24px #32281424;z-index:5}.repo-mi{display:flex;justify-content:space-between;gap:14px;padding:7px 9px;width:100%;text-align:left;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-family:inherit;color:inherit;background:transparent;border-radius:7px;font-size:13px;cursor:pointer}.repo-mi:hover{background:var(--chip)}.repo-mi.on{background:var(--chip);font-weight:600}.repo-mi .ct{font-size:11px;color:var(--ink3)}.repo-mi.all{border-top:1px solid var(--line2);margin-top:4px;padding-top:9px;color:var(--ink2)}.tabs{display:flex;gap:4px;margin-left:auto}.tab{font-size:13px;color:var(--ink2);padding:5px 11px;border:0;border-radius:7px;background:transparent;cursor:pointer;font:inherit}.tab--active{background:var(--ink);color:var(--bg)}.crew{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:26px 0 32px}.person{display:inline-flex;align-items:center;gap:9px;padding:5px 13px 5px 5px;border:1px solid var(--line);border-radius:999px;background:var(--card);font-size:13px}.person--idle{opacity:.55}.avatar{width:24px;height:24px;border-radius:50%;display:grid;place-items:center;font-size:10px;font-weight:700;color:#fff;flex:none}.person__name{font-weight:600}.person__doing{color:var(--ink3);font-size:12px}.board{display:grid;grid-template-columns:1fr 1px 1fr;gap:0}.board__rule{background:var(--line)}.col{padding:0 36px}.col:first-child{padding-left:0}.col:last-child{padding-right:0}.colhead{display:flex;align-items:center;gap:8px;margin-bottom:6px}.colhead h2{font-size:12px;font-weight:600;letter-spacing:.03em;margin:0;text-transform:uppercase;color:var(--ink2)}.colhead .n{font-size:11px;color:var(--ink3);background:var(--chip);border-radius:999px;padding:1px 8px}.task{padding:16px 0;border-bottom:1px solid var(--line2)}.task__r1{display:flex;align-items:center;gap:8px}.task__who{font-size:13.5px;font-weight:600}.task__tag{font-size:11px;color:var(--ink3)}.task__repo{font-size:11px;color:var(--terr);background:#7a6a481a;border-radius:4px;padding:0 6px}.task__stat{margin-left:auto;font-size:11px;font-weight:500;color:var(--ink3)}.task__stat--drop{color:var(--drop)}.livedot{width:7px;height:7px;border-radius:50%;background:var(--live);margin-left:auto}.task__intent{font-size:13.5px;margin:5px 0 0;line-height:1.45}.terr{margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;align-items:center}.terr__lbl{font-size:9.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink3)}.glob{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--terr);background:#7a6a481f;border:1px solid rgba(122,106,72,.22);border-radius:5px;padding:1px 7px}.task__meta{font-size:11.5px;color:var(--ink3);margin-top:6px}.terrx{margin-top:8px}.terrx summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:6px;flex-wrap:wrap;-webkit-user-select:none;user-select:none}.terrx summary::-webkit-details-marker{display:none}.terrx .terr--full{margin-top:8px}.glob--more{cursor:pointer;color:var(--ink2);background:var(--chip);border:1px solid var(--line)}.glob--more .tw{color:var(--ink3);display:inline-block;transition:transform .15s}.terrx[open] summary .glob--more .tw{transform:rotate(90deg)}.grp{padding:16px 0;border-bottom:1px solid var(--line2)}.grp__head{display:flex;align-items:center;gap:8px}.grp__who{font-size:13.5px;font-weight:600}.grp__tag,.grp__count{font-size:11px;color:var(--ink3)}.grp__dot{width:7px;height:7px;border-radius:50%;background:var(--live);margin-left:auto}.claims{margin:10px 0 0 6px;padding-left:14px;border-left:1px solid var(--line)}.claim{padding:6px 0}.claim+.claim{border-top:1px solid var(--line2);margin-top:6px}.fold{margin-top:10px}.fold summary{list-style:none;cursor:pointer;font-size:12px;color:var(--ink2);display:inline-flex;align-items:center;gap:6px;-webkit-user-select:none;user-select:none}.fold summary::-webkit-details-marker{display:none}.fold summary .tw{display:inline-block;color:var(--ink3);transition:transform .15s}.fold[open] summary .tw{transform:rotate(90deg)}.fold__body{margin:8px 0 0 6px;padding-left:14px;border-left:1px dashed var(--line)}.covered{font-size:10.5px;color:var(--ink3);font-style:italic;margin-top:4px}.day{font-size:11px;color:var(--ink3);padding:18px 0 2px;font-weight:600}.more{margin-top:20px;font-size:12.5px;color:var(--ink2);padding:7px 13px;border:1px solid var(--line);border-radius:8px;background:#fffaee59;cursor:pointer}.empty{color:var(--ink3);font-style:italic;padding:14px 0}.chat-wrap{background:var(--card);border:1px solid var(--line);border-radius:12px;display:flex;flex-direction:column;overflow:hidden;height:calc(100vh - 150px);min-height:320px}.chat{flex:1;overflow-y:auto;padding:16px}.chat{scrollbar-width:thin;scrollbar-color:var(--line) transparent}.chat::-webkit-scrollbar{width:10px}.chat::-webkit-scrollbar-track{background:transparent}.chat::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;border:3px solid var(--card);background-clip:padding-box}.chat::-webkit-scrollbar-thumb:hover{background:var(--ink3);background-clip:padding-box}.chat__note{padding:9px 16px;font-size:11.5px;color:var(--ink3)}.msg{display:flex;gap:11px;padding:9px 0}.msg__avatar{flex:none;width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-size:12px;font-weight:700;color:#fff}.msg__body{min-width:0;flex:1}.msg__head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.msg__who{font-weight:600}.msg__human{font-size:12px;color:var(--ink3)}.msg__to{font-size:11.5px;color:var(--terr)}.msg__time{font-size:11.5px;color:var(--ink3);margin-left:auto}.msg__text{margin-top:3px;padding:8px 12px;border-radius:4px 12px 12px;background:var(--chip);white-space:pre-wrap;word-break:break-word}.msg--targeted .msg__text{box-shadow:inset 3px 0 0 var(--terr)}.msg--me,.msg--me .msg__head{flex-direction:row-reverse}.msg--me .msg__time{margin-left:0;margin-right:auto}.msg--me .msg__body{display:flex;flex-direction:column;align-items:flex-end}.msg--me .msg__text{border-radius:12px 4px 12px 12px;background:#d4e7d2}.msg--me.msg--targeted .msg__text{box-shadow:inset -3px 0 0 var(--terr)}.composer{position:relative;border-top:1px solid var(--line);padding:10px 12px}.chat-form{display:flex;gap:8px}.chat-input{flex:1;padding:9px 12px;border-radius:8px;border:1px solid var(--line);background:var(--bg);color:var(--ink);font:inherit}.chat-input:focus{outline:none;border-color:var(--terr)}.chat-send{padding:9px 16px;border-radius:8px;border:1px solid var(--ink);background:var(--ink);color:var(--bg);font-weight:600;cursor:pointer;font:inherit}.chat-send:disabled{opacity:.5;cursor:default}.mention-pop{position:absolute;left:12px;right:12px;bottom:56px;z-index:6;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:5px;box-shadow:0 8px 24px #32281429;max-height:200px;overflow-y:auto}.mention-mi{display:flex;align-items:center;gap:9px;width:100%;text-align:left;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-family:inherit;color:inherit;background:transparent;border-radius:7px;padding:6px 8px;font-size:13px;cursor:pointer}.mention-mi:hover,.mention-mi.on{background:var(--chip)}.mention-mi .ma{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;font-size:9px;font-weight:700;color:#fff;flex:none}.gate{max-width:420px;margin:12vh auto 0;text-align:center}.gate h1{font-size:19px;margin:0 0 6px}.gate p{color:var(--ink2);margin:0 0 20px}.gate form{display:flex;gap:8px}.gate input{flex:1;padding:10px 12px;border-radius:8px;border:1px solid var(--line);background:var(--card);color:var(--ink)}.gate button{padding:10px 16px;border-radius:8px;border:1px solid var(--ink);background:var(--ink);color:var(--bg);font-weight:600;cursor:pointer}
1
+ :root{--bg:#ece0c6;--ink:#322c20;--ink2:#6f6450;--ink3:#9c917a;--line:#d7c9ac;--line2:#e0d4b9;--live:#3d8b58;--drop:#bd4f3a;--chip:#e3d6ba;--terr:#7a6a48;--card:#f5ecd9}*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;-webkit-font-smoothing:antialiased}.wrap{max-width:1100px;margin:0 auto;padding:40px 44px}header{display:flex;align-items:center;gap:12px;padding-bottom:18px;border-bottom:1px solid var(--line)}#board:has(#panel-chat:not([hidden])) header{border-bottom:0}.brand{font-size:17px;font-weight:600}.vitals{color:var(--ink3);font-size:12.5px}.grow{flex:1}.status{font-size:12px;color:var(--ink3)}.status--ok{color:var(--live)}.status--warn{color:var(--terr)}.status--error{color:var(--drop)}.freshness{font-size:12px;color:var(--ink3)}.repo{position:relative}.repo-trig{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-family:inherit;color:var(--ink);-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--line);border-radius:8px;padding:4px 10px;background:#fffaee66;cursor:pointer}.repo-trig .slash{color:var(--ink3)}.repo-menu{position:absolute;top:36px;left:0;min-width:200px;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:5px;box-shadow:0 8px 24px #32281424;z-index:5}.repo-mi{display:flex;justify-content:space-between;gap:14px;padding:7px 9px;width:100%;text-align:left;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-family:inherit;color:inherit;background:transparent;border-radius:7px;font-size:13px;cursor:pointer}.repo-mi:hover{background:var(--chip)}.repo-mi.on{background:var(--chip);font-weight:600}.repo-mi .ct{font-size:11px;color:var(--ink3)}.repo-mi.all{border-top:1px solid var(--line2);margin-top:4px;padding-top:9px;color:var(--ink2)}.tabs{display:flex;gap:4px;margin-left:auto}.tab{font-size:13px;color:var(--ink2);padding:5px 11px;border:0;border-radius:7px;background:transparent;cursor:pointer;font:inherit}.tab--active{background:var(--ink);color:var(--bg)}.crew{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:26px 0 32px}.person{display:inline-flex;align-items:center;gap:9px;padding:5px 13px 5px 5px;border:1px solid var(--line);border-radius:999px;background:var(--card);font-size:13px}.person--idle{opacity:.55}.avatar{width:24px;height:24px;border-radius:50%;display:grid;place-items:center;font-size:10px;font-weight:700;color:#fff;flex:none}.person__name{font-weight:600}.person__doing{color:var(--ink3);font-size:12px}.board{display:grid;grid-template-columns:1fr 1px 1fr;gap:0}.board__rule{background:var(--line)}.col{padding:0 36px}.col:first-child{padding-left:0}.col:last-child{padding-right:0}.colhead{display:flex;align-items:center;gap:8px;margin-bottom:6px}.colhead h2{font-size:12px;font-weight:600;letter-spacing:.03em;margin:0;text-transform:uppercase;color:var(--ink2)}.colhead .n{font-size:11px;color:var(--ink3);background:var(--chip);border-radius:999px;padding:1px 8px}.task{padding:16px 0;border-bottom:1px solid var(--line2)}.task__r1{display:flex;align-items:center;gap:8px}.task__who{font-size:13.5px;font-weight:600}.task__tag{font-size:11px;color:var(--ink3)}.task__repo{font-size:11px;color:var(--terr);background:#7a6a481a;border-radius:4px;padding:0 6px}.task__stat{margin-left:auto;font-size:11px;font-weight:500;color:var(--ink3)}.task__stat--drop{color:var(--drop)}.livedot{width:7px;height:7px;border-radius:50%;background:var(--live);margin-left:auto}.task__intent{font-size:13.5px;margin:5px 0 0;line-height:1.45}.terr{margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;align-items:center}.terr__lbl{font-size:9.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink3)}.glob{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--terr);background:#7a6a481f;border:1px solid rgba(122,106,72,.22);border-radius:5px;padding:1px 7px}.task__meta{font-size:11.5px;color:var(--ink3);margin-top:6px}.terrx{margin-top:8px}.terrx summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:6px;flex-wrap:wrap;-webkit-user-select:none;user-select:none}.terrx summary::-webkit-details-marker{display:none}.terrx .terr--full{margin-top:8px}.glob--more{cursor:pointer;color:var(--ink2);background:var(--chip);border:1px solid var(--line)}.glob--more .tw{color:var(--ink3);display:inline-block;transition:transform .15s}.terrx[open] summary .glob--more .tw{transform:rotate(90deg)}.grp{padding:16px 0;border-bottom:1px solid var(--line2)}.grp__head{display:flex;align-items:center;gap:8px}.grp__who{font-size:13.5px;font-weight:600}.grp__tag,.grp__count{font-size:11px;color:var(--ink3)}.grp__dot{width:7px;height:7px;border-radius:50%;background:var(--live);margin-left:auto}.claims{margin:10px 0 0 6px;padding-left:14px;border-left:1px solid var(--line)}.claim{padding:6px 0}.claim+.claim{border-top:1px solid var(--line2);margin-top:6px}.fold{margin-top:10px}.fold summary{list-style:none;cursor:pointer;font-size:12px;color:var(--ink2);display:inline-flex;align-items:center;gap:6px;-webkit-user-select:none;user-select:none}.fold summary::-webkit-details-marker{display:none}.fold summary .tw{display:inline-block;color:var(--ink3);transition:transform .15s}.fold[open] summary .tw{transform:rotate(90deg)}.fold__body{margin:8px 0 0 6px;padding-left:14px;border-left:1px dashed var(--line)}.covered{font-size:10.5px;color:var(--ink3);font-style:italic;margin-top:4px}.day{font-size:11px;color:var(--ink3);padding:18px 0 2px;font-weight:600}.more{margin-top:20px;font-size:12.5px;color:var(--ink2);padding:7px 13px;border:1px solid var(--line);border-radius:8px;background:#fffaee59;cursor:pointer}.empty{color:var(--ink3);font-style:italic;padding:14px 0}.chat-wrap{background:var(--card);border:1px solid var(--line);border-radius:12px;display:flex;flex-direction:column;overflow:hidden;height:calc(100vh - 150px);min-height:320px}.chat{flex:1;overflow-y:auto;padding:16px}.chat{scrollbar-width:thin;scrollbar-color:var(--line) transparent}.chat::-webkit-scrollbar{width:10px}.chat::-webkit-scrollbar-track{background:transparent}.chat::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;border:3px solid var(--card);background-clip:padding-box}.chat::-webkit-scrollbar-thumb:hover{background:var(--ink3);background-clip:padding-box}.chat__note{padding:9px 16px;font-size:11.5px;color:var(--ink3)}.msg{display:flex;gap:11px;padding:9px 0}.msg__avatar{flex:none;width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-size:12px;font-weight:700;color:#fff}.msg__body{min-width:0;flex:1}.msg__head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.msg__who{font-weight:600}.msg__human{font-size:12px;color:var(--ink3)}.msg__to{font-size:11.5px;color:var(--terr)}.msg__time{font-size:11.5px;color:var(--ink3);margin-left:auto}.msg__text{margin-top:3px;padding:8px 12px;border-radius:4px 12px 12px;background:var(--chip);white-space:pre-wrap;word-break:break-word}.msg--targeted .msg__text{box-shadow:inset 3px 0 0 var(--terr)}.msg--me,.msg--me .msg__head{flex-direction:row-reverse}.msg--me .msg__time{margin-left:0;margin-right:auto}.msg--me .msg__body{display:flex;flex-direction:column;align-items:flex-end}.msg--me .msg__text{border-radius:12px 4px 12px 12px;background:#d4e7d2}.msg--me.msg--targeted .msg__text{box-shadow:inset -3px 0 0 var(--terr)}.composer{position:relative;border-top:1px solid var(--line);padding:10px 12px}.chat-form{display:flex;gap:8px}.chat-input{flex:1;padding:9px 12px;border-radius:8px;border:1px solid var(--line);background:var(--bg);color:var(--ink);font:inherit}.chat-input:focus{outline:none;border-color:var(--terr)}.chat-send{padding:9px 16px;border-radius:8px;border:1px solid var(--ink);background:var(--ink);color:var(--bg);font-weight:600;cursor:pointer;font:inherit}.chat-send:disabled{opacity:.5;cursor:default}.mention-pop{position:absolute;left:12px;right:12px;bottom:56px;z-index:6;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:5px;box-shadow:0 8px 24px #32281429;max-height:200px;overflow-y:auto}.mention-mi{display:flex;align-items:center;gap:9px;width:100%;text-align:left;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-family:inherit;color:inherit;background:transparent;border-radius:7px;padding:6px 8px;font-size:13px;cursor:pointer}.mention-mi:hover,.mention-mi.on{background:var(--chip)}.mention-mi .ma{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;font-size:9px;font-weight:700;color:#fff;flex:none}.gate{max-width:420px;margin:12vh auto 0;text-align:center}.gate h1{font-size:19px;margin:0 0 6px}.gate p{color:var(--ink2);margin:0 0 20px}.gate form{display:flex;gap:8px}.gate input{flex:1;padding:10px 12px;border-radius:8px;border:1px solid var(--line);background:var(--card);color:var(--ink)}.gate button{padding:10px 16px;border-radius:8px;border:1px solid var(--ink);background:var(--ink);color:var(--bg);font-weight:600;cursor:pointer}.shepherd-root{max-width:1100px;margin:0 auto;padding:40px 44px}.shepherd-root>main{padding-top:18px}.shepherd-config{display:flex;flex-direction:column;gap:24px}.shepherd-config h2{font-size:16px;margin:0 0 4px}.shepherd-workspaces,.shepherd-members,.shepherd-connect-agent{display:flex;flex-direction:column;gap:10px;padding:16px;border:1px solid var(--line);border-radius:10px;background:var(--card)}.shepherd-workspaces .switcher{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.shepherd-workspaces .role{color:var(--ink3);font-size:12px}.shepherd-workspaces .admin,.shepherd-workspaces .invites,.shepherd-workspaces .create,.shepherd-workspaces .join{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.shepherd-workspaces .create>label,.shepherd-workspaces .join>label,.shepherd-workspaces .switcher>label{min-width:104px;color:var(--ink2)}.shepherd-workspaces .create>input,.shepherd-workspaces .join>input{flex:1;min-width:180px}.shepherd-config input,.shepherd-config select{padding:8px 10px;border-radius:8px;border:1px solid var(--line);background:var(--bg);color:var(--ink)}.shepherd-config button{padding:8px 14px;border-radius:8px;border:1px solid var(--ink);background:var(--ink);color:var(--bg);font-weight:600;cursor:pointer}.shepherd-config button:disabled{opacity:.55;cursor:default}.shepherd-config .invite-result,.shepherd-connect-agent .token-once{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;word-break:break-all;padding:8px 10px;border-radius:8px;background:var(--chip);border:1px solid var(--line2)}.shepherd-config .revoked{color:var(--ink3);text-decoration:line-through}.shepherd-empty-state{text-align:center;color:var(--ink2);padding:48px 24px}.shepherd-empty-state button{margin-top:14px}
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Shepherd</title>
7
- <script type="module" crossorigin src="/assets/index-BBx3vo2Y.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-tSyzirZa.css">
7
+ <script type="module" crossorigin src="/assets/index-D6rTsbVM.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DnhlP_lc.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -0,0 +1,22 @@
1
+ import type { ReactElement } from "react";
2
+ /**
3
+ * The self-host SPA root and the ONE place a team token is handled — the named
4
+ * auth exception to the auth-agnostic core. Everything below the provider (the
5
+ * client, the polling hook, the {@link Dashboard}) stays token-blind; this
6
+ * component owns the token's lifecycle:
7
+ *
8
+ * - It seeds token state from `localStorage["shepherd.token"]` (back-compat with
9
+ * the legacy board), so a returning viewer skips the gate.
10
+ * - With NO token it renders the {@link Gate}; a successful submit persists the
11
+ * token and re-renders into the dashboard.
12
+ * - With a token it builds a same-origin client (`baseUrl: ""`) that injects the
13
+ * `Authorization: Bearer <token>` header and, on a 401, clears the stored
14
+ * token and resets state — which re-renders straight back to the gate. This is
15
+ * the React analogue of app.js's `clearToken()` + re-prompt 401 flow.
16
+ *
17
+ * The client is memoised on the token so it is rebuilt only when the token
18
+ * actually changes, not on every freshness re-render of the dashboard subtree.
19
+ *
20
+ * @returns Either the token gate or the authenticated dashboard.
21
+ */
22
+ export declare function SelfHostApp(): ReactElement;
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { createShepherdClient } from "./client.js";
4
+ import { ShepherdClientProvider } from "./context.js";
5
+ import { Dashboard } from "./components/Dashboard.js";
6
+ /**
7
+ * localStorage key the operator's team token is stored under. Kept identical to
8
+ * the legacy vanilla board (packages/hub/public/app.js's `TOKEN_KEY`) so a
9
+ * viewer who already authenticated against the old board stays signed in after
10
+ * the React cutover — no re-prompt on upgrade.
11
+ */
12
+ const TOKEN_KEY = "shepherd.token";
13
+ /** localStorage read that never throws (private/quota modes return null). */
14
+ function readToken() {
15
+ try {
16
+ return localStorage.getItem(TOKEN_KEY) ?? "";
17
+ }
18
+ catch {
19
+ return "";
20
+ }
21
+ }
22
+ /**
23
+ * The token gate, ported from the `.gate` markup in packages/hub/public/index.html.
24
+ * Rendered whenever no token is stored; submitting a non-empty token persists it
25
+ * and lifts the gate via {@link onSubmit}.
26
+ *
27
+ * @param props.onSubmit - Called with the entered token once the form is submitted.
28
+ */
29
+ function Gate({ onSubmit }) {
30
+ const [value, setValue] = useState("");
31
+ const handle = (e) => {
32
+ e.preventDefault();
33
+ const trimmed = value.trim();
34
+ // Ignore an empty submit so the gate can't store a blank token and then
35
+ // bounce the viewer straight back to itself on the first 401.
36
+ if (trimmed !== "")
37
+ onSubmit(trimmed);
38
+ };
39
+ return (_jsxs("section", { id: "gate", className: "gate", children: [_jsx("h1", { children: "Shepherd" }), _jsx("p", { children: "Enter the team token to view the workspace." }), _jsxs("form", { id: "gate-form", onSubmit: handle, children: [_jsx("input", { id: "gate-input", type: "password", placeholder: "Team token", "aria-label": "Team token", autoComplete: "off", value: value, onChange: (e) => setValue(e.target.value) }), _jsx("button", { type: "submit", children: "View" })] })] }));
40
+ }
41
+ /**
42
+ * The self-host SPA root and the ONE place a team token is handled — the named
43
+ * auth exception to the auth-agnostic core. Everything below the provider (the
44
+ * client, the polling hook, the {@link Dashboard}) stays token-blind; this
45
+ * component owns the token's lifecycle:
46
+ *
47
+ * - It seeds token state from `localStorage["shepherd.token"]` (back-compat with
48
+ * the legacy board), so a returning viewer skips the gate.
49
+ * - With NO token it renders the {@link Gate}; a successful submit persists the
50
+ * token and re-renders into the dashboard.
51
+ * - With a token it builds a same-origin client (`baseUrl: ""`) that injects the
52
+ * `Authorization: Bearer <token>` header and, on a 401, clears the stored
53
+ * token and resets state — which re-renders straight back to the gate. This is
54
+ * the React analogue of app.js's `clearToken()` + re-prompt 401 flow.
55
+ *
56
+ * The client is memoised on the token so it is rebuilt only when the token
57
+ * actually changes, not on every freshness re-render of the dashboard subtree.
58
+ *
59
+ * @returns Either the token gate or the authenticated dashboard.
60
+ */
61
+ export function SelfHostApp() {
62
+ const [token, setToken] = useState(readToken);
63
+ // Rebuild the client only when the token changes. WHY same-origin (baseUrl
64
+ // ""): the hub serves this SPA, so requests hit the hub's own origin and need
65
+ // no cross-origin base. onUnauthorized clears the token + resets state, which
66
+ // re-renders back to the gate — the automatic 401 re-gate.
67
+ const client = useMemo(() => createShepherdClient({
68
+ baseUrl: "",
69
+ getAuthHeader: () => `Bearer ${token}`,
70
+ onUnauthorized: () => {
71
+ try {
72
+ localStorage.removeItem(TOKEN_KEY);
73
+ }
74
+ catch {
75
+ // Best-effort: even if the remove fails, resetting state below still
76
+ // returns the viewer to the gate for this session.
77
+ }
78
+ setToken("");
79
+ },
80
+ }), [token]);
81
+ if (token === "") {
82
+ return (_jsx(Gate, { onSubmit: (t) => {
83
+ try {
84
+ localStorage.setItem(TOKEN_KEY, t);
85
+ }
86
+ catch {
87
+ // Best-effort persistence; the in-memory token still authenticates
88
+ // this session even if storage is unavailable.
89
+ }
90
+ setToken(t);
91
+ } }));
92
+ }
93
+ // Restore the legacy board's layout container: the ported styles size `.wrap`
94
+ // to max-width:1100px with page gutters. The gate centers itself (`.gate`), so
95
+ // only the dashboard view needs the wrapper.
96
+ return (_jsx("div", { className: "wrap", children: _jsx(ShepherdClientProvider, { client: client, children: _jsx(Dashboard, {}) }) }));
97
+ }
@@ -0,0 +1,6 @@
1
+ import { vi } from "vitest";
2
+ import type { ShepherdClient } from "../client.js";
3
+ export type MockClient = {
4
+ -readonly [K in keyof ShepherdClient]: ShepherdClient[K] extends (...a: infer A) => infer R ? ReturnType<typeof vi.fn<(...a: A) => R>> : ShepherdClient[K];
5
+ };
6
+ export declare function makeMockClient(overrides?: Partial<MockClient>): MockClient;
@@ -0,0 +1,45 @@
1
+ import { vi } from "vitest";
2
+ /** A schema-valid empty landscape, shared by getLandscape and landscape(). */
3
+ const EMPTY_LANDSCAPE = {
4
+ agents: [],
5
+ tasks: [],
6
+ announcements: [],
7
+ serverTime: "2026-06-29T00:00:00.000Z",
8
+ };
9
+ /** A schema-valid empty announce result, shared by announce and announceTo. */
10
+ const EMPTY_ANNOUNCE = { ok: true, announcementIds: [] };
11
+ export function makeMockClient(overrides = {}) {
12
+ const base = {
13
+ baseUrl: "https://hub.example.run.app",
14
+ // --- multi-workspace management surface --------------------------------
15
+ listWorkspaces: vi.fn().mockResolvedValue({ workspaces: [] }),
16
+ createWorkspace: vi.fn().mockResolvedValue({
17
+ id: "ws_new",
18
+ slug: "new",
19
+ name: "New",
20
+ role: "admin",
21
+ }),
22
+ mintToken: vi.fn().mockResolvedValue({ token: "shp_mock", id: "tok_mock" }),
23
+ listTokens: vi.fn().mockResolvedValue({ tokens: [] }),
24
+ revokeToken: vi.fn().mockResolvedValue(undefined),
25
+ createInvite: vi.fn().mockResolvedValue({
26
+ code: "INV-MOCK",
27
+ expiresAt: null,
28
+ maxUses: 1,
29
+ useCount: 0,
30
+ }),
31
+ revokeInvite: vi.fn().mockResolvedValue(undefined),
32
+ redeemInvite: vi.fn().mockResolvedValue({
33
+ workspace: { id: "ws_joined", slug: "joined", name: "Joined", role: "member" },
34
+ }),
35
+ listMembers: vi.fn().mockResolvedValue({ members: [] }),
36
+ removeMember: vi.fn().mockResolvedValue(undefined),
37
+ leave: vi.fn().mockResolvedValue(undefined),
38
+ landscape: vi.fn().mockResolvedValue(EMPTY_LANDSCAPE),
39
+ announceTo: vi.fn().mockResolvedValue(EMPTY_ANNOUNCE),
40
+ // --- self-host singular aliases ----------------------------------------
41
+ getLandscape: vi.fn().mockResolvedValue(EMPTY_LANDSCAPE),
42
+ announce: vi.fn().mockResolvedValue(EMPTY_ANNOUNCE),
43
+ };
44
+ return Object.assign(base, overrides);
45
+ }
@@ -0,0 +1,70 @@
1
+ import type { WorkspaceLandscapeResponseT } from "@shepherd/shared";
2
+ /**
3
+ * Connection state surfaced to the board chrome:
4
+ * - `live`: the most recent poll succeeded.
5
+ * - `reconnecting`: a poll failed transiently; the last good snapshot is kept.
6
+ * - `unauthorized`: the hub returned 401; the last good snapshot is kept while
7
+ * the host-supplied client's `onUnauthorized` handles any token clearing.
8
+ */
9
+ export type LandscapeStatus = "live" | "reconnecting" | "unauthorized";
10
+ /** Options for {@link useLandscapePolling}. */
11
+ export interface UseLandscapePollingOptions {
12
+ /**
13
+ * Workspace to poll. When given, the hook calls `client.landscape(id)` (the
14
+ * hosted, multi-workspace route); when omitted it calls the singular
15
+ * `client.getLandscape()` self-host alias. Changing it re-pulls immediately.
16
+ */
17
+ workspaceId?: string;
18
+ /** Poll cadence in ms; defaults to {@link DEFAULT_POLL_MS} (5000). */
19
+ pollMs?: number;
20
+ /**
21
+ * Whether to poll at all; defaults to `true`. When `false` the hook performs
22
+ * NO request (no initial poll, no interval) and stays on its last value — used
23
+ * by the hosted shell to keep the board from polling when there is no
24
+ * workspace to scope to.
25
+ */
26
+ enabled?: boolean;
27
+ }
28
+ /** The reactive value returned by {@link useLandscapePolling}. */
29
+ export interface LandscapePolling {
30
+ /** Last good landscape, or `null` until the first successful poll. */
31
+ snapshot: WorkspaceLandscapeResponseT | null;
32
+ /** Current connection state. */
33
+ status: LandscapeStatus;
34
+ /** `Date.now()` of the last successful poll, or `null` before the first. */
35
+ lastUpdatedMs: number | null;
36
+ /** Run one poll immediately (used by the composer after a successful send). */
37
+ refresh: () => Promise<void>;
38
+ }
39
+ /**
40
+ * Polls the Shepherd hub's whole-workspace landscape on an interval and exposes
41
+ * the latest good snapshot plus a connection status, mirroring the browser-only
42
+ * polling loop in packages/hub/public/app.js (`poll` + `tickFreshness`).
43
+ *
44
+ * The client is read from {@link useShepherdClient} (context), keeping the hook
45
+ * auth-agnostic — it never touches tokens or the BFF. On mount it polls once
46
+ * immediately, then on `setInterval(pollMs)`. A SECOND `setInterval(1000)` only
47
+ * bumps a tick counter so relative-time text re-renders even between polls (the
48
+ * React analogue of app.js's `tickFreshness`); both intervals are cleared on
49
+ * unmount.
50
+ *
51
+ * Failure handling preserves the original board's resilience: a 401
52
+ * {@link ShepherdClientError} sets `unauthorized`, and ANY other error sets
53
+ * `reconnecting`. In both cases the last good `snapshot`/`lastUpdatedMs` are
54
+ * RETAINED (app.js's `lastSnapshot` retention), so the wallboard never blanks on
55
+ * a transient blip. The same last-good retention holds across a `workspaceId`
56
+ * switch — the prior board stays on screen until the new workspace's first poll
57
+ * lands, and an in-flight poll for the old workspace is dropped so it can't
58
+ * clobber the new one.
59
+ *
60
+ * Two refinements beyond the original loop: a `workspaceId` change re-pulls the
61
+ * new workspace immediately (the poll effect depends on it), and the interval
62
+ * poll is skipped while the tab is hidden (`document.visibilityState`), resuming
63
+ * with an immediate poll on `visibilitychange` back to visible — a backgrounded
64
+ * wallboard shouldn't hammer the hub.
65
+ *
66
+ * @param opts - Optional workspace to scope to and poll cadence override.
67
+ * @returns The current snapshot, status, last-updated timestamp, and a manual
68
+ * {@link LandscapePolling.refresh}.
69
+ */
70
+ export declare function useLandscapePolling(opts?: UseLandscapePollingOptions): LandscapePolling;
@@ -0,0 +1,134 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { ShepherdClientError } from "./client.js";
3
+ import { useShepherdClient } from "./context.js";
4
+ /** Default poll cadence, mirroring `POLL_MS = 5000` in the original app.js. */
5
+ const DEFAULT_POLL_MS = 5000;
6
+ /** The freshness re-render cadence, mirroring app.js's 1s `tickFreshness` loop. */
7
+ const FRESHNESS_MS = 1000;
8
+ /**
9
+ * Polls the Shepherd hub's whole-workspace landscape on an interval and exposes
10
+ * the latest good snapshot plus a connection status, mirroring the browser-only
11
+ * polling loop in packages/hub/public/app.js (`poll` + `tickFreshness`).
12
+ *
13
+ * The client is read from {@link useShepherdClient} (context), keeping the hook
14
+ * auth-agnostic — it never touches tokens or the BFF. On mount it polls once
15
+ * immediately, then on `setInterval(pollMs)`. A SECOND `setInterval(1000)` only
16
+ * bumps a tick counter so relative-time text re-renders even between polls (the
17
+ * React analogue of app.js's `tickFreshness`); both intervals are cleared on
18
+ * unmount.
19
+ *
20
+ * Failure handling preserves the original board's resilience: a 401
21
+ * {@link ShepherdClientError} sets `unauthorized`, and ANY other error sets
22
+ * `reconnecting`. In both cases the last good `snapshot`/`lastUpdatedMs` are
23
+ * RETAINED (app.js's `lastSnapshot` retention), so the wallboard never blanks on
24
+ * a transient blip. The same last-good retention holds across a `workspaceId`
25
+ * switch — the prior board stays on screen until the new workspace's first poll
26
+ * lands, and an in-flight poll for the old workspace is dropped so it can't
27
+ * clobber the new one.
28
+ *
29
+ * Two refinements beyond the original loop: a `workspaceId` change re-pulls the
30
+ * new workspace immediately (the poll effect depends on it), and the interval
31
+ * poll is skipped while the tab is hidden (`document.visibilityState`), resuming
32
+ * with an immediate poll on `visibilitychange` back to visible — a backgrounded
33
+ * wallboard shouldn't hammer the hub.
34
+ *
35
+ * @param opts - Optional workspace to scope to and poll cadence override.
36
+ * @returns The current snapshot, status, last-updated timestamp, and a manual
37
+ * {@link LandscapePolling.refresh}.
38
+ */
39
+ export function useLandscapePolling(opts) {
40
+ const client = useShepherdClient();
41
+ const workspaceId = opts?.workspaceId;
42
+ const pollMs = opts?.pollMs ?? DEFAULT_POLL_MS;
43
+ const enabled = opts?.enabled ?? true;
44
+ const [snapshot, setSnapshot] = useState(null);
45
+ const [status, setStatus] = useState("live");
46
+ const [lastUpdatedMs, setLastUpdatedMs] = useState(null);
47
+ // A monotonically-bumped counter whose only job is to force a re-render every
48
+ // second so relative timestamps in the tree refresh between polls. The value
49
+ // itself is never read — the state change IS the freshness tick.
50
+ const [, setTick] = useState(0);
51
+ // The latest client lives in a ref so the stable `poll`/`refresh` callbacks
52
+ // always call through to the current provider value without re-subscribing.
53
+ const clientRef = useRef(client);
54
+ clientRef.current = client;
55
+ // Each poll effect run bumps this "generation". A poll captures the generation
56
+ // it started under and compares against the live ref before any setState, so a
57
+ // response that resolves after its effect was torn down — by unmount OR a
58
+ // `workspaceId` switch — is dropped and can't clobber a newer workspace's
59
+ // board. A single shared boolean can't do this: a switch runs old-cleanup then
60
+ // new-setup synchronously, so a boolean reset by setup would let the stale
61
+ // promise's late resolve sail through. The generation only ever moves forward,
62
+ // so a superseded poll's captured value is always behind `gen.current`.
63
+ const gen = useRef(0);
64
+ /**
65
+ * Runs exactly one poll, mapping the outcome onto status/snapshot. Reads
66
+ * `workspaceId` via closure (it's a `useCallback` dep): a non-empty id hits the
67
+ * workspace-scoped `landscape(id)` route, an absent one the singular
68
+ * `getLandscape()` alias. `myGen` is the caller's generation — the result is
69
+ * applied only while it's still the current one.
70
+ */
71
+ const poll = useCallback(async (myGen) => {
72
+ try {
73
+ const next = workspaceId
74
+ ? await clientRef.current.landscape(workspaceId)
75
+ : await clientRef.current.getLandscape();
76
+ if (myGen !== gen.current)
77
+ return;
78
+ setSnapshot(next);
79
+ setLastUpdatedMs(Date.now());
80
+ setStatus("live");
81
+ }
82
+ catch (err) {
83
+ if (myGen !== gen.current)
84
+ return;
85
+ // A 401 is terminal-ish: the client's onUnauthorized already ran. Keep
86
+ // the last good board up and flag it; everything else is a transient
87
+ // blip and also keeps the last good board ("reconnecting…" in app.js).
88
+ if (err instanceof ShepherdClientError && err.status === 401) {
89
+ setStatus("unauthorized");
90
+ }
91
+ else {
92
+ setStatus("reconnecting");
93
+ }
94
+ }
95
+ }, [workspaceId]);
96
+ // Poll once on mount, then on the cadence interval. Re-runs when `poll`
97
+ // changes (i.e. on a `workspaceId` switch), re-pulling the new workspace
98
+ // immediately. The interval tick is skipped while the tab is hidden, and a
99
+ // return to visibility polls right away to catch up. The 1s freshness interval
100
+ // is independent so a slow/failed poll never blocks the relative-time refresh.
101
+ useEffect(() => {
102
+ // Disabled: do nothing — no poll, no intervals — so a no-workspace board
103
+ // never hits the hub. Re-enabling re-runs this effect and pulls immediately.
104
+ if (!enabled)
105
+ return;
106
+ const myGen = ++gen.current;
107
+ void poll(myGen);
108
+ const pollTimer = setInterval(() => {
109
+ // A backgrounded wallboard doesn't need to poll; resume below on visible.
110
+ if (document.visibilityState === "hidden")
111
+ return;
112
+ void poll(myGen);
113
+ }, pollMs);
114
+ const freshnessTimer = setInterval(() => {
115
+ setTick((t) => t + 1);
116
+ }, FRESHNESS_MS);
117
+ const onVisible = () => {
118
+ if (document.visibilityState === "visible")
119
+ void poll(myGen);
120
+ };
121
+ document.addEventListener("visibilitychange", onVisible);
122
+ return () => {
123
+ // Bumping the generation invalidates any in-flight poll from this run.
124
+ gen.current++;
125
+ clearInterval(pollTimer);
126
+ clearInterval(freshnessTimer);
127
+ document.removeEventListener("visibilitychange", onVisible);
128
+ };
129
+ }, [poll, pollMs, enabled]);
130
+ // Manual refresh always runs under the current generation, so its result
131
+ // applies as long as the hook hasn't since switched workspace or unmounted.
132
+ const refresh = useCallback(() => poll(gen.current), [poll]);
133
+ return { snapshot, status, lastUpdatedMs, refresh };
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korso/shepherd-ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Auth-agnostic React UI library for the Shepherd wallboard (Dashboard, client, provider). Host injects auth; the library never reads tokens.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",