@hazem3500/komo 0.9.2 → 0.9.5

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 (44) hide show
  1. package/NODE.md +1 -1
  2. package/README.md +84 -9
  3. package/cli/agent.mjs +122 -27
  4. package/cli/design-edit.mjs +269 -0
  5. package/dist/adapters/css-variables.d.ts +9 -0
  6. package/dist/adapters/css-variables.js +1 -0
  7. package/dist/adapters/react-fiber.d.ts +27 -0
  8. package/dist/adapters/react-fiber.js +1 -0
  9. package/dist/agent-prompt.d.ts +3 -1
  10. package/dist/agent-prompt.js +1 -1
  11. package/dist/{chunk-Y2GRLW4Q.js → chunk-DH22G2S5.js} +1 -1
  12. package/dist/chunk-NWRSH6AL.js +1 -0
  13. package/dist/design-comment-MPAF2YAN.js +1 -0
  14. package/dist/design-export-DD2NJI4O.js +1 -0
  15. package/dist/design-export.d.ts +9 -2
  16. package/dist/design-mode-styles.d.ts +1 -1
  17. package/dist/design-mode.js +1 -1
  18. package/dist/design-panel-NU2WUI4D.js +1 -0
  19. package/dist/design-panel-styles.d.ts +1 -1
  20. package/dist/design-system.d.ts +24 -5
  21. package/dist/index.js +1 -1
  22. package/dist/{inline-text-editor-3JLDAUPO.js → inline-text-editor-JWVS7J75.js} +1 -1
  23. package/dist/node.mjs +513 -18
  24. package/dist/postgres/004_personal_access_tokens.sql +25 -0
  25. package/dist/react-props-panel-BW4ZQPD7.js +1 -0
  26. package/dist/react-props-panel.d.ts +19 -2
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/select-menu-V7RYOZFK.js +1 -0
  30. package/dist/select-menu.d.ts +5 -0
  31. package/dist/setup.js +1 -1
  32. package/package.json +14 -3
  33. package/server/account-page.ts +231 -0
  34. package/server/index.ts +62 -5
  35. package/server/migrations/0018_personal_access_tokens.sql +25 -0
  36. package/server/secrets.d.ts +2 -0
  37. package/server/setup-client.txt +54 -59
  38. package/server/tokens.ts +324 -0
  39. package/server/workspaces.ts +2 -0
  40. package/dist/chunk-HCGLCO3R.js +0 -1
  41. package/dist/design-comment-MWNQNKWT.js +0 -1
  42. package/dist/design-export-ZHCBPSWY.js +0 -1
  43. package/dist/design-panel-BPVXFERR.js +0 -1
  44. package/dist/react-props-panel-R5XEVATL.js +0 -1
@@ -0,0 +1,25 @@
1
+ CREATE TABLE personal_access_tokens (
2
+ id text PRIMARY KEY,
3
+ user_id text NOT NULL REFERENCES users(id),
4
+ name text NOT NULL,
5
+ last4 text NOT NULL,
6
+ salt text NOT NULL,
7
+ token_hash text NOT NULL,
8
+ scopes text NOT NULL,
9
+ projects text,
10
+ created_at bigint NOT NULL,
11
+ expires_at bigint,
12
+ last_used_at bigint,
13
+ revoked_at bigint
14
+ );
15
+ CREATE INDEX personal_access_tokens_user ON personal_access_tokens(user_id, created_at);
16
+ CREATE TABLE audit_log (
17
+ id text PRIMARY KEY,
18
+ at bigint NOT NULL,
19
+ actor text NOT NULL,
20
+ action text NOT NULL,
21
+ target text NOT NULL,
22
+ detail text NOT NULL
23
+ );
24
+ CREATE INDEX audit_log_at ON audit_log(at);
25
+ CREATE INDEX audit_log_actor ON audit_log(actor, at);
@@ -0,0 +1 @@
1
+ import{a as e,d as t,e as n,g as o,h as i,i as r,j as a,k as s,l,m as d,n as p,o as c,p as u,q as f,r as g,s as m,t as h,u as b,v as k,w as v,x as y,y as x}from"./chunk-JXIYWNFL.js";import"./chunk-I7AUKTXE.js";var w=new Map,E=new WeakMap,$=!1;function C(e){let t={};for(let n of e?.themes??[])try{t[n.id]=n.current()}catch{}return t}function S(e,t={}){let n=E.get(e);n||E.set(e,n=new Map);let o=JSON.stringify(Object.entries(t).sort()),i=n.get(o);return i||(i=Promise.resolve().then(()=>e.list(Object.keys(t).length?t:void 0)).then(e=>Array.isArray(e)?e.filter(e=>e&&"string"==typeof e.path):[]),i.catch(()=>n.delete(o)),n.set(o,i)),i}function P(e,t){return Promise.resolve().then(()=>e.resolve(t)).then(e=>e??{},()=>({}))}function A(e){let t=e.trim().toLowerCase().replace(/\s+/g,""),n=/^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/.exec(t)?.[1];if(!n)return t.replace(/^rgba\((\d+,\d+,\d+),1\)$/,"rgb($1)");let o=n.length<5?[...n].map(e=>e+e).join(""):n,[i,r,a,s=255]=o.match(/../g).map(e=>parseInt(e,16));return 255===s?`rgb(${i},${r},${a})`:`rgba(${i},${r},${a},${+(s/255).toFixed(3)})`}function N(e,t){let n=A(t);return n?e.find(e=>("color"===e.type||"dimension"===e.type)&&A(e.value)===n):void 0}function M(e){return e.label||e.path}function T(e){return[e.path,e.ref,e.value,e.states?.length?`States: ${e.states.join(", ")}`:"",e.description].filter(Boolean).join(" · ")}function L(){let e=document.createElement("span");return e.className="design-token-off",e.setAttribute("role","img"),e}function j(e,t,n){e.title=`Not from a token · ${t||"unset"}${n?` · matches ${M(n)}`:""}`,e.setAttribute("aria-label",e.title)}function z(e,t,n,o){try{if(e.preview)return e.preview(t,n,o)??(()=>{})}catch{return()=>{}}return function(e,t,n){let o=e.style;if(!o)return()=>{};let i=o.getPropertyValue(t),r=o.getPropertyPriority(t);return o.setProperty(t,n,r),()=>{i?o.setProperty(t,i,r):o.removeProperty(t)}}(t,n,o.value)}function q(e){let t=w.get(e);t?.pop()?.undo(),t&&!t.length&&w.delete(e)}function O(e){return!!w.get(e)?.length}function R(e){for(;w.has(e);)q(e)}function D(e){return[...new Set(w.get(e)?.map(e=>e.property))]}function V(e,t){try{let n=t.adapter.describe?.(e,t.property,t.from,t.token);return n?String(n).replace(/\s+/g," ").replace(/→/g,"->").trim().slice(0,300):void 0}catch{return}}function B(e){let t=[],n=new Set;for(let o of function(e){let t=new Map;for(let n of w.get(e)??[])t.set(n.property,n);return[...t.values()].filter(e=>!("path"in e.from)||e.from.path!==e.token.path)}(e)){n.add(o.adapter.id);let i="path"in o.from?`${o.fromToken?.value?`${o.fromToken.value} `:""}[${o.from.path}]`:o.from.offToken||"(unset)";t.push(`Element ${o.property}: ${i} → ${o.token.value} [${o.token.path}]`);let r=V(e,o);r&&t.push(`Apply: ${r}`)}if(t.length)return["Proposed design-token changes (preview only)",...t,`Token adapter: ${[...n].join(", ")}`].join("\n")}var H=new Map,I=new Map,U=new WeakSet,_=new WeakMap;function F(e){return void 0===e?"unset":JSON.stringify(e)??"unset"}var W=new Map;function J(e){if(e)for(let[e,n]of H){let o=t(e);o&&(W.set(e,o.props),o.setProps({...o.props,...Object.fromEntries(n)}))}else for(let[e,n]of W)t(e)?.setProps(n);e||W.clear(),s(e),function(e){if(e!==$){$=e;for(let[t,n]of w)if(e)for(let e of[...n].reverse())e.undo();else for(let e of n)e.undo=z(e.adapter,t,e.property,e.token)}}(e)}function K(e){return!!H.get(e)?.size}function X(e){let n=t(e),o=n?.definition.textProp,i=n?.definition.props.find(e=>e.name===o);if(!n||!o||"string"!==i?.kind)return null;let r=n.props[o];if("string"!=typeof r||r.length>500)return null;let a=H.get(e);return a||H.set(e,a=new Map),a.has(o)||a.set(o,r),{propName:o,before:r}}function Y(n,o,i){let r=t(n),a=r?.definition.props.find(e=>e.name===o);return!(!r||r.definition.textProp!==o||"string"!==a?.kind||i.length>500||!e(a,i))&&(r.setProps({...r.props,[o]:i}),!0)}function G(e,t,n,o){if(n===o){let o=H.get(e);return void(o?.get(t)===n&&!(I.get(e)??[]).some(e=>e.propName===t)&&(o.delete(t),o.size||H.delete(e)))}let i=I.get(e);i||I.set(e,i=[]),i.push({propName:t,before:n,after:o})}function Q(e,t,n){Y(e,t,n);let o=H.get(e);o?.get(t)===n&&!(I.get(e)??[]).some(e=>e.propName===t)&&(o.delete(t),o.size||H.delete(e))}function Z(e){return!!e&&!!I.get(e)?.length}function ee(e){let n=H.get(e),i=t(e);if(!n?.size||!i)return;let r=o(e),a=[...n].map(([e,t])=>{let n=i.definition.props.find(t=>t.name===e)?.styleProperty,o=i.props[e],a=n&&"string"==typeof o?r?.tokens.find(e=>e.properties.includes(n)&&o===`var(${e.cssVariable})`):void 0;return`- ${e}: ${F(t)} → ${F(o)}${a?` [${a.path}]`:" [custom value]"}`});return["Proposed React prop changes (preview only)",`Component: ${i.definition.name}`,`Source: ${i.definition.source}:${i.definition.line}`,...a,"Preview values use the app's React state callback; Komo does not write source files."].join("\n")}function te(e,o){if(t(e))return null;let i;try{i=o?.components?.identify(e)}catch{}let r=n(e);return i?.name&&(!r||r.root!==i.element&&r.root.contains(i.element))?i:null}var ne=e=>String(e).replace(/\s+/g," ").trim();function oe(e,o){let i=Object.entries(C(o)).map(([e,t])=>`${ne(e)}=${ne(t)}`).join(", "),r=te(e,o),a=t(e),s=a?{root:e,component:a}:r?void 0:n(e);if(s){let{definition:e,props:t}=s.component;r={name:e.name,source:`${e.source}:${e.line}`,props:{...t,...Object.fromEntries(H.get(s.root)??[])}}}let l=Object.entries(r?.props??{}).filter(([,e])=>null===e||["boolean","number"].includes(typeof e)||"string"==typeof e&&e.length<=80).slice(0,12).map(([e,t])=>`${ne(e)}=${JSON.stringify(t).replace(/→/g,"\\u2192")}`);return[i&&`Theme: ${i}`,r?.name&&`Component: ${ne(r.name)}${r.source?` (${ne(r.source)})`:""}`,l.length&&`Props: ${l.join(", ")}`].filter(e=>!!e)}function ie(e){return[ee(e),x(e),B(e)].filter(e=>!!e).join("\n\n")||void 0}function re(e,n,o){_.get(e)?.abort();let i=new AbortController;_.set(e,i),e.addEventListener("click",e=>{let i=n();if(!i)return;let r=x(i)||O(i);!t(i)&&!r||(e.stopImmediatePropagation(),o(i,ie(i)))},{capture:!0,signal:i.signal})}function ae(e){return b(e)}function se(e){return h(e)||O(e)}function le(e){k(e)}function de(e){v(e),R(e)}function pe(e){if(!e||U.has(e))return;let t=document.createElement("style");t.textContent=".design-react-source{display:block;padding:0 0 8px;color:#77736f;font-size:10px;overflow-wrap:anywhere}.design-react-inside{padding:10px 12px}.design-react-inside button{padding:0;border:0;background:none;color:#bda6ef;font:inherit;cursor:pointer}.design-react-inside button:hover{text-decoration:underline}.design-react-field.design-field{display:grid;grid-template-columns:76px minmax(0,1fr);gap:0 8px;height:auto;min-height:28px;margin-top:4px;background:none;box-shadow:none;color:#898680;font-size:11px}.design-react-field>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.design-react-field.design-field>:is(input,select){box-sizing:border-box;width:100%;min-width:0;height:28px;padding:0 8px;border:0;border-radius:6px;background-color:#ffffff0a;color:#f1efeb;font:inherit;font-size:11px}.design-react-field.design-field>:is(input,select):focus-visible{outline:1px solid #bda6ef;outline-offset:-1px}.design-react-field.design-field>select{padding-right:20px}.design-react-field input[type=checkbox]{justify-self:end;width:14px;height:14px;accent-color:#bda6ef}.design-react-field input:disabled{opacity:.55;cursor:not-allowed}.design-react-type{display:none}.design-react-field .design-token-binding{grid-column:2;display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:6px;margin-top:4px;color:#77736f;font-size:10px}.design-react-field .design-token-binding select{min-width:0;height:24px;border:0;border-radius:6px;appearance:none;background:#ffffff0a url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 3 4 5.5 6.5 3' fill='none' stroke='%23898680' stroke-width='1.2'/%3E%3C/svg%3E\") right 6px center/8px no-repeat;color:#e9e6e1;padding:0 18px 0 6px;font:inherit;font-size:10px;cursor:pointer}.design-react-field .design-token-binding[data-mode=token] select{background-color:#bda6ef24;color:#e6dcfb}.design-token-binding__state{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.design-field>.design-token-binding{display:contents}.design-field:has(>.design-token-binding)>input[data-property]{padding-right:28px}.design-token-icon,.design-token-detach,.design-field>.design-token-binding select{position:absolute;top:4px;right:4px;width:20px;height:20px;box-sizing:border-box;border-radius:5px}.design-token-icon{display:grid;place-items:center;color:#77736f;opacity:0;pointer-events:none;transition:opacity .12s}.design-token-icon::before{content:'';width:6px;height:6px;border:1.2px solid currentColor;border-radius:1.5px;rotate:45deg}.design-field:is(:hover,:focus-within) .design-token-icon{opacity:1}.design-field>.design-token-binding select{margin:0;padding:0;border:0;opacity:0;cursor:pointer;appearance:none}.design-field>.design-token-binding select:hover+.design-token-icon,.design-field:has(.design-token-binding select:hover) .design-token-icon{background:#ffffff14;color:#e9e6e1}.design-field:has(.design-token-binding select:focus-visible) :is(.design-token-icon,.design-token-chip){outline:2px solid #bda6ef;outline-offset:1px;opacity:1}.design-field[data-token-mode=override] .design-token-icon{opacity:1;color:#e7c27d}.design-token-chip,.design-token-detach{display:none}.design-field:is([data-token-mode=token],[data-token-mode=connected])>input[data-property]{visibility:hidden}.design-field:is([data-token-mode=token],[data-token-mode=connected]) .design-token-chip{position:absolute;inset:4px 28px 4px var(--lead,4px);display:flex;align-items:center;gap:5px;padding:0 6px;border-radius:4px;background:#bda6ef1f;color:#e6dcfb;font-size:11px;white-space:nowrap;overflow:hidden;pointer-events:none}.design-token-chip::before{content:'';flex:none;width:5px;height:5px;border:1.2px solid #c8b5f4;border-radius:1px;rotate:45deg}.design-field:is([data-token-mode=token],[data-token-mode=connected]) .design-token-binding select{left:var(--lead,4px);right:28px;width:auto}.design-field:is([data-token-mode=token],[data-token-mode=connected]) .design-token-icon{display:none}.design-field:is([data-token-mode=token],[data-token-mode=connected]) .design-token-detach{display:grid;place-items:center;color:#898680}.design-token-detach:hover{background:#ffffff14;color:#f1efeb}.design-token-detach svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:1.2;stroke-linecap:round}.design-theme-group .design-react-source{padding:0 0 6px}.design-theme-heading{display:block;padding:8px 0 2px;color:#77736f;font-size:10px;text-transform:capitalize}.design-theme-token{position:relative;display:grid;grid-template-columns:96px minmax(0,1fr) 20px;align-items:center;gap:6px;margin-top:4px}.design-theme-token>span{overflow:hidden;color:#898680;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.design-theme-token[data-changed]>span{color:#e6dcfb}.design-theme-token[data-changed]::before{content:'';position:absolute;top:calc(50% - 2px);left:-8px;width:4px;height:4px;border-radius:50%;background:#bda6ef}.design-theme-reset{display:grid;place-items:center;width:20px;height:20px;padding:0;border:0;border-radius:5px;background:none;color:#77736f;cursor:pointer}.design-theme-reset:disabled{visibility:hidden}.design-theme-reset:hover{background:#ffffff14;color:#f1efeb}.design-theme-reset svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}.design-token-off{position:absolute;top:8px;right:24px;display:grid;place-items:center;width:12px;height:12px}.design-token-off::before{content:'';width:4px;height:4px;border-radius:50%;background:#e7c27d;opacity:.7}.design-token-off[hidden]{display:none}.design-field:has(>.design-token-binding>.design-token-off:not([hidden]))>input[data-property]{padding-right:38px}.design-theme-switch{padding:6px 12px 8px}.design-theme-switch .design-react-field>span{text-transform:capitalize}.design-theme-options{display:flex;min-width:0;padding:2px;border-radius:6px;background:#ffffff0a}.design-theme-options button{flex:1;min-width:0;height:24px;padding:0 6px;border:0;border-radius:4px;background:none;color:#898680;font:inherit;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.design-theme-options button:hover{color:#f1efeb}.design-theme-options button[aria-pressed=true]{background:#ffffff1a;color:#f1efeb}.design-theme-options button:focus-visible{outline:1px solid #bda6ef;outline-offset:-1px}@media (max-width:520px){.design-theme-options button{height:30px}.design-react-field.design-field>:is(input,select){height:34px}.design-token-icon,.design-token-detach,.design-field>.design-token-binding select{top:7px}.design-token-off{top:11px}}",e.append(t),U.add(e)}function ce(e,t,n,r){let a=o(e);if(!a)return null;let s=m(a,t);if(!s.length)return null;pe(r);let l=document.createElement("span");l.className="design-token-binding",l.setAttribute("data-mode","custom");let c=document.createElement("span");c.className="design-token-icon",c.setAttribute("aria-hidden","true");let h=document.createElement("span");h.className="design-token-chip",h.setAttribute("aria-hidden","true");let b=document.createElement("button");b.type="button",b.className="design-token-detach",b.title="Detach token",b.setAttribute("aria-label",`Detach token from ${t}`),b.innerHTML='<svg viewBox="0 0 12 12"><path d="M5 3.5 6.5 2a2.1 2.1 0 0 1 3 3L8 6.5M7 8.5 5.5 10a2.1 2.1 0 0 1-3-3L4 5.5M2 2l8 8"/></svg>';let k=document.createElement("select");k.setAttribute("aria-label",`Style token for ${t}`);let v=document.createElement("small");v.className="design-token-binding__state",v.setAttribute("aria-live","polite");let y=`komo-token-${t}-${Math.random().toString(36).slice(2,8)}`;v.id=y,k.setAttribute("aria-describedby",y);let x=document.createElement("option");x.value="custom",x.textContent="Custom value",k.append(x);for(let e of s){let t=document.createElement("option"),n=i(a,e);t.value=e.path,t.textContent=n?`${e.path} · ${n}`:e.path,k.append(t)}let w=L(),E=()=>{let n=u(a,e,t),o=n?null:f(a,e,t),r="token"===n?.mode?s.find(e=>e.path===n.tokenPath):void 0,d=r??o?.token;k.value=d?.path??"custom",h.textContent=d?.path??"";let p=n?.declaredPath,c=r?"token":o?"connected":p?"override":"custom";if(l.dataset.mode=c,l.parentElement?.setAttribute("data-token-mode",c),v.textContent="token"===c?p&&p!==r?.path?`Rebound from ${p} · preview`:`Linked to ${r?.path} · preview`:"connected"===c?"Connected "+("inline"===o?.origin?"inline":"from stylesheet"):"override"===c?`Custom override · was ${p}`:"",k.title=v.textContent||"Choose a token",w.hidden="custom"!==c,"custom"===c){let n=getComputedStyle(e).getPropertyValue(t).trim();j(w,n,N(s.map(e=>({path:e.path,type:e.type,value:i(a,e)})),n))}},$=new MutationObserver(E);$.observe(e,{attributes:!0,attributeFilter:["style"]});let C=new MutationObserver(()=>{l.isConnected||($.disconnect(),C.disconnect())});return C.observe(l.ownerDocument,{childList:!0,subtree:!0}),k.addEventListener("change",()=>{let o=e.style.getPropertyValue(t),i=s.find(e=>e.path===k.value),r=u(a,e,t);if(i&&r&&i.path===r.declaredPath)g(a,e,t),n(o,e.style.getPropertyValue(t),i.path);else if(i)(i.path!==f(a,e,t)?.token.path||r)&&d(a,e,i,t,{recordHistory:!1})&&n(o,e.style.getPropertyValue(t),i.path);else{let i=l.parentElement?.querySelector("[data-property]")?.value??getComputedStyle(e).getPropertyValue(t).trim(),r=/^-?[\d.]+$/.test(i.trim())&&!/weight|line/.test(t)?`${i.trim()}px`:i;p(a,e,t,r),e.style.setProperty(t,r),n(o,r)}E()}),b.addEventListener("click",()=>{let e=s.find(e=>e.path===k.value),t=l.parentElement?.querySelector("input[data-property]"),n=e&&i(a,e);t&&n&&(t.value="number"===t.type?String(parseFloat(n)):n),k.value="custom",k.dispatchEvent(new Event("change")),l.parentElement?.querySelector("[data-property]")?.focus()}),l.append(h,k,c,b,w,v),queueMicrotask(E),E(),l}function ue(e){let t=o(e);return new Set([...t?.elementBindings.get(e)?.keys()??[],...D(e)])}function fe(e,t,n,o,i,r,a,s,l){let d=t.flatMap(e=>{let t=[...n.querySelectorAll("[data-field-property]")].find(t=>t.dataset.fieldProperty===e.property),o=t?.querySelector("[data-property]");return t&&o?[{field:e,label:t,input:o}]:[]}),p=t=>{for(let{field:o,label:i,input:s}of d){if(i.querySelector(".design-token-binding")||!n.contains(i))continue;let l=t(o,i,s,()=>{r&&(s.value=r(o,e));let t=i.querySelector('input[type="range"],input[type="color"]');t&&(t.value="color"===o.kind&&a?a(s.value):s.value)});l&&i.append(l)}},c=s?.tokens;if(c)return pe(i),Promise.all([S(c,C(s)),P(c,e)]).then(([t,n])=>{p((o,i,r,a)=>ge(c,e,o.property,t,n[o.property],e=>{l?.(e),a()},i,r))},()=>{});p((t,n,r,a)=>ce(e,t.property,e=>{o(t,r,e),a()},i))}function ge(e,t,n,o,i,r,a,s){let l=function(e,t){return e.filter(e=>e.properties?.includes(t))}(o,n);if(!l.length&&!i)return null;let d=i&&"offToken"in i?i.offToken:void 0,p=void 0===d?void 0:N(l,d),c=document.createElement("span");c.className="design-token-binding";let u=document.createElement("span");u.className="design-token-chip",u.setAttribute("aria-hidden","true");let f=document.createElement("span");f.className="design-token-icon",f.setAttribute("aria-hidden","true");let g=document.createElement("button");g.type="button",g.className="design-token-detach",g.title="Edit the value",g.setAttribute("aria-label",`Edit ${n} value`),g.innerHTML='<svg viewBox="0 0 12 12"><path d="M5 3.5 6.5 2a2.1 2.1 0 0 1 3 3L8 6.5M7 8.5 5.5 10a2.1 2.1 0 0 1-3-3L4 5.5M2 2l8 8"/></svg>';let m=document.createElement("select");m.setAttribute("aria-label",`Style token for ${n}`),m.disabled=!l.length;let h=document.createElement("option");h.value="",h.textContent="Choose a token",m.append(h);let b=new Map([["",m]]);for(let e of l){let t=e.group??"",n=b.get(t);n||(n=document.createElement("optgroup"),n.label=t,b.set(t,n));let o=document.createElement("option");o.value=e.path,o.textContent=`${M(e)}${e.ref?` · ${e.ref}`:""}${e===p?" · same value":""}`,o.title=T(e),n.append(o)}for(let[e,t]of b)e&&m.append(t);let k=L();void 0!==d&&j(k,d,p);let v=document.createElement("small");v.className="design-token-binding__state",v.setAttribute("aria-live","polite");let y=!1,x=()=>{let e=function(e,t){return[...w.get(e)??[]].reverse().find(e=>e.property===t)?.token}(t,n),r=y?void 0:e?.path??(i&&"path"in i?i.path:void 0),s=r?o.find(e=>e.path===r):void 0;m.value=r&&l.some(e=>e.path===r)?r:"",u.textContent=s?M(s):r??"";let p=r?e?"token":"connected":"custom";c.dataset.mode=p,(a??c.parentElement)?.setAttribute("data-token-mode",p),k.hidden=void 0===d||!!e,v.textContent=s?`${e?"Preview":"Uses"} ${T(s)}`:r?`Uses ${r}`:k.hidden?"":k.title,m.title=v.textContent||"Choose a token"};return m.addEventListener("change",()=>{let a=l.find(e=>e.path===m.value);if(!a)return x();y=!1;let s=i??{offToken:getComputedStyle(t).getPropertyValue(n).trim()},d="path"in s?o.find(e=>e.path===s.path):void 0;r(function(e,t,n,o,i,r){let a=w.get(t);a||w.set(t,a=[]);let s=a.find(e=>e.property===n);return a.push({adapter:e,property:n,token:o,from:s?.from??i,fromToken:s?s.fromToken:r,undo:z(e,t,n,o)}),()=>q(t)}(e,t,n,a,s,d)),x()}),g.addEventListener("click",()=>{y=!0,x(),s?.focus()}),c.append(u,m,f,g,k,v),x(),c}var me=!1;function he(e,t,n,i){let r=o(e);if(!r||!r.tokens.length)return null;pe(i);let a=document.createElement("details");a.className="design-group design-theme-group",a.open=me,a.addEventListener("toggle",()=>{a.isConnected&&(me=a.open)});let s=document.createElement("summary");s.textContent="Shared theme";let l=document.createElement("small");l.className="design-react-source",l.textContent="Changes every element using these tokens",a.append(s,l);let d=new Map;for(let e of r.tokens){let t=e.path.split(".")[0];d.set(t,[...d.get(t)??[],e])}let p=r.tokens.length>6&&d.size>1;for(let[e,o]of p?d:new Map([["",r.tokens]])){if(e){let t=document.createElement("small");t.className="design-theme-heading",t.textContent=e,a.append(t)}for(let i of o)a.append(be(r,i,t,n,e))}return a}function be(e,t,n,o,s){let d=document.createElement("div");d.className="design-theme-token";let p=document.createElement("span");p.textContent=s&&t.path.slice(s.length+1)||t.path,p.title=`${t.path} · ${r(e,t)} matching · ${t.impactSelector}`;let c=n.fields.find(e=>t.properties.includes(e.property)),u={property:t.cssVariable,label:`Shared theme value for ${t.path}`,kind:"dimension"===t.type?"size":c?.kind??"text",glyph:"color"===t.type?void 0:c?.glyph,values:c?.values},f=!1,g=n.fieldControl(u,{element:e.root,begin:()=>{f=!1},apply:(n,i,r)=>{let s=null!==r&&a(e,t,r.trim(),f);n.setAttribute("aria-invalid",String(!s)),s&&(f=!0,k(),o())},commit:(e,t,n)=>{!1!==n&&(f=!1,"true"===e.getAttribute("aria-invalid")&&b())}}),m=g.querySelector("[data-property]"),h=g.querySelector('input[type="color"]'),b=()=>{let o=i(e,t);m.value=o,h&&(h.value=n.toHex(o)),m.removeAttribute("aria-invalid"),k()},k=()=>{v.disabled=!d.toggleAttribute("data-changed",e.themeEdits.has(t.path)),"fontFamily"===t.type&&(m.title=m.value)},v=document.createElement("button");return v.type="button",v.className="design-theme-reset",v.title="Reset to page value",v.setAttribute("aria-label",`Reset ${t.path}`),v.innerHTML='<svg viewBox="0 0 16 16"><path d="M2.5 2.5v3h3M3 5.5a5.5 5.5 0 1 1-.5 2.5"/></svg>',v.addEventListener("click",()=>{l(e,t),b(),o()}),b(),d.append(p,g,v),d}function ke(e,t,n){let i=o(e);i&&p(i,e,t,n)}function ve(e,t){let n=o(e);n&&c(n,e,t)}function ye(e){let n=I.get(e)?.pop(),o=t(e);if(!n||!o)return;o.setProps({...o.props,[n.propName]:n.before});let i=H.get(e);if(i){let t=i.get(n.propName);Object.is(t,n.before)&&i.delete(n.propName),i.size||H.delete(e)}}function xe(){for(let e of[...H.keys()])Ae(e);H.clear(),I.clear(),y(),function(){for(let e of[...w.keys()])R(e)}(),Ce()}var we,Ee=new Map;function $e(e){try{return String(e.current())}catch{return}}function Ce(){for(let[e,t]of Ee)try{$e(e)!==t&&e.apply(t)}catch{}Ee.clear()}var Se=(e,t)=>e||t.charAt(0).toUpperCase()+t.slice(1);function Pe(e,t,n,o){if(e.querySelector(".design-theme-switch"))return;let i=document.createElement("div");i.className="design-group design-theme-switch";let r,a=t?.themes;for(let e of Array.isArray(a)?a:[]){let t=$e(e),o=Array.isArray(e?.options)?e.options.filter(e=>"string"==typeof e?.id):[];if(void 0===t||o.length<2)continue;let a=Se(e.label,String(e.id)),s=o=>{let i=$e(e);if(o!==i){Ee.has(e)||Ee.set(e,i??t),we=e;try{e.apply(o)}catch{}n()}},l=document.createElement(o.length>3?"label":"div");l.className="design-field design-react-field";let d,p=document.createElement("span");if(p.textContent=a,o.length>3){let e=document.createElement("select");for(let t of o)e.append(new Option(Se(t.label,t.id),t.id));e.value=t,e.setAttribute("aria-label",`${a} theme`),e.addEventListener("change",()=>s(e.value)),d=e}else{d=document.createElement("div"),d.className="design-theme-options",d.setAttribute("role","group"),d.setAttribute("aria-label",`${a} theme`);for(let e of o){let n=document.createElement("button");n.type="button",n.textContent=Se(e.label,e.id),n.setAttribute("aria-pressed",String(e.id===t)),n.addEventListener("click",()=>s(e.id)),d.append(n)}}we===e&&(r=d.querySelector("[aria-pressed=true]")??d),l.append(p,d),i.append(l)}we=void 0,i.childElementCount&&(pe(o),e.prepend(i),r?.focus({preventScroll:!0}))}function Ae(e){let n=H.get(e),o=t(e);!n?.size||!o||(o.setProps({...o.props,...Object.fromEntries(n)}),H.delete(e),I.delete(e))}function Ne(t,n,o,i,r,a){let s=n.definition.props.find(e=>e.name===o);if(!s||!e(s,i))return r.setAttribute("aria-invalid","true"),!1;r.removeAttribute("aria-invalid");let l=n.props[o];if(Object.is(l,i))return!0;let d=H.get(t);d||H.set(t,d=new Map);let p=d.has(o)?d.get(o):l;Object.is(p,i)?d.delete(o):d.set(o,p),d.size||H.delete(t);let c=I.get(t);return c||I.set(t,c=[]),c.push({propName:o,before:l,after:i}),n.setProps({...n.props,[o]:i}),a(),!0}function Me(e,i,r,a,s){let l=t(e);if(!l){let t=te(e,s),o=null;try{o=t&&s?.components?.panel?.(t)||null}catch{}return o&&pe(r),o??function(e,t,o){let i=n(e);if(!i)return null;pe(o);let r=document.createElement("small");if(r.className="design-react-source design-react-group design-react-inside",r.append(`Inside ${i.component.definition.name}`),t){let e=document.createElement("button");e.type="button",e.textContent="Select component",e.addEventListener("click",()=>t(i.root)),r.append(" · ",e)}return r}(e,a,r)}pe(r);let d=document.createElement("details");d.className="design-group design-react-group",d.open=!0;let p=document.createElement("summary");p.textContent="Properties",d.append(p);let c=document.createElement("small");c.className="design-react-source",c.textContent=`${l.definition.name} · ${l.definition.source}:${l.definition.line}`,d.append(c);for(let t of l.definition.props){let n=t.styleProperty,r=document.createElement("label");r.className="design-field design-react-field";let a=document.createElement("span");a.textContent=t.name;let s,p=l.props[t.name];if("enum"===t.kind){let e=document.createElement("select");for(let n of t.values??[]){let t=document.createElement("option");t.value=String(n),t.textContent=String(n),e.append(t)}if(void 0===p&&!t.required){let t=document.createElement("option");t.value="",t.textContent="unset",e.prepend(t)}e.value=void 0===p?"":String(p),s=e}else{let e=document.createElement("input");"boolean"===t.kind?(e.type="checkbox",e.checked=!0===p):"number"===t.kind?(e.type="number",e.step="any",e.value="number"==typeof p?String(p):""):(e.type="text",e.maxLength=500,e.autocomplete="off",e.value="string"==typeof p?p:""),s=e}if(s.setAttribute("aria-label",`${l.definition.name} ${t.name}`),"unsupported"===t.kind)s=document.createElement("input"),s.disabled=!0,s.value=`Read only · ${t.typeLabel}`,s.setAttribute("aria-label",`${l.definition.name} ${t.name}, read only`),r.title=`Komo does not edit ${t.typeLabel} props.`;else{let n=()=>{let n=s instanceof HTMLInputElement&&"checkbox"===s.type?s.checked:s.value,o=n;if("number"===t.kind)o=""===String(n).trim()?void 0:Number(n);else if("enum"===t.kind){let e=(t.values??[]).find(e=>String(e)===n);o=""!==n||t.required?e:void 0}else"boolean"===t.kind&&(o=!0===n);Ne(e,l,t.name,o,s,i)};s.addEventListener("change",n)}r.append(a,s);let c=document.createElement("small");if(c.className="design-react-type",c.textContent=`${t.typeLabel}${t.required?" · required":" · optional"}`,r.append(c),r.title||=c.textContent,"string"===t.kind&&n){let a=o(e),d=a?m(a,n):[];if(d.length){let o=document.createElement("span");o.className="design-token-binding";let a=document.createElement("span");a.textContent="Style token";let p=document.createElement("select");p.setAttribute("aria-label",`Style token for ${t.name}`);let c=document.createElement("option");c.value="custom",c.textContent="Custom value",p.append(c);for(let e of d){let t=document.createElement("option");t.value=e.path,t.textContent=e.path,p.append(t)}let u=()=>{let e=d.find(e=>l.props[t.name]===`var(${e.cssVariable})`);p.value=e?.path??"custom",o.dataset.mode=e?"token":"custom"};p.addEventListener("change",()=>{let o=d.find(e=>e.path===p.value),r=o?`var(${o.cssVariable})`:"string"==typeof l.props[t.name]&&String(l.props[t.name]).startsWith("var(")?e.ownerDocument.defaultView?.getComputedStyle(e).getPropertyValue(n).trim()??"":"string"==typeof l.props[t.name]?l.props[t.name]:"";Ne(e,l,t.name,r,s,i)&&u()}),o.append(a,p),r.append(o),u()}}d.append(r)}return d}export{fe as attachStyleTokenControls,Pe as attachThemeSwitcher,X as beginReactTextEdit,ae as canUndoDesignTokenEdit,Z as canUndoReactProp,Q as cancelReactTextEdit,ge as createAdapterTokenControl,he as createDesignThemePanel,ce as createElementTokenControl,Me as createReactPropsPanel,oe as designContext,G as finishReactTextEdit,se as hasDesignTokenEdit,K as hasReactPropChanges,re as installCommentCapture,ke as noteDesignTokenOverride,Y as previewReactTextEdit,ie as proposedDesignChanges,ee as proposedReactChanges,de as resetDesignTokenEdits,xe as resetPreviews,Ae as resetReactProps,Ce as restoreThemes,J as showDesignBefore,ve as syncDesignTokenBinding,ue as tokenBoundProperties,le as undoDesignTokenEdit,ye as undoReactProp};
@@ -14,6 +14,13 @@ export declare function finishReactTextEdit(element: HTMLElement, propName: stri
14
14
  export declare function cancelReactTextEdit(element: HTMLElement, propName: string, before: string): void;
15
15
  export declare function canUndoReactProp(element: HTMLElement | null): boolean;
16
16
  export declare function proposedReactChanges(element: HTMLElement): string | undefined;
17
+ /**
18
+ * Context lines for a proposal, so agents and the CLI know what the change was
19
+ * made in: "Theme: mode=dark", "Component: PromoCard (src/card.tsx:19)" and
20
+ * 'Props: tone="calm"'. The registered React component wins, then the app's
21
+ * component adapter, then the registered component around the element.
22
+ */
23
+ export declare function designContext(element: HTMLElement, design?: DesignSystem): string[];
17
24
  export declare function proposedDesignChanges(element: HTMLElement): string | undefined;
18
25
  export declare function installCommentCapture(button: HTMLButtonElement, selected: () => HTMLElement | null, onComment: (element: Element, proposal?: string) => void): void;
19
26
  export declare function canUndoDesignTokenEdit(element: HTMLElement | null): boolean;
@@ -39,8 +46,18 @@ export declare function createDesignThemePanel(element: HTMLElement, panel: Them
39
46
  export declare function noteDesignTokenOverride(element: HTMLElement, property: string, value: string): void;
40
47
  export declare function syncDesignTokenBinding(element: HTMLElement, property: string): void;
41
48
  export declare function undoReactProp(element: HTMLElement): void;
42
- /** Undo every prop, token and theme preview, e.g. when Design mode closes. */
49
+ /** Undo every prop, token and theme preview, and put switched themes back, e.g. when Design mode closes. */
43
50
  export declare function resetPreviews(): void;
51
+ /** Put every axis Design mode switched back to its original option. */
52
+ export declare function restoreThemes(): void;
53
+ /**
54
+ * One row per theme axis at the top of the panel: a segmented control for two or
55
+ * three options, a select for more. Theme is context, not an edit, so switching
56
+ * isn't counted, undone, reset or flipped by Before/After; Design mode restores
57
+ * each axis's original option when it closes. `onSwitch` re-renders the panel,
58
+ * so token lists, chips and values follow the new theme.
59
+ */
60
+ export declare function attachThemeSwitcher(container: HTMLElement, design: DesignSystem | undefined, onSwitch: () => void, styleRoot?: ShadowRoot): void;
44
61
  export declare function resetReactProps(element: HTMLElement): void;
45
- export declare function createReactPropsPanel(element: HTMLElement, onEdit: () => void, styleRoot?: ShadowRoot, select?: (root: HTMLElement) => void): HTMLElement | null;
62
+ export declare function createReactPropsPanel(element: HTMLElement, onEdit: () => void, styleRoot?: ShadowRoot, select?: (root: HTMLElement) => void, design?: DesignSystem): HTMLElement | null;
46
63
  export {};
package/dist/react.d.ts CHANGED
@@ -21,7 +21,7 @@ export interface UseKomoInspectableOptions {
21
21
  }
22
22
  /**
23
23
  * Opt one React DOM root into the Design mode prop inspector. The component
24
- * must provide its own state update callback; Komo never reads Fiber internals
24
+ * must provide its own state update callback; Komo's core never reads Fiber internals
25
25
  * or writes the host application's source files.
26
26
  */
27
27
  export declare function useKomoInspectable(options: UseKomoInspectableOptions): {
package/dist/react.js CHANGED
@@ -1 +1 @@
1
- "use client";import{b as e,c as n,f as t,z as r}from"./chunk-JXIYWNFL.js";import{b as o}from"./chunk-HCGLCO3R.js";import"./chunk-KMYXE4O6.js";import"./chunk-Y2GRLW4Q.js";import"./chunk-7YUXYVQA.js";import"./chunk-62YUEO4G.js";import"./chunk-I7AUKTXE.js";import{useEffect as s,useRef as c}from"react";function u(e){let n=c(null);s(()=>{if(n.current&&function(e,n){return[...new Set([...Object.keys(e),...Object.keys(n)])].every(t=>{if("shortcuts"===t||"features"===t){let r=e[t]??{},o=n[t]??{};return Object.keys({...r,...o}).every(e=>Object.is(r[e],o[e]))}if("design"===t){let t=e.design,r=n.design;if(!t||!r)return t===r;let o=t.themes??[],s=r.themes??[];return t.tokens===r.tokens&&t.components===r.components&&o.length===s.length&&o.every((e,n)=>e===s[n])}if("onboarding"!==t)return Object.is(e[t],n[t]);let r=e.onboarding,o=n.onboarding;return r&&o?Object.keys({...r,...o}).every(e=>"sites"===e?r.sites?.length===o.sites?.length&&(r.sites??[]).every((e,n)=>e===o.sites?.[n]):Object.is(r[e],o[e])):r===o})}(n.current.config,e)||(n.current?.controller.destroy(),n.current=null,!1===e.enabled))return;let t={...e,...e.onboarding?{onboarding:{...e.onboarding,...e.onboarding.sites?{sites:[...e.onboarding.sites]}:{}}}:{}};n.current={config:t,controller:o(t)}}),s(()=>()=>{n.current?.controller.destroy(),n.current=null},[])}function i(t){let r=c(null),o=c(null);return o.current||(o.current=e=>{r.current=e}),s(()=>{if(!0===t.enabled&&r.current)return e(r.current,t.component,t.props,t.onChange)},[t.enabled,t.component]),s(()=>{!0===t.enabled&&n(r.current,t.component,t.props,t.onChange)},[t.enabled,t.component,t.props,t.onChange]),{ref:o.current,"data-komo-inspectable":t.component.id}}function l(e){s(()=>{if(!0!==e.enabled)return;let n=e.root??document.documentElement;return t(n,e.tokens)},[e.root,e.tokens,e.enabled])}export{r as cssVariableTokens,u as useKomo,l as useKomoDesignTokens,i as useKomoInspectable};
1
+ "use client";import{b as e,c as n,f as t,z as r}from"./chunk-JXIYWNFL.js";import{b as o}from"./chunk-NWRSH6AL.js";import"./chunk-KMYXE4O6.js";import"./chunk-DH22G2S5.js";import"./chunk-7YUXYVQA.js";import"./chunk-62YUEO4G.js";import"./chunk-I7AUKTXE.js";import{useEffect as s,useRef as c}from"react";function u(e){let n=c(null);s(()=>{if(n.current&&function(e,n){return[...new Set([...Object.keys(e),...Object.keys(n)])].every(t=>{if("shortcuts"===t||"features"===t){let r=e[t]??{},o=n[t]??{};return Object.keys({...r,...o}).every(e=>Object.is(r[e],o[e]))}if("design"===t){let t=e.design,r=n.design;if(!t||!r)return t===r;let o=t.themes??[],s=r.themes??[];return t.tokens===r.tokens&&t.components===r.components&&o.length===s.length&&o.every((e,n)=>e===s[n])}if("onboarding"!==t)return Object.is(e[t],n[t]);let r=e.onboarding,o=n.onboarding;return r&&o?Object.keys({...r,...o}).every(e=>"sites"===e?r.sites?.length===o.sites?.length&&(r.sites??[]).every((e,n)=>e===o.sites?.[n]):Object.is(r[e],o[e])):r===o})}(n.current.config,e)||(n.current?.controller.destroy(),n.current=null,!1===e.enabled))return;let t={...e,...e.onboarding?{onboarding:{...e.onboarding,...e.onboarding.sites?{sites:[...e.onboarding.sites]}:{}}}:{}};n.current={config:t,controller:o(t)}}),s(()=>()=>{n.current?.controller.destroy(),n.current=null},[])}function i(t){let r=c(null),o=c(null);return o.current||(o.current=e=>{r.current=e}),s(()=>{if(!0===t.enabled&&r.current)return e(r.current,t.component,t.props,t.onChange)},[t.enabled,t.component]),s(()=>{!0===t.enabled&&n(r.current,t.component,t.props,t.onChange)},[t.enabled,t.component,t.props,t.onChange]),{ref:o.current,"data-komo-inspectable":t.component.id}}function l(e){s(()=>{if(!0!==e.enabled)return;let n=e.root??document.documentElement;return t(n,e.tokens)},[e.root,e.tokens,e.enabled])}export{r as cssVariableTokens,u as useKomo,l as useKomoDesignTokens,i as useKomoInspectable};
@@ -0,0 +1 @@
1
+ import"./chunk-I7AUKTXE.js";var e,t=new WeakSet,o=new WeakSet,n=0;function i(n){e&&!e.select.isConnected&&e.close();let i=n.getRootNode();if((i instanceof ShadowRoot||i===document)&&!o.has(i)){o.add(i);let e=document.createElement("style");e.textContent=".design-field>select{appearance:none;padding-right:20px;cursor:pointer;background:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 3 4 5.5 6.5 3' fill='none' stroke='%23898680' stroke-width='1.2'/%3E%3C/svg%3E\") right 7px center/8px no-repeat}.komo-select{position:absolute;z-index:20;box-sizing:border-box;width:max-content;max-height:240px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin;padding:4px;border-radius:10px;background:#232323;box-shadow:0 0 0 1px #ffffff14,0 12px 32px #0008;color:#d9d6d1;font-size:11px;font-weight:400;line-height:1.2;text-align:left;cursor:default;transform-origin:top;animation:komo-select-in var(--duration-fast,250ms) var(--ease-smooth-out,cubic-bezier(.22,1,.36,1)) backwards}.komo-select[data-up]{transform-origin:bottom}@keyframes komo-select-in{from{opacity:0;transform:scale(.97)}}@media (prefers-reduced-motion:reduce){.komo-select{animation:none}}.komo-select [role=option]{display:flex;align-items:center;gap:8px;min-height:26px;padding:0 8px;border-radius:6px;white-space:nowrap;cursor:pointer}.komo-select [role=option] span{flex:1;overflow:hidden;text-overflow:ellipsis}.komo-select [data-active]{background:#ffffff14;color:#f1efeb}.komo-select [aria-selected=true]{color:#f1efeb}.komo-select [aria-selected=true]::after{content:'';flex:none;width:7px;height:3.5px;margin:-3px 1px 0 0;border:solid #bda6ef;border-width:0 0 1.5px 1.5px;rotate:-45deg}.komo-select [aria-disabled=true]{opacity:.4;cursor:default}.komo-select small{display:block;padding:8px 8px 4px;color:#77736f;font-size:10px;font-weight:500}.komo-select [role=group]+[role=group],.komo-select [role=option]+[role=group]{margin-top:4px;border-top:1px solid #ffffff0d}@media (pointer:coarse){.komo-select{font-size:13px}.komo-select [role=option]{min-height:40px}}",(i instanceof ShadowRoot?i:document.head).append(e)}if(t.has(n))return;t.add(n);let d=e=>{let t=e.composedPath()[0];return t instanceof HTMLSelectElement&&!t.disabled&&!t.multiple?t:null};n.addEventListener("focusin",e=>{let t=d(e);t&&!t.hasAttribute("aria-haspopup")&&(t.setAttribute("aria-haspopup","listbox"),t.setAttribute("aria-expanded","false"))}),n.addEventListener("pointerdown",e=>{a="mouse"===e.pointerType?void 0:e;let t=d(e);!t||e.button||(e.preventDefault(),a||r(t))},!0),n.addEventListener("pointerup",e=>{let t=d(e);t&&a&&!l(e)&&r(t)},!0);for(let e of["mousedown","touchend"])n.addEventListener(e,e=>{d(e)&&e.cancelable&&e.preventDefault()},!0);n.addEventListener("keydown",t=>{let o=d(t);!o||e?.select===o||t.ctrlKey||t.metaKey||["Enter"," ","ArrowDown","ArrowUp"].includes(t.key)&&(t.preventDefault(),s(o))},!0)}var a,l=e=>a&&Math.abs(e.clientY-a.clientY)>10;function r(t){if(e?.select===t)return e.close();a||t.focus(),s(t)}function s(t){e?.close(),a=void 0;let o="komo-select-"+ ++n,i=document.createElement("div");i.className="komo-select",i.id=o,i.setAttribute("role","listbox"),i.setAttribute("aria-label",t.getAttribute("aria-label")??t.title);let r=[],s=(e,t)=>{let n=document.createElement("div");n.id=`${o}-${r.length}`,n.setAttribute("role","option"),n.setAttribute("aria-selected",String(e.selected)),e.disabled&&n.setAttribute("aria-disabled","true"),e.title&&(n.title=e.title);let i=document.createElement("span");i.textContent=e.label,n.append(i),t.append(n),r.push({node:n,option:e})};for(let e of t.children)if(e instanceof HTMLOptionElement)s(e,i);else if(e instanceof HTMLOptGroupElement){let t=document.createElement("div");t.setAttribute("role","group"),t.setAttribute("aria-labelledby",`${o}-g${r.length}`);let n=document.createElement("small");n.id=`${o}-g${r.length}`,n.textContent=e.label,t.append(n);for(let o of e.querySelectorAll("option"))s(o,t);i.append(t)}let d=-1,p=r.flatMap((e,t)=>e.option.disabled?[]:[t]),c=e=>{if(!(e>=0))return;r[d]?.node.removeAttribute("data-active"),d=e;let o=r[e].node;o.toggleAttribute("data-active",!0),t.setAttribute("aria-activedescendant",o.id);let n=o.offsetTop,a=n+o.offsetHeight;n<i.scrollTop?i.scrollTop=n-4:a>i.scrollTop+i.clientHeight&&(i.scrollTop=a-i.clientHeight+4)},f=e=>{let t=p.indexOf(d);c(p[t<0?0:Math.max(0,Math.min(p.length-1,t+e))])},u=e=>{let o=r[e]?.option;h(),o&&!o.disabled&&!o.selected&&(o.selected=!0,t.dispatchEvent(new Event("input",{bubbles:!0,composed:!0})),t.dispatchEvent(new Event("change",{bubbles:!0})))},m=new AbortController,{signal:g}=m;function h(){e?.select===t&&(e=void 0,m.abort(),i.remove(),t.setAttribute("aria-expanded","false"),t.removeAttribute("aria-activedescendant"),t.removeAttribute("aria-controls"))}e={select:t,close:h};let b="",x=0;addEventListener("keydown",e=>{if(!t.isConnected||e.composedPath()[0]!==t)return h();let o=e.key;if(!(e.ctrlKey||e.metaKey||e.altKey&&!o.startsWith("Arrow"))){if("Tab"===o)return h();if("ArrowDown"===o||"ArrowUp"===o)f("ArrowDown"===o?1:-1);else if("Home"===o||"PageUp"===o)c(p[0]);else if("End"===o||"PageDown"===o)c(p[p.length-1]);else if("Escape"===o)h();else if("Enter"===o||" "===o&&!b)u(d);else{if(1!==o.length)return;{b=e.timeStamp-x<1e3?b+o.toLowerCase():o.toLowerCase(),x=e.timeStamp;let t=Math.max(0,p.indexOf(d)+(b[1]?0:1)),n=[...p.slice(t),...p.slice(0,t)].find(e=>r[e].option.label.toLowerCase().startsWith(b));void 0!==n&&c(n)}}e.preventDefault(),e.stopImmediatePropagation()}},{capture:!0,signal:g}),addEventListener("pointerdown",e=>{let o=e.composedPath();!o.includes(i)&&!o.includes(t)&&h()},{capture:!0,signal:g});let v=e=>{e.target!==i&&h()};addEventListener("scroll",v,{capture:!0,signal:g}),t.getRootNode().addEventListener("scroll",v,{capture:!0,signal:g}),addEventListener("resize",h,{signal:g}),t.addEventListener("blur",h,{signal:g}),i.addEventListener("mousedown",e=>e.preventDefault(),{signal:g}),i.addEventListener("pointermove",e=>{let t=e.target.closest?.("[role=option]"),o=r.findIndex(e=>e.node===t);p.includes(o)&&o!==d&&c(o)},{signal:g}),i.addEventListener("click",e=>{if(e.preventDefault(),l(e))return;let t=e.target.closest?.("[role=option]"),o=r.findIndex(e=>e.node===t);p.includes(o)&&u(o)},{signal:g});let w=t.closest(".design-field")??t.parentElement;w.append(i),t.setAttribute("aria-haspopup","listbox"),t.setAttribute("aria-expanded","true"),t.setAttribute("aria-controls",o);let E=(t.offsetWidth<60?w:t).getBoundingClientRect(),k=function(e){let t={top:0,left:0,bottom:innerHeight,right:innerWidth};for(let o=e.parentElement;o;o=o.parentElement)if(/auto|scroll|hidden/.test(getComputedStyle(o).overflowY)){let e=o.getBoundingClientRect();return{top:Math.max(e.top,0),left:Math.max(e.left,0),bottom:Math.min(e.bottom,t.bottom),right:Math.min(e.right,t.right)}}return t}(w),A=(i.offsetParent??w).getBoundingClientRect(),y=Math.min(Math.max(E.width,i.offsetWidth+2,120),k.right-k.left-8),L=k.bottom-E.bottom-8,M=E.top-k.top-8,C=i.scrollHeight,T=C>L&&M>L,D=Math.max(T?M:L,60),H=Math.min(C,240,D);i.style.width=`${y}px`,i.style.maxHeight=`${Math.min(240,D)}px`,i.style.left=Math.max(k.left+4,Math.min(E.left,k.right-y-4))-A.left+"px",i.style.top=(T?E.top-H-4:E.bottom+4)-A.top+"px",i.toggleAttribute("data-up",T),c(r.findIndex(e=>e.option.selected&&!e.option.disabled)),d<0&&f(0)}export{i as attach};
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Replace the system popup of every select inside `container`, now and later.
3
+ * Called on each render, it also closes a popup whose select was just replaced.
4
+ */
5
+ export declare function attach(container: HTMLElement): void;
package/dist/setup.js CHANGED
@@ -1 +1 @@
1
- import{a as o,c as n}from"./chunk-HCGLCO3R.js";import"./chunk-KMYXE4O6.js";import"./chunk-Y2GRLW4Q.js";import"./chunk-7YUXYVQA.js";import"./chunk-62YUEO4G.js";import"./chunk-I7AUKTXE.js";function r(r){return(s={})=>n(o({...r,...s}))}export{r as defineKomo};
1
+ import{a as o,c as n}from"./chunk-NWRSH6AL.js";import"./chunk-KMYXE4O6.js";import"./chunk-DH22G2S5.js";import"./chunk-7YUXYVQA.js";import"./chunk-62YUEO4G.js";import"./chunk-I7AUKTXE.js";function r(r){return(s={})=>n(o({...r,...s}))}export{r as defineKomo};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hazem3500/komo",
3
- "version": "0.9.2",
3
+ "version": "0.9.5",
4
4
  "description": "Point, area, and threaded comments for website branches.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,9 @@
28
28
  "server/import-project.ts",
29
29
  "server/digest.ts",
30
30
  "server/project-sites.ts",
31
- "server/iap.ts"
31
+ "server/iap.ts",
32
+ "server/tokens.ts",
33
+ "server/account-page.ts"
32
34
  ],
33
35
  "main": "./dist/index.js",
34
36
  "types": "./dist/index.d.ts",
@@ -50,6 +52,14 @@
50
52
  "types": "./dist/adapters/tailwind.d.ts",
51
53
  "import": "./dist/adapters/tailwind.js"
52
54
  },
55
+ "./adapters/css-variables": {
56
+ "types": "./dist/adapters/css-variables.d.ts",
57
+ "import": "./dist/adapters/css-variables.js"
58
+ },
59
+ "./adapters/react-fiber": {
60
+ "types": "./dist/adapters/react-fiber.d.ts",
61
+ "import": "./dist/adapters/react-fiber.js"
62
+ },
53
63
  "./adapters/class-theme": {
54
64
  "types": "./dist/adapters/class-theme.d.ts",
55
65
  "import": "./dist/adapters/class-theme.js"
@@ -96,6 +106,7 @@
96
106
  "esbuild": "^0.25.0",
97
107
  "jsdom": "^30.1.0",
98
108
  "motion": "^12.43.0",
109
+ "playwright-core": "1.64.1",
99
110
  "react": "19.2.7",
100
111
  "react-colorful": "^5.8.1",
101
112
  "react-dom": "19.2.7",
@@ -109,7 +120,7 @@
109
120
  "pg": "^8.23.0"
110
121
  },
111
122
  "bin": {
112
- "komo": "./cli/index.mjs"
123
+ "komo": "cli/index.mjs"
113
124
  },
114
125
  "engines": {
115
126
  "node": ">=22.12"
@@ -0,0 +1,231 @@
1
+ // The hosted account page: sign in with Google, then create, list and revoke
2
+ // personal access tokens for bots and CI. Rendered with DOM APIs only (no
3
+ // innerHTML), under a nonce-based CSP.
4
+ const css = `
5
+ :root{--bg:#fff;--surface:#f7f7f9;--fg:#1d1d1f;--fg2:#6e6e73;--fg3:#6f6f75;--accent:#c8b5f4;--accent-fg:#352a50;--line:#0000001a;--danger:#b3261e;--ok:#1d7a46;color-scheme:light}
6
+ @media (prefers-color-scheme:dark){:root{--bg:#1b1b1e;--surface:#262629;--fg:#eeeef0;--fg2:#b0afb7;--fg3:#a4a2ad;--accent:#d3bdf5;--line:#ffffff1f;--danger:#ff8a80;--ok:#7fd69f;color-scheme:dark}}
7
+ *{box-sizing:border-box}
8
+ body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased}
9
+ button,input,select{font:inherit;color:inherit}
10
+ button{cursor:pointer}
11
+ a{color:inherit}
12
+ :focus-visible{outline:2px solid var(--accent);outline-offset:3px}
13
+ .skip{position:absolute;left:12px;top:-60px;background:var(--surface);padding:8px 12px;border-radius:8px;z-index:5}
14
+ .skip:focus{top:12px}
15
+ header{display:flex;align-items:center;justify-content:space-between;gap:16px;max-width:760px;margin:0 auto;padding:28px 24px 0}
16
+ .brand{font-size:24px;font-weight:600;letter-spacing:-.8px;color:var(--accent);text-decoration:none}
17
+ .who{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--fg2);min-width:0}
18
+ .who img,.who .initial{width:28px;height:28px;border-radius:50%;flex:none;background:var(--surface);display:grid;place-items:center;font-weight:600;color:var(--fg2)}
19
+ .who span.name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
20
+ main{max-width:760px;margin:0 auto;padding:44px 24px 96px}
21
+ h1{font-size:32px;letter-spacing:-1.2px;line-height:1.15;font-weight:600;margin:0 0 10px}
22
+ .lede{margin:0;color:var(--fg2);max-width:560px}
23
+ .bar{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin-bottom:28px}
24
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 20px;border:0;border-radius:12px;background:var(--surface);color:var(--fg);font-weight:500;box-shadow:inset 0 0 0 1px var(--line);transition:background .15s,transform .15s}
25
+ .btn:hover{background:var(--line)}
26
+ .btn:active{transform:scale(.98)}
27
+ .btn.primary{background:var(--accent);color:var(--accent-fg);box-shadow:inset 0 1px 0 #ffffff40,0 2px 4px #0002}
28
+ .btn.primary:hover{filter:brightness(1.05)}
29
+ .btn.danger{color:var(--danger)}
30
+ .btn[disabled]{opacity:.55;cursor:default}
31
+ .panel{background:var(--surface);border-radius:16px;box-shadow:inset 0 0 0 1px var(--line)}
32
+ .notice{padding:20px 22px;color:var(--fg2)}
33
+ .notice strong{display:block;color:var(--fg);font-weight:600;margin-bottom:4px}
34
+ .notice.error{color:var(--danger)}
35
+ .tokens{list-style:none;margin:0;padding:0;display:grid;gap:12px}
36
+ .token{padding:18px 20px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 16px;align-items:start}
37
+ .token h2{margin:0;font-size:16px;font-weight:600;display:flex;flex-wrap:wrap;align-items:center;gap:8px}
38
+ .mono{font-family:ui-monospace,"SFMono-Regular",Consolas,"Liberation Mono",monospace;font-size:13px;color:var(--fg2);overflow-wrap:anywhere}
39
+ .badge{font-size:12px;font-weight:500;line-height:1;padding:5px 8px;border-radius:999px;background:var(--bg);box-shadow:inset 0 0 0 1px var(--line);color:var(--fg2)}
40
+ .badge.warn{color:var(--danger)}
41
+ .meta{grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px 20px;margin:10px 0 0;font-size:13px}
42
+ .meta div{min-width:0}
43
+ .meta dt{color:var(--fg3);font-size:12px;margin-bottom:2px}
44
+ .meta dd{margin:0;overflow-wrap:anywhere}
45
+ .skeleton{height:96px;border-radius:16px;background:linear-gradient(90deg,var(--surface),var(--line),var(--surface));background-size:200% 100%;animation:shimmer 1.4s infinite}
46
+ @keyframes shimmer{to{background-position:-200% 0}}
47
+ dialog{width:min(480px,calc(100vw - 24px));max-height:calc(100dvh - 24px);overflow:auto;border:0;border-radius:20px;padding:0;background:var(--bg);color:var(--fg);box-shadow:0 0 0 1px var(--line),0 24px 60px -12px #0007}
48
+ dialog::backdrop{background:#0006;backdrop-filter:blur(2px)}
49
+ dialog[open]{animation:pop .18s ease-out}
50
+ @keyframes pop{from{opacity:0;transform:translateY(8px) scale(.98)}}
51
+ form,.reveal{padding:24px;display:grid;gap:18px}
52
+ dialog h2{margin:0;font-size:20px;letter-spacing:-.4px}
53
+ label.field,fieldset{display:grid;gap:6px;border:0;margin:0;padding:0;min-width:0;font-size:14px}
54
+ legend,label.field>span{font-weight:500;padding:0}
55
+ input[type=text],select{min-height:44px;padding:0 12px;border-radius:10px;border:0;background:var(--surface);box-shadow:inset 0 0 0 1px var(--line);width:100%}
56
+ .choice{display:flex;align-items:center;gap:10px;min-height:36px;color:var(--fg)}
57
+ .choice input{width:18px;height:18px;accent-color:var(--accent);margin:0}
58
+ .choice small{color:var(--fg3)}
59
+ .projects{display:grid;gap:2px;max-height:160px;overflow:auto;padding:6px 10px;border-radius:10px;background:var(--surface);box-shadow:inset 0 0 0 1px var(--line)}
60
+ .hint{color:var(--fg3);font-size:13px;margin:0}
61
+ .actions{display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}
62
+ .secret{display:flex;gap:8px;align-items:stretch}
63
+ .secret code{flex:1;min-width:0;padding:12px;border-radius:10px;background:var(--surface);box-shadow:inset 0 0 0 1px var(--line);font:13px/1.5 ui-monospace,"SFMono-Regular",Consolas,monospace;overflow-wrap:anywhere;user-select:all}
64
+ .warning{padding:12px 14px;border-radius:10px;background:var(--surface);box-shadow:inset 0 0 0 1px var(--line);font-size:14px;margin:0}
65
+ .error-text{color:var(--danger);font-size:14px;margin:0}
66
+ .live{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
67
+ [hidden]{display:none!important}
68
+ @media (max-width:560px){main{padding-top:32px}h1{font-size:28px}.bar{flex-direction:column;align-items:stretch}.bar .btn{width:100%}.token{grid-template-columns:1fr}.token .btn{width:100%}.secret{flex-direction:column}.actions .btn{flex:1}.who span.name{display:none}}
69
+ @media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
70
+ `;
71
+
72
+ const script = `
73
+ const cfg=JSON.parse(document.getElementById("cfg").textContent);
74
+ const $=(s)=>document.querySelector(s);
75
+ const KEY="komo-account-session";
76
+ let session=null,user=null,data=null;
77
+ const el=(tag,props,...kids)=>{const n=document.createElement(tag);for(const k in props||{}){if(k==="class")n.className=props[k];else if(k.startsWith("on"))n.addEventListener(k.slice(2),props[k]);else if(props[k]===true)n.setAttribute(k,"");else if(props[k]!==false&&props[k]!=null)n.setAttribute(k,props[k])}n.append(...kids.flat().filter((x)=>x!=null));return n};
78
+ const live=(t)=>{$("#live").textContent="";setTimeout(()=>{$("#live").textContent=t},30)};
79
+ const date=(t)=>new Intl.DateTimeFormat(undefined,{dateStyle:"medium"}).format(new Date(t));
80
+ const ago=(t)=>{if(!t)return "Never";const s=Math.max(0,(Date.now()-t)/1000);if(s<90)return "Just now";if(s<5400)return Math.round(s/60)+" min ago";if(s<129600)return Math.round(s/3600)+" hr ago";return date(t)};
81
+ async function api(path,method,body){
82
+ const r=await fetch(cfg.base+path+"?project="+encodeURIComponent(cfg.project),{method:method||"GET",headers:{...(session?{Authorization:"Bearer "+session}:{}),...(body?{"Content-Type":"application/json"}:{})},body:body?JSON.stringify(body):undefined});
83
+ let j={};try{j=await r.json()}catch{}
84
+ if(r.status===401&&session&&path!=="/auth/google/start"){session=null;sessionStorage.removeItem(KEY);user=null;render()}
85
+ if(!r.ok){const e=new Error(j.error||"Request failed ("+r.status+").");e.status=r.status;throw e}
86
+ return j}
87
+ function view(name){for(const v of ["loading","signin","guest","tokens"])$("#v-"+v).hidden=v!==name}
88
+ function render(){
89
+ const header=$("#who");header.replaceChildren();
90
+ if(!session||!user){view(session?"loading":"signin");return}
91
+ const google=user.verified&&String(user.id).startsWith("google:");
92
+ header.append(user.avatarUrl?el("img",{src:user.avatarUrl,alt:"",referrerpolicy:"no-referrer"}):el("span",{class:"initial","aria-hidden":"true"},(user.name||"?").slice(0,1).toUpperCase()),el("span",{class:"name"},user.name),el("button",{class:"btn",type:"button",onclick:signOut},"Sign out"));
93
+ if(!google){view("guest");return}
94
+ view("tokens");list()}
95
+ function list(){
96
+ const ul=$("#list");ul.replaceChildren();
97
+ if(!data){ul.append(el("li",{class:"skeleton"}));return}
98
+ $("#empty").hidden=data.tokens.length>0;
99
+ for(const t of data.tokens){
100
+ const scopes=t.scopes.join(", ");
101
+ const projects=t.projects?t.projects.join(", "):"All projects";
102
+ ul.append(el("li",{class:"panel token"},
103
+ el("div",null,el("h2",null,t.name,t.expired?el("span",{class:"badge warn"},"Expired"):null),el("div",{class:"mono"},t.prefix+"…"+t.last4)),
104
+ el("button",{class:"btn danger",type:"button","aria-label":"Revoke "+t.name,onclick:()=>confirmRevoke(t)},"Revoke"),
105
+ el("dl",{class:"meta"},
106
+ meta("Scopes",scopes),meta("Projects",projects),meta("Created",date(t.createdAt)),meta("Expires",t.expiresAt?date(t.expiresAt):"Never"),meta("Last used",ago(t.lastUsedAt)))))}}
107
+ const meta=(k,v)=>el("div",null,el("dt",null,k),el("dd",null,v));
108
+ async function load(){
109
+ try{data=await api("/account/tokens");list()}
110
+ catch(e){if(e.status===403)view("guest");else if(e.status!==401){$("#list").replaceChildren(el("li",{class:"panel notice error"},e.message))}}}
111
+ async function boot(){
112
+ session=sessionStorage.getItem(KEY);
113
+ if(!session){render();return}
114
+ view("loading");
115
+ try{user=(await api("/me")).user;render();await load()}catch(e){if(e.status!==401){view("signin");$("#signin-error").textContent=e.message}}}
116
+ async function signIn(){
117
+ const btn=$("#signin");btn.disabled=true;$("#signin-error").textContent="";
118
+ const popup=window.open("about:blank","komo-account-auth","width=500,height=700");
119
+ try{
120
+ const {url}=await api("/auth/google/start","POST");
121
+ if(!popup){throw new Error("Allow pop-ups to sign in with Google.")}
122
+ popup.location.href=url;
123
+ await new Promise((resolve,reject)=>{
124
+ const done=(fn,v)=>{removeEventListener("message",on);clearInterval(poll);fn(v)};
125
+ const on=(e)=>{if(e.origin!==location.origin||e.source!==popup||e.data?.type!=="branch-comments:auth")return;session=e.data.token;sessionStorage.setItem(KEY,session);user=e.data.user;done(resolve)};
126
+ const poll=setInterval(()=>{if(popup.closed&&!session)done(reject,new Error("Sign-in was cancelled."))},600);
127
+ addEventListener("message",on)});
128
+ render();await load()
129
+ }catch(e){try{popup&&popup.close()}catch{}$("#signin-error").textContent=e.message}finally{btn.disabled=false}}
130
+ async function signOut(){
131
+ try{await api("/me","DELETE")}catch{}
132
+ session=null;user=null;data=null;sessionStorage.removeItem(KEY);render()}
133
+
134
+ const dlg=$("#create"),rev=$("#reveal"),conf=$("#confirm");
135
+ $("#new").onclick=()=>{
136
+ $("#form").reset();$("#form-error").textContent="";
137
+ const has=data&&data.projects.length>0;
138
+ $("#project-scope").hidden=!has;
139
+ const box=$("#project-list");box.replaceChildren(...(data?data.projects:[]).map((p)=>el("label",{class:"choice"},el("input",{type:"checkbox",name:"project",value:p}),p)));
140
+ box.hidden=true;
141
+ dlg.showModal();$("#name").focus()};
142
+ for(const r of document.querySelectorAll("input[name=limit]"))r.onchange=()=>{$("#project-list").hidden=document.querySelector("input[name=limit]:checked").value!=="some"};
143
+ $("#cancel").onclick=()=>dlg.close();
144
+ $("#form").onsubmit=async(e)=>{
145
+ e.preventDefault();
146
+ const f=e.target,b=$("#submit");$("#form-error").textContent="";
147
+ const scopes=[...f.querySelectorAll("input[name=scope]:checked")].map((i)=>i.value);
148
+ if(!scopes.length){$("#form-error").textContent="Choose at least one scope.";return}
149
+ const some=f.querySelector("input[name=limit]:checked").value==="some";
150
+ const projects=some?[...f.querySelectorAll("input[name=project]:checked")].map((i)=>i.value):[];
151
+ if(some&&!projects.length){$("#form-error").textContent="Choose at least one project, or allow all projects.";return}
152
+ const days=f.expiry.value;
153
+ b.disabled=true;
154
+ try{
155
+ const r=await api("/account/tokens","POST",{name:f.tokenName.value.trim(),scopes,projects,expiresInDays:days==="never"?null:Number(days)});
156
+ dlg.close();
157
+ $("#secret").textContent=r.token;$("#copy").textContent="Copy";
158
+ rev.showModal();$("#copy").focus();live("Token created. Copy it now; it will not be shown again.");
159
+ data.tokens.unshift(r.item);list();
160
+ }catch(err){$("#form-error").textContent=err.message}finally{b.disabled=false}};
161
+ $("#copy").onclick=async()=>{
162
+ const t=$("#secret").textContent;
163
+ try{await navigator.clipboard.writeText(t)}catch{const r=document.createRange();r.selectNodeContents($("#secret"));getSelection().removeAllRanges();getSelection().addRange(r);document.execCommand&&document.execCommand("copy")}
164
+ $("#copy").textContent="Copied";live("Token copied to clipboard.")};
165
+ $("#done").onclick=()=>{rev.close()};
166
+ rev.addEventListener("close",()=>{$("#secret").textContent=""});
167
+ rev.addEventListener("cancel",(e)=>{if(!confirm("Close without copying? This token will not be shown again."))e.preventDefault()});
168
+ let target=null;
169
+ function confirmRevoke(t){target=t;$("#confirm-text").textContent="Revoke \\u201C"+t.name+"\\u201D? Anything using it stops working immediately. This can\\u2019t be undone.";$("#confirm-error").textContent="";conf.showModal()}
170
+ $("#keep").onclick=()=>conf.close();
171
+ $("#revoke").onclick=async()=>{
172
+ const b=$("#revoke");b.disabled=true;
173
+ try{await api("/account/tokens/"+target.id,"DELETE");data.tokens=data.tokens.filter((t)=>t.id!==target.id);conf.close();list();live("Token revoked.");$("#new").focus()}
174
+ catch(e){$("#confirm-error").textContent=e.message}finally{b.disabled=false}};
175
+ $("#signin").onclick=signIn;
176
+ $("#guest-out").onclick=signOut;
177
+ boot();
178
+ `;
179
+
180
+ export function accountPage(env: Env, url: URL) {
181
+ const nonce = crypto.randomUUID();
182
+ const configured = Object.keys(JSON.parse(env.PROJECTS));
183
+ const requested = url.searchParams.get("project");
184
+ // Hosted sign-in uses the management project; self-hosted servers use one of their own.
185
+ const project =
186
+ env.KOMO_HOSTED === "true"
187
+ ? "_komo"
188
+ : requested && configured.includes(requested)
189
+ ? requested
190
+ : (configured[0] ?? "_komo");
191
+ const config = JSON.stringify({
192
+ base: env.KOMO_PUBLIC_PATH ?? "",
193
+ project,
194
+ }).replace(/</g, "\\u003c");
195
+ return new Response(
196
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><meta name="robots" content="noindex"><title>Access tokens · komo</title><link rel="icon" href="data:,"><style nonce="${nonce}">${css}</style></head><body>
197
+ <a class="skip" href="#main">Skip to content</a>
198
+ <header><a class="brand" href="/">komo</a><div class="who" id="who"></div></header>
199
+ <main id="main">
200
+ <div id="v-loading"><div class="skeleton"></div></div>
201
+ <section id="v-signin" hidden><h1>Access tokens</h1><p class="lede">Sign in with Google to create tokens that let bots and CI use the komo CLI as you.</p><p><button class="btn primary" id="signin" type="button">Continue with Google</button></p><p class="error-text" id="signin-error" role="alert"></p></section>
202
+ <section id="v-guest" hidden><h1>Access tokens</h1><div class="panel notice"><strong>Sign in with Google to manage tokens</strong><span>Guest sessions can’t create access tokens, because a token would carry an identity that can’t be verified.</span></div><p><button class="btn" id="guest-out" type="button">Sign out</button></p></section>
203
+ <section id="v-tokens" hidden>
204
+ <div class="bar"><div><h1>Access tokens</h1><p class="lede">Use a token as <code class="mono">KOMO_TOKEN</code> so bots and CI can read or write comments as you. Treat tokens like passwords.</p></div><button class="btn primary" id="new" type="button">New token</button></div>
205
+ <ul class="tokens" id="list" aria-label="Your access tokens"></ul>
206
+ <div class="panel notice" id="empty" hidden><strong>No tokens yet</strong>Create one for a bot, a script, or a CI job.</div>
207
+ </section>
208
+ </main>
209
+ <dialog id="create" aria-labelledby="create-title"><form id="form" novalidate><h2 id="create-title">New access token</h2>
210
+ <label class="field"><span>Name</span><input id="name" name="tokenName" type="text" required maxlength="60" autocomplete="off" placeholder="e.g. Deploy bot"></label>
211
+ <label class="field"><span>Expiration</span><select name="expiry"><option value="7">7 days</option><option value="30" selected>30 days</option><option value="90">90 days</option><option value="365">1 year</option><option value="never">No expiration</option></select></label>
212
+ <fieldset><legend>Scopes</legend><label class="choice"><input type="checkbox" name="scope" value="comments:read" checked>comments:read <small>list and read threads</small></label><label class="choice"><input type="checkbox" name="scope" value="comments:write">comments:write <small>create, reply, resolve</small></label></fieldset>
213
+ <fieldset id="project-scope"><legend>Projects</legend><label class="choice"><input type="radio" name="limit" value="all" checked>All projects you can access</label><label class="choice"><input type="radio" name="limit" value="some">Only selected projects</label><div class="projects" id="project-list" hidden></div></fieldset>
214
+ <p class="error-text" id="form-error" role="alert"></p>
215
+ <div class="actions"><button class="btn" id="cancel" type="button">Cancel</button><button class="btn primary" id="submit" type="submit">Create token</button></div></form></dialog>
216
+ <dialog id="reveal" aria-labelledby="reveal-title"><div class="reveal"><h2 id="reveal-title">Copy your token</h2><p class="warning">This is the only time it will be shown. Store it in your secret manager, never in source control.</p><div class="secret"><code id="secret" tabindex="0" aria-label="Access token"></code><button class="btn primary" id="copy" type="button">Copy</button></div><div class="actions"><button class="btn" id="done" type="button">Done</button></div></div></dialog>
217
+ <dialog id="confirm" aria-labelledby="confirm-title"><div class="reveal"><h2 id="confirm-title">Revoke token</h2><p id="confirm-text" class="hint"></p><p class="error-text" id="confirm-error" role="alert"></p><div class="actions"><button class="btn" id="keep" type="button">Keep token</button><button class="btn danger" id="revoke" type="button">Revoke</button></div></div></dialog>
218
+ <div class="live" id="live" role="status" aria-live="polite"></div>
219
+ <script type="application/json" id="cfg">${config}</script>
220
+ <script nonce="${nonce}">${script}</script></body></html>`,
221
+ {
222
+ headers: {
223
+ "Content-Type": "text/html; charset=utf-8",
224
+ "Cache-Control": "no-store",
225
+ "Referrer-Policy": "no-referrer",
226
+ "X-Content-Type-Options": "nosniff",
227
+ "Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; img-src https: data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'`,
228
+ },
229
+ }
230
+ );
231
+ }