pandorga 1.4.6 → 1.4.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8ea9638b4cbf0679fd3eeb328969878e5c4e7080be6a338257ccfc831f66213
4
- data.tar.gz: 865776c26a7094cdf151ff21c6ecf492dd9e91171a6608bb23567323ff6ef8a2
3
+ metadata.gz: fdc6dcec6dea0007093b40500ca2ec287d17170029f68b3f4284090fe14236a1
4
+ data.tar.gz: be5ea215e27ebe6755ce5d09875c8ca175d16daf3d8328c94161255fba179531
5
5
  SHA512:
6
- metadata.gz: f14807c5d0798c3dd75253faa2571fd142ee5f9e69017486a89cf65eead3c45707b683ab044bd45c3be03fe177287732a0cb95dfc1a57597917ea9a6e2b272b4
7
- data.tar.gz: 86216efa57c77d99e3f1369d467edd57580f3046e558325b425ad1ecc85c5b0c9dacb162407840229c50f87de9233f8e668ef3abaa5cd2b34714b76644697042
6
+ metadata.gz: f2f5de4ed2c7737a20b33fe03018c49f8a2267a4903ed065c5af9f99bf1b36e708305186c3d022786152efc15ba39791c6f55c2cf9427d2bac4f97a61cda77a7
7
+ data.tar.gz: 43e93a55bd09913445387e511741522588d2fb51d37589dc51abc4d0d4e4145fa3fef32c7087a7b4d59fa841fe25b2e5cc799bc1c88c2efa3883e7dfb5d546d4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.8] — 2026-10-08
4
+
5
+ ### Changed
6
+
7
+ - Home project bench: specimen and inventory share equal ~45.6% columns;
8
+ inventory thumbnails fill row height at 16:9; list titles, copy, and
9
+ resource marks retuned (neutral gray icons, tighter type)
10
+ - Specimen tagline flexes into spare card height and ellipsizes with a
11
+ proper "…" (CSS line-clamp was hard-clipping mid-glyph)
12
+
13
+ ## [1.4.7] — 2026-10-07
14
+
15
+ ### Fixed
16
+
17
+ - Studio keeps the editor focused while the content-pipeline chip is spinning.
18
+ Status polls patch the toolbar chip instead of rebuilding the shell.
19
+ - Home draft cards use 16:9 thumbnails and are 20% shorter on desktop and mobile.
20
+
3
21
  ## [1.4.6] — 2026-10-07
4
22
 
5
23
  ### Fixed
data/_layouts/home.html CHANGED
@@ -250,6 +250,43 @@ layout: shell
250
250
  /* Lock height to the tallest project so swapping never moves the
251
251
  inventory. Remeasure on resize when the specimen column width
252
252
  changes (chip wrap / plate width). */
253
+ /* `-webkit-line-clamp` fails on the specimen card (mid-glyph crop,
254
+ no ellipsis). Copy flexes into spare card height; fit text here. */
255
+ function ellipsizeSpecimenCopy(root) {
256
+ var copy = root.querySelector('.ledger-card-copy');
257
+ if (!copy) return;
258
+ var full = copy.getAttribute('data-full-text') || copy.textContent;
259
+ copy.setAttribute('data-full-text', full);
260
+ copy.textContent = full;
261
+ if (copy.scrollHeight <= copy.clientHeight + 1) return;
262
+
263
+ function fitted(n) {
264
+ if (n >= full.length) {
265
+ copy.textContent = full;
266
+ return copy.scrollHeight <= copy.clientHeight + 1;
267
+ }
268
+ var slice = full.slice(0, n);
269
+ var sp = slice.lastIndexOf(' ');
270
+ if (sp > Math.floor(n * 0.5)) slice = slice.slice(0, sp);
271
+ copy.textContent = slice.replace(/[.,;:\s]+$/u, '') + '…';
272
+ return copy.scrollHeight <= copy.clientHeight + 1;
273
+ }
274
+
275
+ var lo = 0;
276
+ var hi = full.length;
277
+ var best = 0;
278
+ while (lo <= hi) {
279
+ var mid = (lo + hi) >> 1;
280
+ if (fitted(mid)) {
281
+ best = mid;
282
+ lo = mid + 1;
283
+ } else {
284
+ hi = mid - 1;
285
+ }
286
+ }
287
+ fitted(best);
288
+ }
289
+
253
290
  function paintSpecimen(entry) {
254
291
  specimenSlot.innerHTML = '';
255
292
  specimenSlot.appendChild(runtime.renderProjectSpecimen(asItem(entry), {
@@ -258,6 +295,7 @@ layout: shell
258
295
  folio: positionOf(entry.slug) + 1,
259
296
  openLabel: 'Open project'
260
297
  }));
298
+ ellipsizeSpecimenCopy(specimenSlot);
261
299
  }
262
300
 
263
301
  function measureTallestSpecimen() {
@@ -302,8 +340,21 @@ layout: shell
302
340
  var bench = specimenSlot.closest('.home-projects-bench');
303
341
  if (!bench) return;
304
342
  var tallest = measureTallestSpecimen();
305
- if (tallest > 0) {
306
- bench.style.setProperty('--home-specimen-height', tallest + 'px');
343
+ /* Floor at the CSS inventory minimum so six rows always clear
344
+ three stacked resource icons (see --home-inventory-min-height). */
345
+ var inventoryMin = 0;
346
+ var raw = window.getComputedStyle(bench)
347
+ .getPropertyValue('--home-inventory-min-height').trim();
348
+ if (raw) {
349
+ var probe = document.createElement('div');
350
+ probe.style.cssText = 'position:absolute;visibility:hidden;height:' + raw;
351
+ bench.appendChild(probe);
352
+ inventoryMin = Math.ceil(probe.getBoundingClientRect().height);
353
+ probe.remove();
354
+ }
355
+ var locked = Math.max(tallest, inventoryMin);
356
+ if (locked > 0) {
357
+ bench.style.setProperty('--home-specimen-height', locked + 'px');
307
358
  }
308
359
  }
309
360
 
@@ -845,10 +845,9 @@ html[data-theme="light"] .home-hero-plate {
845
845
  border-color var(--motion-duration-fast) var(--motion-ease-default);
846
846
  }
847
847
 
848
- /* ~40% taller poster than the shared 16/9 plate so the dropped-sheet body
849
- keeps room for the excerpt (four-up columns were clipping copy to a sliver). */
848
+ /* Shared 16:9 plate. The previous 16/12.6 crop made these thumbs tall. */
850
849
  .home-draft-card .ledger-poster {
851
- aspect-ratio: 16 / 12.6;
850
+ aspect-ratio: 16 / 9;
852
851
  }
853
852
 
854
853
  /* A handful of fixed degrees per grid position — not a JS random() — so the
@@ -875,8 +874,9 @@ html[data-theme="light"] .home-hero-plate {
875
874
  }
876
875
 
877
876
  .home-draft-card .ledger-card-body {
878
- padding: var(--space-md);
879
- gap: var(--space-xs);
877
+ /* 20% shorter stack than the previous dropped-sheet body, both breakpoints. */
878
+ padding: calc(var(--space-md) * 0.8);
879
+ gap: calc(var(--space-xs) * 0.8);
880
880
  }
881
881
 
882
882
  /* Up from 1rem. The four dropped sheets are the only place on the home page
@@ -903,19 +903,29 @@ html[data-theme="light"] .home-hero-plate {
903
903
  `flex: 1` gives the paragraph a definite block size from the flex
904
904
  allocation, so line-clamp/overflow clip against that height instead of
905
905
  growing past the ellipsis (the old flex:1 + clamp:2 bug that left text
906
- after "…" and sliced mid-glyph at the card edge).
907
- min-height ~40% above the old six-line band so short posters still leave
908
- readable excerpt room. */
906
+ after "…" and sliced mid-glyph at the card edge). */
909
907
  flex: 1 1 auto;
910
- min-height: calc(var(--type-subtitle) * var(--leading-body) * 8.4);
908
+ /* With the 16:9 plate, this band keeps the whole card about 20% shorter
909
+ than the old 16/12.6 poster and 8.4-line excerpt, on narrow and wide grids. */
910
+ min-height: calc(var(--type-subtitle) * var(--leading-body) * 5.33);
911
911
  height: 0;
912
912
  display: -webkit-box;
913
- -webkit-line-clamp: 8;
914
- line-clamp: 8;
913
+ -webkit-line-clamp: 5;
914
+ line-clamp: 5;
915
915
  -webkit-box-orient: vertical;
916
916
  overflow: hidden;
917
917
  }
918
918
 
919
+ /* Four-up posters were the tall part of the old card. Give the excerpt a
920
+ little of that height back so the wide card is 20% shorter too, not more. */
921
+ @media (min-width: 1024px) {
922
+ .home-draft-card .ledger-card-copy {
923
+ min-height: calc(var(--type-subtitle) * var(--leading-body) * 6.05);
924
+ -webkit-line-clamp: 6;
925
+ line-clamp: 6;
926
+ }
927
+ }
928
+
919
929
  .home-draft-card .ledger-card-foot .ledger-cta {
920
930
  font-size: var(--type-cta);
921
931
  }
@@ -947,12 +957,13 @@ html[data-theme="light"] .home-hero-plate {
947
957
  A specimen card beside a filing rack of project cards. Height is locked to
948
958
  the tallest project (home.html measures once, then sets
949
959
  `--home-specimen-height`), so swapping projects never resizes the band or
950
- the inventory. Taglines stay full length and chips wrap whole pills like
951
- the projects index; spare height parks chips and the foot on the floor.
952
- Every plate is 16:9, the ratio the thumbnails are drawn at, with the title
953
- below it. From 900px the specimen is half the band and the rack a narrower
954
- column beside it, the rest left blank. Below 900px the rack is an icon strip
955
- above the specimen, one column per project. */
960
+ the inventory. Taglines clamp to two lines with an ellipsis; chips wrap
961
+ whole pills like the projects index; spare height parks chips and the foot
962
+ on the floor. Every plate is 16:9, the ratio the thumbnails are drawn at,
963
+ with the title below it. From 900px the specimen and the rack share equal
964
+ columns (~45.6% each); rows reserve height for three resource icons.
965
+ Below 900px the rack is an icon strip above the specimen, one column
966
+ per project. */
956
967
  .home-projects-bench {
957
968
  --home-specimen-height: 32rem;
958
969
  --home-icon-btn-size: 2.75rem;
@@ -1128,12 +1139,15 @@ html[data-theme="light"] .home-hero-plate {
1128
1139
  white-space: nowrap;
1129
1140
  }
1130
1141
 
1131
- /* Full tagline, same as `.ledger-system-card` on the projects index — no
1132
- clamp and no flex-shrink that would crop mid-sentence. The chip row wraps
1133
- whole pills to the next line (never mid-label); spare height still parks
1134
- them and the foot on the floor. */
1142
+ /* Tagline fills spare height between the plate and the chips/foot.
1143
+ `-webkit-line-clamp` hard-clips mid-glyph on this card, so home.html
1144
+ ellipsizes against the flex-allocated box after paint (`height: 0` +
1145
+ `flex: 1` is the same definite-size trick as `.home-draft-card`). */
1135
1146
  .home-project-specimen .ledger-card-copy {
1136
- flex: 0 0 auto;
1147
+ flex: 1 1 auto;
1148
+ min-height: calc(var(--type-body) * var(--leading-body) * 2);
1149
+ height: 0;
1150
+ overflow: hidden;
1137
1151
  font-size: var(--type-body);
1138
1152
  line-height: var(--leading-body);
1139
1153
  color: var(--color-on-surface-variant);
@@ -1143,14 +1157,14 @@ html[data-theme="light"] .home-hero-plate {
1143
1157
  flex: 0 0 auto;
1144
1158
  flex-wrap: wrap;
1145
1159
  gap: var(--space-xs);
1146
- margin: auto 0 0;
1160
+ margin: 0;
1147
1161
  }
1148
1162
 
1149
1163
  .home-specimen-foot {
1150
1164
  flex: 0 0 auto;
1151
1165
  flex-wrap: wrap;
1152
1166
  gap: var(--space-md);
1153
- margin-top: auto;
1167
+ margin-top: var(--space-sm);
1154
1168
  padding-top: var(--space-md);
1155
1169
  }
1156
1170
 
@@ -1271,11 +1285,25 @@ html[data-theme="light"] .home-hero-plate {
1271
1285
 
1272
1286
  @media (min-width: 900px) {
1273
1287
 
1274
- /* Desktop fallback before JS measures the tallest specimen. */
1288
+ /* Desktop fallback before JS measures the tallest specimen.
1289
+ Specimen and inventory share equal columns (specimen was 38% then
1290
+ +20% → 45.6%; the list matches that width). Rows still reserve
1291
+ height for three stacked resource icons. */
1275
1292
  .home-projects-bench {
1276
- --home-specimen-height: 36rem;
1293
+ --home-inventory-icon-size: 1.55rem;
1294
+ --home-inventory-icon-gap: 0.2rem;
1295
+ --home-inventory-resources-height: calc(
1296
+ 3 * var(--home-inventory-icon-size) + 2 * var(--home-inventory-icon-gap)
1297
+ );
1298
+ --home-inventory-row-min: calc(
1299
+ var(--home-inventory-resources-height) + 2 * var(--space-sm)
1300
+ );
1301
+ --home-inventory-min-height: calc(
1302
+ 6 * var(--home-inventory-row-min) + 5 * var(--space-sm) + 2 * var(--space-md)
1303
+ );
1304
+ --home-specimen-height: max(36rem, var(--home-inventory-min-height));
1277
1305
  display: grid;
1278
- grid-template-columns: minmax(0, 50%) minmax(20rem, 40%);
1306
+ grid-template-columns: minmax(0, 45.6%) minmax(0, 45.6%);
1279
1307
  column-gap: var(--space-xl);
1280
1308
  align-items: stretch;
1281
1309
  }
@@ -1285,6 +1313,9 @@ html[data-theme="light"] .home-hero-plate {
1285
1313
  display: flex;
1286
1314
  flex-direction: column;
1287
1315
  gap: var(--space-sm);
1316
+ box-sizing: border-box;
1317
+ width: 100%;
1318
+ min-width: 0;
1288
1319
  min-height: 0;
1289
1320
  overflow: hidden;
1290
1321
  padding: var(--space-md);
@@ -1297,10 +1328,11 @@ html[data-theme="light"] .home-hero-plate {
1297
1328
 
1298
1329
  .home-project-inventory-row {
1299
1330
  flex: 1 1 0;
1331
+ align-items: flex-start;
1300
1332
  justify-content: flex-start;
1301
1333
  gap: var(--space-md);
1302
1334
  height: auto;
1303
- min-height: 0;
1335
+ min-height: var(--home-inventory-row-min);
1304
1336
  padding: var(--space-sm) var(--space-md);
1305
1337
  border: 1px solid var(--border-gentle-divider);
1306
1338
  background: var(--color-surface-card);
@@ -1319,10 +1351,14 @@ html[data-theme="light"] .home-hero-plate {
1319
1351
  box-shadow: inset 3px 0 0 var(--color-primary);
1320
1352
  }
1321
1353
 
1354
+ /* Plate fills the row’s content height; width follows 16:9. Stretch keeps
1355
+ it tall while title/copy/icons stay top-aligned with its top edge. */
1322
1356
  .home-project-inventory-media {
1323
1357
  display: block;
1324
1358
  flex: 0 0 auto;
1325
- width: 5.5rem;
1359
+ align-self: stretch;
1360
+ width: auto;
1361
+ height: auto;
1326
1362
  aspect-ratio: 16 / 9;
1327
1363
  overflow: hidden;
1328
1364
  background: var(--color-surface-high);
@@ -1354,12 +1390,12 @@ html[data-theme="light"] .home-hero-plate {
1354
1390
  }
1355
1391
 
1356
1392
  .home-project-inventory-mark .material-symbols-outlined {
1357
- font-size: 1.5rem;
1393
+ font-size: 2rem;
1358
1394
  }
1359
1395
 
1360
1396
  .home-project-inventory-mark--initial {
1361
1397
  font-family: var(--font-content-title);
1362
- font-size: var(--type-card);
1398
+ font-size: var(--type-card-lg);
1363
1399
  font-weight: var(--weight-title);
1364
1400
  line-height: 1;
1365
1401
  color: var(--color-on-surface);
@@ -1368,10 +1404,12 @@ html[data-theme="light"] .home-hero-plate {
1368
1404
  .home-project-inventory-body {
1369
1405
  display: flex;
1370
1406
  flex-direction: column;
1371
- justify-content: center;
1372
- gap: 0.1rem;
1407
+ justify-content: flex-start;
1408
+ align-self: flex-start;
1409
+ gap: 0.2rem;
1373
1410
  flex: 1 1 0;
1374
1411
  min-width: 0;
1412
+ padding-top: 0;
1375
1413
  }
1376
1414
 
1377
1415
  .home-project-inventory-resources {
@@ -1379,45 +1417,44 @@ html[data-theme="light"] .home-hero-plate {
1379
1417
  flex-direction: column;
1380
1418
  justify-content: flex-start;
1381
1419
  align-items: center;
1382
- align-self: stretch;
1420
+ align-self: flex-start;
1383
1421
  flex: 0 0 auto;
1384
- gap: 0.2rem;
1385
- min-height: 0;
1422
+ gap: var(--home-inventory-icon-gap);
1423
+ width: var(--home-inventory-icon-size);
1424
+ min-height: var(--home-inventory-resources-height);
1386
1425
  position: relative;
1387
1426
  z-index: 1;
1388
1427
  }
1389
1428
 
1429
+ /* Neutral marks — same voice as list descriptions, not primary CTAs. */
1390
1430
  .home-project-inventory-resource {
1391
1431
  display: inline-flex;
1392
1432
  align-items: center;
1393
1433
  justify-content: center;
1394
- width: 1.55rem;
1395
- height: 1.55rem;
1434
+ width: var(--home-inventory-icon-size);
1435
+ height: var(--home-inventory-icon-size);
1396
1436
  flex: 0 0 auto;
1397
- background: var(--color-surface-card);
1398
- color: var(--color-primary);
1399
- border: 1px solid color-mix(in srgb, var(--border-gentle-divider) 80%, transparent);
1400
- }
1401
-
1402
- .home-band--contrast .home-project-inventory-resource {
1403
- background: var(--color-surface-card-contrast);
1404
- }
1405
-
1406
- .home-project-inventory-row.is-selected .home-project-inventory-resource,
1407
- .home-band--contrast .home-project-inventory-row.is-selected .home-project-inventory-resource {
1408
- background: var(--color-surface-card);
1437
+ background: transparent;
1438
+ color: var(--color-on-surface-variant);
1439
+ border: 0;
1409
1440
  }
1410
1441
 
1411
1442
  .home-project-inventory-resource .material-symbols-outlined {
1412
1443
  font-size: 1rem;
1413
1444
  line-height: 1;
1414
- font-variation-settings: "FILL" 0, "wght" 500, "GRAD" 0, "opsz" 20;
1445
+ font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 20;
1415
1446
  }
1416
1447
 
1417
- .home-project-inventory-title {
1418
- margin: 0;
1448
+ .home-project-inventory-code .ledger-meta {
1419
1449
  font-size: var(--type-label-lg);
1420
- line-height: 1.25;
1450
+ }
1451
+
1452
+ .home-project-inventory-title {
1453
+ margin: 0.2rem 0 0;
1454
+ font-family: var(--font-content-title);
1455
+ font-size: calc(var(--type-card) - 2pt);
1456
+ font-weight: var(--weight-title);
1457
+ line-height: 1.2;
1421
1458
  color: var(--color-on-surface);
1422
1459
  overflow: hidden;
1423
1460
  text-overflow: ellipsis;
@@ -1432,9 +1469,9 @@ html[data-theme="light"] .home-hero-plate {
1432
1469
 
1433
1470
  .home-project-inventory-copy {
1434
1471
  margin: 0;
1435
- font-size: var(--type-meta);
1472
+ font-size: var(--type-label-lg);
1436
1473
  font-weight: var(--weight-body);
1437
- line-height: 1.4;
1474
+ line-height: 1.35;
1438
1475
  color: var(--color-on-surface-variant);
1439
1476
  overflow: hidden;
1440
1477
  text-overflow: ellipsis;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.4.6"
4
+ VERSION = "1.4.8"
5
5
  end
@@ -32,7 +32,6 @@
32
32
  "article.home-project-specimen",
33
33
  "div.home-specimen-strip",
34
34
  "div.ledger-meta",
35
- "span.ledger-status",
36
35
  "div.home-specimen-figure",
37
36
  "div.ledger-poster",
38
37
  "img",
@@ -45,13 +44,16 @@
45
44
  "span.home-specimen-open"
46
45
  ],
47
46
  "forbidden": [
48
- "projects-plate"
47
+ "projects-plate",
48
+ "span.ledger-status"
49
+ ],
50
+ "excludes": [
51
+ ">Ongoing<"
49
52
  ],
50
53
  "contains": [
51
54
  ">#01<",
52
55
  ">Simulation<",
53
56
  ">2024<",
54
- ">Ongoing<",
55
57
  ">ARCO<",
56
58
  ">Repository<",
57
59
  ">Article<",
@@ -27,22 +27,27 @@
27
27
  "div.ledger-poster",
28
28
  "img",
29
29
  "div.ledger-meta",
30
- "span.ledger-status",
31
30
  "h3.ledger-card-title",
32
31
  "a.ledger-card-stretch",
33
32
  "p.ledger-card-copy",
34
33
  "div.ledger-chip-row",
35
34
  "div.ledger-card-links"
36
35
  ],
36
+ "forbidden": [
37
+ "span.ledger-status"
38
+ ],
39
+ "excludes": [
40
+ ">Ongoing<"
41
+ ],
37
42
  "contains": [
38
43
  ">Repository<",
39
44
  ">Article<",
40
45
  ">Visit<",
41
46
  ">ARCO<",
42
- ">Simulation"
47
+ ">Simulation<"
43
48
  ],
44
49
  "counts": {
45
- "ledger-meta-sep": 1,
50
+ "ledger-meta-sep": 0,
46
51
  "ledger-cta": 3
47
52
  }
48
53
  }
@@ -45,6 +45,76 @@ check!(REPO_ROOT.join("studio-app/src/main.js").read.include?("armPipelineWatch"
45
45
  "SPA must watch the content pipeline after save")
46
46
  check!(REPO_ROOT.join("studio-app/src/main.js").read.include?("refreshPipelineStatus"),
47
47
  "SPA must hydrate pipeline status on boot")
48
+
49
+ # Pipeline polls used to call renderShell(), which wipes #app and remounts
50
+ # TipTap. That drops editor focus for the whole run.
51
+ def function_source(source, name)
52
+ re = /(?:async\s+)?function\s+#{Regexp.escape(name)}\s*\(/
53
+ m = source.match(re)
54
+ raise "missing function #{name}" unless m
55
+ i = m.end(0)
56
+ paren = 1
57
+ while i < source.length && paren.positive?
58
+ c = source[i]
59
+ if c == "'" || c == '"' || c == "`"
60
+ q = c
61
+ i += 1
62
+ while i < source.length && source[i] != q
63
+ i += source[i] == "\\" ? 2 : 1
64
+ end
65
+ i += 1
66
+ elsif c == "/" && source[i + 1] == "/"
67
+ nl = source.index("\n", i)
68
+ i = nl ? nl + 1 : source.length
69
+ else
70
+ paren += 1 if c == "("
71
+ paren -= 1 if c == ")"
72
+ i += 1
73
+ end
74
+ end
75
+ i += 1 while i < source.length && source[i] != "{"
76
+ raise "missing body #{name}" if i >= source.length
77
+ depth = 0
78
+ start = i
79
+ while i < source.length
80
+ c = source[i]
81
+ if c == "{"
82
+ depth += 1
83
+ i += 1
84
+ elsif c == "}"
85
+ depth -= 1
86
+ return source[start..i] if depth == 0
87
+ i += 1
88
+ elsif c == "'" || c == '"' || c == "`"
89
+ q = c
90
+ i += 1
91
+ while i < source.length && source[i] != q
92
+ i += source[i] == "\\" ? 2 : 1
93
+ end
94
+ i += 1
95
+ elsif c == "/" && source[i + 1] == "/"
96
+ nl = source.index("\n", i)
97
+ i = nl ? nl + 1 : source.length
98
+ else
99
+ i += 1
100
+ end
101
+ end
102
+ raise "unclosed function #{name}"
103
+ end
104
+
105
+ studio_main = REPO_ROOT.join("studio-app/src/main.js").read
106
+ %w[pollPipelineStatus refreshPipelineStatus armPipelineWatch].each do |fn|
107
+ body = function_source(studio_main, fn)
108
+ check!(!body.include?("renderShell"),
109
+ "#{fn} must not rebuild the shell (that drops editor focus while the chip spins)")
110
+ check!(body.include?("syncPipelineChip"),
111
+ "#{fn} must patch the pipeline chip in place")
112
+ end
113
+ reexport = function_source(studio_main, "reexportLocalContent")
114
+ check!(!reexport.include?("renderShell"),
115
+ "local export must not rebuild the shell while the button spins")
116
+ check!(reexport.include?("syncLocalExportButton"),
117
+ "local export must patch the export button in place")
48
118
  check!(!REPO_ROOT.join("studio-app/src/main.js").read.match?(/action:\s*"delete"/),
49
119
  "toolbar must not include a Delete action")
50
120