@hazem3500/komo 0.8.0 → 0.9.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 (46) hide show
  1. package/NODE.md +20 -0
  2. package/README.md +64 -0
  3. package/dist/accent-picker-F5UI2NP7.js +1 -0
  4. package/dist/adapters/class-theme.d.ts +22 -0
  5. package/dist/adapters/class-theme.js +1 -0
  6. package/dist/adapters/tailwind.d.ts +62 -0
  7. package/dist/adapters/tailwind.js +1 -0
  8. package/dist/chunk-5LHGJFWQ.js +1 -0
  9. package/dist/chunk-JC6FXUNO.js +1 -0
  10. package/dist/{chunk-RPKLGTEU.js → chunk-Q7WSWLMV.js} +1 -1
  11. package/dist/{chunk-E5X4IQJ2.js → chunk-YZP3KX4Y.js} +1 -1
  12. package/dist/design-comment-RTVUFVDE.js +1 -0
  13. package/dist/design-comment.d.ts +50 -0
  14. package/dist/design-mode-styles.d.ts +1 -1
  15. package/dist/design-mode.d.ts +2 -0
  16. package/dist/design-mode.js +1 -1
  17. package/dist/design-system.d.ts +150 -0
  18. package/dist/design-tokens.d.ts +2 -0
  19. package/dist/dom.d.ts +10 -0
  20. package/dist/index.js +1 -1
  21. package/dist/inline-text-editor-SWPVB35B.js +1 -0
  22. package/dist/inline-text-editor.d.ts +3 -0
  23. package/dist/node.mjs +218 -65
  24. package/dist/{onboarding-YA63RUKK.js → onboarding-NO3RQWR7.js} +1 -1
  25. package/dist/react-inspector.d.ts +9 -1
  26. package/dist/react-props-panel-XQ4T3RLV.js +1 -0
  27. package/dist/react-props-panel.d.ts +3 -1
  28. package/dist/react.js +1 -1
  29. package/dist/review-layout.d.ts +3 -1
  30. package/dist/setup.js +1 -1
  31. package/dist/tag-editor-ZA7NLPOJ.js +1 -0
  32. package/dist/toolbar-runtime-2JO6YXD5.js +1 -0
  33. package/dist/types.d.ts +20 -0
  34. package/package.json +11 -2
  35. package/server/iap.ts +133 -0
  36. package/server/index.ts +108 -17
  37. package/server/secrets.d.ts +6 -0
  38. package/server/setup-client.txt +142 -70
  39. package/src/types.ts +21 -0
  40. package/dist/accent-picker-HSUCA4KE.js +0 -1
  41. package/dist/chunk-Z2ABBESO.js +0 -1
  42. package/dist/chunk-ZUNTYWJF.js +0 -1
  43. package/dist/inline-text-editor-F7B6FQQ3.js +0 -1
  44. package/dist/react-props-panel-XSMDR2CY.js +0 -1
  45. package/dist/tag-editor-VDPZ6CO3.js +0 -1
  46. package/dist/toolbar-runtime-MYOLQPTJ.js +0 -1
package/NODE.md CHANGED
@@ -29,6 +29,26 @@ The browser widget and CLI use the same API protocol on Cloudflare and Node. Thi
29
29
 
30
30
  For a guest-only project, you can skip the generator and set `DATABASE_URL`, `PUBLIC_URL`, and `PROJECTS` directly. For example, `PROJECTS={"review":{"repo":"your-org/your-repo","origins":["https://pr-*.preview.example.com"],"allowGuests":true}}`. Anyone on an approved site can then comment; do not use guest-only mode for confidential reviews. `PROJECTS` must remain on one line in an env file. Google sign-in requires both `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`.
31
31
 
32
+ ### Serve under a path
33
+
34
+ If a reverse proxy serves komo under a path, include it in `PUBLIC_URL`, for example `PUBLIC_URL=https://labs.example.com/komo`. Sign-in links and redirect URLs then include `/komo`. Requests are accepted whether or not the proxy strips the path. Register OAuth redirect URIs with the path, for example `https://labs.example.com/komo/auth/google/callback`.
35
+
36
+ ### Google Cloud Identity-Aware Proxy (IAP) sign-in
37
+
38
+ Behind a Google Cloud load balancer with IAP, komo can trust IAP's signed identity instead of running its own Google OAuth client. This is off by default. To turn it on:
39
+
40
+ - **`KOMO_IAP_AUDIENCE`:** set it to the IAP backend audience, `/projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID`. komo then verifies the `x-goog-iap-jwt-assertion` header against Google's IAP signing keys, checking the issuer, audience and expiry. It mints a session for that Google account, so `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` aren't needed.
41
+ - **`KOMO_AUTH_PREFIX`** (optional, for example `/backoffice`): serves the sign-in handlers only under this prefix, so the load balancer can protect just that path with IAP. The handlers are `PREFIX/auth/google/authorize` (the browser popup) and `POST PREFIX/auth/iap/session?project=KEY` (agents exchange their IAP identity for a project session). The same handlers without the prefix return 404.
42
+
43
+ While IAP is on:
44
+
45
+ - Google is the only sign-in method. Guest and GitHub sign-in are refused.
46
+ - Reading or writing comments requires a session from an IAP-verified account.
47
+ - Missing, forged, expired or wrong-audience assertions are rejected.
48
+ - The rest of the API stays outside IAP: the widget calls it cross-origin with the bearer session from the popup.
49
+
50
+ Make sure clients can't reach the prefixed handlers without going through IAP. The signed assertion check rejects forged headers either way.
51
+
32
52
  ## Connect a preview and bot
33
53
 
34
54
  Use the existing widget options; only its API endpoint changes:
package/README.md CHANGED
@@ -115,6 +115,58 @@ Komo adapts those patterns to an embedded live page: direct hover/select, a comp
115
115
 
116
116
  Sources: [Figma layer properties and styles](https://help.figma.com/hc/en-us/articles/360039832014-Design-prototype-and-explore-layer-properties-in-the-right-sidebar), [Figma Dev Mode](https://help.figma.com/hc/en-us/articles/15023124644247-Guide-to-Dev-Mode), [pen.dev installation and IDE design mode](https://docs.pen.dev/getting-started/installation), [pen.dev interface and undo/redo](https://docs.pen.dev/core-concepts/pencil-interface), [OpenPencil features and properties panel](https://openpencil.dev/overview/features), [OpenPencil layers and selection sync](https://openpencil.dev/user-guide/layers-and-pages), [Puck component configuration](https://puckeditor.com/docs/integrating-puck/component-configuration), [Puck nested object and array fields](https://puckeditor.com/docs/api-reference/fields/object), [Puck permissions](https://puckeditor.com/docs/api-reference/permissions), [Puck source and MIT license](https://github.com/puckeditor/puck), and the [Design Tokens Format Module 2025.10](https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/).
117
117
 
118
+ ### Design-system adapters
119
+
120
+ `design: { tokens, themes, components }` tells Design mode about your design system. These are types in `DesignSystem`, and all three are optional. komo never guesses tokens from computed styles or reads framework internals; your adapters answer.
121
+
122
+ - `tokens: TokenAdapter` lists tokens (aliases resolved, per theme). For a picked element, it says which token each CSS property uses, or that the value is off-token. It can also preview a token, for example by swapping a utility class, and describe how to apply the change in source.
123
+ - `themes: ThemeAxis[]` are independent axes reviewers can flip, such as light/dark or a product palette. Proposals record the theme state they were made in.
124
+ - `components: ComponentAdapter` identifies the component behind an element (name, props, source), so Design mode shows "Button (variant=secondary)". It works alongside `useKomoInspectable`.
125
+
126
+ Design proposals also carry a versioned `DesignEdit` record (token swaps, prop changes, text, and a raw CSS fallback) that the CLI and agents can apply.
127
+
128
+ The API is landing in stages, and this section is updated as each part ships. Without `design.tokens`, Design mode uses the CSS custom properties registered with `useKomoDesignTokens`.
129
+
130
+ #### Tailwind and class themes
131
+
132
+ Two optional adapters ship as their own entry points, outside the core bundle. For utility-class design systems, a token is a class: komo previews by swapping classes, not by setting CSS variables.
133
+
134
+ ```ts
135
+ import { tailwindTokens, tailwindSpacing, type TailwindToken } from "@hazem3500/komo/adapters/tailwind";
136
+ import { classTheme } from "@hazem3500/komo/adapters/class-theme";
137
+
138
+ const mode = classTheme({
139
+ id: "mode",
140
+ options: [{ id: "light", class: "light" }, { id: "dark", class: "dark" }],
141
+ colorScheme: true, // also sets color-scheme: light | dark on <html>
142
+ });
143
+ const product = classTheme({
144
+ id: "product",
145
+ options: [{ id: "focus", attribute: { "data-product": "focus" } }, { id: "track", attribute: { "data-product": "track" } }],
146
+ });
147
+
148
+ // Generate this from your design-system foundations.
149
+ const tokens: TailwindToken[] = [
150
+ { path: "color.text.primary", type: "color", class: "text-primary", properties: ["color"], group: "Text",
151
+ values: { light: "#1f1f1f", dark: "#f5f5f5", "track+dark": "#fce5d8" } },
152
+ { path: "color.bg.primary-interactive", type: "color", class: "bg-primary-interactive",
153
+ properties: ["background-color"], value: "#e57cd8", states: ["hover", "active", "disabled"] },
154
+ { path: "radius.md", type: "dimension", class: "rounded-md", properties: ["border-radius"], value: "6px" },
155
+ { path: "typography.h1", type: "typography", class: "text-h1",
156
+ properties: ["font-size", "line-height", "font-weight"], value: "32px/40px 600" },
157
+ ...tailwindSpacing(), // p-*, px-*, …, m-*, gap-* on the 4px scale
158
+ ];
159
+
160
+ initKomo({ project, design: { tokens: tailwindTokens({ tokens }, { themes: [mode, product] }), themes: [mode, product] } });
161
+ ```
162
+
163
+ - `class` is the utility class; an array lists aliases, and the first is the one previews add. `properties` are the CSS properties the class sets, as the Design panel names them. A shorthand such as `padding` or `border-radius` also covers its longhands.
164
+ - `values` keys are option ids (`dark`), `axis=option` pairs (`mode=dark`), or combinations joined with `+` (`track+dark`). The most specific match wins, then `value`.
165
+ - `resolve()` reads base-state classes only; variants such as `hover:` or `md:` are ignored. When classes overlap, the narrower class wins, as in Tailwind (`p-4 px-2` reads `padding-left` from `px-2`). Properties with no table class are reported off-token with their computed value; pass `offToken: [...]` to narrow the list or `false` to turn it off.
166
+ - Previews replace table classes the new class fully covers, keep everything else in place, and stack; undoing restores the original `class` exactly. Classes outside the table, such as `text-[14px]`, are left alone.
167
+ - `tailwindSpacing({ prefix, scale, steps })` narrows the families (`p`, `px`, `py`, `pt`, `pr`, `pb`, `pl`, `m`, `mx`, …, `gap`, `gap-x`, `gap-y`), the pixels per step (default 4), and the steps (default Tailwind's 0 to 96). Paths are `spacing.<class>`, for example `spacing.p-4`.
168
+ - `classTheme` options take `class`, `attribute`, or both, on `target()` (default `<html>`). An option with neither is the default. With `colorScheme`, applying an option sets `color-scheme` to its `colorScheme` or id, and returning to the starting option restores the page's own value.
169
+
118
170
  ## Agent CLI
119
171
 
120
172
  `komo init` adds the default comment workflow to `AGENTS.md`, preserving existing instructions. For an existing project, run `komo agents setup` from its root. Commit the instructions so every agent uses them.
@@ -256,6 +308,8 @@ Pass these to `initKomo(config)` from `@hazem3500/komo` or `useKomo(config)` fro
256
308
  | `branch` | `string` | Inferred by `komo sync` | Required only for branch scope. |
257
309
  | `enabled` | `boolean` | `true` | Set from your build environment to restrict review UI. |
258
310
  | `pageRoot` | `HTMLElement` | Sole existing content root, when present | Mounted element inside body to scale in Frame mode. Body and html are not supported. |
311
+ | `pickRoots()` | `() => Element[]` | All of body except komo | Extra roots, besides `pageRoot`, that can be picked for comments and Design mode. By default body-level portals such as dialogs and popovers are pickable; return `[]` to limit picking to `pageRoot`, or return your portal container to allow only it. In Frame, portals keep their own size and position. |
312
+ | `isolateEvents` | `boolean` | `true` | Keep komo's events out of the host page. See [Host events](#host-events). |
259
313
  | `source(element)` | `(element: Element) => string \| undefined` | Anchor metadata | Return a repository-relative source path. |
260
314
  | `sourceUrl(source, branch)` | `(source: string, branch: string) => string` | GitHub viewer | Custom source or editor link. |
261
315
  | `page()` | `() => string` | `location.pathname` | Canonical page identifier. |
@@ -276,6 +330,16 @@ The lower-level `initComments` export remains available. It requires explicit `e
276
330
 
277
331
  The returned controller has `open()`, `close()`, `comment(element)`, `refresh()`, and `destroy()`. Repeated initialization returns the current controller; destroy it before switching projects. SSR returns a no-op controller. In React, use the `useKomo` hook. With Astro view transitions, destroy before swapping the document and initialize after navigation.
278
332
 
333
+ ### Host events
334
+
335
+ komo renders in a shadow root, so page listeners see its events as coming from komo's host `<div>`. By default komo stops them at that element:
336
+
337
+ - **Typing.** `keydown`, `keyup` and `keypress` from komo's inputs don't reach `document` or `window`, so app shortcuts (such as `react-hotkeys-hook` or a custom `keydown` listener) don't fire while someone writes a comment. Keys pressed on komo's buttons still reach the page.
338
+ - **Modals.** `pointerdown`, `mousedown`, `touchstart`, `touchmove`, `touchend`, `click`, `wheel`, `focusin` and `focusout` from komo's UI don't reach page listeners. An open Radix or similar dialog doesn't treat komo as an outside interaction, so it doesn't close, take focus back from the composer, or block the sidebar's scroll.
339
+ - **Focus traps.** When focus moves from the page into komo, komo stops that `focusout` for `document` listeners added after komo mounted, such as Radix's FocusScope.
340
+
341
+ Limits: capture-phase listeners on `document` or `window` run before komo can stop anything. For example, Radix's Escape handler (`onEscapeKeyDown`) still closes a dialog when Escape is pressed in komo's composer; prevent that with `event.target.closest("[data-branch-comments]")`. Libraries that detect outside clicks in the capture phase, such as Headless UI, need their own check. Modal libraries that hide other content with `aria-hidden` also hide komo from assistive technology. Set `isolateEvents: false` if your app needs to observe these events.
342
+
279
343
  ### CLI
280
344
 
281
345
  | Command / flag | Purpose |
@@ -0,0 +1 @@
1
+ import{g as e,h as t}from"./chunk-YZP3KX4Y.js";import"./chunk-I7AUKTXE.js";import{useEffect as r,useLayoutEffect as n,useRef as o,useState as a}from"react";import{createRoot as c}from"react-dom/client";import l,{useRef as i,useMemo as u,useEffect as s,useState as f,useCallback as d,useLayoutEffect as h}from"react";function p(){return(p=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e}).apply(this,arguments)}function v(e,t){if(null==e)return{};var r,n,o={},a=Object.keys(e);for(n=0;n<a.length;n++)t.indexOf(r=a[n])>=0||(o[r]=e[r]);return o}function m(e){var t=i(e),r=i(function(e){t.current&&t.current(e)});return t.current=e,r.current}var g=function(e,t,r){return void 0===t&&(t=0),void 0===r&&(r=1),e>r?r:e<t?t:e},b=function(e){return"touches"in e},_=function(e){return e&&e.ownerDocument.defaultView||self},w=function(e,t,r){var n=e.getBoundingClientRect(),o=b(t)?function(e,t){for(var r=0;r<e.length;r++)if(e[r].identifier===t)return e[r];return e[0]}(t.touches,r):t;return{left:g((o.pageX-(n.left+_(e).pageXOffset))/n.width),top:g((o.pageY-(n.top+_(e).pageYOffset))/n.height)}},x=function(e){!b(e)&&e.preventDefault()},C=l.memo(function(e){var t=e.onMove,r=e.onKey,n=e.onEnd,o=v(e,["onMove","onKey","onEnd"]),a=i(null),c=m(t),f=m(r),d=m(n),h=i(null),g=i(!1),C=u(function(){var e=function(e){x(e),(b(e)?e.touches.length>0:e.buttons>0)&&a.current?c(w(a.current,e,h.current)):(r(!1),d())},t=function(){r(!1),d()};function r(r){var n=g.current,o=_(a.current),c=r?o.addEventListener:o.removeEventListener;c(n?"touchmove":"mousemove",e),c(n?"touchend":"mouseup",t)}return[function(e){var t,n=e.nativeEvent,o=a.current;if(o&&(x(n),t=n,(!g.current||b(t))&&o)){if(b(n)){g.current=!0;var l=n.changedTouches||[];l.length&&(h.current=l[0].identifier)}o.focus(),c(w(o,n,h.current)),r(!0)}},function(e){var t=e.which||e.keyCode;t<37||t>40||(e.preventDefault(),f({left:39===t?.05:37===t?-.05:0,top:40===t?.05:38===t?-.05:0}))},function(e){var t=e.which||e.keyCode;t>=37&&t<=40&&d()},r]},[f,c,d]),E=C[0],y=C[1],k=C[2],N=C[3];return s(function(){return N},[N]),l.createElement("div",p({},o,{onTouchStart:E,onMouseDown:E,className:"react-colorful__interactive",ref:a,onKeyDown:y,onKeyUp:k,tabIndex:0,role:"slider"}))}),E=function(e){return e.filter(Boolean).join(" ")},y=function(e){var t=e.color,r=e.left,n=e.top,o=void 0===n?.5:n,a=E(["react-colorful__pointer",e.className]);return l.createElement("div",{className:a,style:{top:100*o+"%",left:100*r+"%"}},l.createElement("div",{className:"react-colorful__pointer-fill",style:{backgroundColor:t}}))},k=function(e,t,r){return void 0===t&&(t=0),void 0===r&&(r=Math.pow(10,t)),Math.round(r*e)/r},N=(Math.PI,function(e){return"#"===e[0]&&(e=e.substring(1)),e.length<6?{r:parseInt(e[0]+e[0],16),g:parseInt(e[1]+e[1],16),b:parseInt(e[2]+e[2],16),a:4===e.length?k(parseInt(e[3]+e[3],16)/255,2):1}:{r:parseInt(e.substring(0,2),16),g:parseInt(e.substring(2,4),16),b:parseInt(e.substring(4,6),16),a:8===e.length?k(parseInt(e.substring(6,8),16)/255,2):1}}),M=function(e){var t=function(e){var t=e.s,r=e.v,n=e.a,o=(200-t)*r/100;return{h:k(e.h),s:k(o>0&&o<200?t*r/100/(o<=100?o:200-o)*100:0),l:k(o/2),a:k(n,2)}}(e);return"hsl("+t.h+", "+t.s+"%, "+t.l+"%)"},L=function(e){var t=e.h,r=e.s,n=e.v,o=e.a;t=t/360*6,r/=100,n/=100;var a=Math.floor(t),c=n*(1-r),l=n*(1-(t-a)*r),i=n*(1-(1-t+a)*r),u=a%6;return{r:k(255*[n,l,c,c,i,n][u]),g:k(255*[i,n,n,l,c,c][u]),b:k(255*[c,c,i,n,n,l][u]),a:k(o,2)}},P=function(e){var t=e.toString(16);return t.length<2?"0"+t:t},j=function(e){var t=e.r,r=e.g,n=e.b,o=e.a,a=o<1?P(k(255*o)):"";return"#"+P(t)+P(r)+P(n)+a},H=function(e){var t=e.r,r=e.g,n=e.b,o=e.a,a=Math.max(t,r,n),c=a-Math.min(t,r,n),l=c?a===t?(r-n)/c:a===r?2+(n-t)/c:4+(t-r)/c:0;return{h:60*(l<0?l+6:l),s:a?c/a*100:0,v:a/255*100,a:o}},I=l.memo(function(e){var t=e.hue,r=e.onChange,n=e.onChangeEnd,o=E(["react-colorful__hue",e.className]);return l.createElement("div",{className:o},l.createElement(C,{onMove:function(e){r({h:360*e.left})},onKey:function(e){r({h:g(t+360*e.left,0,360)})},onEnd:n,"aria-label":"Hue","aria-valuenow":k(t),"aria-valuemax":"360","aria-valuemin":"0"},l.createElement(y,{className:"react-colorful__hue-pointer",left:t/360,color:M({h:t,s:100,v:100,a:1})})))}),z=l.memo(function(e){var t=e.hsva,r=e.onChange,n=e.onChangeEnd,o={backgroundColor:M({h:t.h,s:100,v:100,a:1})};return l.createElement("div",{className:"react-colorful__saturation",style:o},l.createElement(C,{onMove:function(e){r({s:100*e.left,v:100-100*e.top})},onKey:function(e){r({s:g(t.s+100*e.left,0,100),v:g(t.v-100*e.top,0,100)})},onEnd:n,"aria-label":"Color","aria-valuetext":"Saturation "+k(t.s)+"%, Brightness "+k(t.v)+"%"},l.createElement(y,{className:"react-colorful__saturation-pointer",top:1-t.v/100,left:t.s/100,color:M(t)})))}),D=function(e,t){if(e===t)return!0;for(var r in e)if(e[r]!==t[r])return!1;return!0};var B=typeof window<"u"?h:s,K=new WeakMap,O=function(e){B(function(){var t=e.current;if(typeof document<"u"&&t){var r=t.getRootNode?t.getRootNode():t.ownerDocument,n=r&&("head"in r||"host"in r)?r:t.ownerDocument;if(!K.has(n)){var o="head"in n?n.head:n,a=(o.ownerDocument||document).createElement("style");a.innerHTML='.react-colorful{position:relative;display:flex;flex-direction:column;width:200px;height:200px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}.react-colorful__saturation{position:relative;flex-grow:1;border-color:transparent;border-bottom:12px solid #000;border-radius:8px 8px 0 0;background-image:linear-gradient(0deg,#000,transparent),linear-gradient(90deg,#fff,hsla(0,0%,100%,0))}.react-colorful__alpha-gradient,.react-colorful__pointer-fill{content:"";position:absolute;left:0;top:0;right:0;bottom:0;pointer-events:none;border-radius:inherit}.react-colorful__alpha-gradient,.react-colorful__saturation{box-shadow:inset 0 0 0 1px rgba(0,0,0,.05)}.react-colorful__alpha,.react-colorful__hue{position:relative;height:24px}.react-colorful__hue{background:linear-gradient(90deg,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red)}.react-colorful__last-control{border-radius:0 0 8px 8px}.react-colorful__interactive{position:absolute;left:0;top:0;right:0;bottom:0;border-radius:inherit;outline:none;touch-action:none}.react-colorful__pointer{position:absolute;z-index:1;box-sizing:border-box;width:28px;height:28px;transform:translate(-50%,-50%);background-color:#fff;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2)}.react-colorful__interactive:focus .react-colorful__pointer{transform:translate(-50%,-50%) scale(1.1)}.react-colorful__alpha,.react-colorful__alpha-pointer{background-color:#fff;background-image:url(\'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill-opacity=".05"><path d="M8 0h8v8H8zM0 8h8v8H0z"/></svg>\')}.react-colorful__saturation-pointer{z-index:3}.react-colorful__hue-pointer{z-index:2}';var c=typeof __webpack_nonce__<"u"?__webpack_nonce__:void 0;c&&a.setAttribute("nonce",c),K.set(n,a),o.appendChild(a)}}},[])},A=function(e){var t=e.className,r=e.colorModel,n=e.color,o=void 0===n?r.defaultColor:n,a=e.onChange,c=e.onChangeEnd,u=v(e,["className","colorModel","color","onChange","onChangeEnd"]),h=i(null);O(h);var g=function(e,t,r,n){var o=m(r),a=m(n),c=f(function(){return e.toHsva(t)}),l=c[0],u=c[1],h=i({color:t,hsva:l}),p=i(!1);s(function(){if(!e.equal(t,h.current.color)){var r=e.toHsva(t);h.current={hsva:r,color:t},u(r),p.current=!1}},[t,e]),s(function(){var t;D(l,h.current.hsva)||e.equal(t=e.fromHsva(l),h.current.color)||(h.current={hsva:l,color:t},o(t),p.current=!0)},[l,e,o]);var v=d(function(e){u(function(t){return Object.assign({},t,e)})},[]),g=d(function(){p.current&&(p.current=!1,a(h.current.color))},[a]);return[l,v,g]}(r,o,a,c),b=g[0],_=g[1],w=g[2],x=E(["react-colorful",t]);return l.createElement("div",p({},u,{ref:h,className:x}),l.createElement(z,{hsva:b,onChange:_,onChangeEnd:w}),l.createElement(I,{hue:b.h,onChange:_,onChangeEnd:w,className:"react-colorful__last-control"}))},R={defaultColor:"000",toHsva:function(e){return H(N(e))},fromHsva:function(e){return function(e){return j(L(e))}({h:e.h,s:e.s,v:e.v,a:1})},equal:function(e,t){return e.toLowerCase()===t.toLowerCase()||D(N(e),N(t))}},S=function(e){return l.createElement(A,p({},e,{colorModel:R}))},T=/^#?([0-9A-F]{3,8})$/i,W=function(e){var t=e.color,r=void 0===t?"":t,n=e.onChange,o=e.onBlur,a=e.escape,c=e.validate,i=e.format,u=e.process,h=v(e,["color","onChange","onBlur","escape","validate","format","process"]),g=f(function(){return a(r)}),b=g[0],_=g[1],w=m(n),x=m(o),C=d(function(e){var t=a(e.target.value);_(t),c(t)&&w(u?u(t):t)},[a,u,c,w]),E=d(function(e){c(e.target.value)||_(a(r)),x(e)},[r,a,c,x]);return s(function(){_(a(r))},[r,a]),l.createElement("input",p({},h,{value:i?i(b):b,spellCheck:"false",onChange:C,onBlur:E}))},Y=function(e){return"#"+e},q=function(e){var t=e.prefixed,r=e.alpha,n=v(e,["prefixed","alpha"]),o=d(function(e){return e.replace(/([^0-9A-F]+)/gi,"").substring(0,r?8:6)},[r]),a=d(function(e){return t=e,n=r,o=T.exec(t),3===(a=o?o[1].length:0)||6===a||!!n&&4===a||!!n&&8===a;var t,n,o,a},[r]);return l.createElement(W,p({},n,{escape:o,format:t?Y:void 0,process:Y,validate:a}))};import*as X from"react";var $=({size:e=24,color:t="currentColor",...r})=>X.createElement("svg",{viewBox:"0 0 24 24",width:e,height:e,stroke:t,strokeWidth:"2",fill:"none",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",...r},X.createElement("path",{d:"M12 5v14m-7-7h14"}));$.displayName="Plus";import{jsx as F,jsxs as U}from"react/jsx-runtime";var V=[["Lavender","#c8b5f4"],["Blue","#85b5ff"],["Mint","#84d9bb"],["Yellow","#f4d779"],["Peach","#f5af87"],["Pink","#efa5c6"]];function Z({initial:t,onChange:c}){let[l,i]=a(t),[u,s]=a(!1),f=o(null),d=o(null),h=!V.some(([,e])=>e===l),p=e=>{if(!/^#[0-9a-f]{6}$/i.test(e))return;let t=e.toLowerCase();i(t),c(t)};return n(()=>{let e=f.current;if(!e)return;if(!u){let t=window.setTimeout(()=>e.hidePopover(),150);return()=>clearTimeout(t)}e.showPopover();let t=()=>{let t=d.current.getBoundingClientRect();e.style.left=`${Math.max(8,Math.min(t.right-e.offsetWidth,innerWidth-e.offsetWidth-8))}px`,e.style.top=`${Math.max(8,t.top-e.offsetHeight-8)}px`};return t(),e.querySelector('[role="slider"]')?.focus({preventScroll:!0}),window.addEventListener("resize",t),()=>window.removeEventListener("resize",t)},[u]),r(()=>{if(!u)return;let t=new AbortController;return e(d.current,"pointerdown",e=>{!e.composedPath().includes(f.current)&&!e.composedPath().includes(d.current)&&s(!1)},{signal:t.signal}),()=>t.abort()},[u]),U("div",{className:"account-accent",children:[F("span",{className:"account-accent-label",children:"Accent color"}),U("div",{className:"accent-swatches",role:"group","aria-label":"Accent color",children:[V.map(([e,t])=>F("button",{type:"button",className:"accent-swatch","aria-label":e,title:e,"aria-pressed":l===t,style:{"--swatch":t},onClick:()=>{p(t),s(!1)}},e)),F("button",{ref:d,type:"button",className:"accent-swatch accent-custom","aria-label":"Custom accent color","aria-haspopup":"dialog","aria-expanded":u,"aria-pressed":h,onClick:()=>s(!u),children:F($,{"aria-hidden":"true"})})]}),U("div",{ref:f,popover:"manual",role:"dialog","aria-label":"Custom accent color",className:u?"accent-popover t-dropdown is-open":"accent-popover t-dropdown is-closing",onKeyDown:e=>{e.stopPropagation(),"Escape"===e.key&&(e.preventDefault(),s(!1),d.current?.focus())},children:[U("div",{className:"accent-popover-head",children:[F("span",{children:"Color"}),F("span",{children:l})]}),U("div",{className:"accent-picker-body",children:[F(S,{color:l,onChange:p}),F(q,{"aria-label":"Hex accent color",className:"accent-hex",color:l,onChange:p,prefixed:!0})]})]})]})}function G(e,r){let n=t("div","account-accent-mount"),o=c(n);return o.render(F(Z,{initial:e,onChange:r})),{element:n,destroy:()=>o.unmount()}}export{G as accentPicker};
@@ -0,0 +1,22 @@
1
+ import type { ThemeAxis } from "../design-system.js";
2
+ export interface ClassThemeOption {
3
+ id: string;
4
+ label?: string;
5
+ /** Class(es) present on the target while this option is active, e.g. "dark". */
6
+ class?: string | string[];
7
+ /** Attributes set on the target while this option is active, e.g. { "data-theme": "dark" }. */
8
+ attribute?: Record<string, string>;
9
+ /** `color-scheme` to set when the axis has `colorScheme`; defaults to the option id. */
10
+ colorScheme?: string;
11
+ }
12
+ export interface ClassThemeOptions {
13
+ id: string;
14
+ label?: string;
15
+ options: ClassThemeOption[];
16
+ /** Element carrying the classes or attributes; defaults to `<html>`. */
17
+ target?: () => Element;
18
+ /** Also set the target's inline `color-scheme` (e.g. for light/dark). */
19
+ colorScheme?: boolean;
20
+ }
21
+ /** A `ThemeAxis` that reads and switches classes or attributes on one element. */
22
+ export declare function classTheme(config: ClassThemeOptions): ThemeAxis;
@@ -0,0 +1 @@
1
+ var t=t=>(Array.isArray(t.class)?t.class:[t.class??""]).flatMap(t=>t.split(/\s+/)).filter(Boolean);function e(e){let o,r=e.target??(()=>document.documentElement),i=e=>t(e).length+Object.keys(e.attribute??{}).length,l=(e,o)=>t(o).every(t=>e.classList.contains(t))&&Object.entries(o.attribute??{}).every(([t,o])=>e.getAttribute(t)===o);function s(){let t,o=r();for(let r of e.options)l(o,r)&&(!t||i(r)>i(t))&&(t=r);return(t??e.options[0])?.id??""}return{id:e.id,...e.label?{label:e.label}:{},options:e.options.map(({id:t,label:e})=>({id:t,...e?{label:e}:{}})),current:s,apply(i){let l=e.options.find(t=>t.id===i);if(!l)return;let a=r(),c=a.style;o??={option:s(),colorScheme:c?.colorScheme??""};for(let o of e.options)if(o!==l){a.classList.remove(...t(o));for(let[t,e]of Object.entries(o.attribute??{}))a.getAttribute(t)===e&&!(t in(l.attribute??{}))&&a.removeAttribute(t)}a.classList.add(...t(l));for(let[t,e]of Object.entries(l.attribute??{}))a.setAttribute(t,e);e.colorScheme&&c&&(c.colorScheme=i===o.option?o.colorScheme:l.colorScheme??l.id)}}}export{e as classTheme};
@@ -0,0 +1,62 @@
1
+ import type { DesignTokenType, ThemeAxis, ThemeState, TokenAdapter } from "../design-system.js";
2
+ /** One utility class (or a set of equivalent classes) that sets design tokens. */
3
+ export interface TailwindToken {
4
+ /** Stable dot path recorded on proposals, e.g. "color.text.primary". Unique in the table. */
5
+ path: string;
6
+ type: DesignTokenType;
7
+ /**
8
+ * The utility class, e.g. "text-primary". With several, the first is the canonical
9
+ * class komo adds when previewing; the rest are recognised aliases (e.g. "rounded-md").
10
+ */
11
+ class: string | string[];
12
+ /**
13
+ * CSS properties the class sets, as the Design panel names them: "color",
14
+ * "background-color", "padding", "padding-left", "border-radius", "font-size"...
15
+ * A shorthand also covers its longhands ("padding" covers "padding-top", etc.).
16
+ */
17
+ properties: string[];
18
+ /** Value when no theme-specific value applies, e.g. "#1f1f1f" or "8px". */
19
+ value?: string;
20
+ /**
21
+ * Values per theme. Keys are option ids ("dark"), "axis=option" pairs ("mode=dark"),
22
+ * or combinations joined with "+" ("track+dark"). The most specific key that
23
+ * fully matches the theme state wins; otherwise `value` is used.
24
+ */
25
+ values?: Record<string, string>;
26
+ label?: string;
27
+ group?: string;
28
+ description?: string;
29
+ /** Interaction states the class carries itself, e.g. ["hover", "active", "disabled"]. */
30
+ states?: string[];
31
+ }
32
+ export interface TailwindTable {
33
+ tokens: TailwindToken[];
34
+ }
35
+ export interface TailwindTokensOptions {
36
+ /** Theme axes used to work out the current theme when `list()` gets none. */
37
+ themes?: ThemeAxis[];
38
+ /**
39
+ * Properties reported as `{ offToken: computed value }` when no table class sets
40
+ * them. Defaults to every property named in the table; `false` reports none.
41
+ */
42
+ offToken?: string[] | false;
43
+ }
44
+ /** Pick the theme-specific value: the most specific fully-matching key, else `value`. */
45
+ export declare function tokenValue(token: Pick<TailwindToken, "value" | "values">, theme?: ThemeState): string;
46
+ /** A `TokenAdapter` whose tokens are utility classes listed in `table`. */
47
+ export declare function tailwindTokens(table: TailwindTable, options?: TailwindTokensOptions): TokenAdapter;
48
+ /** Tailwind's default spacing steps (multiples of the base unit). */
49
+ export declare const defaultSpacingSteps: number[];
50
+ export interface TailwindSpacingOptions {
51
+ /** Utility families; defaults to every padding, margin and gap family (p, px, ..., m, ..., gap, gap-x, gap-y). */
52
+ prefix?: string[];
53
+ /** Pixels per step; Tailwind's default is 4. */
54
+ scale?: number;
55
+ /** Steps to emit; defaults to Tailwind's spacing scale (0 to 96). */
56
+ steps?: number[];
57
+ }
58
+ /**
59
+ * Table entries for spacing utilities, e.g. `p-4` → padding 16px at scale 4.
60
+ * Paths are "spacing.<class>", e.g. "spacing.px-2.5".
61
+ */
62
+ export declare function tailwindSpacing(options?: TailwindSpacingOptions): TailwindToken[];
@@ -0,0 +1 @@
1
+ var e=["top","right","bottom","left"],t={padding:e.map(e=>`padding-${e}`),margin:e.map(e=>`margin-${e}`),gap:["row-gap","column-gap"],"border-radius":["top-left","top-right","bottom-right","bottom-left"].map(e=>`border-${e}-radius`),"border-width":e.map(e=>`border-${e}-width`),"border-color":e.map(e=>`border-${e}-color`)},r=e=>(Array.isArray(e)?e:[e]).flatMap(e=>e.split(/\s+/)).filter(Boolean),a=e=>e.getAttribute("class"),n=e=>e.includes(":")?null:e.replace(/^!|!$/g,"");function i(e,t={}){let r,a=0;for(let[n,i]of Object.entries(e.values??{})){let e=n.split("+");e.every(e=>{let[r,a]=e.split("=");return void 0===a?Object.values(t).includes(r):t[r]===a})&&e.length>a&&([r,a]=[i,e.length])}return r??e.value??""}function o(e,o={}){let l=e.tokens.map(e=>({token:e,classes:r(e.class),covers:new Set(e.properties.flatMap(e=>[e,...t[e]??[]]))})),s=new Map;for(let e of l)for(let t of e.classes)s.set(t,e);let p=new Map(l.map(e=>[e.token.path,e])),g=!1===o.offToken?[]:o.offToken??[...new Set(e.tokens.flatMap(e=>e.properties))],d=new WeakMap,c=e=>(e??"").split(/\s+/).flatMap(e=>{let t=n(e),r=t?s.get(t):void 0;return r?[{name:e,entry:r}]:[]});function f(e,r){if(!r.stack.length)return null===r.original?e.removeAttribute("class"):e.setAttribute("class",r.original),void d.delete(e);e.setAttribute("class",r.stack.reduce((e,r)=>function(e,r,a){let i=e.split(/\s+/).filter(Boolean),o=a.classes[0],l=-1,p=i.filter((e,i)=>{let o=n(e),p=o?s.get(o):void 0,g=!!p&&(p===a||[...p.covers].every(e=>a.covers.has(e))&&(p.covers.has(r)||(t[r]??[]).some(e=>p.covers.has(e))));return g&&l<0&&(l=i),!g}),g=i.slice(0,l<0?i.length:l).filter(e=>p.includes(e)).length;return p.splice(g,0,o),p.join(" ")}(e,r.property,r.entry),r.original??""))}return{id:"tailwind",list(e){let t=e??Object.fromEntries((o.themes??[]).map(e=>[e.id,e.current()]));return l.map(({token:e,classes:r})=>({path:e.path,type:e.type,value:i(e,t),properties:[...e.properties],ref:r[0],...e.label?{label:e.label}:{},...e.group?{group:e.group}:{},...e.description?{description:e.description}:{},...e.states?{states:[...e.states]}:{}}))},resolve(e){let r,n={},i=function(e){let r=new Map;for(let t of c(e))for(let e of t.entry.covers){let a=r.get(e);(!a||t.entry.covers.size<=a.entry.covers.size)&&r.set(e,t)}for(let[e,a]of r)t[e]?.some(e=>r.get(e)?.entry!==a.entry)&&r.delete(e);return r}(a(e));for(let[e,t]of i)n[e]={path:t.entry.token.path};for(let a of g){if(i.has(a)||t[a]?.some(e=>i.has(e)))continue;r??=getComputedStyle(e);let o=r.getPropertyValue(a);o&&(n[a]={offToken:o})}return n},preview(e,t,r){let n=p.get(r.path);if(!n)return()=>{};let i=d.get(e)??{original:a(e),stack:[]};d.set(e,i);let o={property:t,entry:n};return i.stack.push(o),f(e,i),()=>{let t=i.stack.indexOf(o);t<0||(i.stack.splice(t,1),f(e,i))}},describe(e,t,r,n){let i=p.get(n.path)?.classes[0]??n.ref??n.path;if("path"in r){let t=p.get(r.path),n=d.get(e)?.original??a(e),o=c(n).find(e=>e.entry===t)?.name??t?.classes[0]??r.path;return o===i?`Keep class "${i}"`:`Replace class "${o}" with "${i}"`}return`Add class "${i}"`}}}var l={p:["Padding",["padding"]],px:["Padding",["padding-left","padding-right"]],py:["Padding",["padding-top","padding-bottom"]],pt:["Padding",["padding-top"]],pr:["Padding",["padding-right"]],pb:["Padding",["padding-bottom"]],pl:["Padding",["padding-left"]],m:["Margin",["margin"]],mx:["Margin",["margin-left","margin-right"]],my:["Margin",["margin-top","margin-bottom"]],mt:["Margin",["margin-top"]],mr:["Margin",["margin-right"]],mb:["Margin",["margin-bottom"]],ml:["Margin",["margin-left"]],gap:["Gap",["gap"]],"gap-x":["Gap",["column-gap"]],"gap-y":["Gap",["row-gap"]]},s=[0,.5,1,1.5,2,2.5,3,3.5,4,5,6,7,8,9,10,11,12,14,16,20,24,28,32,36,40,44,48,52,56,60,64,72,80,96];function p(e={}){let t=e.scale??4,r=e.steps??s;return(e.prefix??Object.keys(l)).flatMap(e=>{let a=l[e];if(!a)throw new Error(`komo: unknown spacing prefix "${e}"`);return r.map(r=>{let n=`${e}-${r}`;return{path:`spacing.${n}`,type:"dimension",class:n,properties:a[1],value:r*t+"px",group:a[0],label:n}})})}export{s as defaultSpacingSteps,p as tailwindSpacing,o as tailwindTokens,i as tokenValue};