@joewinke/jatui 0.2.5 → 0.2.7

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?`, `title?` (panel heading, default "Annotations"; pass "Comments" to match host vocabulary), `emptyText?` (empty-state line, default "No annotations yet"), `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,12 @@
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
12
+ /** Panel heading. Defaults to "Annotations"; pass e.g. "Comments" to match host vocabulary. */
13
+ title?: string
14
+ /** Empty-state line. Defaults to "No annotations yet". */
15
+ emptyText?: string
10
16
  onAnnotationSelect?: (annotation: Annotation | null) => void
11
17
  onAnnotationsChanged?: () => void
12
18
  }
@@ -17,6 +23,9 @@
17
23
  activeAnnotationId = null,
18
24
  currentUserId,
19
25
  callbacks,
26
+ readOnly = false,
27
+ title = "Annotations",
28
+ emptyText = "No annotations yet",
20
29
  onAnnotationSelect,
21
30
  onAnnotationsChanged,
22
31
  }: Props = $props()
@@ -132,7 +141,7 @@
132
141
  <!-- Header -->
133
142
  <div class="flex items-center justify-between px-3 py-2 border-b border-base-300 flex-shrink-0">
134
143
  <div class="flex items-center gap-2">
135
- <h4 class="text-sm font-semibold">Annotations</h4>
144
+ <h4 class="text-sm font-semibold">{title}</h4>
136
145
  {#if totalRoots > 0}
137
146
  <span class="badge badge-xs badge-ghost">{totalRoots - resolvedCount} open</span>
138
147
  {/if}
@@ -156,7 +165,7 @@
156
165
  <svg class="mx-auto h-10 w-10 text-base-content/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
157
166
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
158
167
  </svg>
159
- <p class="text-sm text-base-content/40 mt-2">No annotations yet</p>
168
+ <p class="text-sm text-base-content/40 mt-2">{emptyText}</p>
160
169
  <p class="text-xs text-base-content/30 mt-1">Click the pin icon then click on the image to add one</p>
161
170
  </div>
162
171
  {:else}
@@ -202,37 +211,39 @@
202
211
  {#if isActive}
203
212
  <div class="px-3 pb-3 space-y-2">
204
213
  <!-- 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>
214
+ {#if !readOnly}
215
+ <div class="flex items-center gap-1 ml-8">
227
216
  <button
228
- class="btn btn-ghost btn-xs text-error/70"
229
- onclick={() => deleteAnnotation(thread.root.id)}
230
- >Delete</button>
231
- {/if}
232
- </div>
217
+ class="btn btn-ghost btn-xs gap-1 {thread.root.resolved ? 'text-warning' : 'text-success'}"
218
+ onclick={() => toggleResolve(thread.root)}
219
+ >
220
+ {#if thread.root.resolved}
221
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
222
+ <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" />
223
+ </svg>
224
+ Reopen
225
+ {:else}
226
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
227
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
228
+ </svg>
229
+ Resolve
230
+ {/if}
231
+ </button>
232
+ {#if thread.root.user_id === currentUserId}
233
+ <button
234
+ class="btn btn-ghost btn-xs text-base-content/50"
235
+ onclick={() => startEdit(thread.root)}
236
+ >Edit</button>
237
+ <button
238
+ class="btn btn-ghost btn-xs text-error/70"
239
+ onclick={() => deleteAnnotation(thread.root.id)}
240
+ >Delete</button>
241
+ {/if}
242
+ </div>
243
+ {/if}
233
244
 
234
245
  <!-- Edit form for root -->
235
- {#if editingId === thread.root.id}
246
+ {#if !readOnly && editingId === thread.root.id}
236
247
  <div class="ml-8 space-y-1.5">
237
248
  <textarea
238
249
  class="textarea textarea-bordered textarea-sm w-full"
@@ -257,7 +268,7 @@
257
268
  <span class="text-xs font-medium">{getAuthorName(reply.user_id)}</span>
258
269
  <span class="text-[10px] text-base-content/40">{formatTime(reply.created_at)}</span>
259
270
  </div>
260
- {#if editingId === reply.id}
271
+ {#if !readOnly && editingId === reply.id}
261
272
  <div class="mt-1 space-y-1.5">
262
273
  <textarea
263
274
  class="textarea textarea-bordered textarea-xs w-full"
@@ -271,7 +282,7 @@
271
282
  </div>
272
283
  {:else}
273
284
  <p class="text-sm text-base-content/80 mt-0.5">{reply.content}</p>
274
- {#if reply.user_id === currentUserId}
285
+ {#if !readOnly && reply.user_id === currentUserId}
275
286
  <div class="flex gap-1 mt-1">
276
287
  <button class="btn btn-ghost btn-xs text-base-content/40 h-5 min-h-0" onclick={() => startEdit(reply)}>Edit</button>
277
288
  <button class="btn btn-ghost btn-xs text-error/50 h-5 min-h-0" onclick={() => deleteAnnotation(reply.id)}>Delete</button>
@@ -283,33 +294,35 @@
283
294
  {/each}
284
295
 
285
296
  <!-- 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>
297
+ {#if !readOnly}
298
+ <div class="ml-8 flex gap-2">
299
+ <textarea
300
+ class="textarea textarea-bordered textarea-sm flex-1 min-h-[36px]"
301
+ rows="1"
302
+ placeholder="Reply..."
303
+ bind:value={replyContent}
304
+ onkeydown={(e) => {
305
+ if (e.key === "Enter" && !e.shiftKey) {
306
+ e.preventDefault()
307
+ submitReply(thread.root.id)
308
+ }
309
+ }}
310
+ ></textarea>
311
+ <button
312
+ class="btn btn-sm btn-primary self-end"
313
+ disabled={!replyContent.trim() || isSubmitting}
314
+ onclick={() => submitReply(thread.root.id)}
315
+ >
316
+ {#if isSubmitting}
317
+ <span class="loading loading-spinner loading-xs"></span>
318
+ {:else}
319
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
320
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
321
+ </svg>
322
+ {/if}
323
+ </button>
324
+ </div>
325
+ {/if}
313
326
  </div>
314
327
  {/if}
315
328
  </div>
@@ -5,6 +5,12 @@ 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;
10
+ /** Panel heading. Defaults to "Annotations"; pass e.g. "Comments" to match host vocabulary. */
11
+ title?: string;
12
+ /** Empty-state line. Defaults to "No annotations yet". */
13
+ emptyText?: string;
8
14
  onAnnotationSelect?: (annotation: Annotation | null) => void;
9
15
  onAnnotationsChanged?: () => void;
10
16
  }
@@ -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 */
@@ -25,6 +25,7 @@
25
25
  disabled = false,
26
26
  displayValue,
27
27
  colorFn,
28
+ header,
28
29
  footer,
29
30
  variant = 'default',
30
31
  size = 'md',
@@ -41,6 +42,8 @@
41
42
  disabled?: boolean;
42
43
  displayValue?: string;
43
44
  colorFn?: (value: string) => string | undefined;
45
+ /** Rendered above the search input — use for panel titles, descriptions, or controls */
46
+ header?: Snippet;
44
47
  footer?: Snippet;
45
48
  /** 'chip' renders a compact TopBar-style pill instead of the full-width trigger */
46
49
  variant?: 'default' | 'chip';
@@ -197,6 +200,11 @@
197
200
  </script>
198
201
 
199
202
  {#snippet panelBody()}
203
+ {#if header}
204
+ <div class="sd-header">
205
+ {@render header()}
206
+ </div>
207
+ {/if}
200
208
  <!-- Search input -->
201
209
  <div class="sd-search">
202
210
  <div class="sd-search-inner">
@@ -525,6 +533,11 @@
525
533
  box-shadow: 0 -4px 24px oklch(0 0 0 / 0.4);
526
534
  }
527
535
 
536
+ /* Header section */
537
+ .sd-header {
538
+ border-bottom: 1px solid oklch(0.25 0.02 250);
539
+ }
540
+
528
541
  /* Search section */
529
542
  .sd-search {
530
543
  padding: 0.375rem 0.5rem;
@@ -16,6 +16,8 @@ type $$ComponentProps = {
16
16
  disabled?: boolean;
17
17
  displayValue?: string;
18
18
  colorFn?: (value: string) => string | undefined;
19
+ /** Rendered above the search input — use for panel titles, descriptions, or controls */
20
+ header?: Snippet;
19
21
  footer?: Snippet;
20
22
  /** 'chip' renders a compact TopBar-style pill instead of the full-width trigger */
21
23
  variant?: 'default' | 'chip';
@@ -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.7",
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",