@joewinke/jatui 0.34.0 → 0.34.2

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.
@@ -411,7 +411,7 @@
411
411
 
412
412
  <div
413
413
  bind:this={dialogEl}
414
- class="fixed inset-0 flex flex-col bg-base-300/95 text-base-content outline-none {className}"
414
+ class="fixed inset-0 flex flex-col bg-base-300 text-base-content outline-none {className}"
415
415
  style:z-index={zIndex}
416
416
  role="dialog"
417
417
  aria-modal="true"
@@ -613,7 +613,14 @@
613
613
  >
614
614
  </div>
615
615
  {#if showHint}
616
- <p class="w-full text-2xs text-base-content/60" data-media-zoom-hint>{hint}</p>
616
+ <!-- Keys are useless on a touch screen: hidden on coarse pointers
617
+ (the checklist block's footer hint hides the same way,
618
+ mos-lsmve.16 F7 — the 4-line wrap pushed the note below the
619
+ fold). -->
620
+ <p
621
+ class="w-full text-2xs text-base-content/60 [@media(pointer:coarse)]:hidden"
622
+ data-media-zoom-hint>{hint}</p
623
+ >
617
624
  {/if}
618
625
  </div>
619
626
 
@@ -6,7 +6,9 @@
6
6
  marked `opensNote` (Reject / Needs work / Fail…). It does NOT open on
7
7
  mount, on a repeat of the same verdict, or when the round changes.
8
8
  - The note is optional: under an EMPTY note on a note-worthy verdict sits
9
- the soft NOTE_HINT — never a validation block.
9
+ the soft NOTE_HINT — never a validation block. The hint never claims the
10
+ verdict was saved when it was not: a `saveFailed` host (its own save
11
+ state) or this note's own failed save silences it (mos-lsmve.16 F6).
10
12
  - Auto-grows with its content.
11
13
  - Per-note save state from `onSave`: "Saving…" → "Saved ✓", or
12
14
  "Not saved" + Retry when it throws/rejects. Saves are debounced while
@@ -16,7 +18,12 @@
16
18
  discard.
17
19
  - `round` keys the note to the current round: a new round resets the save
18
20
  state, drops any pending save for the old round, and collapses the note
19
- if the host gives it an empty value.
21
+ if the host gives it an empty value. The round effect is DECLARED BEFORE
22
+ the verdict effect on purpose (mos-lsmve.16 F9): when a round change and
23
+ a note-worthy verdict change land in the same flush, Svelte runs
24
+ instance effects in declaration order, so the verdict's open runs LAST
25
+ and survives the round's collapse — the reviewer is looking at a note
26
+ for the verdict they just cast, and it must not blink away.
20
27
  -->
21
28
  <script module lang="ts">
22
29
  /** The soft inline hint under an EMPTY note field on a note-worthy verdict. */
@@ -42,6 +49,10 @@
42
49
  options?: readonly VerdictOption[];
43
50
  /** Current round key (version, retestSince…). */
44
51
  round?: string | number | null;
52
+ /** The host's own save failed (e.g. the verdict's save): the hint must
53
+ * not claim the verdict is saved while the block shows "Not saved"
54
+ * (mos-lsmve.16 F6). */
55
+ saveFailed?: boolean;
45
56
  /** Persist the note. Throw / reject to show "Not saved". */
46
57
  onSave?: (text: string, ctx: { round: string | number | null }) => void | Promise<void>;
47
58
  /** Every keystroke, before the debounce. */
@@ -66,6 +77,7 @@
66
77
  verdict = null,
67
78
  options = [],
68
79
  round = null,
80
+ saveFailed = false,
69
81
  onSave,
70
82
  onInput,
71
83
  debounceMs = 600,
@@ -88,20 +100,12 @@
88
100
 
89
101
  const noteWorthy = $derived(!!verdict && options.some((o) => o.id === verdict && o.opensNote));
90
102
 
91
- // Open + focus when the verdict CHANGES to a note-worthy one. The first run
92
- // only records the starting verdict, so a saved state never pops open.
93
- let lastVerdict: string | null | undefined = undefined;
94
- $effect(() => {
95
- const v = verdict;
96
- const prev = lastVerdict;
97
- lastVerdict = v;
98
- if (prev === undefined || v === prev || !v) return;
99
- if (!untrack(() => options.some((o) => o.id === v && o.opensNote))) return;
100
- open = true;
101
- tick().then(() => textarea?.focus());
102
- });
103
-
104
103
  // A new round: forget the old round's save state and pending save.
104
+ // DECLARED BEFORE the verdict effect below (mos-lsmve.16 F9): instance
105
+ // effects run in declaration order, so when a round change and a
106
+ // note-worthy verdict change land in the same flush, the round's
107
+ // collapse happens FIRST and the verdict's open survives — the note
108
+ // never blinks away under the verdict the reviewer just cast.
105
109
  let lastRound: string | number | null | undefined = undefined;
106
110
  $effect(() => {
107
111
  const r = round;
@@ -115,6 +119,19 @@
115
119
  if (!untrack(() => value)) open = false;
116
120
  });
117
121
 
122
+ // Open + focus when the verdict CHANGES to a note-worthy one. The first run
123
+ // only records the starting verdict, so a saved state never pops open.
124
+ let lastVerdict: string | null | undefined = undefined;
125
+ $effect(() => {
126
+ const v = verdict;
127
+ const prev = lastVerdict;
128
+ lastVerdict = v;
129
+ if (prev === undefined || v === prev || !v) return;
130
+ if (!untrack(() => options.some((o) => o.id === v && o.opensNote))) return;
131
+ open = true;
132
+ tick().then(() => textarea?.focus());
133
+ });
134
+
118
135
  // Auto-grow: reset, then fit the content.
119
136
  $effect(() => {
120
137
  void value;
@@ -206,7 +223,11 @@
206
223
  onblur={flush}
207
224
  onkeydown={onKeydown}
208
225
  ></textarea>
209
- {#if noteWorthy && !value.trim()}
226
+ <!-- The hint asserts the verdict was saved — it must not say that
227
+ while a save has failed (the host's own, or this note's own
228
+ retryable one): the alert above is saying the opposite
229
+ (mos-lsmve.16 F6). -->
230
+ {#if noteWorthy && !value.trim() && saveState !== 'error' && !saveFailed}
210
231
  <p class="text-xs text-base-content/60">{NOTE_HINT}</p>
211
232
  {/if}
212
233
  </div>
@@ -13,6 +13,10 @@ interface Props {
13
13
  options?: readonly VerdictOption[];
14
14
  /** Current round key (version, retestSince…). */
15
15
  round?: string | number | null;
16
+ /** The host's own save failed (e.g. the verdict's save): the hint must
17
+ * not claim the verdict is saved while the block shows "Not saved"
18
+ * (mos-lsmve.16 F6). */
19
+ saveFailed?: boolean;
16
20
  /** Persist the note. Throw / reject to show "Not saved". */
17
21
  onSave?: (text: string, ctx: {
18
22
  round: string | number | null;
@@ -13,12 +13,17 @@
13
13
  - One-key hotkeys from `options[].key` (and `u` = Undo) while focus is in
14
14
  the control. Handled keys are preventDefault-ed so a page-level
15
15
  reviewKeys() handler skips them.
16
+ - `trailing` renders the row's trailing actions (a Note toggle…) in ONE
17
+ non-breaking group with Undo, so a narrow card wraps the whole group —
18
+ the note button never strands alone on its own row (mos-lsmve.16 F7:
19
+ it used to wrap alone, below Undo).
16
20
  - Sizes: `sm` (cards, default) and `xs` (inline strips).
17
21
 
18
22
  The `options` array is the same one ReviewNote (`opensNote`) and
19
23
  reviewKeys (`key`) take.
20
24
  -->
21
25
  <script lang="ts">
26
+ import type { Snippet } from 'svelte';
22
27
  import type { VerdictOption, VerdictTone } from '../../actions/reviewKeys';
23
28
 
24
29
  interface Props {
@@ -35,6 +40,9 @@
35
40
  undoLabel?: string;
36
41
  /** Show each option's hotkey as a keycap inside its button. */
37
42
  showKeys?: boolean;
43
+ /** Trailing actions (Note toggle…), kept in one non-breaking group
44
+ * with Undo so the wrap boundary falls before the pair. */
45
+ trailing?: Snippet;
38
46
  /** Local hotkeys while focus is inside the control. Default true. */
39
47
  hotkeys?: boolean;
40
48
  disabled?: boolean;
@@ -50,6 +58,7 @@
50
58
  label = 'Verdict',
51
59
  undoLabel = 'Undo',
52
60
  showKeys = false,
61
+ trailing = undefined,
53
62
  hotkeys = true,
54
63
  disabled = false,
55
64
  class: className = ''
@@ -145,19 +154,27 @@
145
154
  </button>
146
155
  {/each}
147
156
  </div>
148
- {#if value && onUndo}
149
- <button
150
- type="button"
151
- class="btn btn-ghost {sizeClass} gap-1 px-2 text-base-content/75 hover:text-base-content"
152
- aria-label="{undoLabel} — {label}"
153
- aria-keyshortcuts="u"
154
- {disabled}
155
- onclick={() => onUndo?.()}
156
- >
157
- <svg viewBox="0 0 24 24" class="size-3.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
158
- <path d="M9 14 4 9l5-5" /><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11" />
159
- </svg>
160
- {undoLabel}
161
- </button>
157
+ <!-- Undo + trailing actions in one ATOMIC span: the row's wrap boundary
158
+ falls before the pair, so Undo and whatever trails it (the Note
159
+ toggle) always sit on the same line (mos-lsmve.16 F7). -->
160
+ {#if value && onUndo || trailing}
161
+ <span class="flex items-center gap-2">
162
+ {#if value && onUndo}
163
+ <button
164
+ type="button"
165
+ class="btn btn-ghost {sizeClass} gap-1 px-2 text-base-content/75 hover:text-base-content"
166
+ aria-label="{undoLabel} — {label}"
167
+ aria-keyshortcuts="u"
168
+ {disabled}
169
+ onclick={() => onUndo?.()}
170
+ >
171
+ <svg viewBox="0 0 24 24" class="size-3.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
172
+ <path d="M9 14 4 9l5-5" /><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11" />
173
+ </svg>
174
+ {undoLabel}
175
+ </button>
176
+ {/if}
177
+ {#if trailing}{@render trailing()}{/if}
178
+ </span>
162
179
  {/if}
163
180
  </div>
@@ -1,3 +1,4 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { VerdictOption } from '../../actions/reviewKeys';
2
3
  interface Props {
3
4
  options: readonly VerdictOption[];
@@ -13,6 +14,9 @@ interface Props {
13
14
  undoLabel?: string;
14
15
  /** Show each option's hotkey as a keycap inside its button. */
15
16
  showKeys?: boolean;
17
+ /** Trailing actions (Note toggle…), kept in one non-breaking group
18
+ * with Undo so the wrap boundary falls before the pair. */
19
+ trailing?: Snippet;
16
20
  /** Local hotkeys while focus is inside the control. Default true. */
17
21
  hotkeys?: boolean;
18
22
  disabled?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.34.0",
3
+ "version": "0.34.2",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",