@joewinke/jatui 0.2.5 → 0.2.6

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/CLAUDE.md CHANGED
@@ -106,9 +106,9 @@ Then import:
106
106
  |-----------|---------|-----------|
107
107
  | `MarkupEditor` | Fullscreen canvas markup tool — draw arrows, boxes, ellipses, revision clouds, freehand, text over an image | `imageUrl`, `initialShapes?: MarkupShape[]`, `onsave: (shapes) => void`, `oncancel: () => void` |
108
108
  | `MarkupOverlay` | Read-only canvas layer that renders saved `MarkupShape[]` over an image | `shapes: MarkupShape[]`, `imageUrl` |
109
- | `ImageLightbox` | Inline image viewer with fullscreen modal, zoom controls, optional markup + annotation overlays | `imageUrl`, `filename?`, `markupShapes?`, `annotations?`, `activeAnnotationId?`, `annotationMode?`, `onAnnotationClick?`, `onImageClick?`, `onClose?` |
109
+ | `ImageLightbox` | Inline image viewer with fullscreen modal, zoom controls, optional markup + annotation overlays | `imageUrl`, `filename?`, `markupShapes?`, `annotations?`, `activeAnnotationId?`, `annotationMode?`, `showResolved?` (forwarded to the internal `AnnotationLayer`; default false), `onAnnotationClick?`, `onImageClick?`, `onClose?` |
110
110
  | `AnnotationLayer` | Absolute-positioned pin overlay — renders threaded annotation pins at `x_percent`/`y_percent` coordinates | `annotations: Annotation[]`, `activeAnnotationId?`, `annotationMode?`, `showResolved?`, `onAnnotationClick?`, `onImageClick?` |
111
- | `AnnotationPanel` | Sidebar thread list for annotations — reply, resolve, edit, delete. Uses callback props for async ops. | `annotations`, `profiles`, `activeAnnotationId?`, `currentUserId`, `callbacks: AnnotationCallbacks`, `onAnnotationSelect?`, `onAnnotationsChanged?` |
111
+ | `AnnotationPanel` | Sidebar thread list for annotations — reply, resolve, edit, delete. Uses callback props for async ops. | `annotations`, `profiles`, `activeAnnotationId?`, `currentUserId`, `callbacks: AnnotationCallbacks`, `readOnly?` (hides reply/resolve/edit/delete affordances; thread list stays readable), `onAnnotationSelect?`, `onAnnotationsChanged?` |
112
112
 
113
113
  **Types & utilities:**
114
114
  ```typescript
@@ -239,6 +239,23 @@ If the project uses JST (Joe's Software Template), browse component demos at `/c
239
239
  - Check the component source in `src/lib/components/` for full prop types
240
240
  - Prefer jatui components over building custom ones when there's a match
241
241
 
242
+ ## Releasing — never run `npm publish`
243
+
244
+ **To cut a release: bump `version` in `package.json`, commit, push to master. That's the whole job.**
245
+
246
+ `.github/workflows/publish.yml` publishes to npm on any push to master that touches
247
+ `package.json`, authenticating via npm Trusted Publishing (OIDC) — no token, and every
248
+ release carries a provenance attestation. The workflow is idempotent: if the version is
249
+ already on the registry it exits green without republishing, so re-running it is safe.
250
+
251
+ Publishing from a dev box instead does two bad things: the release is attributed to a
252
+ person rather than the attested `GitHub Actions` identity (**provenance is lost**), and it
253
+ races the workflow — that is exactly how 0.2.5 shipped by hand at 16:16:21Z and left CI red
254
+ with an `E403: cannot publish over the previously published version`.
255
+
256
+ `prepublishOnly` now runs `scripts/guard-ci-publish.mjs`, which hard-fails any publish
257
+ outside CI. If you hit that error, you are doing it wrong — bump the version and push.
258
+
242
259
  # Layering Policy (DaisyUI is an implementation detail)
243
260
 
244
261
  Three rules keep DaisyUI swappable instead of load-bearing:
@@ -7,6 +7,8 @@
7
7
  activeAnnotationId?: string | null
8
8
  currentUserId: string
9
9
  callbacks: AnnotationCallbacks
10
+ /** Hides reply/resolve/edit/delete affordances — thread list stays readable */
11
+ readOnly?: boolean
10
12
  onAnnotationSelect?: (annotation: Annotation | null) => void
11
13
  onAnnotationsChanged?: () => void
12
14
  }
@@ -17,6 +19,7 @@
17
19
  activeAnnotationId = null,
18
20
  currentUserId,
19
21
  callbacks,
22
+ readOnly = false,
20
23
  onAnnotationSelect,
21
24
  onAnnotationsChanged,
22
25
  }: Props = $props()
@@ -202,37 +205,39 @@
202
205
  {#if isActive}
203
206
  <div class="px-3 pb-3 space-y-2">
204
207
  <!-- Actions for root -->
205
- <div class="flex items-center gap-1 ml-8">
206
- <button
207
- class="btn btn-ghost btn-xs gap-1 {thread.root.resolved ? 'text-warning' : 'text-success'}"
208
- onclick={() => toggleResolve(thread.root)}
209
- >
210
- {#if thread.root.resolved}
211
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
212
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
213
- </svg>
214
- Reopen
215
- {:else}
216
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
217
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
218
- </svg>
219
- Resolve
220
- {/if}
221
- </button>
222
- {#if thread.root.user_id === currentUserId}
223
- <button
224
- class="btn btn-ghost btn-xs text-base-content/50"
225
- onclick={() => startEdit(thread.root)}
226
- >Edit</button>
208
+ {#if !readOnly}
209
+ <div class="flex items-center gap-1 ml-8">
227
210
  <button
228
- class="btn btn-ghost btn-xs text-error/70"
229
- onclick={() => deleteAnnotation(thread.root.id)}
230
- >Delete</button>
231
- {/if}
232
- </div>
211
+ class="btn btn-ghost btn-xs gap-1 {thread.root.resolved ? 'text-warning' : 'text-success'}"
212
+ onclick={() => toggleResolve(thread.root)}
213
+ >
214
+ {#if thread.root.resolved}
215
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
216
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
217
+ </svg>
218
+ Reopen
219
+ {:else}
220
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
221
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
222
+ </svg>
223
+ Resolve
224
+ {/if}
225
+ </button>
226
+ {#if thread.root.user_id === currentUserId}
227
+ <button
228
+ class="btn btn-ghost btn-xs text-base-content/50"
229
+ onclick={() => startEdit(thread.root)}
230
+ >Edit</button>
231
+ <button
232
+ class="btn btn-ghost btn-xs text-error/70"
233
+ onclick={() => deleteAnnotation(thread.root.id)}
234
+ >Delete</button>
235
+ {/if}
236
+ </div>
237
+ {/if}
233
238
 
234
239
  <!-- Edit form for root -->
235
- {#if editingId === thread.root.id}
240
+ {#if !readOnly && editingId === thread.root.id}
236
241
  <div class="ml-8 space-y-1.5">
237
242
  <textarea
238
243
  class="textarea textarea-bordered textarea-sm w-full"
@@ -257,7 +262,7 @@
257
262
  <span class="text-xs font-medium">{getAuthorName(reply.user_id)}</span>
258
263
  <span class="text-[10px] text-base-content/40">{formatTime(reply.created_at)}</span>
259
264
  </div>
260
- {#if editingId === reply.id}
265
+ {#if !readOnly && editingId === reply.id}
261
266
  <div class="mt-1 space-y-1.5">
262
267
  <textarea
263
268
  class="textarea textarea-bordered textarea-xs w-full"
@@ -271,7 +276,7 @@
271
276
  </div>
272
277
  {:else}
273
278
  <p class="text-sm text-base-content/80 mt-0.5">{reply.content}</p>
274
- {#if reply.user_id === currentUserId}
279
+ {#if !readOnly && reply.user_id === currentUserId}
275
280
  <div class="flex gap-1 mt-1">
276
281
  <button class="btn btn-ghost btn-xs text-base-content/40 h-5 min-h-0" onclick={() => startEdit(reply)}>Edit</button>
277
282
  <button class="btn btn-ghost btn-xs text-error/50 h-5 min-h-0" onclick={() => deleteAnnotation(reply.id)}>Delete</button>
@@ -283,33 +288,35 @@
283
288
  {/each}
284
289
 
285
290
  <!-- Reply input -->
286
- <div class="ml-8 flex gap-2">
287
- <textarea
288
- class="textarea textarea-bordered textarea-sm flex-1 min-h-[36px]"
289
- rows="1"
290
- placeholder="Reply..."
291
- bind:value={replyContent}
292
- onkeydown={(e) => {
293
- if (e.key === "Enter" && !e.shiftKey) {
294
- e.preventDefault()
295
- submitReply(thread.root.id)
296
- }
297
- }}
298
- ></textarea>
299
- <button
300
- class="btn btn-sm btn-primary self-end"
301
- disabled={!replyContent.trim() || isSubmitting}
302
- onclick={() => submitReply(thread.root.id)}
303
- >
304
- {#if isSubmitting}
305
- <span class="loading loading-spinner loading-xs"></span>
306
- {:else}
307
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
308
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
309
- </svg>
310
- {/if}
311
- </button>
312
- </div>
291
+ {#if !readOnly}
292
+ <div class="ml-8 flex gap-2">
293
+ <textarea
294
+ class="textarea textarea-bordered textarea-sm flex-1 min-h-[36px]"
295
+ rows="1"
296
+ placeholder="Reply..."
297
+ bind:value={replyContent}
298
+ onkeydown={(e) => {
299
+ if (e.key === "Enter" && !e.shiftKey) {
300
+ e.preventDefault()
301
+ submitReply(thread.root.id)
302
+ }
303
+ }}
304
+ ></textarea>
305
+ <button
306
+ class="btn btn-sm btn-primary self-end"
307
+ disabled={!replyContent.trim() || isSubmitting}
308
+ onclick={() => submitReply(thread.root.id)}
309
+ >
310
+ {#if isSubmitting}
311
+ <span class="loading loading-spinner loading-xs"></span>
312
+ {:else}
313
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
314
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
315
+ </svg>
316
+ {/if}
317
+ </button>
318
+ </div>
319
+ {/if}
313
320
  </div>
314
321
  {/if}
315
322
  </div>
@@ -5,6 +5,8 @@ interface Props {
5
5
  activeAnnotationId?: string | null;
6
6
  currentUserId: string;
7
7
  callbacks: AnnotationCallbacks;
8
+ /** Hides reply/resolve/edit/delete affordances — thread list stays readable */
9
+ readOnly?: boolean;
8
10
  onAnnotationSelect?: (annotation: Annotation | null) => void;
9
11
  onAnnotationsChanged?: () => void;
10
12
  }
@@ -17,6 +17,8 @@
17
17
  activeAnnotationId?: string | null
18
18
  /** Whether annotation placement mode is active */
19
19
  annotationMode?: boolean
20
+ /** Whether resolved annotation pins are shown on the image (default false, matching AnnotationLayer) */
21
+ showResolved?: boolean
20
22
  /** Called when a pin is clicked */
21
23
  onAnnotationClick?: (annotation: Annotation) => void
22
24
  /** Called when the image is clicked in annotation mode */
@@ -32,6 +34,7 @@
32
34
  annotations = [],
33
35
  activeAnnotationId = null,
34
36
  annotationMode = false,
37
+ showResolved = false,
35
38
  onAnnotationClick,
36
39
  onImageClick,
37
40
  onClose,
@@ -168,6 +171,7 @@
168
171
  {annotations}
169
172
  {activeAnnotationId}
170
173
  {annotationMode}
174
+ {showResolved}
171
175
  {onAnnotationClick}
172
176
  {onImageClick}
173
177
  />
@@ -265,6 +269,7 @@
265
269
  {annotations}
266
270
  {activeAnnotationId}
267
271
  annotationMode={false}
272
+ {showResolved}
268
273
  {onAnnotationClick}
269
274
  />
270
275
  {/if}
@@ -13,6 +13,8 @@ interface Props {
13
13
  activeAnnotationId?: string | null;
14
14
  /** Whether annotation placement mode is active */
15
15
  annotationMode?: boolean;
16
+ /** Whether resolved annotation pins are shown on the image (default false, matching AnnotationLayer) */
17
+ showResolved?: boolean;
16
18
  /** Called when a pin is clicked */
17
19
  onAnnotationClick?: (annotation: Annotation) => void;
18
20
  /** Called when the image is clicked in annotation mode */
@@ -2,16 +2,7 @@ import type { Input } from '@atlaskit/pragmatic-drag-and-drop/types';
2
2
  export interface DragData extends Record<string | symbol, unknown> {
3
3
  readonly type: string;
4
4
  }
5
- /**
6
- * Module-scoped Symbol brand for board-managed items.
7
- * ⚠ Symbol identity = module identity. Two copies of this module on one page makes
8
- * isItemData() return false across the boundary — dnd/ must exist EXACTLY ONCE.
9
- * Declare all @atlaskit/pragmatic-drag-and-drop* packages as peerDependencies in
10
- * the host app (host owns the single instance).
11
- */
12
- declare const ITEM: unique symbol;
13
5
  export interface ItemDragData<TItem = unknown> extends DragData {
14
- readonly [ITEM]: true;
15
6
  readonly key: string;
16
7
  readonly item: TItem;
17
8
  readonly container: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -70,7 +70,7 @@
70
70
  "package": "svelte-package --input src/lib --output dist && node scripts/compile-rune-modules.mjs",
71
71
  "check:demos": "node scripts/check-demo-coverage.mjs",
72
72
  "guard": "node scripts/check-demo-coverage.mjs",
73
- "prepublishOnly": "npm run package"
73
+ "prepublishOnly": "node scripts/guard-ci-publish.mjs && npm run package"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@atlaskit/pragmatic-drag-and-drop": "~2.0.0",