@swmansion/argent 0.13.0 → 0.13.1-next.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.
@@ -64,6 +64,7 @@
64
64
  .stage,
65
65
  .vcard,
66
66
  .float-controls,
67
+ .float-toolbelt,
67
68
  .float-status,
68
69
  .footbar,
69
70
  .comment-pop,
@@ -83,6 +84,13 @@
83
84
  gap: var(--gap-list);
84
85
  z-index: var(--z-overlay);
85
86
  }
87
+ /* macOS traffic lights (native window controls) occupy the top-left in
88
+ Electron. Shift the status pill clear of them so a "Connecting…" or
89
+ error message never lands under the buttons. A browser tab has no
90
+ buttons, so .native-frame is absent and the pill keeps its corner. */
91
+ .native-frame .float-status {
92
+ left: var(--traffic-light-gutter);
93
+ }
86
94
  .status {
87
95
  background: var(--color-surface);
88
96
  border: var(--border-hairline) solid var(--color-border);
@@ -297,8 +305,9 @@
297
305
  font-weight: var(--fw-semibold);
298
306
  box-shadow: var(--shadow-card);
299
307
  }
300
- /* Floating top-right controls (theme, comment, close): a row of square
301
- icon buttons that sit over the stream, away from the slim drag toolbar. */
308
+ /* Floating top-right controls (just the theme toggle now — comment moved
309
+ to the bottom toolbelt, close is a native traffic light): square icon
310
+ buttons that sit over the stream, away from the slim drag toolbar. */
302
311
  .float-controls {
303
312
  position: absolute;
304
313
  top: var(--footbar-bottom);
@@ -308,6 +317,19 @@
308
317
  z-index: var(--z-overlay);
309
318
  -webkit-app-region: no-drag;
310
319
  }
320
+ /* Bottom-centre toolbelt — floats below the stream. Today it carries the
321
+ comment toggle; it's shaped to grow into a Figma-style bottom tool tab,
322
+ so it's a horizontal row centred on the window. */
323
+ .float-toolbelt {
324
+ position: absolute;
325
+ bottom: var(--footbar-bottom);
326
+ left: 50%;
327
+ transform: translateX(-50%);
328
+ display: flex;
329
+ gap: var(--gap-list);
330
+ z-index: var(--z-overlay);
331
+ -webkit-app-region: no-drag;
332
+ }
311
333
  /* Icon-only controls: a uniform square button so theme / comment / close
312
334
  read as one consistent cluster (no mismatched text pills). The glyph is
313
335
  inline SVG drawn in currentColor, so it themes for free and the hover /
@@ -726,14 +748,14 @@
726
748
  }
727
749
  .spot-hit {
728
750
  position: absolute;
729
- cursor: crosshair;
751
+ cursor: var(--cursor-comment);
730
752
  z-index: var(--z-spot-hit);
731
753
  /* The spotlight root sets pointer-events:none; this capture layer must
732
754
  opt back in or hover/click never reaches it and nothing un-blurs. */
733
755
  pointer-events: auto;
734
756
  }
735
757
  .inspecting .stage img {
736
- cursor: crosshair;
758
+ cursor: var(--cursor-comment);
737
759
  }
738
760
 
739
761
  /* Comment box bubble anchored next to the inspected element. */
@@ -1208,6 +1230,18 @@
1208
1230
  <span id="hidden-badge" class="hidden-badge" hidden></span>
1209
1231
  </div>
1210
1232
  <div class="float-controls" id="float-controls">
1233
+ <button
1234
+ id="theme-btn"
1235
+ class="float-btn"
1236
+ type="button"
1237
+ aria-label="Theme"
1238
+ title="Theme"></button>
1239
+ </div>
1240
+ <!-- Bottom-centre toolbelt that floats below the stream. Holds the
1241
+ comment toggle for now; this is the seed of a Figma-style bottom
1242
+ tool tab we'll grow later. Window close/minimise live in the native
1243
+ traffic lights, not a handcrafted button. -->
1244
+ <div class="float-toolbelt" id="float-toolbelt">
1211
1245
  <button
1212
1246
  id="comment-btn"
1213
1247
  class="float-btn"
@@ -1227,28 +1261,6 @@
1227
1261
  d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
1228
1262
  </svg>
1229
1263
  </button>
1230
- <button
1231
- id="theme-btn"
1232
- class="float-btn"
1233
- type="button"
1234
- aria-label="Theme"
1235
- title="Theme"></button>
1236
- <button
1237
- id="close-btn"
1238
- class="float-btn"
1239
- type="button"
1240
- aria-label="Close preview window"
1241
- title="Close preview window">
1242
- <svg
1243
- viewBox="0 0 24 24"
1244
- fill="none"
1245
- stroke="currentColor"
1246
- stroke-width="2"
1247
- stroke-linecap="round"
1248
- aria-hidden="true">
1249
- <path d="M6 6 18 18M18 6 6 18" />
1250
- </svg>
1251
- </button>
1252
1264
  </div>
1253
1265
  <div class="offscreen-prompt" id="offscreen-prompt" hidden>
1254
1266
  <div class="op-card">
@@ -2023,15 +2035,15 @@
2023
2035
  }
2024
2036
 
2025
2037
  // ── Events ──────────────────────────────────────────────────────────
2026
- // Close button: ask the tool-server to dismiss the Electron window (it
2027
- // owns the window lifecycle over stdin, so it plays the close animation
2028
- // and quits the host cleanly — more reliable than renderer window.close()
2029
- // under the sandboxed BrowserWindow).
2030
- const closeBtn = document.getElementById("close-btn");
2031
- closeBtn.addEventListener("click", () => {
2032
- closeBtn.blur();
2033
- fetch(`${TOOL_SERVER}/preview/close`, { method: "POST" }).catch(() => {});
2034
- });
2038
+ // Window close/minimise are the native macOS traffic lights now (the host
2039
+ // window uses titleBarStyle:"hidden"), so there's no handcrafted close
2040
+ // button to wire up. Closing the native window quits the host; the
2041
+ // tool-server re-opens it on the next round, exactly as the old button's
2042
+ // dismiss did. In Electron, flag the document so the top-left chrome can
2043
+ // sidestep the traffic lights (a browser tab has none).
2044
+ if (/Electron/i.test(navigator.userAgent)) {
2045
+ document.documentElement.classList.add("native-frame");
2046
+ }
2035
2047
 
2036
2048
  // ── Theme (light / dark) ────────────────────────────────────────────
2037
2049
  // Colours are light-dark(LIGHT, DARK) tokens in theme.css; forcing
@@ -2553,18 +2565,33 @@
2553
2565
  );
2554
2566
  let best = null,
2555
2567
  bestScore = Infinity;
2568
+ // The size gate is a PREFERENCE for the first match, never an eliminator.
2569
+ // `fallback` keeps the nearest candidate even when the gate rejects it, so
2570
+ // a clearly on-screen element is never reported off-screen just because the
2571
+ // propose-time frame is a poor size hint: that frame is a thumbnail CROP
2572
+ // region (often a whole component) while `match` can anchor on a small
2573
+ // sub-element inside it — e.g. crop the header band, anchor on its tiny
2574
+ // "Export" button — so the seed size legitimately dwarfs the real match.
2575
+ let fallback = null,
2576
+ fbScore = Infinity;
2556
2577
  for (const n of pool) {
2557
2578
  const f = n.frame;
2558
2579
  const score = anchor ? dist(f) : f.width * f.height;
2580
+ if (score < fbScore) {
2581
+ fbScore = score;
2582
+ fallback = n;
2583
+ }
2559
2584
  if (anchor && !hasDesc && !sizeMatches(f)) continue; // size gate only bootstraps identity
2560
2585
  if (score < bestScore) {
2561
2586
  bestScore = score;
2562
2587
  best = n;
2563
2588
  }
2564
2589
  }
2565
- // A locked identity must never be dropped over a size wobble: if the size
2566
- // gate isn't in play (hasDesc) `best` is simply the nearest identity match.
2567
- return best;
2590
+ // A locked identity must never be dropped over a size wobble (hasDesc →
2591
+ // `best` is the nearest identity match). When the size gate rejects every
2592
+ // candidate (an unreliable propose-time frame), fall back to the nearest
2593
+ // match rather than reporting a visible element off-screen.
2594
+ return best || fallback;
2568
2595
  }
2569
2596
 
2570
2597
  // Topmost on-screen node whose frame contains a normalized point
@@ -3192,6 +3219,37 @@
3192
3219
  else return home;
3193
3220
  return { x: nx, y: home.y };
3194
3221
  }
3222
+ // Deterministic de-overlap of the spring HOMES (targets), run once per
3223
+ // layoutCards pass — i.e. per describe poll, NOT per animation frame. That
3224
+ // is the whole point: the damped bubble springs then glide to homes that
3225
+ // already don't overlap, so there is no live correction force and thus
3226
+ // none of the micro-jitter / wedge-wiggle a per-frame position fix causes.
3227
+ // Stable top-down sweep: walking cards in y order, each one is dropped to
3228
+ // sit just below every card above it that shares its vertical lane. Moving
3229
+ // only ever downward makes it monotonic — it cannot oscillate. Pinned
3230
+ // (user-dropped) cards are fixed anchors the others flow past.
3231
+ function deOverlapHomes(items, cardW, gap, top, bottom) {
3232
+ const sorted = items.slice().sort((a, b) => a.rec.home.y - b.rec.home.y);
3233
+ for (let i = 1; i < sorted.length; i++) {
3234
+ const cur = sorted[i];
3235
+ if (cur.rec.pinned) continue;
3236
+ let floor = top;
3237
+ for (let j = 0; j < i; j++) {
3238
+ const prev = sorted[j];
3239
+ // Same vertical lane? (cards in different columns never constrain.)
3240
+ const oX =
3241
+ Math.min(cur.rec.home.x, prev.rec.home.x) +
3242
+ cardW -
3243
+ Math.max(cur.rec.home.x, prev.rec.home.x);
3244
+ if (oX <= 0) continue;
3245
+ const prevBottom = prev.rec.home.y + prev.ch + gap;
3246
+ if (prevBottom > floor) floor = prevBottom;
3247
+ }
3248
+ // Drop to clear the cards above; clamp so it can't leave the window
3249
+ // (true overflow piles at the bottom rather than vanishing off-screen).
3250
+ if (cur.rec.home.y < floor) cur.rec.home.y = Math.min(floor, bottom - cur.ch);
3251
+ }
3252
+ }
3195
3253
  function layoutCards() {
3196
3254
  const now = Date.now();
3197
3255
  const mr = mainEl.getBoundingClientRect();
@@ -3305,7 +3363,7 @@
3305
3363
  // • The final maxHeight write is diffed against the inline
3306
3364
  // value so unchanged frames are no-ops — keeps scrollTop.
3307
3365
  const COL_PAD = 8;
3308
- const GAP = 12;
3366
+ const GAP = 8;
3309
3367
  const want = vis.map((rec, i) => {
3310
3368
  const body = rec.el.querySelector(".vcard-body");
3311
3369
  if (body && rec.chromeH == null) {
@@ -3371,11 +3429,19 @@
3371
3429
  // the "ready" pop. offsetTop ignores the transform (offsetParent is the
3372
3430
  // position:relative #main), keeping the column height stable.
3373
3431
  const footTop = footEl && !footEl.hidden ? footEl.offsetTop - COL_PAD : mh;
3374
- const colBottom = Math.min(mh - COL_PAD, footTop);
3375
- const colH = colBottom - colTop;
3432
+ // The footbar is anchored bottom-RIGHT, so only the column(s) that
3433
+ // actually sit above it should surrender height for it. A column clear
3434
+ // of the footbar's x-range gets the full window height — otherwise the
3435
+ // left column was needlessly squeezed (and its cards clipped) by a
3436
+ // footbar that only overlaps the right column.
3437
+ const footLeft = footEl && !footEl.hidden ? footEl.offsetLeft : mw;
3376
3438
  for (const col of ["L", "R"]) {
3377
3439
  const cwant = want.filter((w) => w.side === col);
3378
3440
  if (cwant.length === 0) continue;
3441
+ const colX = col === "L" ? leftX : rightX;
3442
+ const colBottom =
3443
+ colX + cardW > footLeft ? Math.min(mh - COL_PAD, footTop) : mh - COL_PAD;
3444
+ const colH = colBottom - colTop;
3379
3445
  const naturalSum = cwant.reduce((s, w) => s + w.natural, 0) + (cwant.length - 1) * GAP;
3380
3446
  if (naturalSum <= colH) {
3381
3447
  cwant.sort((a, b) => a.anchor - b.anchor);
@@ -3468,6 +3534,9 @@
3468
3534
  w.rec.vel.y += Math.random() * 18 - 9;
3469
3535
  }
3470
3536
  }
3537
+ // Guarantee no two resting homes overlap. Deterministic + once per pass,
3538
+ // so the springs simply glide to clear targets (no live force, no jitter).
3539
+ deOverlapHomes(want, cardW, GAP, COL_PAD, mh - COL_PAD);
3471
3540
  }
3472
3541
 
3473
3542
  // Reuses SVG nodes (no per-frame teardown) and draws to the *eased*
@@ -131,7 +131,7 @@
131
131
  --pad-code: 2px 6px;
132
132
  --pad-pill: 3px 11px;
133
133
  --pad-card-head: 7px 10px;
134
- --pad-row: 8px 10px;
134
+ --pad-row: 5px 10px; /* tighter vertical so a 2-variant card fits its column without clipping */
135
135
  --pad-cmt: 6px 10px; /* in-card comment box */
136
136
  --pad-input: 6px 8px; /* popover textareas */
137
137
  --pad-pop: 10px; /* comment / annotation popover */
@@ -168,6 +168,7 @@
168
168
  --footbar-bottom: 14px;
169
169
  --footbar-right: 14px;
170
170
  --resize-edge: 6px; /* drag-layer inset so the frameless window's edges still resize */
171
+ --traffic-light-gutter: 84px; /* left inset for top-left chrome to clear the native macOS window buttons (Electron only) */
171
172
  --footbar-max-width: 70%;
172
173
  --select-min-width: 240px;
173
174
  --debug-min-width: 220px;
@@ -181,6 +182,18 @@
181
182
  --stream-max-h: 80dvh; /* dvh, not vh: in a browser tab `vh` is the toolbar-retracted (large) viewport, so the centered stream overflowed the %-height container and got clipped at the bottom. `dvh` tracks the visible viewport. In the frameless Electron window (no toolbar) dvh == vh, so production is unchanged. */
182
183
  --backdrop-blur: 1.67px; /* light haze over the dimmed stream (3× softer than the former 5px) */
183
184
 
185
+ /* ── Cursors ─────────────────────────────────────────────────────────── */
186
+ /* Comment-mode cursor — a Figma-style speech bubble that makes "you are
187
+ placing a comment" unmistakable (replaces the generic crosshair). Accent
188
+ fill + white outline & dots so it reads on the bright stream AND on the
189
+ dark spotlight scrim. "7 24" is the hotspot: the tail tip, where the click
190
+ actually lands. Baked as a data-URI because a cursor image can't reference
191
+ CSS vars — so the colours live here, in the theme, like every other value. */
192
+ --cursor-comment:
193
+ url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2228%22%20height%3D%2228%22%20viewBox%3D%220%200%2028%2028%22%3E%3Cpath%20d%3D%22M6%203h16a3%203%200%200%201%203%203v10a3%203%200%200%201-3%203h-9l-6%205v-5h-1a3%203%200%200%201-3-3V6a3%203%200%200%201%203-3z%22%20fill%3D%22%232f6fd0%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%221.5%22%20stroke-linejoin%3D%22round%22%2F%3E%3Ccircle%20cx%3D%229%22%20cy%3D%2211%22%20r%3D%221.4%22%20fill%3D%22%23ffffff%22%2F%3E%3Ccircle%20cx%3D%2214%22%20cy%3D%2211%22%20r%3D%221.4%22%20fill%3D%22%23ffffff%22%2F%3E%3Ccircle%20cx%3D%2219%22%20cy%3D%2211%22%20r%3D%221.4%22%20fill%3D%22%23ffffff%22%2F%3E%3C%2Fsvg%3E")
194
+ 7 24,
195
+ crosshair;
196
+
184
197
  /* ── Opacities ───────────────────────────────────────────────────────── */
185
198
  --opacity-disabled: 0.45;
186
199
  --opacity-connector: 0.7;
@@ -25,6 +25,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  // ../preview-window/src/main.ts
26
26
  var import_electron = require("electron");
27
27
  var readline = __toESM(require("node:readline"));
28
+ import_electron.app.setName("Argent Lens");
28
29
  var TARGET_WIDTH = 1200;
29
30
  var TARGET_HEIGHT = 820;
30
31
  var ANIMATION_MS = 320;
@@ -105,7 +106,17 @@ async function createWindow() {
105
106
  win = new import_electron.BrowserWindow({
106
107
  width: TARGET_WIDTH,
107
108
  height: TARGET_HEIGHT,
108
- frame: false,
109
+ title: "Argent Lens",
110
+ // Native window controls (the macOS traffic lights) instead of a
111
+ // handcrafted close button: `titleBarStyle: "hidden"` keeps the real OS
112
+ // buttons in the top-left while hiding the rest of the title bar, so the
113
+ // window still reads as frameless. (`frame: false` would suppress the
114
+ // buttons entirely — that's what we used to do.)
115
+ titleBarStyle: "hidden",
116
+ // Inset the traffic lights to share the same 14px padding the in-window
117
+ // corner panels use (theme toggle, status pill), so the native buttons and
118
+ // the handcrafted chrome read as one consistent margin.
119
+ trafficLightPosition: { x: 14, y: 18 },
109
120
  show: false,
110
121
  // Transparent + no native shadow: the OS window is just a passthrough
111
122
  // for the renderer. The visible "card" is whatever <html> + <body>
@@ -119,6 +130,8 @@ async function createWindow() {
119
130
  sandbox: true
120
131
  }
121
132
  });
133
+ win.setWindowButtonVisibility?.(true);
134
+ win.setTitle("Argent Lens");
122
135
  win.on("closed", () => {
123
136
  win = null;
124
137
  import_electron.app.quit();