@hazem3500/palette 0.0.0-stage → 0.9.106

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 (244) hide show
  1. package/LICENSE +21 -0
  2. package/NODE.md +166 -0
  3. package/NOTICE.md +45 -0
  4. package/README.md +912 -2
  5. package/cli/agent.mjs +752 -0
  6. package/cli/config.mjs +34 -0
  7. package/cli/design-edit.mjs +336 -0
  8. package/cli/index.mjs +405 -0
  9. package/cli/project.mjs +151 -0
  10. package/cli/snapshots.mjs +61 -0
  11. package/cli/workflow.mjs +42 -0
  12. package/dist/MorphingMenu.d.ts +38 -0
  13. package/dist/PointerIcon.d.ts +2 -0
  14. package/dist/SmileIcon.d.ts +2 -0
  15. package/dist/THIRD_PARTY_NOTICES.txt +387 -0
  16. package/dist/accent-picker-TTIZ65XF.js +1 -0
  17. package/dist/accent-picker.d.ts +4 -0
  18. package/dist/accent.d.ts +2 -0
  19. package/dist/account-tokens-PINQNSNE.js +1 -0
  20. package/dist/account-tokens.d.ts +25 -0
  21. package/dist/account-usage.d.ts +2 -0
  22. package/dist/action-menu.d.ts +10 -0
  23. package/dist/adapters/class-theme.d.ts +25 -0
  24. package/dist/adapters/class-theme.js +1 -0
  25. package/dist/adapters/css-variables.d.ts +15 -0
  26. package/dist/adapters/css-variables.js +1 -0
  27. package/dist/adapters/react-fiber.d.ts +56 -0
  28. package/dist/adapters/react-fiber.js +1 -0
  29. package/dist/adapters/react-preview.d.ts +17 -0
  30. package/dist/adapters/react-preview.js +1 -0
  31. package/dist/adapters/tailwind.d.ts +90 -0
  32. package/dist/adapters/tailwind.js +1 -0
  33. package/dist/adapters/variable-preview.d.ts +15 -0
  34. package/dist/agent-prompt.d.ts +10 -0
  35. package/dist/agent-prompt.js +1 -0
  36. package/dist/anchors.d.ts +52 -0
  37. package/dist/api.d.ts +40 -0
  38. package/dist/approved-sites.d.ts +12 -0
  39. package/dist/card-motion.d.ts +8 -0
  40. package/dist/chunk-2BS2OWWH.js +1 -0
  41. package/dist/chunk-2J3IID4N.js +1 -0
  42. package/dist/chunk-3BVM3I52.js +1 -0
  43. package/dist/chunk-4FX5IS2U.js +1 -0
  44. package/dist/chunk-4KEVPMJJ.js +1 -0
  45. package/dist/chunk-4XQD75A2.js +1 -0
  46. package/dist/chunk-57MG3M5P.js +1 -0
  47. package/dist/chunk-5OVDW2RU.js +1 -0
  48. package/dist/chunk-6CIC7VU7.js +1 -0
  49. package/dist/chunk-6HM4JQLV.js +1 -0
  50. package/dist/chunk-AYGLN3MI.js +1 -0
  51. package/dist/chunk-DLC3CRFV.js +1 -0
  52. package/dist/chunk-DVYSFT6O.js +1 -0
  53. package/dist/chunk-DZRM3YUQ.js +1 -0
  54. package/dist/chunk-FD4GACV2.js +1 -0
  55. package/dist/chunk-GGU2XLWE.js +1 -0
  56. package/dist/chunk-IQOPM3JF.js +1 -0
  57. package/dist/chunk-LMXFSXO6.js +1 -0
  58. package/dist/chunk-QFDNGHSH.js +1 -0
  59. package/dist/chunk-RBHY7UDS.js +1 -0
  60. package/dist/chunk-UEZCLURX.js +1 -0
  61. package/dist/chunk-XOMOONGK.js +1 -0
  62. package/dist/chunk-XP22SMEQ.js +1 -0
  63. package/dist/color-editor-ATWWSENS.js +1 -0
  64. package/dist/color-editor.d.ts +37 -0
  65. package/dist/component-adapters.d.ts +3 -0
  66. package/dist/config.d.ts +13 -0
  67. package/dist/connect-project.d.ts +14 -0
  68. package/dist/connection-issue.d.ts +7 -0
  69. package/dist/css-value-parts.d.ts +14 -0
  70. package/dist/custom-field.d.ts +12 -0
  71. package/dist/design-commands.d.ts +46 -0
  72. package/dist/design-comment-RXUCDTYA.js +1 -0
  73. package/dist/design-comment.d.ts +98 -0
  74. package/dist/design-export-BNNHI4JP.js +1 -0
  75. package/dist/design-export.d.ts +87 -0
  76. package/dist/design-geometry-DPMHGQEK.js +1 -0
  77. package/dist/design-geometry.d.ts +61 -0
  78. package/dist/design-keyboard.d.ts +27 -0
  79. package/dist/design-mode-styles.d.ts +1 -0
  80. package/dist/design-mode.d.ts +48 -0
  81. package/dist/design-mode.js +1 -0
  82. package/dist/design-move.d.ts +75 -0
  83. package/dist/design-panel-HTN6LR3V.js +1 -0
  84. package/dist/design-panel-styles.d.ts +1 -0
  85. package/dist/design-panel.d.ts +42 -0
  86. package/dist/design-popup.d.ts +11 -0
  87. package/dist/design-preview.d.ts +42 -0
  88. package/dist/design-system.d.ts +314 -0
  89. package/dist/design-target.d.ts +5 -0
  90. package/dist/design-tokens.d.ts +93 -0
  91. package/dist/dom.d.ts +35 -0
  92. package/dist/drawer-expand-motion.d.ts +46 -0
  93. package/dist/drawer-presence.d.ts +4 -0
  94. package/dist/edge-sidebar.d.ts +39 -0
  95. package/dist/element-label.d.ts +7 -0
  96. package/dist/emoji-history.d.ts +8 -0
  97. package/dist/emoji-sequence.d.ts +1 -0
  98. package/dist/emoji.d.ts +1 -0
  99. package/dist/field-controls-4ROTPXMO.js +1 -0
  100. package/dist/field-controls.d.ts +1 -0
  101. package/dist/field-label.d.ts +8 -0
  102. package/dist/floating-drag.d.ts +9 -0
  103. package/dist/frame-iframe-WBEP3R6S.js +1 -0
  104. package/dist/frame-iframe.d.ts +38 -0
  105. package/dist/frame-portals.d.ts +14 -0
  106. package/dist/frame-size-MRCN6XYD.js +1 -0
  107. package/dist/frame-size.d.ts +105 -0
  108. package/dist/frame-target.d.ts +65 -0
  109. package/dist/frame-viewport.d.ts +8 -0
  110. package/dist/highlight.d.ts +11 -0
  111. package/dist/host-motion.d.ts +6 -0
  112. package/dist/icon-markup.d.ts +4 -0
  113. package/dist/index.d.ts +10 -0
  114. package/dist/index.js +1 -0
  115. package/dist/inline-text-editor-AMFRKW5Z.js +1 -0
  116. package/dist/inline-text-editor.d.ts +66 -0
  117. package/dist/kbd.d.ts +17 -0
  118. package/dist/lazy-accent-picker.d.ts +4 -0
  119. package/dist/lazy-motion.d.ts +17 -0
  120. package/dist/lazy-toolbar.d.ts +22 -0
  121. package/dist/live-colors-GWOVVQSO.js +1 -0
  122. package/dist/live-colors.d.ts +13 -0
  123. package/dist/message-links.d.ts +2 -0
  124. package/dist/morphing-menu-styles.d.ts +1 -0
  125. package/dist/motion-animate-J4PEZPLG.js +1 -0
  126. package/dist/motion-animate.d.ts +1 -0
  127. package/dist/node.mjs +3731 -0
  128. package/dist/onboarding-BVTCFEHL.js +1 -0
  129. package/dist/onboarding.d.ts +6 -0
  130. package/dist/open-state.d.ts +6 -0
  131. package/dist/optimistic.d.ts +26 -0
  132. package/dist/page-label.d.ts +2 -0
  133. package/dist/page.d.ts +2 -0
  134. package/dist/picker-CMP4UZRF.js +1 -0
  135. package/dist/pin-direction.d.ts +12 -0
  136. package/dist/pin-groups-YAYFPJYA.js +1 -0
  137. package/dist/pin-groups.d.ts +28 -0
  138. package/dist/pin-move-PG54WWPU.js +1 -0
  139. package/dist/pin-move.d.ts +22 -0
  140. package/dist/pin-stacks.d.ts +22 -0
  141. package/dist/polling.d.ts +10 -0
  142. package/dist/postgres/001_initial.sql +128 -0
  143. package/dist/postgres/002_thread_tags.sql +1 -0
  144. package/dist/postgres/003_project_webhooks.sql +4 -0
  145. package/dist/postgres/004_personal_access_tokens.sql +25 -0
  146. package/dist/postgres/005_snapshots.sql +23 -0
  147. package/dist/postgres/006_thread_scope.sql +1 -0
  148. package/dist/postgres/007_session_origin.sql +4 -0
  149. package/dist/project-management.d.ts +2 -0
  150. package/dist/public-env.d.ts +37 -0
  151. package/dist/react-inspector.d.ts +54 -0
  152. package/dist/react-instrument.d.ts +18 -0
  153. package/dist/react-instrument.js +1 -0
  154. package/dist/react-props-panel-4XO3NSX3.js +1 -0
  155. package/dist/react-props-panel.d.ts +107 -0
  156. package/dist/react.d.ts +50 -0
  157. package/dist/react.js +1 -0
  158. package/dist/reaction-picker.d.ts +1 -0
  159. package/dist/review-layout.d.ts +62 -0
  160. package/dist/select-menu-QAYV7NKJ.js +1 -0
  161. package/dist/select-menu.d.ts +5 -0
  162. package/dist/server.d.ts +8 -0
  163. package/dist/server.js +3348 -0
  164. package/dist/setup-browser.d.ts +1 -0
  165. package/dist/setup.d.ts +6 -0
  166. package/dist/setup.js +1 -0
  167. package/dist/shell-mode-SUYLWDWW.js +1 -0
  168. package/dist/shell-mode.d.ts +12 -0
  169. package/dist/shell-session.d.ts +11 -0
  170. package/dist/shell.d.ts +14 -0
  171. package/dist/shell.js +1 -0
  172. package/dist/shortcut-help-XRDNX7MZ.js +1 -0
  173. package/dist/shortcut-help.d.ts +16 -0
  174. package/dist/shortcuts.d.ts +19 -0
  175. package/dist/snapshot-V264ZMQG.js +1 -0
  176. package/dist/snapshot.d.ts +49 -0
  177. package/dist/source-info.d.ts +10 -0
  178. package/dist/static-svg.d.ts +3 -0
  179. package/dist/styles.d.ts +1 -0
  180. package/dist/tag-editor-MMAMOEI4.js +1 -0
  181. package/dist/tag-editor.d.ts +14 -0
  182. package/dist/text-preview.d.ts +28 -0
  183. package/dist/thread-scope.d.ts +11 -0
  184. package/dist/thread-snapshot-HLTNHS4N.js +1 -0
  185. package/dist/thread-snapshot.d.ts +54 -0
  186. package/dist/token-adapter.d.ts +71 -0
  187. package/dist/token-custom-UCA6CBOV.js +1 -0
  188. package/dist/token-custom.d.ts +22 -0
  189. package/dist/token-page-PGPWPUKU.js +1 -0
  190. package/dist/token-page.d.ts +63 -0
  191. package/dist/token-picker-WMWUKLKJ.js +1 -0
  192. package/dist/token-picker.d.ts +44 -0
  193. package/dist/toolbar-items.d.ts +13 -0
  194. package/dist/toolbar-runtime-QCZONSWE.js +1 -0
  195. package/dist/toolbar-runtime.d.ts +5 -0
  196. package/dist/typed-custom.d.ts +28 -0
  197. package/dist/types.d.ts +365 -0
  198. package/package.json +184 -4
  199. package/server/account-page.ts +231 -0
  200. package/server/digest.ts +8 -0
  201. package/server/iap.ts +165 -0
  202. package/server/import-project.ts +204 -0
  203. package/server/index.ts +2043 -0
  204. package/server/migrations/0001_comments.sql +54 -0
  205. package/server/migrations/0002_identity.sql +2 -0
  206. package/server/migrations/0003_accent_color.sql +1 -0
  207. package/server/migrations/0004_workspaces.sql +46 -0
  208. package/server/migrations/0005_domains.sql +6 -0
  209. package/server/migrations/0006_member_budget.sql +16 -0
  210. package/server/migrations/0007_revisions.sql +17 -0
  211. package/server/migrations/0008_starter_comments.sql +1 -0
  212. package/server/migrations/0009_retention.sql +19 -0
  213. package/server/migrations/0010_reviewer_history.sql +1 -0
  214. package/server/migrations/0011_project_management.sql +52 -0
  215. package/server/migrations/0012_setup_sites.sql +3 -0
  216. package/server/migrations/0013_project_sites.sql +8 -0
  217. package/server/migrations/0014_project_site_removals.sql +3 -0
  218. package/server/migrations/0015_query_efficiency.sql +6 -0
  219. package/server/migrations/0016_thread_tags.sql +1 -0
  220. package/server/migrations/0017_project_webhooks.sql +4 -0
  221. package/server/migrations/0018_personal_access_tokens.sql +25 -0
  222. package/server/migrations/0019_snapshots.sql +30 -0
  223. package/server/migrations/0020_thread_scope.sql +1 -0
  224. package/server/migrations/0021_session_origin.sql +4 -0
  225. package/server/node-webhooks.ts +126 -0
  226. package/server/project-management.ts +398 -0
  227. package/server/project-sites.ts +117 -0
  228. package/server/proxy-auth.ts +130 -0
  229. package/server/public-env.ts +40 -0
  230. package/server/secrets.d.ts +13 -0
  231. package/server/setup-client.txt +3083 -0
  232. package/server/setup-page.ts +15 -0
  233. package/server/snapshots.ts +249 -0
  234. package/server/tokens.ts +324 -0
  235. package/server/tsconfig.json +12 -0
  236. package/server/validation.ts +235 -0
  237. package/server/webhooks.ts +206 -0
  238. package/server/workspaces.ts +176 -0
  239. package/src/emoji.ts +7 -0
  240. package/src/page.ts +4 -0
  241. package/src/types.ts +371 -0
  242. package/vite/index.d.mts +29 -0
  243. package/vite/index.mjs +97 -0
  244. package/vite/metadata.mjs +196 -0
package/README.md CHANGED
@@ -1,3 +1,913 @@
1
- # Temporary Holding Version
1
+ # Palette
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@hazem3500/palette)](https://www.npmjs.com/package/@hazem3500/palette)
4
+ [![downloads](https://img.shields.io/npm/dm/@hazem3500/palette)](https://www.npmjs.com/package/@hazem3500/palette)
5
+
6
+ > Palette is a maintained fork of [tjcages/komo](https://github.com/tjcages/komo). It is published as `@hazem3500/palette`. Install it with `npm install @hazem3500/palette`.
7
+
8
+ Comments for your website. Point at an element, mark an area, and leave feedback your team can reply to, resolve, or copy into a coding agent.
9
+
10
+ Framework independent. Shared persistence. Google identities and guest reviewers.
11
+
12
+ [Documentation](https://github.com/hazem3500/palette#readme)
13
+
14
+ Review actions update immediately while saving in the background. Failed writes roll back with a notice; unsaved comment text is retained.
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ npm install @hazem3500/palette
20
+ npx @hazem3500/palette init
21
+ ```
22
+
23
+ Requires Node.js 22 or newer. The unscoped `komo` package on npm is unrelated; use `@hazem3500/palette`.
24
+
25
+ The setup command detects your Git repository and generates `komo.config.js` before sign-in. Mount that helper in your app:
26
+
27
+ ```js
28
+ import { initPalette } from './komo.config.js';
29
+ initPalette();
30
+ ```
31
+
32
+ Keep the setup terminal open and start your app in another terminal. Choose **Connect** in your app’s sidebar. Google sign-in opens in a separate window; Palette creates the project and returns you to feedback on your site. The CLI saves the final public settings to `.komo/project.json`, updates the helper, and prints an inline configuration. Use `--origin` for a different dev port or deployed site. If interrupted or expired, run `palette init` again to resume; completed projects are never overwritten.
33
+
34
+ Hosted onboarding is available through the dedicated Palette service. Self-hosting is available through the same CLI.
35
+
36
+ ## Usage
37
+
38
+ Mount after the page has loaded:
39
+
40
+ ```js
41
+ import { initPalette } from '@hazem3500/palette';
42
+ initPalette({ project: 'YOUR_PROJECT_KEY' });
43
+ ```
44
+
45
+ Optional configuration stays at the call site:
46
+
47
+ ```js
48
+ initPalette({
49
+ project: 'YOUR_PROJECT_KEY',
50
+ enabled: import.meta.env.DEV || import.meta.env.PUBLIC_PREVIEW === 'true',
51
+ pageRoot: document.querySelector('#app'),
52
+ });
53
+ ```
54
+
55
+ ### React
56
+
57
+ With React 18.2 or 19, call the hook once near your app root:
58
+
59
+ ```tsx
60
+ 'use client'; // Required for Next.js App Router client components.
61
+ import { usePalette } from '@hazem3500/palette/react';
62
+
63
+ export function Palette() {
64
+ usePalette({ project: 'YOUR_PROJECT_KEY' });
65
+ return null;
66
+ }
67
+ ```
68
+
69
+ Render `<Palette />` in your app or layout. The hook handles mounting, cleanup, and React Strict Mode. Inline configuration objects are supported; unchanged values do not restart Palette. Changing configuration remounts it, and `enabled: false` removes it. Memoize callback options with `useCallback` and pass stable DOM elements for `pageRoot` or `drawerContainer`. Mount only one hook per page; do not combine it with a separate `initPalette()` call. Server rendering does not mount the tool.
70
+
71
+ The **endpoint is the comments API**, not your website or preview URL. Hosted Palette defaults to `https://komo.offbr.co`. Google sign-in returns to `https://komo.offbr.co/auth/google/callback`, regardless of the website embedding Palette. For self-hosting, pass your Worker or Node API URL as `endpoint`. Your current page comes from the browser. Setup detects repository metadata from Git; include its printed `repo` value to enrich agent prompts.
72
+
73
+ Comments are shared across deployments by default. Use `pnpm exec palette init --branch-scope` to separate them by branch. For automatic branch detection, import from the optional generated `komo.config.js` helper and run `palette sync` before builds. It detects the current branch from deployment environment variables or Git; set `KOMO_BRANCH` if neither is available. It fails rather than silently grouping unknown branches.
74
+
75
+ ## Migrating from komo
76
+
77
+ Palette was called komo. Existing installs keep working without changes.
78
+
79
+ - **API names.** `initPalette`, `definePalette`, `usePalette`, `usePaletteInspectable`, `usePaletteDesignTokens` and the `Palette*` types are the main names. `initKomo`, `defineKomo`, `useKomo`, `useKomoInspectable`, `useKomoDesignTokens` and the `Komo*` types still work and are marked deprecated.
80
+ - **CLI.** The command is `palette`. `komo` still works as an alias.
81
+ - **Package.** The package is now `@hazem3500/palette`. Replace `@hazem3500/komo` in your dependencies and imports (including subpaths such as `/react` and `/vite`); `@hazem3500/komo` is deprecated and gets no new releases.
82
+ - **Unchanged for now.** Stored data and storage keys, `data-komo-*` attributes, `--komo-*` CSS custom properties, `KOMO_*` environment variables, the `komo_pat_` token prefix, the `.komo/` directory and `komo.config.js` keep their names.
83
+
84
+ ## Features
85
+
86
+ - Point and area comments anchored to page elements.
87
+ - On-demand design mode for temporary typography, color, spacing, border, and layout previews; copy the resulting CSS or attach a comment to the selected element.
88
+ - Replies, reactions, author-only editing, resolution, and undo.
89
+ - Group tags: add them while writing a comment or on an open thread, with suggestions from tags already in the project.
90
+ - Safe HTTP(S) links and simple Markdown links in comment messages.
91
+ - Searchable sidebar, draggable dock, and Google or guest profiles.
92
+
93
+ - Dismiss the toolbar for the current tab with ×; restore it with Shift+K or `controller.open()`. Set `showToolbar: false` to start hidden.
94
+ - **More → Hide comments** (H) clears pins, comment cards and the open sidebar or Frame from the page while review stays on. Unsent comments and replies, the open thread and the list's filters and scroll come back with **Show comments**. Comments return only when the reviewer asks: Show comments, or starting a comment, opening the list or a thread. The choice lasts for the current tab (session storage).
95
+ - **More → Pin size** switches pins between Full, Compact (a small initial), Dot (the author's accent at full strength, with a brief breathing halo, until you open its latest comment in this browser; quieter after that) and Auto (dots while browsing, full while commenting or with the sidebar open). Small pins keep a 24px target and expand in place on hover, focus, a nearby pointer or selection. The choice is remembered per project and wins over `pins.size`.
96
+ - **Crowded pins** consolidate. Comments on the same target (its persistent owner, including a custom `anchorTarget`) draw as one tight avatar stack at the oldest comment's anchor; more than three collapse to the first author and a count. Hover, keyboard focus or a tap fans the stack out in creation order, and each pin opens its own thread. Pins on different targets whose 24px targets overlap at the current zoom or Frame scale merge into a count bubble; it opens a keyboard-navigable list of those comments. Grouping never changes anchors or saves anything, the open comment's pin is always drawn at its own anchor, and a pin moved with **Move pin** stays out of stacks and bubbles. Set `pins.cluster: false` to draw every pin on its own.
97
+ - Keyboard shortcuts: C to comment, V to browse, D to toggle Design mode, L to view all comments, H to hide or show comments, Shift+K to restore. Remap or disable them with `shortcuts`; see [Choosing shortcuts](#choosing-shortcuts). In Design mode, ? or **Keyboard shortcuts** in the empty inspector lists every canvas key and gesture.
98
+ - Copy open feedback with selectors, source paths, page URLs, replies, and geometry into an agent.
99
+ - Hosted Cloudflare storage, or your own Worker/D1 or Node/PostgreSQL service.
100
+
101
+ Comment pins stay anchored during ordinary pointer drags. Authors and verified reviewers can choose **Comment actions → Move pin** to reposition one explicitly: drag it, or use arrow keys (Shift for larger steps), then Enter or **Save position**. Escape or **Cancel move** restores its previous position without saving. Completed moves use the existing thread anchor endpoint and remain attached after reload; the page element itself never moves. Dragging a floating comment card is independent of pin placement.
102
+
103
+ Design mode edits the rendered page only. Preview styles and inline text edits are temporary and reset when the page refreshes; styles are restored when the Palette widget is destroyed. Use **Copy CSS** to apply a rule in your site stylesheet; Palette does not write source files. Double-click text, or use **Edit text**, to edit it inline. Edits accept plain text only and change text in place: nested markup, links, and buttons keep their structure. Text marked `data-private` or `data-sensitive`, form fields, and text inside graphics, media, or frames stay read-only. An explicitly registered React `textProp` routes edits through the app's state callback. The inspector can select an iframe as one element but cannot edit its isolated contents. Elements inside open or closed shadow roots are not traversed; select the component host instead.
104
+
105
+ The design inspector loads as a separate optional browser chunk when Design mode is first opened, so the default widget load does not pay its bundle cost. The package size check measures this chunk separately from the core bundle.
106
+
107
+ The inspector takes direct UX/UI inspiration from [DialKit](https://www.dialkit.dev/): a compact dark floating panel, collapsible Color/Typography/Layout/Border groups, horizontal live sliders with editable numeric values, and color swatches paired with CSS color inputs. Group collapse state survives selection and Undo/Reset. Each slider gesture is one undoable edit; numeric fields remain available for precise keyboard entry. No DialKit runtime dependency is added.
108
+
109
+ Design mode also takes interaction cues from established design tools: select a visible element, keep its controls contextual, and make each change easy to reverse. Figma places editable layer properties and file-local styles or variables beside the selected layer; its Dev Mode provides inspection and measurements. pen.dev pairs an IDE design-mode toggle with a canvas, layer list, contextual property panel, and undo/redo. OpenPencil synchronizes layer-tree and canvas selection and offers contextual typography/layout controls, variables, and undoable operations.
110
+
111
+ ### Opt-in React prop inspection
112
+
113
+ React teams can register selected component roots for typed prop previews through the `./react` entry. A build-time TypeScript Compiler API pass generates the prop manifest from an explicitly marked function component and props type; the inspector edits only strings, numbers, booleans, and literal unions. Functions, refs, `ReactNode`, objects, generics and unsupported types are omitted from editable controls. This metadata is a safe allowlist, not runtime discovery of every React component; `defaultProps` and every React component form are not inferred. A literal-union prop previews each option as you hover it or move to it with the arrow keys, through the component's own `onChange`; click or Enter commits one Undo step, and moving off the list, Escape or clicking elsewhere puts the opening value back. While Palette drives a preview (an option, a token under the pointer, Before/After), the element's own CSS transitions are held, so each step renders at once and the panel reads final values; the element's attributes and `style` are left exactly as they were.
114
+
115
+ The app provides an `onChange` callback that updates its own state. Palette previews those values through React's normal render cycle, offers undo/reset, and can prefill an explicit comment with the proposed before/after values and source path. Posting still requires the user to submit the comment. Palette's core never accesses React Fiber internals and never writes source files; the one exception is the separate, opt-in [React fiber adapter](#discovering-react-components), which reads React internals only for apps that install it. Preview prop changes reset on refresh. A component may opt one leaf render into direct text editing by setting `textProp` to the name of an allowed string prop; Palette does not infer which child text belongs to a prop. See [`examples/react-props`](./examples/react-props/README.md) for the generated fixture and limitations.
116
+
117
+ ### Opt-in design tokens
118
+
119
+ `usePaletteDesignTokens` can register a bounded list of named CSS custom properties from a host-owned source. Each entry supplies its token path, DTCG scalar type, CSS variable, eligible element properties, and an explicit selector for the estimated theme-preview scope. Palette does not scan arbitrary computed styles or infer a complete semantic system. The first adapter uses the Design Tokens Format Module 2025.10 naming/type vocabulary; it is not a general DTCG file importer, and composite typography tokens are not editable.
120
+
121
+ Compatible registered tokens appear inside each existing style control, next to the color, spacing, typography, or radius value they affect. The chooser filters by that control's CSS property; it never guesses a token by name, matches computed values, or snaps arbitrary values to a scale. A control reads **Connected** only when the element's winning declaration (inline, or a same-origin stylesheet rule ranked by `!important`, specificity, and order) references the registered `var(--token)`. Typing a value, or choosing **Custom value**, records a custom override that keeps the original token as provenance; choosing another token rebinds the preview, and choosing the original token again removes the override. The comment proposal lists each change once, with its token before and after. Undo and Reset restore the original declaration and token provenance. Cross-origin stylesheets are skipped (their properties read as not connected), and cascade layers are ranked as if unlayered. For registered React components, a build-generated string prop can opt into one allowlisted `styleProperty` mapping (for example `brandColor` → `color`); the token reference then goes through the component's supplied React state callback and stays in the proposed prop diff. Unsupported prop types remain read-only.
122
+
123
+ Shared theme edits remain secondary under **Edit shared theme values**. Their explicit Preview action changes the registered variable temporarily and reports the author-declared impact selector/count. This is not a persisted design-system edit; it resets on refresh, widget destruction, or Reset. Selectors are estimates, not proof that every matching element consumes the variable. Both element and shared changes remain previews until the user explicitly submits a comment; Palette does not write CSS or React source. Enable registration only in a trusted development/demo environment, and exclude confidential values from the token list.
124
+
125
+ This approach uses React Developer Tools as a reference for component props but avoids depending on its browser DevTools UI. TypeScript's compiler can generate type metadata before type erasure, while a host callback keeps live previews within the app's public state model. React 18 and 19 are both exercised (see the fixture); the registration hook uses public hooks and DOM refs and does not inspect version-specific React internals. Only the opt-in React fiber adapter does.
126
+
127
+ Puck is a useful reference for the **explicit component contract**: its config registers renderable components and declares typed editor fields, defaults, nested object/array fields, and global/component/per-instance edit permissions. Palette borrows the opt-in registration, typed allowlist, contextual controls, and fail-closed edit permissions. It does not reuse Puck's editor/runtime: Puck owns a structured page document and supports component insertion, nesting, drag-and-drop, and publishing, while Palette edits a temporary preview of the host app's existing React state and turns proposed values into a comment. A Puck dependency would add a full page-builder interaction/data model and runtime cost where Palette needs a small optional inspector; Puck's MIT license permits reuse, but licensing is not the deciding constraint. For the first version, nested object/array, slot, custom, and dynamic fields remain read-only until the adapter can represent and validate them safely without implying broader source persistence.
128
+
129
+ Palette adapts those patterns to an embedded live page: direct hover/select, a compact inspector, immediate CSS preview, grouped undo/reset, and CSS export. It does not add a persistent design document, a shared theme editor, or a source-code writer. Named tokens can be previewed only when the host explicitly registers their CSS-variable source; this does not imply that browser preview edits changed application source.
130
+
131
+ 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/).
132
+
133
+ ### Design-system adapters
134
+
135
+ `design: { tokens, themes, components }` tells Design mode about your design system. These are types in `DesignSystem`, and all three are optional. Palette never guesses tokens from computed styles, and its core never reads framework internals; your adapters answer. The single exception is the opt-in [React fiber adapter](#discovering-react-components): it is off unless you install it, and it is the only Palette code that touches React's private fields.
136
+
137
+ - `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.
138
+ - `themes: ThemeAxis[]` are independent axes reviewers can flip from the Design panel, such as light/dark or a product palette. Proposals record the theme state they were made in.
139
+ - `components: ComponentAdapter` identifies the component behind an element (name, props, source). Proposals name it with its props. A component registered with `usePaletteInspectable` always takes precedence and keeps its editable props. An adapter may also supply `panel(identity)` to show read-only details in the Design panel.
140
+
141
+ Each Design mode comment starts with its context, for example `Theme: mode=dark`, `Component: Button (src/ui/Button.tsx:12)` and `Props: variant="secondary"`. Its changes follow: token swaps, shared token values, prop changes, text, and raw CSS as a fallback. The CLI reads the comment back as a versioned `DesignEdit` record (`theme`, `target.component`, `changes`) that agents can apply; comments without context lines still parse. Token values edited from the [Tokens view](#editing-tokens-without-picking-an-element) become `{ kind: "token-value", path, theme?, from, to }` changes on target `:root`; `theme` is optional, so earlier version 1 records stay valid. See [Agent CLI](#agent-cli).
142
+
143
+ #### Themes
144
+
145
+ The Design panel and Tokens view headers show one theme control. A single axis with exactly two options is a one-click switch: a sun or moon when the options are light and dark (by id or label), otherwise both labels as two segments; its tooltip names the next state. More options or more axes open a compact menu with a row per axis: a segmented control for two or three options, a select for more. With no axis to choose from, there is no control; Palette never adds a System option. Switching calls `apply(id)`, then the panel re-reads `current()`, lists tokens for the new theme state, and refreshes the picked element's values, chips and outline. Closing Design mode restores each axis's original option.
146
+
147
+ ```ts
148
+ import { classTheme } from "@hazem3500/palette/adapters/class-theme";
149
+
150
+ const mode = classTheme({ id: "mode", options: [{ id: "light" }, { id: "dark", class: "dark" }], colorScheme: true });
151
+ initPalette({ project: "your-project", design: { themes: [mode] } });
152
+ ```
153
+
154
+ Theme is context, not an edit. Switching isn't counted as a change, and Undo, Reset and Before/After leave it alone. When Design mode closes, or the widget is destroyed, Palette applies each axis's original option again. An axis whose `current()` throws is left out; an `apply()` that throws doesn't break the panel. Any object with `id`, `options`, `current()` and `apply()` works, so a theme kept in React state or a cookie fits too.
155
+
156
+ #### Token adapters
157
+
158
+ Without `design.tokens`, Design mode uses the built-in `cssVariableTokens()` adapter (id `"css-variables"`, from `@hazem3500/palette/react`) over the CSS custom properties registered with `usePaletteDesignTokens`. Passing `design.tokens` replaces it on every style field: the chips, the suggestions and the off-token markers all come from your adapter, so an element never shows two answers. The **Shared theme** section for registered CSS variables stays available, since it edits variable values rather than an element. To keep CSS-variable chips too, fall back to `cssVariableTokens()` inside your own `resolve()`.
159
+
160
+ In Design mode, each style field of the picked element:
161
+
162
+ - shows the token `resolve(element)` reports for that property as a chip;
163
+ - opens a token picker listing the `list()` tokens whose `properties` include it, with **Show all** for every token. Rows sit under sticky `group` headings and show a preview for the token's `type` (a color swatch, a bar or corner for dimensions, a shadow card, "Ag" in the font), its `label` (or the last path segment), `ref` and value, `states` as chips and `description` as a second line. The current token is pinned first. Search matches label, path, ref and value; arrow keys and Enter choose, Escape closes. With a theme axis of two or three options, color swatches split by option, using `list(theme)` for each, so the page never switches to read them;
164
+ - marks an `{ offToken }` value with a small dot, "Not from a token · 13px", naming a token with exactly that value. Colors compare across hex, `rgb()` and `rgba()` spellings. When a token suggested for the property matches, a "Matches Primary · Use token" line offers it; the field stays unbound until you choose it;
165
+ - previews a chosen token with `preview()`, or by setting its value inline when there is no `preview()`. Undo, Reset and Before/After use the undo it returns. States such as hover are listed, not previewed.
166
+
167
+ `list()` and `resolve()` may return promises; the panel renders first and the token controls join when they resolve. `list()` is called once per theme state. If your token set can change at runtime, add `subscribe(onChange): () => void`: Palette subscribes once Design mode shows your tokens (the panel and an unsaved proposal may each hold a subscription), unsubscribes when the widget is destroyed, and on each `onChange()` drops its cached lists, calls `list()` again and refreshes chips, proposals and any open picker, keeping its search, scroll, active row and preview. A comment on a token swap records `Element color: #6b6b6b [color.text.secondary] → #1f1f1f [color.text.primary]`, followed by your `describe()` text on an `Apply:` line.
168
+
169
+ Returning `{ path: "tone.base" }` declares a semantic binding, including when the host renders that token as a concrete inline value. Its Before/Undo baseline follows the current theme's resolved token value; existing classes, CSS-variable references, declaration priorities and unrelated styles are preserved. Return `{ offToken: value }` for a genuine custom literal, even if it happens to equal a token. Supply current resolved values through `list(theme)` and notify subscribers after applying a theme change.
170
+
171
+ Writing an adapter for utility classes:
172
+
173
+ ```ts
174
+ import type { DesignToken, TokenAdapter, TokenUsage } from "@hazem3500/palette";
175
+
176
+ const tokens: DesignToken[] = [
177
+ { path: "color.text.primary", type: "color", value: "#1f1f1f", properties: ["color"], group: "Text", ref: "text-primary" },
178
+ { path: "color.text.secondary", type: "color", value: "#6b6b6b", properties: ["color"], group: "Text", ref: "text-secondary" },
179
+ ];
180
+ const textClass = (element: Element) => tokens.find((token) => element.classList.contains(token.ref!));
181
+
182
+ const classTokens: TokenAdapter = {
183
+ id: "classes",
184
+ list: () => tokens,
185
+ resolve(element): Record<string, TokenUsage> {
186
+ const token = textClass(element);
187
+ return { color: token ? { path: token.path } : { offToken: getComputedStyle(element).color } };
188
+ },
189
+ preview(element, _property, token) {
190
+ const previous = textClass(element)?.ref;
191
+ if (previous) element.classList.remove(previous);
192
+ element.classList.add(token.ref!);
193
+ return () => {
194
+ element.classList.remove(token.ref!);
195
+ if (previous) element.classList.add(previous);
196
+ };
197
+ },
198
+ describe: (element, _property, _from, to) => `Replace class "${textClass(element)?.ref}" with "${to.ref}"`,
199
+ };
200
+
201
+ initPalette({ project: "your-project", design: { tokens: classTokens } });
202
+ ```
203
+
204
+ Keep `resolve()` cheap and honest: report only properties you can vouch for. Define the adapter once, outside render, so `usePalette` sees the same object. The React props demo shows this adapter with `?tokens=classes`.
205
+
206
+ #### Editing tokens without picking an element
207
+
208
+ With nothing selected, the Design panel offers **Edit tokens**. The Tokens view lists every `list()` token in the same rows, sticky groups and search as the picker. A row opens the field popup in **Custom** for the token's raw value; a token with `alias` (the path it references) can instead be repointed from **Tokens**. With theme axes, an edit belongs to the current option, so the switcher at the top chooses which option you edit and the preview follows it. Edited tokens show a dot and a reset. The footer has the change count, Before/After (← → or B, flipping the whole preview), chronological Undo, Reset all and **Comment**, which posts one comment on the page:
209
+
210
+ ```text
211
+ Proposed token changes (preview only)
212
+ Token: color.text.primary (dark) #e6e6e6 → #f5f5f5
213
+ Token: color.accent {color.blue.500} → {color.violet.500}
214
+ Token adapter: css-variables
215
+ ```
216
+
217
+ Token edits stay pending exactly like element previews. Escape or closing Design mode shows Before while it is closed, and reopening previews the edits again, never silently: with an element selected, the footer count includes them and a **Token changes · N** chip opens the Tokens view. Undo steps through token and element edits in the order they were made, Before/After flips both, the element's **Comment** adds the token section to its comment and then clears both.
218
+
219
+ With nothing selected, a card lists what is pending: "N unsaved changes", then one row per changed element and one for token changes, each with its kind and count. Hovering a row outlines its element; clicking it selects the element. **Comment with changes** opens the composer with every change, the copy button copies them, and **Discard all** clears them without a confirm: ⌘Z (Ctrl+Z) brings them back and ⌘⇧Z discards them again, until something else is selected.
220
+
221
+ The comment's **Token changes** card has the same Before/After switch as an element's card when the adapter implements `previewValues`: Before runs the preview's cleanup, and After previews the edits again. Rows name each token by its label, or else the last part of its path. Palette's tooltip shows the full path and theme.
222
+
223
+ The theme in parentheses is the theme key: the current option of each switchable axis, joined with `+` (`docs+dark`). A repointed alias records token references in braces. Live preview is opt-in:
224
+
225
+ - `previewValues(changes, theme, target)` receives every current edit, `{ path, value, theme?, alias? }`, on each edit and theme switch, after Palette runs the previous cleanup. Return a cleanup that restores the page exactly; Palette also runs it for Before, cancel, Reset, closing Design mode and destroy. `theme` is the current key, so an adapter that cannot scope a value to its theme can preview only the edits for it. `target` is `{ document, root }` for the reviewed page; in Frame's simulated viewport it is the frame's document, so write injected styles there.
226
+ - Without `previewValues`, edits are still recorded and commented, and the view says "No live preview".
227
+ - A token may name its `cssVariable` and a `toCss(value)` transform. A variable holding raw channels such as `19 18 19`, used as `rgb(var(--color-surface))`, shows and edits as `#131213` while the variable receives `19 18 19`.
228
+
229
+ `discoverCssTokens({ scopes: { dark: ".dark" } })` and `tailwindTokens(table, { scopes })` implement `previewValues` with one injected stylesheet: an edit without a theme sets the variable on `:root`, an option's edit uses its scope selector, and an option without one uses `:root` outside the other scopes. Removing the stylesheet restores the page. `discoverCssTokens` reports `alias` for a variable declared as exactly `var(--other)` (in `:root`, or in the current option's scope), and lists `--color-*` channel triplets as hex with `toCss`. Tailwind tables preview only tokens that set `cssVariable` (and `toCss` when needed). `cssVariableTokens()` previews registered tokens on their root's inline style, for the current theme only.
230
+
231
+ #### Discovering CSS variables
232
+
233
+ `discoverCssTokens()` from `@hazem3500/palette/adapters/css-variables` is the no-table option for sites whose tokens are CSS custom properties, including the variables Tailwind v4 emits for `@theme`. It is opt-in, reads `--*` declarations on `:root`/`html` from same-origin stylesheets (cross-origin sheets are skipped), and is meant for development builds.
234
+
235
+ ```ts
236
+ import { discoverCssTokens } from "@hazem3500/palette/adapters/css-variables";
237
+
238
+ initPalette({ project, design: { tokens: discoverCssTokens() } });
239
+ ```
240
+
241
+ - Tailwind namespaces set the type and the properties a token may style: `--color-*`, `--spacing`/`--spacing-*`, `--radius-*`, `--text-*`, `--font-*`, `--font-weight-*`, `--leading-*`. Other variables are kept only when their value is a color or a length. `--tw-*` is skipped. Paths follow the name, so `--color-brand-500` is `color.brand-500`.
242
+ - A property shows a token only when the element's winning declaration (inline, then stylesheet by `!important`, specificity and order) is exactly one `var()` of a discovered variable. An equal value is not a match.
243
+ - Choosing a token previews `property: var(--name)`, and proposals say `Replace var(--old) with var(--new) for property`, so agents write the reference, not the resolved value.
244
+ - `include(name)` filters variables and `limit` caps the list (300). Values come from the computed style, so they follow the current theme. The scan reruns on every `list()`; `@layer` and `@media` blocks are read, but variables set only under a theme selector such as `[data-mode=dark]` are not listed on their own.
245
+ - Selector-based impact counts and **Edit shared theme values** need `usePaletteDesignTokens` and are not part of this adapter.
246
+
247
+ #### Instrumenting components at build time
248
+
249
+ `paletteComponents()` from `@hazem3500/palette/vite` makes a design system's components, and the app components you list, editable in Design mode with no code at their call sites. It is a Vite plugin (Vite 5 or later) for **preview builds only**: pass `enabled: true` only for builds that never reach customers.
250
+
251
+ ```ts
252
+ // vite.config.ts
253
+ import { paletteComponents } from "@hazem3500/palette/vite";
254
+
255
+ export default defineConfig({
256
+ plugins: [
257
+ paletteComponents({
258
+ enabled: process.env.PREVIEW_BUILD === "1",
259
+ // Every component export of these packages that has editable props.
260
+ packages: ["@acme/ui"],
261
+ // App components, by module and export, relative to the Vite root.
262
+ components: ["src/cards/ProjectCard.tsx#ProjectCard", "src/nav/TopBar.tsx"],
263
+ }),
264
+ ],
265
+ });
266
+ ```
267
+
268
+ - **Disabled, it does nothing.** With `enabled` off, the plugin has no hooks: the build is byte-for-byte the build without it, with no runtime, registry or component metadata. The package's tests check this on a minified production build.
269
+ - **What gets instrumented.** The plugin reads each export's types with TypeScript, the way the app already compiles. A React component (function, class, `forwardRef` or `memo`) is instrumented when it has at least one editable prop:
270
+ - string, number and boolean props;
271
+ - unions of their literals, such as a `variant` or `size`, including `cva`-style variant types.
272
+
273
+ These are never instrumented:
274
+ - **Never controls:** callbacks, refs, React nodes, objects, `className`, `style`, `children`, and attributes inherited from React's DOM types.
275
+ - **Never wrapped:** hooks, tokens, helpers, and names ending in `Provider`, `Context` or `Icon` (`exclude(name, from)` replaces that list).
276
+
277
+ The build logs how many components were instrumented. It also warns about any listed app export it skipped, and why.
278
+ - **How it edits.** Imports of the listed packages and modules resolve to generated modules that re-export everything and wrap each eligible component once. The wrapper:
279
+ - renders the original component;
280
+ - adds no DOM element;
281
+ - forwards `ref`;
282
+ - keeps `memo` and its compare function;
283
+ - keeps statics such as `Select.Option`.
284
+
285
+ Each mounted instance keeps its own previews in its own state, so edits go through React's normal render path in production React 18 and 19. Previews hold through parent re-renders, and Before/After, Undo, Redo and Reset work as for registered components. Reset restores a prop exactly, including one that was never passed. Previews exist only once someone edits in Design mode. Unmounting drops an instance's previews; they never move to another instance.
286
+ - **Finding the instance.** Picking an element reads React's fiber links (`return`, `child`, `memoizedState`) to find the nearest instrumented instance, like the fiber adapter, and never writes them. In Viewport, the framed app's own instances answer.
287
+ - **Cost.** The preview build carries a 2 KB gzip runtime plus each component's prop list. The test fixture measured 2.3 KB for 13 components, and on an Apple M4 Max about 0.6 µs per instance to mount and 0.3 µs per update.
288
+ - **Limits.**
289
+ - Code that compares element types, such as `child.type === Tab` inside the package, sees the wrapper instead of the original. Exclude such components.
290
+ - A component with several root elements is selected by its first one.
291
+ - Components used only inside the package itself, not imported by the app, aren't instrumented.
292
+
293
+ #### Discovering React components
294
+
295
+ `discoverReactComponents()` from `@hazem3500/palette/adapters/react-fiber` finds the React component behind a picked element, so Design mode can show its name and where it was rendered without registering each component by hand. It is **opt-in and off by default**, and it is the one place Palette reads React's private fields. Use it in development builds.
296
+
297
+ ```ts
298
+ import { discoverReactComponents } from "@hazem3500/palette/adapters/react-fiber";
299
+
300
+ initPalette({ project, design: { components: discoverReactComponents() } });
301
+ // Keep your own adapter first: discoverReactComponents({ adapter: myAdapter })
302
+ ```
303
+
304
+ - **What it reads.** The `__reactFiber$*` key React puts on DOM nodes (or, when present, the renderers of `window.__REACT_DEVTOOLS_GLOBAL_HOOK__`), then `return`/`child` links, `memoizedProps`, `_debugSource` (React 18) and `_debugStack` (React 19 dev). It climbs from the picked element, through shadow hosts, to the nearest function, class, `forwardRef` or `memo` component, and picks that fiber's current copy so props are not stale after updates. Several React roots and renderers on a page are fine.
305
+ - **Only declared props.** The panel shows the component's name, and lists only the props you declare through `schema`. Undeclared props are not shown. Comments still get up to `maxProps` (24) of its strings, finite numbers and booleans as context, read without invoking getters. Callbacks, refs, React children, opaque objects, technical attributes, secret-looking names and `redact(name)` matches are always left out.
306
+ - **Location and precedence.** The ⓘ beside the component name holds the JSX call site (`file:line`) with Copy, and Open when `sourceUrl` can link a repository-relative path. Use `resolveSource(file, line, column)` to map bundled locations. Explicit `usePaletteInspectable` registrations and your own `adapter` win. A descendant points to its component root; props edit that instance only.
307
+ - **Runtime editing is optional.** Import `@hazem3500/palette/adapters/react-preview` before the first `react-dom/client` import, then use `discoverReactComponents({ edit: true })`. The separate bootstrap captures React 18/19 development renderer capabilities, preserves an existing DevTools hook, and calls the renderer's own prop override method. It never assigns Fiber fields. Production and unsupported renderers, or renderers whose capabilities were not captured, stay read-only. Discovered class components need an explicit editor/registration. Import order matters: calling the installer after React has loaded cannot recover missing renderer capabilities. In Viewport, Palette edits through the framed app's own React and bootstrap, so keep the import in the app entry that the frame loads.
308
+
309
+ ```ts
310
+ // First import in the app's development entry, before its React/router graph.
311
+ import "@hazem3500/palette/adapters/react-preview";
312
+ // In the lazily enabled feedback configuration:
313
+ const components = discoverReactComponents({
314
+ edit: true,
315
+ schema: identity => identity.name === "Button"
316
+ ? [{ name: "label", kind: "string", required: true, typeLabel: "string" }]
317
+ : undefined,
318
+ });
319
+ ```
320
+
321
+ `schema(identity)` lists the editable props of each component; Palette does not guess them from current values. A component without one gets no controls. Edits stay applied through parent re-renders, suspend outside Design, and support Before/After, Undo, per-field Reset and Reset all. Reset restores the pre-edit value and releases the override; unmounting never transfers it to another instance. These are runtime previews, with proposed changes available for comments, not source edits.
322
+
323
+ For a framework-independent integration, a `ComponentAdapter` may supply `editor(identity)` returning a stable `ComponentEditor` with `schema`, `read()` and `apply(next)`. Apply through the app's state or supported renderer API, preserve other props, and keep the preview stable through re-renders. Optional `available()` rejects detached instances; `release(properties)` stops retaining overrides after baseline restoration, and `subscribe(listener)` refreshes computed controls after a render commits. Existing `usePaletteInspectable` registrations remain supported and take precedence.
324
+
325
+ - **Graceful no-op.** Every access is guarded. Non-React pages, unknown React versions or fiber shapes, production builds and hostile props yield nothing rather than an error. Production React keeps no debug fields, so there is no location; names that look minified (one or two characters) or are missing are not shown. `include(name)` hides components you don't want listed.
326
+ - **Fragile by design.** These fields are private React implementation details, and React can rename or remove them in any release. React 18.3 and 19.2 are tested here (unit tests and a Chrome run of the demo for each); another version may quietly show nothing until the adapter is updated. If that matters more than convenience, register the components you care about with `usePaletteInspectable` instead.
327
+
328
+ #### Tailwind and class themes
329
+
330
+ Two optional adapters ship as their own entry points, outside the core bundle. For utility-class design systems, a token is a class: Palette previews by swapping classes, not by setting CSS variables.
331
+
332
+ ```ts
333
+ import { tailwindTokens, tailwindSpacing, type TailwindToken } from "@hazem3500/palette/adapters/tailwind";
334
+ import { classTheme } from "@hazem3500/palette/adapters/class-theme";
335
+
336
+ const mode = classTheme({
337
+ id: "mode",
338
+ options: [{ id: "light", class: "light" }, { id: "dark", class: "dark" }],
339
+ colorScheme: true, // also sets color-scheme: light | dark on <html>
340
+ });
341
+ const product = classTheme({
342
+ id: "product",
343
+ options: [{ id: "studio", attribute: { "data-product": "studio" } }, { id: "docs", attribute: { "data-product": "docs" } }],
344
+ });
345
+
346
+ // Generate this from your design-system foundations.
347
+ const tokens: TailwindToken[] = [
348
+ { path: "color.text.primary", type: "color", class: "text-primary", properties: ["color"], group: "Text",
349
+ values: { light: "#1f1f1f", dark: "#f5f5f5", "docs+dark": "#fce5d8" } },
350
+ { path: "color.bg.primary-interactive", type: "color", class: "bg-primary-interactive",
351
+ properties: ["background-color"], value: "#e57cd8", states: ["hover", "active", "disabled"] },
352
+ { path: "radius.md", type: "dimension", class: "rounded-md", properties: ["border-radius"], value: "6px" },
353
+ { path: "typography.h1", type: "typography", class: "text-h1",
354
+ properties: ["font-size", "line-height", "font-weight"], value: "32px/40px 600" },
355
+ ...tailwindSpacing(), // p-*, px-*, …, m-*, gap-* on the 4px scale
356
+ ];
357
+
358
+ initPalette({ project, design: { tokens: tailwindTokens({ tokens }, { themes: [mode, product] }), themes: [mode, product] } });
359
+ ```
360
+
361
+ - `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.
362
+ - `values` keys are option ids (`dark`), `axis=option` pairs (`mode=dark`), or combinations joined with `+` (`track+dark`). The most specific match wins, then `value`.
363
+ - `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.
364
+ - 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.
365
+ - `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`.
366
+ - Tailwind's JIT only emits CSS for classes it finds in source, so a full table can list classes such as `p-9` or `mt-96` that have no rule and preview nothing. `tailwindTokens(table, { onlyGenerated: true })` lists only tokens whose first class has a rule in the page's stylesheets, including rules inside `@media`, `@supports`, `@layer`, `@container`, nested rules and same-origin `@import`. Names are matched exactly as escaped CSS, so `p-1.5`, `w-1/2`, `-mt-4`, `p-[13px]`, `md:p-4` and `!p-4` each need their own rule. The scan runs on the first `list()` and again only when a stylesheet is added, removed, replaced, loads or gains rules, including `adoptedStyleSheets`. With `onlyGenerated`, the adapter also has `subscribe()`: adding, removing, editing or loading a `<style>` or `<link>` refreshes the list in Design mode, including an open picker. Rules added only through `insertRule()` or `adoptedStyleSheets` show with the next such change.
367
+ - Cross-origin sheets: Palette cannot read a cross-origin stylesheet served without CORS, such as a font stylesheet pulled in with `@import url(https://fonts…)`. By default it skips each unreadable sheet or `@import`, assumes it holds no utility CSS, and filters with the rest. It hides nothing while a `<link>` stylesheet is still loading or failed, or when no readable sheet has a style rule. If your utilities themselves come from another origin, pass `onlyGenerated: { strict: true }`, which hides nothing while any sheet is unreadable, or serve that CSS with `crossorigin` on the `<link>` and `Access-Control-Allow-Origin`. `onlyGenerated: true` is the default object.
368
+ - `filter(token)` returning `false` leaves a token out of `list()`, alone or with `onlyGenerated`. Hidden tokens still resolve on elements that already use their class.
369
+ - `classTheme` options take `class`, `attribute`, or both, on `target(page)` (default the reviewed page's `<html>`, which is the frame's in the simulated viewport). 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.
370
+ - Theme changes refresh semantic previews, field values, open token pickers and unsaved proposals without adding an edit. `classTheme` observes its target. Custom axes may provide `subscribe(listener): () => void` for host state changes; axes without it detect changes carried on `<html>` or `<body>` attributes. Custom literals and saved comment bodies keep their recorded values.
371
+
372
+ ## Agent CLI
373
+
374
+ `palette init` adds the default comment workflow to `AGENTS.md`, preserving existing instructions. For an existing project, run `palette agents setup` from its root. Commit the instructions so every agent uses them.
375
+
376
+ Before editing, agents read open comments, export the relevant prompt, and read replies. After verifying a simple fix, an agent can reply with evidence and resolve it at **90%+ confidence**. Complex or uncertain work stays open with a question or progress reply. This is guidance for your coding agent; the CLI does not run an agent or measure its confidence.
377
+
378
+ Requires `@hazem3500/palette` 0.2.0 or later. Connect your coding agent to the same comments your team sees in the browser:
379
+
380
+ ```sh
381
+ npm install @hazem3500/palette@latest
382
+ npx @hazem3500/palette login
383
+ npx @hazem3500/palette comments list
384
+ npx @hazem3500/palette comments get THREAD_ID
385
+ npx @hazem3500/palette comments reply THREAD_ID --body "Fixed and verified on mobile."
386
+ npx @hazem3500/palette comments resolve THREAD_ID
387
+ ```
388
+
389
+ Run these from a project configured with `palette init`. The CLI searches parent directories for `.komo/project.json`. Google sign-in opens a local browser handoff; `--no-open` prints the local sign-in link instead. Credentials are stored outside your repository in `~/.config/komo`, with a private file per API/project. Sessions stay valid until you sign out. `palette logout` revokes the session and removes its file.
390
+
391
+ For an existing inline installation, pass `--project YOUR_PROJECT_KEY --origin https://your-site.example`. `--origin` is an approved website address; `--endpoint` is the comments API. New setups remember the website address. Older setups default to `http://localhost:3000`; override it if that site is not approved. `--repo` and `--branch` override detected scope; project scope defaults to `shared`.
392
+
393
+ ### Commands
394
+
395
+ | Command | Purpose |
396
+ | --- | --- |
397
+ | `palette schema` | JSON command reference for agents; no sign-in needed. |
398
+ | `palette whoami` | Show the current reviewer and project scope. |
399
+ | `palette comments list` | Open threads as JSON; `--status open\|resolved\|all`, `--page`, `--tag`, `--limit 1–250`, `--offset`. Default page size: 50. |
400
+ | `palette comments tag THREAD_ID TAG` | Add a normalized group tag to a thread. |
401
+ | `palette comments untag THREAD_ID TAG` | Remove a group tag from a thread. |
402
+ | `palette comments get THREAD_ID` | Full thread, replies, selector, source reference, and anchor. |
403
+ | `palette comments prompt` | The UI’s open-feedback prompt as Markdown; optional `--page` and `--json`. `--snapshots` saves element snapshots and lists their files. |
404
+ | `palette comments snapshots THREAD_ID` | Save a thread’s element snapshots as files: `THREAD_ID.webp` (or `.png`) and `THREAD_ID.html.txt`. `--out DIR` picks the folder; the default, `.komo/snapshots`, ignores itself in Git. `comments get THREAD_ID --snapshots` does the same and adds `snapshotFiles`. |
405
+ | `palette comments reply THREAD_ID --body TEXT` | Add a reply as the signed-in reviewer. |
406
+ | `palette comments resolve THREAD_ID` | Mark verified work resolved. |
407
+ | `palette comments reopen THREAD_ID` | Reopen a thread. |
408
+ | `palette comments create --page /path --anchor-file FILE --body TEXT` | Create a thread from captured anchor JSON. |
409
+ | `palette comments edit THREAD_ID COMMENT_ID --body TEXT` | Edit your own message. |
410
+ | `palette comments delete THREAD_ID COMMENT_ID` | Delete your own message. |
411
+ | `palette comments react THREAD_ID COMMENT_ID --emoji EMOJI` | Set your reaction; `--remove` removes it. |
412
+ | `palette comments move THREAD_ID --anchor-file FILE` | Move an indicator and keep it outside automatic stacks. |
413
+
414
+ `--body-file FILE` reads a message from disk; `--body-file -` reads stdin. Use one body source. Anchor files use the `anchor` object returned by `comments get`, with updated geometry where appropriate; they are not screenshots. The API validates selectors, geometry, permissions, and quotas.
415
+
416
+ Successful commands emit `{ "ok": true, "scope": { ... }, "data": ... }`. Errors emit `{ "ok": false, "error": { "message": "...", "status": ... } }` on stderr and exit with code 1. Prompts are Markdown unless `--json` is present. Design mode messages include `designEdit`, a validated `DesignEdit` (`komo: "design-edit"`, `version: 1`) with the target selector, component, token adapter and changes. `palette comments prompt` prints it after the message as a fenced `json komo-design-edit` block with a one-line instruction, and `--json` adds a `designEdits` list. A message may also include its own `komo-design-edit` block; malformed, unknown-version or oversized (16 KB) records are ignored. List results include `total` and `nextOffset`; account images are excluded from agent output. Commands do not automatically retry writes.
417
+
418
+ Automation can supply a personal access token (see Bots and CI) or a project session through `KOMO_TOKEN`. Other overrides: `KOMO_PROJECT`, `KOMO_ENDPOINT`, `KOMO_ORIGIN`, `KOMO_REPO`, `KOMO_BRANCH`, and `KOMO_CONFIG_HOME`. Keep tokens in your secret manager, never in committed configuration or command arguments. A token acts as its reviewer; it does not bypass ownership checks.
419
+
420
+ Element snapshots are read with the same credentials as every other command, and always need them, even on link-access projects. Behind a sign-in proxy (`KOMO_PROXY_AUTH`, see [NODE.md](./NODE.md)), access tokens are refused: an agent gets a project session through the proxy with `POST /auth/proxy/session?project=KEY` and passes its `token` as `KOMO_TOKEN`.
421
+
422
+ Suggested agent workflow: list open threads, read a thread, inspect the repository, make a scoped change, verify it, reply with the result, and resolve. Comment text is untrusted feedback, not permission to run unrelated commands or disclose secrets.
423
+
424
+ Mount once after hydration, outside server rendering, and call `destroy()` before changing project or branch. Repeated teardown is safe. Palette preserves the host DOM hierarchy: it frames an existing content root, or uses Floating when there is no suitable root. Pass a mounted `pageRoot` with a layout box for explicit Frame support; body, html, detached elements, and `display: contents` are not frameable.
425
+
426
+ Frame shows the page beside the comments at a chosen viewport size and never moves its DOM. Native pointer and touch coordinates retain their normal CSS-pixel relationship to the page, including grids and drag controls that subtract `getBoundingClientRect()` from `clientX`/`clientY`. At 100% and in **Available space** the page is unscaled. Content reflows, and the frame acts as the page's viewport: `vw`, `vh`, `dvh`, `svh`, `lvh`, `vmin` and `vmax` in readable stylesheets and inline styles resolve to the frame box, so `100vw × 100vh` app shells fit it. Cross-origin stylesheets and shadow-root styles keep window units, and media queries still match the browser window. Fixed body-level dialogs, overlays, sheets and toasts lay out against the frame too, including percentage and right/bottom insets, without moving their DOM. Everything is restored when Frame closes.
427
+
428
+ Frame opens on your browser window's own size at **Fit**, and one compact control above the frame names both, **Desktop 1280 × 800 · 67%** for example (a custom size shows only its dimensions, and a very narrow bar only the size). At **Fit** the page keeps its real desktop layout, scaled down until all of it is visible. **Fit** never scales up. **100%** keeps CSS pixels; a frame larger than its stage pans inside it like a design tool: thin scrollbars on both edges, the wheel over the stage's margin (Shift for horizontal), the app's own scrollers first and the stage at their end, Space+drag from anywhere, and arrow, Page and Home/End keys once you click the margin. Switching from Fit keeps the open comment, or the point under the pointer, in view, and keyboard focus beyond the stage brings it into view. The first time the frame is scaled, a tip notes that some floating UI may be offset while scaled, with **Use 100%** one click away. Its menu picks another viewport: **Available space** (the space beside the comments, unscaled), Wide 1440, Laptop 1280, Tablet 768, Mobile 390, or a custom width × height; then the zoom, **Fit** (⇧1) or **100%** (⇧0). Both keys work while Frame is on, also with focus in the simulated viewport, and pause while you type; a Palette shortcut you configure on either key takes precedence. You can also drag the frame's right edge or bottom-right corner, or press the arrow keys on a focused handle (Shift for 100px steps). The page reflows at that logical size, and viewport units follow it. Palette remembers the size and zoom for each project. A fitted scale changes visual rectangles but not layout. Palette's pins, picking, outlines and portals follow the scale, its own cards stay unscaled beside their pins, and host code that combines client coordinates with element rectangles stays correct. Handlers that mix client coordinates with unscaled lengths, such as a fixed row height, need 100% or Available space. Media queries, `matchMedia`, `window.innerWidth` and resize events still match the browser window, because in-place reflow cannot give the page a separate viewport. When the chosen width differs from the window, an ⓘ beside the control explains that the layout follows your browser window; resize the browser window to see another breakpoint, or use a simulated viewport (below).
429
+
430
+ #### Simulated viewport
431
+
432
+ `frame: { mode: "iframe" }` adds a simulated viewport, like a browser's device mode. The current URL renders in a same-origin `<iframe>` sized to the chosen width × height, at Fit or 100%, so media queries, `vw`/`vh`, `matchMedia`, resize events and `window.innerWidth` are real. Palette keeps running on your page and reaches into the frame: picking, hover outlines, pins, threads, Design mode (inspector, previews, Undo, Before/After), Tokens previews and keyboard shortcuts all work there, including with focus inside the frame. The page's own Palette, booted inside the frame, sees that Palette framed it and stays inert, so there is only ever one instance. Reviewers turn it on or off with **Simulate real viewport**, a checkbox row at the end of the Frame size menu ("Reloads the page in a frame so breakpoints and popovers match this size. Runs a second copy of the app."). Unchecked, Frame shows the page itself. Palette remembers the choice per project.
433
+
434
+ - **Opt-in.** The frame runs a second live copy of your app, with its own requests, sockets, analytics and auth refreshes, so the default stays `"transform"` (the page itself). Turn it on once your app is safe to run twice. With `defaultMode: "transform"`, Frame opens on the page itself and offers the simulated viewport, and a reviewer's own choice is remembered over the default. Without `mode: "iframe"` (an `iframeUrl` alone included) Frame never creates the iframe and the row doesn't appear; a choice remembered earlier is kept, unused, and applies again if you turn the mode back on.
435
+ - **`iframeUrl(location)`** picks the URL to frame, e.g. a preview route for an app that needs its own bootstrap. It must be same-origin. If it changes the path, pass `page(location)` too, so comments keep the page they belong to: in the simulated viewport Palette passes the frame's location to `page`.
436
+ - **Fallback.** When the frame can't show a same-origin page, Frame falls back to the page itself and the ⓘ beside the control says why: X-Frame-Options `DENY`, a CSP `frame-ancestors` that excludes the page, a redirect to another origin, or no load within 15 seconds. Turning **Simulate real viewport** off and on again retries.
437
+ - **Keys.** A click on the framed page focuses the frame, so its keys fire in the frame's document; Palette listens there too, after every reload. Review and Design keys, Frame's ⇧1 and ⇧0, held keys such as the visibility peek, and Space+drag work as they do on the page itself, and the framed app's own text fields keep their keys. At 100%, Space+drag scrolls the frame's port.
438
+ - **Navigation.** Client-side routes inside the frame (`pushState`, back and forward, hash changes) are Palette's page; your page's URL stays where it was. Opening a comment on another page navigates the frame.
439
+ - **Anchors.** Comments are anchored by page and selector, so a comment made in either mode resolves the same in the other. A draft, an open thread and the Design selection carry over when you switch.
440
+ - **Exit.** Closing Frame removes the iframe. Your page underneath was never moved, so its layout and scroll are exactly as you left them; the frame opens at the same scroll position.
441
+ - **Adapters** receive the reviewed page: `previewValues(changes, theme, target)` and theme axes' `current(target)` and `apply(id, target)` get `{ document, root }`, which is the frame's document in the simulated viewport. Write page-wide previews there. The built-in adapters already do. Element-level `preview(element, …)` needs nothing new: the element is in the frame.
442
+ - **React registrations.** `usePaletteInspectable` and `usePaletteDesignTokens` registrations made by the framed copy of the app are read through a small bridge that Palette, inert inside the frame, publishes; prop edits go through the framed app's own state, and token previews apply to its roots. The React fiber adapter works too.
443
+ - **Single copy.** With `shell: true`, the simulated viewport runs the app only once, in the frame (see below).
444
+ - **Limits.** `pickRoots` names your page's elements, so in the simulated viewport all of the frame's body is pickable. Experimental move/resize geometry works in both modes: handles, snapping and structural moves map through the frame's scale.
445
+
446
+ ##### Single-copy simulated viewport
447
+
448
+ By default the simulated viewport runs a second copy of your app in the frame, while your page keeps running beneath it. `frame: { mode: "iframe", shell: true }` avoids the second copy: turning on **Simulate real viewport** reloads the page as a **shell** for Palette, and the app runs once, in the frame. Your app's entry skips mounting in the shell:
449
+
450
+ ```ts
451
+ import { isShell } from "@hazem3500/palette/shell";
452
+
453
+ const palette = { project: "YOUR_PROJECT_KEY", frame: { mode: "iframe", shell: true } } as const;
454
+ if (import.meta.env.DEV && isShell(palette.project)) {
455
+ // Palette's shell: start Palette only. It loads the app in the frame.
456
+ const { initPalette } = await import("@hazem3500/palette");
457
+ initPalette(palette);
458
+ } else {
459
+ mountApp(); // Your app, which starts Palette as usual.
460
+ }
461
+ ```
462
+
463
+ - **Free in production.** `@hazem3500/palette/shell` is side-effect free and reads nothing until `isShell()` is called, so a build-time guard such as `import.meta.env.DEV && isShell(…)` drops it, and the shell branch, from production builds. Palette's package checks verify this on a minified build.
464
+ - **Only Palette makes a shell.** `isShell(project)` is true only when Palette set a random nonce in this tab's sessionStorage and the URL carries the same one, for the same project and origin. A link with `?komo-shell=…` alone, or a flag left by another project, returns false, so a crafted link can't blank your app. If Palette doesn't start within 4 seconds (`isShell(project, { timeout })`), the shell clears itself and loads the normal page.
465
+ - **Reloads.** Turning it on asks first, because the reload drops unsaved state in the app and Design previews you haven't posted. Your app's own `beforeunload` guard can also refuse the reload. Cancel or a refused reload keeps the second copy instead. A remembered Frame reopens in the shell on load without asking. Turning it off, Floating, closing Frame or a frame that can't load all reload the normal page on the frame's route.
466
+ - **URL.** The shell's URL follows the frame's route, so reloading or sharing it stays on the same page. The frame's own URL never carries the nonce.
467
+ - **Two copies again.** If a shell ends early (Palette didn't start, or the frame couldn't load), the tab keeps the two-copy simulated viewport until the reviewer turns it on again. Server-rendered markup the guard can't skip still paints in the shell.
468
+
469
+ `frame: { scale: true }` explicitly opts into shrinking the original viewport instead. A CSS transform changes visual rectangles without changing local layout dimensions, so arbitrary coordinate-sensitive host handlers may select the wrong value under scaling. This option requires host controls that already account for transforms; Palette does not rewrite native events or browser prototypes. Use the default unscaled Frame or Floating for interactive pages that do not support scaling.
470
+
471
+ ## Bots and CI
472
+
473
+ Give a bot its own revocable credential instead of sharing your login.
474
+
475
+ 1. **Create a token.** Signed in with Google, open **Account → API tokens** in the Palette toolbar, or `/account` on your Palette server (`https://komo.offbr.co/account` when hosted). Name the token, pick an expiry, choose scopes, and optionally limit it to projects you own or have joined. The token is shown once; copy it into your secret manager. Guest sessions cannot create tokens.
476
+ 2. **Set `KOMO_TOKEN`.** Export it in the bot's environment, or save it once with `printf %s "$KOMO_TOKEN" | palette login --token` (stdin only; a token passed as an argument is refused). Check it with `palette whoami`. `palette logout` removes saved credentials.
477
+ 3. **Use the CLI as usual.** The project, endpoint, and origin come from `.komo/project.json` or the `KOMO_*` overrides.
478
+
479
+ Tokens start with `komo_pat_`, so secret scanners can find them. Palette stores only a salted hash, and records create, revoke, and use events (use is logged at most once a minute per token).
480
+
481
+ | Scope | Allows |
482
+ | --- | --- |
483
+ | `comments:read` | List and read threads, `palette whoami`. |
484
+ | `comments:write` | Create threads, reply, edit, react, resolve, tag, move. |
485
+
486
+ A token acts as its owner, narrowed by its scopes and project list: it never passes an ownership check or private-project invitation its owner would fail, and it cannot manage tokens, projects, members, or webhooks. Credentials resolve in this order: `KOMO_TOKEN`, a token saved by `palette login --token`, then a browser login session.
487
+
488
+ **Rotate** by creating a replacement, updating the secret, confirming `palette whoami`, then revoking the old token. Prefer an expiry of 90 days or less. **Revoke** from **Account → API tokens** or `/account`; the token stops working on its next request (HTTP 401), and so does an expired one.
489
+
490
+ GitHub Actions:
491
+
492
+ ```yaml
493
+ - name: Export open feedback
494
+ env:
495
+ KOMO_TOKEN: ${{ secrets.KOMO_TOKEN }} # a comments:read token, limited to this project
496
+ run: npx --no-install palette comments prompt
497
+ ```
498
+
499
+ Add the token under **Settings → Secrets and variables → Actions**. Self-hosted servers can set `KOMO_TOKEN_PEPPER` (a long random secret) to mix into stored hashes; changing it revokes every token.
500
+
501
+ ## How it works
502
+
503
+ The package adds an isolated ShadowRoot to your site. Comments live in the API’s database, separately from the host application. Reviewers using the same project and scope see the same feedback. Paths identify pages; query strings and fragments are excluded by default.
504
+
505
+ Every new hosted workspace has a Google-authenticated owner. Guests can review but cannot create or own a workspace. Self-hosted setup also requires a Google owner claim before guest commenting becomes available. Signing in later does not silently transfer old guest comments based on a matching name.
506
+
507
+ Visible, active reviews start polling every four seconds and back off to 15 seconds when unchanged; idle widgets poll about once a minute. Hidden/offline tabs pause. Successful writes refresh immediately. Local storage restores the last known account/comments before revalidation; this is an offline snapshot, not proof of current access. Server authorization still checks every request. Sign-out clears this API/project’s account and all channel snapshots, even if remote revocation fails.
508
+
509
+ Sessions are isolated by API endpoint and project. HTTPS cookies are host-only by default; HTTP development uses local storage instead of sending a bearer cookie to every localhost port. `sessionDomain` explicitly opts into a trusted parent domain: **every sibling host, including unrelated apps, can receive that cookie** ([browser cookie scope](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie)). Do not enable it for untrusted previews. Unrelated origins cannot share browser storage. If trusted preview gateways all proxy the same API, set the same `sessionEndpoint` canonical URL alongside the trusted `sessionDomain`; never reuse it across independent APIs. The Palette website opts in for its own controlled previews.
510
+
511
+ Legacy project-only cookies are retired rather than trusted across API installations. Existing endpoint-scoped local sessions can migrate after server validation; cookie-only users may need to sign in once. Browser storage can be disabled independently; sessions remain usable in memory when neither cookie nor local storage is available. Client credentials and cached feedback are accessible to scripts on the embedding site—install only on sites you trust.
512
+
513
+ On localhost, the widget defaults to a separate `local` channel in the **configured API’s database**. It is not a browser-only database or a private workspace per developer. Project permissions still apply; use a separate project/API for isolated development data. Switching to Shared selects the configured shared branch. `pnpm dev:api` is different: it starts a local API with its own local database.
514
+
515
+ Projects default to link access. Owners can restrict feedback to invited Google accounts in Account → Project settings. The public project key identifies a workspace; it is not a credential. Approved origins control embedding, and private-project membership controls feedback access. Your website and repository permissions remain separate.
516
+
517
+ ## Hosted setup
518
+
519
+ ```sh
520
+ pnpm exec palette init
521
+ ```
522
+
523
+ Mount the generated `komo.config.js` helper and open your app. Select **Connect** in the sidebar and sign in with Google. Review the detected production and preview addresses in the sidebar and add any others, one per line. Connecting approves the current site and those listed addresses, including production sites that have not launched. Remove a suggested address to leave it unapproved. Google credentials and account-wide management stay on the Palette service; your site receives a session for its new project.
524
+
525
+ The CLI waits up to ten minutes and replaces the temporary setup configuration after connection. Keep the terminal open. Run `palette init` again after an interruption or expiry. The temporary polling secret stays in ignored `.komo/setup.json` and is never bundled into your site. A reload can resume commenting while the CLI finishes. Run `palette sync` before builds that use the generated helper.
526
+
527
+ If the app cannot run, use the hosted recovery link printed in the terminal, sign in, and select **Create project**. The CLI still writes the final configuration.
528
+
529
+ Approve your deployed website in **Approved sites**. Palette detects deployment URLs when available; `--origin` can supply one. You can paste a full preview link—the modal extracts its site address. No DNS changes are required, including for `pages.dev` previews.
530
+
531
+ The setup page manages your account and approved sites. Leave comments on the website where you installed Palette.
532
+
533
+ Return to `/setup?workspace=<project key>` to approve additional sites (up to ten). Only the Google owner can approve sites. The site address controls where that owner’s project can be embedded; it does not grant ownership of a domain or access to other projects. The API endpoint is the Palette service, not your website URL.
534
+
535
+ Starter limits per owner/workspace:
536
+
537
+ | Limit | Allowance |
538
+ | --- | --- |
539
+ | Projects per Google owner | 3 |
540
+ | Stored comments, including replies | 250 |
541
+ | Logical storage budget | 10 MiB |
542
+ | Write requests per project | 500/day |
543
+ | Request body | 16 KiB |
544
+ | Comment / reviewer name | 4,000 / 60 characters |
545
+ | Element snapshots per thread | One image (256 KiB) and one HTML capture (32 KiB) |
546
+ | Snapshot storage | 5 MiB, counted within the storage budget |
547
+
548
+ The storage budget accounts for text, anchors, reactions, element snapshots, and a fixed profile allowance per member. It is deliberately conservative, not a measurement of D1 disk usage. Soft-deleting or resolving a comment does not reset its quota. Element snapshots are the only attachments; avatars have a bounded representation.
549
+
550
+ The service also applies per-IP and per-user limits, database-enforced quotas, owner-approved site restrictions, payload validation, project suspension, hourly cleanup, and a global daily request ceiling. Edge rate limiting runs before database work. These controls bound ordinary abuse; Google accounts and CORS do not eliminate automated abuse. The service remains a starter offering, not an unlimited storage service. Project exports, imports, and quota recovery are available below. Billing upgrades are not implemented yet.
551
+
552
+ ## Self-host
553
+
554
+ Install Palette in your project, then run:
555
+
556
+ ```sh
557
+ pnpm exec palette init --self-host --origin https://preview.example.com
558
+ ```
559
+
560
+ The CLI signs into Cloudflare, creates a dedicated D1 database, applies migrations, deploys a Worker, and prompts for the Google client secret through Wrangler. It writes the deployment configuration into `.komo/` so you own and can change it.
561
+
562
+ Create a Google OAuth **Web application** client with `openid profile email` access. Pass its public client ID with `--google-client-id`, or enter it when prompted. Add the callback printed after deployment:
563
+
564
+ ```text
565
+ https://YOUR-WORKER.workers.dev/auth/google/callback
566
+ ```
567
+
568
+ Open the owner-claim link printed by setup and sign in with Google. The one-time claim key is saved in `.komo/owner-key`, excluded from Git, and only its hash is deployed. Ownership cannot be claimed again. Keep the Google secret out of browser code and configuration files.
569
+
570
+ If setup is interrupted, run `pnpm exec palette deploy` to resume the existing deployment. Do not rerun initialization to create another database. The generated `.komo/wrangler.json` supports explicit allowed preview domains and single-label wildcard origins. Only allow domains whose scripts you trust.
571
+
572
+ Self-hosted storage belongs to your Cloudflare account; its service limits and charges apply. Use `palette deploy` for package migrations and `palette project export` for feedback backups. The hosted service and self-hosted installations use separate databases.
573
+
574
+ For a Node container with PostgreSQL (including Cloud Run), use the [Node self-hosting guide](./NODE.md). It uses the same `endpoint`, project configuration, and CLI commands without Wrangler. Existing Worker installations do not change.
575
+
576
+ ## Configuration
577
+
578
+ ### Client options
579
+
580
+ Pass these to `initPalette(config)` from `@hazem3500/palette` or `usePalette(config)` from `@hazem3500/palette/react`.
581
+
582
+ `YOUR_PROJECT_KEY` is a placeholder for a string. Hosted Palette generates the key during `palette init`; copy that value. For self-hosting, use the project identifier configured on your server. Reuse the same key across sites that should share feedback.
583
+
584
+ | Option | Type | Default | Purpose |
585
+ | --- | --- | --- | --- |
586
+ | `endpoint` | `string` | Hosted Palette API | Comments API root; HTTPS outside localhost. |
587
+ | `project` | `string` | Required | Public project identifier returned by setup. |
588
+ | `repo` | `string` | Project key | Repository identifier, usually `owner/repo`. |
589
+ | `scope` | `"project" \| "branch"` | `"project"` | `"project"` shares comments; `"branch"` separates branches. |
590
+ | `branch` | `string` | Inferred by `palette sync` | Required only for branch scope. |
591
+ | `enabled` | `boolean` | `true` | Set from your build environment to restrict review UI. |
592
+ | `pageRoot` | `HTMLElement` | Sole existing content root, when present | Mounted element inside body to frame. Body and html are not supported. |
593
+ | `pickRoots()` | `() => Element[]` | All of body except Palette | 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. |
594
+ | `frame.scale` | `boolean` | `false` | Reflow at the available width without scaling. Set `true` only for hosts whose interactive controls account for transformed coordinates; keeps the original viewport layout and scales its contents. |
595
+ | `frame.mode` | `"iframe" \| "transform"` | `"transform"` | `"iframe"` offers a simulated viewport (**Simulate real viewport** in the Frame size menu): the page in a same-origin iframe sized to the chosen viewport, with real media queries. `"transform"` shows the page itself. Falls back to the page itself with the reason when the page can't be framed. |
596
+ | `frame.defaultMode` | `"iframe" \| "transform"` | `"iframe"` | With `mode: "iframe"`, where Frame starts until the reviewer chooses: `"iframe"` the simulated viewport, `"transform"` the page itself, so the second copy of the app loads only when asked for. A remembered choice wins. |
597
+ | `frame.shell` | `boolean` | `false` | With `mode: "iframe"`, the simulated viewport reloads the page as Palette's shell, so the app runs once, in the frame. Requires the entry guard `isShell(project)` from `@hazem3500/palette/shell`. See Single-copy simulated viewport. |
598
+ | `frame.iframeUrl(location)` | `(location: Location) => string` | The current URL | Same-origin URL to load in the simulated viewport (needs `mode: "iframe"`), e.g. a preview route. Pair it with `page(location)`. Pass a stable function. |
599
+ | `frame.include(element)` | `(element: HTMLElement) => boolean` | Fixed portals, including fixed descendants of static wrappers | Keep body-level dialogs, overlays and toasts inside Frame. Host transforms, animations and focus are preserved without moving portal DOM. Return `true` to map a whole body child through Frame or `false` to exclude its subtree; Palette's own host is never framed. |
600
+ | `isolateEvents` | `boolean` | `true` | Keep Palette's events out of the host page. See [Host events](#host-events). |
601
+ | `source(element)` | `(element: Element) => string \| undefined` | Anchor metadata | Return a repository-relative source path. |
602
+ | `anchorTarget(element)` | `(element: Element) => Element \| undefined` | Declared owners and stable semantic cards | Optional persistent ancestor for comment capture. Offsets stay relative to this owner when hover-only children disappear. Owners cannot cross a dialog/popover boundary. Design inspection continues to target the exact element. |
603
+ | `sourceUrl(source, branch)` | `(source: string, branch: string) => string` | GitHub viewer | Custom source or editor link. |
604
+ | `page()` | `() => string` | `location.pathname` | Canonical page identifier. |
605
+ | `persistentRegions` | `string[]` | None | Selectors for layout shown on every page, e.g. `["nav", "[data-sidebar]", "header"]`. Comments made inside them show on every page where their element exists, are labelled **All pages** in the list, and keep their original page for links. Reviewers can switch a comment between **Show on all pages** and **Only on this page** in its **…** menu. Self-hosted APIs need the thread scope migration (D1 `0020`, applied automatically on Node). |
606
+ | `isPersistent(element)` | `(element: Element) => boolean` | None | Custom test for persistent layout, checked alongside `persistentRegions` with the comment's element. |
607
+ | `navigate(url)` | `(url: URL) => void \| Promise<void>` | `location.assign` | Route to a comment without reloading, e.g. `(url) => router.navigate({ href: url.pathname + url.search + url.hash })`. Rejected or redirected navigation keeps the requested conversation open with destination context; the package never forces a reload or retries your router automatically. Missing elements remain separate from navigation status. |
608
+ | `drawerContainer` | `HTMLElement` | Viewport | Element used to center the drawer before it is dragged. |
609
+ | `autoHideDrawer` | `boolean` | `true` | Set `false` to keep the drawer visible away from the pointer. |
610
+ | `showToolbar` | `boolean` | `true` | Set `false` to keep the toolbar, drawer, and comment pins hidden until `controller.open()` or the restore shortcut. |
611
+ | `shortcuts` | `{ comment?, browse?, design?, comments?, restore?, visibility? }` | `C`, `V`, `D`, `L`, `Shift+K`, `H` | A key with optional `Ctrl`, `Alt`, `Shift`, or `Meta`, such as `"M"` or `"Shift+J"`. Set `false` to disable one. Escape and Tab are reserved. Tooltips, tips and keyboard help show the configured keys. The `visibility` default steps aside when another configured shortcut already uses H. |
612
+ | `toolbar.items` | `ToolbarItem[]` | `browse`, `comment`, `design`, `\|`, `comments`, `\|`, `more`, `account`, `\|`, `dismiss` | Toolbar order and visibility. See [Arranging the toolbar](#arranging-the-toolbar). |
613
+ | `pins` | `{ size?: "full" \| "compact" \| "dot" \| "auto"; cluster?: boolean }` | `{ size: "full", cluster: true }` | Default pin size. A reviewer's choice in **More → Pin size** wins and is remembered per project. `cluster: false` turns off same-target stacks and collision bubbles. |
614
+ | `features` | `{ designMode?, tags? }` | All `true` | Set a feature to `false` to remove it from the review UI, e.g. `{ designMode: false, tags: false }`. Existing tags stay stored and remain available to the CLI. |
615
+ | `sidebar` | `"background" \| "edge"` | `"edge"` | `"edge"` (Floating) is a draggable sidebar that parks off and peeks from the viewport edge while closed. `"background"` (Frame) reflows the unscaled page beside the review sidebar. Account → Sidebar switches the two; that choice is remembered per project. |
616
+ | `restoreOpen` | `"session" \| "always" \| false` | `"session"` | Reopen the sidebar if it was left open: after a reload in the same tab, across tabs and visits, or never. |
617
+ | `emojiDataSource` | `string` | jsDelivr emoji data 1.8.0 | Full emoji JSON URL; fetched only after “Choose another emoji”. |
618
+ | `pollInterval` | `number` | `4000` | Refresh interval in milliseconds, minimum 2000. |
619
+ | `sessionDomain` | `string` | Host-only | Explicit trusted parent domain; all sibling hosts can receive the session. Empty string disables sharing. |
620
+ | `sessionEndpoint` | `string` | `endpoint` | Canonical API identity for trusted gateways to the same service; never share across independent APIs. |
621
+ | `avatar` | `(user: { id, name }) => string \| undefined` | None | Photo URL for signed-in reviewers the server has none for, such as IAP sign-in. HTTPS only; guests keep their initial. |
622
+ | `snapshots` | `{ image?, html?, maxBytes?, redact?(element) }` | Off | Save an image and/or trimmed HTML of the commented element with each new thread. See [Comment snapshots](#comment-snapshots). |
623
+
624
+ Google sign-in uses the account photo (`picture`) automatically, loaded from `lh3.googleusercontent.com`. Under a restrictive CSP, allow that host (or your `avatar` host) in `img-src`; blocked or failed photos fall back to the initial.
625
+
626
+ For a restricted CSP or offline deployment, host `emoji-picker-element-data@1.8.0/en/emojibase/data.json` on your site and pass `emojiDataSource: "/emoji/data.json"`. Allow that URL in `connect-src`; cache it with your service worker for first-use offline access. Quick reactions need no emoji data download. The full picker caches its data in IndexedDB after the first successful load.
627
+
628
+ The lower-level `initComments` export remains available. It requires explicit `endpoint`, `project`, `repo`, and `branch`; it does not infer scope. Existing integrations keep their branch grouping.
629
+
630
+ 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 `usePalette` hook. With Astro view transitions, destroy before swapping the document and initialize after navigation.
631
+
632
+ ### Choosing shortcuts
633
+
634
+ The single-letter defaults suit content sites that have no shortcuts of their own. Apps usually own bare letters, so prefer Shift+letter bindings, or `false`:
635
+
636
+ ```ts
637
+ shortcuts: {
638
+ comment: "Shift+C",
639
+ browse: false,
640
+ design: "Shift+D",
641
+ comments: "Shift+L",
642
+ restore: "Shift+K",
643
+ visibility: "Shift+H",
644
+ }
645
+ ```
646
+
647
+ Shortcuts never fire while focus is in a field or editable region, on the page or in Palette. Bare letters follow the keyboard layout; with a modifier, Palette also matches the physical key, so Caps Lock, other layouts, and macOS Alt characters still work. Bare letters keep working while focus is on Palette's own buttons, sidebar, and panels.
648
+
649
+ ### Arranging the toolbar
650
+
651
+ `toolbar.items` sets which controls appear in the toolbar and in what order. Omitted ids are hidden from the toolbar; `"|"` draws a separator.
652
+
653
+ ```ts
654
+ toolbar: {
655
+ items: ["comment", "browse", "|", "comments", "|", "more"],
656
+ }
657
+ ```
658
+
659
+ Ids are `browse`, `comment`, `design`, `comments` (All comments, with an open-thread badge), `more`, `account` and `dismiss`. Every layout, including vertical edges, the open sidebar and mobile, follows the same order, and Tab and arrow keys move through it in that order.
660
+
661
+ - Ids for disabled features, such as `design` with `features.designMode: false`, are skipped. Leading, trailing and doubled separators collapse.
662
+ - Unknown or repeated ids are ignored with one console warning.
663
+ - Hidden controls stay in More. Without `account`, reviewers are still asked for a name before their first comment. Without `dismiss`, the restore shortcut still works. Hiding `more` also hides the tools that live only there, such as copying comments for an agent.
664
+ - An empty list hides the toolbar; shortcuts keep working.
665
+ - First-run setup always shows its own account and project controls.
666
+
667
+ ### Host events
668
+
669
+ Palette renders in a shadow root, so page listeners see its events as coming from Palette's host `<div>`. By default Palette stops them at that element:
670
+
671
+ - **Typing.** `keydown`, `keyup` and `keypress` from Palette'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 Palette's buttons still reach the page.
672
+ - **Modals.** `pointerdown`, `mousedown`, `touchstart`, `touchmove`, `touchend`, `click`, `wheel`, `focusin` and `focusout` from Palette's UI don't reach page listeners. An open Radix or similar dialog doesn't treat Palette as an outside interaction, so it doesn't close, take focus back from the composer, or block the sidebar's scroll.
673
+ - **Focus traps.** When focus moves from the page into Palette, Palette stops that `focusout` for `document` listeners added after Palette mounted, such as Radix's FocusScope.
674
+ - **Escape.** Escape from feedback fields and controls is marked handled in the host's own window before document capture listeners run. Feedback still receives it to close its composer or menu; Radix's document handler leaves the host dialog open. This also works when feedback runs inside an iframe.
675
+
676
+ Other capture-phase listeners still run. Set `isolateEvents: false` if your app needs to observe feedback events or handle Escape itself.
677
+
678
+ ### Comment snapshots
679
+
680
+ Snapshots are off by default. Turn on each kind you want:
681
+
682
+ ```ts
683
+ snapshots: {
684
+ image: true,
685
+ html: true,
686
+ maxBytes: 250_000,
687
+ redact: (element) => element.matches(".account-balance"),
688
+ }
689
+ ```
690
+
691
+ When a signed-in reviewer starts a comment, Palette captures the commented element in the page, in Frame's viewport iframe when it shows the page. Capture needs a server that supports [element snapshots](#element-snapshots); guests capture only when the project allows guest snapshots. The capture code loads on demand. Each kind uploads as its own file once the thread is saved and never holds up the comment: if an upload fails, the comment still posts and Palette logs a note to the console.
692
+
693
+ - **`image`** is a WebP image of the element with 16px of padding, at most 1200px wide and `maxBytes` in size (250,000 by default). Quality steps down from 0.8, then the size. Palette draws the element itself, with no extension or third-party library: it copies the element with its computed styles into an SVG `foreignObject` and draws that to a canvas, after `document.fonts.ready`. An image from another origin without CORS would taint the canvas, so Palette saves the HTML instead, starting with a comment that says why.
694
+ - **`html`** is the element's `outerHTML`, trimmed to about 20 KB. Scripts, styles and inline event handlers are removed, and long attribute values and text are cut short.
695
+ - **Redaction.** Input, textarea and password values never appear in either snapshot. `redact(element)` returning true replaces that element's text with `[redacted]`.
696
+ - **Privacy.** Palette's own UI is never captured. Add `data-komo-no-snapshot` to any element to keep it out: a comment inside it captures nothing, and inside a captured element it shows as an empty box.
697
+
698
+ #### Viewing and removing snapshots
699
+
700
+ Threads with a snapshot show a small thumbnail on their list row and in the open comment; Enter or a click opens it full size. Thumbnails load only when their row or comment is on screen, with the reviewer's session, and Palette reads at most 20 snapshots a minute so the server's per-reviewer request limit stays free for comments. A thread with only HTML shows an **HTML snapshot** button that opens the markup as text.
701
+
702
+ When a comment's element isn't on the page any more, its card says **Element not found on this page**, shows the snapshot and the route the comment was left on, which opens the page again. Without an image it shows the HTML as plain text, never as markup, with the note explaining why the image is missing.
703
+
704
+ While composing, the captured snapshot appears under the comment box. **Remove snapshot** drops it before posting. After posting, the comment's author or any signed-in reviewer can choose **Remove snapshot** from the comment's actions menu; Palette asks to confirm, removes it at once and puts it back if the server refuses. Agents read snapshots with `palette comments snapshots THREAD_ID` (see [Agent CLI](#agent-cli)), and `palette comments prompt` names threads that have one. Behind a sign-in proxy, agents read them with a session from `POST /auth/proxy/session`.
705
+
706
+ #### Privacy
707
+
708
+ - **Opt-in.** Nothing is captured unless `snapshots.image` or `snapshots.html` is set, and the server supports snapshots.
709
+ - **What is captured.** The commented element only, as you saw it: an image (WebP at quality 0.8 by default, stepping down to fit `maxBytes`) and its trimmed HTML. Palette's own UI, scripts, styles and event handlers are never included.
710
+ - **What is redacted.** Input, textarea and password values are always replaced with `[redacted]`. `redact(element)` redacts any other element's text. Anything inside `data-komo-no-snapshot` is left out, and a comment inside such a region captures nothing.
711
+ - **Who can see it.** Snapshots are never public: reading one needs a reviewer's credentials for the project (a session, or an access token where the server allows them), and HTML is served as plain text.
712
+ - **Guests.** Guest sessions don't capture or upload unless the project sets `allowGuestSnapshots`.
713
+ - **Retention.** A snapshot lives as long as its thread and is deleted with it: when resolved threads are cleared, by retention, or with the project. Exports include snapshots.
714
+ - **Deleting.** A signed-in reviewer, or the comment's author, can remove it from the comment's actions menu, or with `DELETE /threads/:id/snapshot/:kind`. Guest authors need `allowGuestSnapshots`.
715
+
716
+ Limits: the image shows the element at the top of its scroll position and up to 2,000px tall. It leaves out iframes, shadow DOM contents, `<use>` references to sprites outside the element, and web fonts served from another origin without CORS, which fall back to a local font. A target with more than 3,000 elements gets no image.
717
+
718
+ ### CLI
719
+
720
+ | Command / flag | Purpose |
721
+ | --- | --- |
722
+ | `palette init` | Create a Google-owned hosted workspace. |
723
+ | `palette init --self-host` | Create your Worker and D1 database. |
724
+ | `palette sync` | Regenerate the public client helper. Run before dev/build. |
725
+ | `palette deploy` | Resume or redeploy generated self-host infrastructure. |
726
+ | `--origin URL` | Optional website address. Otherwise detected from Cloudflare Pages, Vercel, or Netlify; defaults to localhost. |
727
+ | `--endpoint URL` | Override hosted API or self-host deployment URL. |
728
+ | `--repo OWNER/REPO` | Override Git detection. |
729
+ | `--branch-scope` | Opt into separate comments per branch. |
730
+ | `--google-client-id ID` | Self-hosted Google OAuth client ID. |
731
+ | `KOMO_BRANCH` | Explicit build-time branch override. |
732
+
733
+ Branch detection also supports `CF_PAGES_BRANCH`, `WORKERS_CI_BRANCH`, `VERCEL_GIT_COMMIT_REF`, `GITHUB_HEAD_REF`, `GITHUB_REF_NAME`, and `BRANCH`, followed by the Git checkout.
734
+
735
+ ### Backend
736
+
737
+ `PROJECTS` is a JSON object keyed by public project ID. Each entry supports:
738
+
739
+ | Option | Default | Purpose |
740
+ | --- | --- | --- |
741
+ | `repo` | Required | Repository allowed for the project. |
742
+ | `origins` | Required | Exact allowed origins; self-host supports single-label wildcards. |
743
+ | `allowGuests` | `true` | Enable named guest sessions. |
744
+ | `allowGuestResolve` | `true` | Allow guests to resolve threads and switch any thread between all pages and its own page. Authors can always switch their own. |
745
+ | `requireOwner` | `true` in generated setups | Block reviewing until Google ownership is claimed. Legacy static projects remain compatible. |
746
+ | `bootstrapHash` | Generated | SHA-256 owner-claim key hash. |
747
+ | `suspended` | `false` | Disable a project. |
748
+ | `writesPerDay` | `10000` self-hosted | Daily write-request allowance; hosted uses 500. |
749
+ | `allowGuestSnapshots` | `false` | Let guest sessions attach element snapshots to their own threads. Has no effect when the server turns guests off (`KOMO_ALLOW_GUESTS=false`). |
750
+ | `maxSnapshotBytes` | `52428800` (50 MiB) | Total snapshot bytes stored per project; hosted uses 5 MiB. |
751
+
752
+ Worker bindings: `DB` (D1), optional `EDGE_LIMIT` (rate limiter). Variables: `PROJECTS`, `GOOGLE_CLIENT_ID`, optional `GITHUB_CLIENT_ID`, `KOMO_HOSTED="true"` to enable hosted provisioning, and `KOMO_PAUSED="true"` to pause service requests. Store `GOOGLE_CLIENT_SECRET` and optional `GITHUB_CLIENT_SECRET` as Worker secrets. Hosted configuration uses an hourly cleanup trigger. `/health` remains available while paused.
753
+
754
+ ### Element snapshots
755
+
756
+ A comment can carry a snapshot of its target element: one image (`image/webp` or `image/png`, up to 256 KiB) and one trimmed HTML capture (`text/html`, UTF-8, up to 32 KiB) per thread. Capture is opt-in on the client. **Snapshots may contain page content and user data**, so treat them like the comments they belong to.
757
+
758
+ - `POST /threads/:id/snapshot?kind=image|html` takes the raw file with its `Content-Type`, or `multipart/form-data` with `kind` and `file` fields. A new upload replaces the thread's previous snapshot of that kind. Uploads need a session with write access to the project; guests are refused unless the project sets `allowGuestSnapshots`, and then only on their own threads. The thread's author or a signed-in reviewer can replace or `DELETE /threads/:id/snapshot/:kind`.
759
+ - `GET /threads/:id/snapshot/:kind` needs a session, even on link-access projects, and returns `Cache-Control: private`, `X-Content-Type-Options: nosniff` and `Content-Security-Policy: sandbox`. HTML is always served as `text/plain`; show it to agents or in a sandboxed viewer, never as live markup.
760
+ - Thread listings add `snapshot: { image?, html? }` URLs when a thread has snapshots. `/config` reports `snapshots: { guests, maxImageBytes, maxHtmlBytes }`.
761
+
762
+ Snapshots are deleted with their thread, by clearing resolved threads, retention, or project deletion. Exports include them, base64-encoded. Their bytes count toward database quotas, and each project stores at most `maxSnapshotBytes` of snapshots. Uploads share the write rate limits, plus 60 per reviewer every ten minutes.
763
+
764
+ ## Source context
765
+
766
+ Stable attributes make annotations resilient to layout changes:
767
+
768
+ ```html
769
+ <section data-comment-anchor="pricing" data-comment-source="src/components/Pricing.tsx">
770
+ ...
771
+ </section>
772
+ ```
773
+
774
+ Anchors may include bounded `context` strings for element tag, role, accessible label, nearby and selected text, CSS classes, styles, and DOM scope. Browser and CLI prompts quote these capture-time hints; agents must verify them against the current implementation. Older anchors remain supported.
775
+
776
+ If an element disappears, the original page position remains available. Palette cannot inspect closed shadow roots, canvas internals, or cross-origin iframe content. It does not invent source line numbers.
777
+
778
+ Choose **Copy all comments for agent** to copy open feedback across the configured scope; the sidebar copy button includes open comments on its current page. Resolved threads and deleted messages are excluded. Copying does not send anything to an agent.
779
+
780
+ ## Development
781
+
782
+ Requires Node.js 22+, a modern browser, and Cloudflare access for deployment.
783
+
784
+ ```sh
785
+ pnpm --filter @hazem3500/palette typecheck
786
+ pnpm --filter @hazem3500/palette test
787
+ pnpm --filter @hazem3500/palette build
788
+ ```
789
+
790
+ Tests use real workerd and isolated SQLite databases. To publish this repository’s package artifact and website review preview, run `pnpm run comments:preview` from a feature branch. It does not deploy the marketing Worker or replace the existing review database.
791
+
792
+ ## Credits
793
+
794
+ The dock adapts [Danny Williams’s morphing menu](https://dannyjpwilliams.com/playground/morphing-menu/). Icons come from [Untitled UI](https://github.com/untitleduico/icons). See [NOTICE.md](./NOTICE.md) for attribution.
795
+
796
+ ## License
797
+
798
+ MIT.
799
+
800
+ ## Development
801
+
802
+ ```sh
803
+ pnpm install
804
+ pnpm build
805
+ pnpm typecheck
806
+ pnpm test
807
+ pnpm size
808
+ pnpm dev
809
+ ```
810
+
811
+ The npm package, CLI and API live in `packages/komo`; the website lives in `packages/komo-site`. `pnpm dev:api` starts a local API. Production deployments retain the existing hosted database; local development uses a separate database.
812
+
813
+ ## Source
814
+
815
+ Palette is a fork of [tjcages/komo](https://github.com/tjcages/komo), which was extracted from the komo product at source commit `f57f3521` in the Cloudflare marketing workspace. Upstream development continues in tjcages/komo. Third-party credits are preserved in each package’s NOTICE.md.
816
+
817
+
818
+ ## Project management
819
+
820
+ Sign in as the Google owner with `npx @hazem3500/palette login`. In the widget, open **Account → Project settings** to manage access, download feedback, clear resolved threads, or delete a hosted project.
821
+
822
+ Self-hosted Node deployments can also send signed webhooks for new comments, replies, and resolved threads. In **Project settings → Comment webhooks**, enter an HTTPS endpoint and a signing secret of at least 32 characters. Palette encrypts the secret at rest using the `KOMO_WEBHOOK_KEYS` environment variable; generate a 32-byte key with `openssl rand -base64 32` and configure a JSON keyring such as `{"current":"2026-10","keys":{"2026-10":"BASE64_KEY"}}`. Keep the keyring in your secret manager. Cloudflare Worker deployments currently disable webhook delivery because they do not yet have the DNS-pinning egress transport used by the Node adapter.
823
+
824
+ Each request includes `X-Komo-Event`, `X-Komo-Delivery`, and `X-Komo-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>`. Verify the HMAC over `<timestamp>.<raw-request-body>` and reject timestamps outside your replay window. Treat `X-Komo-Delivery` as the idempotency key. Palette sends once without retries, does not follow redirects, and never logs secrets or response bodies. Master-key rotation requires replacing each project's signing secret and updating its receiver before removing the old key from the keyring.
825
+
826
+ ```sh
827
+ npx @hazem3500/palette project info
828
+ npx @hazem3500/palette project access --access private
829
+ npx @hazem3500/palette project invite --email teammate@example.com
830
+ npx @hazem3500/palette project export --out comments.json
831
+ ```
832
+
833
+ On localhost, the widget defaults to a separate `local` channel in the **configured API’s database**. It is not a browser-only database or a private workspace per developer. Project permissions still apply; use a separate project/API for isolated development data. Switching to Shared selects the configured shared branch. `pnpm dev:api` is different: it starts a local API with its own local database.
834
+
835
+ Projects default to link access. Private projects require Google sign-in and owner-approved membership for reads and writes, including the CLI. Invitations match a verified email address, expire after seven days, and are single-use. Members can review; only owners manage access, export, import, or delete. Google accounts used before this release should sign out and back in to verify their email. Repository access is separate.
836
+
837
+ ### Quota recovery and migration
838
+
839
+ Export first, then permanently clear resolved threads with `npx @hazem3500/palette project clear-resolved --confirm PROJECT_KEY`. This reclaims stored-comment quota. In the sidebar’s Resolved view, owners can also use the trash button to permanently delete only the currently filtered threads and their replies after confirmation. `project delete --confirm PROJECT_KEY` permanently deletes a hosted project and frees its project slot. Both are owner-only. The account panel also offers these actions with typed confirmation.
840
+
841
+ To migrate, export from your source, set up a destination using `npx @hazem3500/palette init --self-host` in another directory, then sign in there and run:
842
+
843
+ ```sh
844
+ npx @hazem3500/palette project import --file /path/to/comments.json
845
+ ```
846
+
847
+ Exports include threads, replies, reactions, element snapshots, anchors, and historical profiles across all pages and branches, including resolved feedback. They exclude sessions, credentials, verified emails, and membership. Imports use the destination repository and keep imported authors unverified. Retrying the same export is safe; existing imported records are not overwritten. Destination quotas still apply. Export retries are required if feedback changes while downloading. Keep export files private.
848
+
849
+ Upgrade self-hosted deployments with `npm install @hazem3500/palette@latest` then `npx @hazem3500/palette deploy`; the CLI applies bundled database migrations before redeploying. Self-hosted project removal is controlled by your Worker configuration. Account-wide data requests remain available at ty@offbr.co.
850
+
851
+ Without `pageRoot`, a sole existing content element with a layout box enables Frame. Script and style elements do not count as content roots. Multiple roots or `display: contents` use Floating; pass a suitable existing app container for Frame.
852
+
853
+ The `@hazem3500/palette/server` export loads built ESM in Node and includes Worker handler types. Wrangler selects the original TypeScript through the `workerd` condition; keep `nodejs_compat` enabled. Package metadata is available at `@hazem3500/palette/package.json`.
854
+
855
+ ### Experimental page-element resize and move
856
+
857
+ Both are **off by default**. Opt in explicitly in the public configuration:
858
+
859
+ ```ts
860
+ initPalette({
861
+ // ...your existing endpoint/project/repo/branch options
862
+ design: { experimentalGeometry: true },
863
+ });
864
+ ```
865
+
866
+ This loads geometry tools lazily in Design mode. A supported selection gets a crisp 1px outline on its border box and design-tool handles: four corners and four edge midpoints, 7px white squares with a 1px border in the selection color, centred on the outline and aligned to device pixels. Each sits in a 12px hit area that reaches outward, so the body stays draggable and double-click still edits text. Hovering a handle fills it, and each handle has its resize cursor. Only applicable handles appear: aspect-locked boxes get corners only, and an axis pinned by equal `min-`/`max-` sizes has no handles. Sides under 40px drop their midpoint squares, and boxes under 24px draw no squares at all; their edges and corners still drag from just outside. Handles, outline and the size chip keep their on-screen size at any Frame zoom.
867
+
868
+ Handles show only when the pointer is on or near the selection, while resizing or moving, or when the selection has keyboard focus; otherwise only the outline shows. They fade out (120ms, none with reduced motion) and leave a faint outline while the pointer is over the Design panel, while a field is focused or scrubbed, and while a select or color picker is open. During a select's hover preview the outline goes too. Before (B) hides the selection.
869
+
870
+ **Hide the overlays.** In Design mode the comments visibility shortcut (**H** by default, `shortcuts.visibility`) hides every Design overlay: the selection, handles, hover outline, pins and move feedback. A tap toggles them and holding the key peeks until it's released; letting go of any key in the combo, or the window losing focus, ends the peek. The eye button in the inspector header (**Hide selection** / **Show selection**) does the same. Shortcut help lists the key.
871
+
872
+ **Selection color.** Outlines, handles, the size chip, the hover outline and move feedback use a canvas selection color, separate from Palette's panel accent. It defaults to `#0c93f5`, a design-tool blue with 3:1 contrast on white and near-black. On a background close to it, the selection turns magenta. Set it with `design: { selectionColor: "#7c3aed" }` (with experimental geometry), or in any case from CSS with the `--komo-selection` custom property on Palette's host element (`[data-branch-comments]`).
873
+
874
+ - **Drag** any handle. A W × H chip shows below the selection. The moving edge snaps to the parent's content edges and centre and to siblings' edges across the flow within 4px, with a thin guide; hold **Alt/Option** to resize freely. **Shift** on a corner keeps the box's proportions. Escape cancels.
875
+ - **Keyboard:** press **M** on the canvas (or Tab) to focus the selection, then **Ctrl+arrows** (or **⌘+arrows** on macOS, where Ctrl+arrows switch spaces) to resize from the bottom-right by one local CSS pixel, Shift for ten; snapped grid items step one track. Escape returns to the canvas, where plain arrows and **Alt+arrows** keep their Design meaning (sibling and parent/child selection). Each step is announced politely, for example "Resized: width 241px".
876
+ - Minimum dimensions are 16 CSS pixels, or the border and padding footprint when larger. Each completed gesture is one chronological Undo. The panel's Layout fields (Width, Height and the rest) show the new values as soon as a gesture commits, and again after Undo, Reset and Before/After. A field edit after a resize joins the same history; the proposal then reports the field's net value instead of the superseded resize.
877
+
878
+ **Move** changes an element's place among other elements; it is never free-form:
879
+
880
+ - **Drag** the selection itself (the cursor shows `move` over it). A press only becomes a drag after 4px, so a click still selects and a double-click still edits text. While dragging, an insertion line sits between siblings along the parent's flow (stacked or side by side, including flex rows, columns and grid order), a dashed outline marks a container you would drop *into*, and a chip names the spot, for example "After Button “Month”" or "Into div#panel · position 2". The nearest valid target wins: never into text runs, `display: contents` wrappers, form or replaced elements, Palette's own UI, or the element itself. Escape cancels.
881
+ - Until the first move, the Layout section shows a short hint: "Drag to reorder · M, then ⌥↑/⌥↓".
882
+ - **Keyboard**, on the focused selection: **Alt/⌥+↑** moves before the previous sibling and **Alt/⌥+↓** after the next one (← and → work too); **Alt/⌥+Shift+↑** moves out, after its parent; **Alt/⌥+Shift+↓** moves into the previous sibling as its last child. Each move is announced, for example "Moved before List item “One”". **⌘Z / Ctrl+Z** (or the panel's Undo) undoes moves one at a time, from the focused selection or the canvas; Design mode has no redo.
883
+ - **Preview without moving nodes:** the page's renderer keeps owning its DOM. Palette hides the original in place (`display: none`) so the layout reflows, and inserts an inert, `aria-hidden` stand-in with the original's computed styles inlined at the destination. The stand-in follows renderer commits (text, style and sibling changes) and is removed on Before, Undo, Reset, leaving Design mode and destroy, which restore the original exactly.
884
+ - **Limits**, each with its reason and **Select parent** in the panel: the page and app roots (`html`, `body` and its direct children), SVG shapes, table sections, and elements inside inline text runs. Table rows only move within their section and cells within a row of their table.
885
+
886
+ These are reversible runtime previews, not source or backend edits. Nothing in the page is reparented or reordered. Undo returns to the previous preview; Reset restores the host baseline, including the exact original `style` attribute. Before/After, Browse suspension and Comment transfer use the existing Design proposal lifecycle. Handles disappear while the clean editing gate is active; target the canvas again to show them. The flag does not resize comment pins or the page-preview Frame.
887
+
888
+ How each layout previews:
889
+
890
+ - **Resize** sets `width`/`height`. On a flex item's main axis it also sets `flex-basis` when the basis isn't `auto`, `flex-grow: 0` when it grows, and `flex-shrink: 0` only when the box would otherwise shrink. Cross-axis and grid sizes override `align-self`/`justify-self: stretch`. Min/max limits clamp the preview and say so. Layout decides which edge moves: a left or top handle grows the box too, and normal flow keeps its start edge.
891
+ - **Grid items** resize within their cell. **Snap to grid tracks** (in the panel's Layout section) changes `grid-column-end`/`grid-row-end` to numeric `span N` values instead.
892
+ - **Images, video, canvas, iframes, SVG roots and `aspect-ratio` boxes** resize with their aspect ratio locked.
893
+
894
+ Side effects show as a quiet note in the Design panel's Layout section and in the proposal, for example `width overrides flex: 1 1 0%` or `sets flex-basis: 240px`. Comment proposals add a `Proposed geometry changes` section after the CSS rows:
895
+
896
+ ```text
897
+ Proposed geometry changes (preview only)
898
+ Target: #save
899
+ Layout: flex item, row
900
+ - Resize (flex-basis) width: 260px → 240px · width overrides flex: 1 1 0% · sets flex-basis: 240px · sets flex-grow: 0
901
+ ```
902
+
903
+ A move adds a human line and the places agents need, as selectors and indexes among element children (counted from 0, without the element):
904
+
905
+ ```text
906
+ - Move Button “Invite”: from Toolbar “Views” (3rd) → Sidebar (1st)
907
+ From: #toolbar · index 2
908
+ To: #side > nav · index 0 · inside · component SideNav
909
+ ```
910
+
911
+ The hidden original's `display` is not proposed as a style change. The CLI reads these lines into `geometry` DesignEdit changes with `mode` (`size`, `flex-basis` or `span`), the values, `layout` and `effects`, and moves into `move` changes (`target`, `label`, `from` and `to` with `parent`, `index`, `position` and any `component`), telling agents to move the element's markup in source. Earlier proposals with `Move (translate)` or `Move (offset)` lines still parse and render as `translate`/`offset` changes; the current UI no longer produces free-form moves.
912
+
913
+ Supported targets are HTML boxes and SVG roots in normal flow, flex or grid, or relatively, absolutely, fixed or sticky positioned, through positive axis-aligned 2D scale chains and normal, reflow or explicitly scaled Frames. Pointer deltas are converted to local CSS pixels, and scroll during pointer capture is accounted for. Only boxes that can't be previewed are excluded, each with its own reason in the panel and a **Select parent** action that picks the nearest resizable ancestor: inline text boxes, SVG shapes inside an SVG, `display: contents` or unrendered elements, table cells and rows, boxes split across lines or columns, rotated/skewed/3D/mirrored chains, and geometry animations in progress. A running animation only counts when it moves or sizes the box and Palette did not start it; it is named after 400ms, as a dot by the Layout title, so brief host transitions never show and the panel never reflows. This is deliberately not a universal layout editor.