@pixelmatters/markup 1.3.11 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,31 +35,34 @@ CDN drop-in (no build step) — paste this just before `</body>`:
35
35
  ```html
36
36
  <script type="module">
37
37
  // Pin the exact version — esm.sh resolves it from npm
38
- import { init } from 'https://esm.sh/@pixelmatters/markup@1.3.11'
38
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.4.0'
39
39
  // or
40
- // import { init } from 'https://esm.run/@pixelmatters/markup@1.3.11'
40
+ // import { init } from 'https://esm.run/@pixelmatters/markup@1.4.0'
41
41
 
42
42
  init({
43
43
  apiUrl: 'https://your-deployment.convex.site',
44
44
  apiKey: 'markup_...',
45
- position: 'bottom-right', // optional
46
- theme: 'auto', // optional
45
+ position: 'bottom-right', // optional: 'bottom-right' | 'bottom-left'
46
+ theme: 'auto', // optional: 'auto' | 'light' | 'dark'
47
+ fab: 'default', // optional: 'default' | 'icon-only'
47
48
  })
48
49
  </script>
49
50
  ```
50
51
 
51
- > **Why pin the version?** CDN URLs without a version (`@pixelmatters/markup`) resolve to whatever's `latest` on npm — a future major release will break your page silently. Always pin (`@pixelmatters/markup@1.3.11`).
52
+ > **Why pin the version?** CDN URLs without a version (`@pixelmatters/markup`) resolve to whatever's `latest` on npm — a future major release will break your page silently. Always pin (`@pixelmatters/markup@1.4.0`).
52
53
 
53
54
  If your platform doesn't allow inline JS (some CMS / page-builder editors), use the auto-init form instead — point a `<script src=…>` at the bundle and pass config via `data-*` attributes:
54
55
 
55
56
  ```html
56
57
  <script
57
58
  type="module"
58
- src="https://esm.sh/@pixelmatters/markup@1.3.11"
59
+ src="https://esm.sh/@pixelmatters/markup@1.4.0"
59
60
  data-markup-widget="true"
60
61
  data-api-url="https://your-deployment.convex.site"
61
62
  data-api-key="markup_..."
62
63
  data-position="bottom-right"
64
+ data-theme="auto"
65
+ data-fab="default"
63
66
  ></script>
64
67
  ```
65
68
 
@@ -75,8 +78,9 @@ import { init, destroy } from '@pixelmatters/markup'
75
78
  const stop = init({
76
79
  apiUrl: 'https://your-deployment.convex.site',
77
80
  apiKey: 'markup_...',
78
- position: 'bottom-right', // optional
79
- theme: 'light', // optional
81
+ position: 'bottom-right', // optional: 'bottom-right' | 'bottom-left'
82
+ theme: 'light', // optional: 'auto' | 'light' | 'dark'
83
+ fab: 'default', // optional: 'default' | 'icon-only'
80
84
  })
81
85
 
82
86
  // Tear down on logout / route change / page unload:
@@ -94,8 +98,9 @@ export default function App() {
94
98
  return init({
95
99
  apiUrl: import.meta.env.VITE_MARKUP_API_URL,
96
100
  apiKey: import.meta.env.VITE_MARKUP_API_KEY,
97
- position: 'bottom-right', // optional
98
- theme: 'auto', // optional
101
+ position: 'bottom-right', // optional: 'bottom-right' | 'bottom-left'
102
+ theme: 'auto', // optional: 'auto' | 'light' | 'dark'
103
+ fab: 'default', // optional: 'default' | 'icon-only'
99
104
  })
100
105
  }, [])
101
106
 
@@ -115,6 +120,9 @@ onMounted(() => {
115
120
  stop = init({
116
121
  apiUrl: import.meta.env.VITE_MARKUP_API_URL,
117
122
  apiKey: import.meta.env.VITE_MARKUP_API_KEY,
123
+ position: 'bottom-right', // optional: 'bottom-right' | 'bottom-left'
124
+ theme: 'auto', // optional: 'auto' | 'light' | 'dark'
125
+ fab: 'default', // optional: 'default' | 'icon-only'
118
126
  })
119
127
  })
120
128
  onBeforeUnmount(() => stop?.())
@@ -132,6 +140,9 @@ export default function App() {
132
140
  const stop = init({
133
141
  apiUrl: import.meta.env.VITE_MARKUP_API_URL,
134
142
  apiKey: import.meta.env.VITE_MARKUP_API_KEY,
143
+ position: 'bottom-right', // optional: 'bottom-right' | 'bottom-left'
144
+ theme: 'auto', // optional: 'auto' | 'light' | 'dark'
145
+ fab: 'default', // optional: 'default' | 'icon-only'
135
146
  })
136
147
  onCleanup(stop)
137
148
  })
@@ -152,6 +163,7 @@ Mounts the widget. Always tears down any existing instance before mounting, so c
152
163
  | `apiKey` | `string` | required | Project API key — mint one in the dashboard |
153
164
  | `position` | `'bottom-right' \| 'bottom-left'` | `'bottom-right'` | Initial corner for the floating action button — users can drag it across the viewport midline to flip corners at runtime |
154
165
  | `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Visual theme — `'light'` or `'dark'`, or `'auto'` (default) to follow the host's `prefers-color-scheme` |
166
+ | `fab` | `'default' \| 'icon-only'` | `'default'` | Floating button variant — `'icon-only'` drops the "Markup" label for a circular icon button |
155
167
 
156
168
  ### `destroy()`
157
169
 
@@ -172,16 +184,16 @@ Unmounts the widget and removes the host element. Safe to call when nothing is m
172
184
  - All UI lives in that shadow root, with `:host { all: initial }` blocking style inheritance.
173
185
  - The host element is `position: fixed; inset: 0; pointer-events: none`, so the widget paints over the entire viewport without blocking the host's clicks; only the FAB and active popovers opt back in to pointer events.
174
186
  - Pins are anchored as `(x, y)` fractions of the document plus a best-effort CSS selector (via [`@medv/finder`](https://github.com/antonmedv/finder)). The selector wins when it still resolves; the fraction is the fallback so pins survive layout changes.
175
- - Identity lives in **host-page** `localStorage` under `markup.identity`, keyed to the top-level site. New visitors get a random `clientId` so the backend can group their comments across sessions.
187
+ - Identity lives in **host-page** `localStorage` under `markup.identity`, keyed to the top-level site. On first load the widget mints a server-signed anonymous JWT via `POST /widget/anon-identity` so the backend can verify the `authorClientId` on every anon write — tampering with the cached `clientId` invalidates the signature. Verified identities upgrade to a `Bearer` JWT via the popup flow described below.
176
188
 
177
189
  ## Identity
178
190
 
179
191
  Anonymous by default, with two opt-in upgrade paths:
180
192
 
181
- - **Sign in with Markup** — opens a popup to `${apiUrl}/widget/auth`. Because the popup is first-party to the deployment origin, the better-auth session cookie is sent normally (sidestepping third-party cookie blocks). The popup `postMessage`s a JWT-backed verified identity back to the host page, which persists it under `markup.identity`.
182
- - **Continue as a guest** — name (and optional email), stored under the same key without `isVerified`.
193
+ - **Sign in with Markup** — the widget POSTs to `/widget/popup-exchange` with the apiKey + Origin gate, gets back a single-use 60-second code, then opens `${apiUrl}/widget/auth?code=…` in a popup. Because the popup is first-party to the deployment origin, the better-auth session cookie is sent normally (sidestepping third-party cookie blocks). The popup `postMessage`s a verified identity plus a 7-day server-signed `Bearer` JWT back to the host page, which persists them under `markup.identity` and attaches the JWT as `Authorization: Bearer` on subsequent writes. The apiKey never appears in the popup URL.
194
+ - **Continue as a guest** — name (and optional email), stored alongside the server-issued anonymous `clientId` + JWT minted on first load.
183
195
 
184
- The popup origin is validated against the project's `allowedDomains` before any identity is returned, so only embeds on approved domains can resolve dashboard sessions.
196
+ The popup origin is validated against the project's `allowedDomains` before any identity is returned, so only embeds on approved domains can resolve dashboard sessions. Verified JWTs can be invalidated before their TTL via the dashboard's "Sign out everywhere" action.
185
197
 
186
198
  > **Why a popup, not auto-detect?** Safari ITP, Chrome Storage Partitioning, and Firefox TCP all partition third-party storage and cookies by top-level site. A cross-origin fetch from `customer.com` to `convex.site` cannot see the dashboard session. The popup is the only reliable way to bridge identity across sites without per-host configuration.
187
199
 
@@ -219,6 +231,7 @@ init({
219
231
  apiKey: string, // required
220
232
  position?: 'bottom-right' | 'bottom-left', // default 'bottom-right'
221
233
  theme?: 'light' | 'dark' | 'auto', // default 'auto'
234
+ fab?: 'default' | 'icon-only', // default 'default'
222
235
  }) // returns a destroy() function — call it on unmount / logout / route teardown
223
236
  ```
224
237
 
@@ -231,13 +244,14 @@ For a `<script>` tag drop-in (no bundler), use the inline ESM form and **pin the
231
244
 
232
245
  ```html
233
246
  <script type="module">
234
- import { init } from 'https://esm.sh/@pixelmatters/markup@1.3.11'
247
+ import { init } from 'https://esm.sh/@pixelmatters/markup@1.4.0'
235
248
 
236
249
  init({
237
250
  apiUrl: '...',
238
251
  apiKey: '...',
239
252
  position: 'bottom-right',
240
253
  theme: 'auto',
254
+ fab: 'default',
241
255
  })
242
256
  </script>
243
257
  ```
@@ -247,11 +261,12 @@ If inline JS is disallowed (some CMS / page-builder editors), use the auto-init
247
261
  ```html
248
262
  <script
249
263
  type="module"
250
- src="https://esm.sh/@pixelmatters/markup@1.3.11"
264
+ src="https://esm.sh/@pixelmatters/markup@1.4.0"
251
265
  data-markup-widget="true"
252
266
  data-api-url="..."
253
267
  data-api-key="..."
254
268
  data-position="bottom-right"
269
+ data-fab="default"
255
270
  ></script>
256
271
  ```
257
272
 
package/dist/widget.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type WidgetTheme = 'light' | 'dark' | 'auto';
2
+ export type WidgetFab = 'default' | 'icon-only';
2
3
  export interface WidgetConfig {
3
4
  /** Convex deployment site URL, e.g. `https://your-deployment.convex.site` */
4
5
  apiUrl: string;
@@ -13,6 +14,14 @@ export interface WidgetConfig {
13
14
  * - `'auto'` (default) — follow the host's `prefers-color-scheme`
14
15
  */
15
16
  theme?: WidgetTheme;
17
+ /**
18
+ * Floating action button variant.
19
+ * - `'default'` — pill button with icon + "Markup" label
20
+ * - `'icon-only'` — circular icon-only button (no label)
21
+ *
22
+ * Defaults to `'default'`.
23
+ */
24
+ fab?: WidgetFab;
16
25
  }
17
26
  /**
18
27
  * Mounts the feedback widget on the page. Idempotent — calling init again