changeledger 0.6.4 → 0.8.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.
- package/AGENTS.md +15 -8
- package/bin/changeledger.mjs +136 -44
- package/package.json +1 -1
- package/src/check.mjs +106 -5
- package/src/commands/agent.mjs +44 -4
- package/src/commands/context.mjs +95 -15
- package/src/commands/graduate.mjs +20 -8
- package/src/commands/view.mjs +29 -4
- package/src/contract.mjs +11 -6
- package/src/lifecycle.mjs +19 -2
- package/src/metrics.mjs +9 -6
- package/src/viewer/domain.mjs +16 -6
- package/src/viewer/public/app-state.js +15 -0
- package/src/viewer/public/app.js +115 -13
- package/src/viewer/public/styles.css +205 -14
- package/src/viewer/public/view-parts.js +52 -0
- package/templates/contract/close.md +7 -1
- package/templates/contract/core.md +33 -21
- package/templates/contract/delegation.md +2 -1
- package/templates/contract/implement.md +23 -9
- package/templates/contract/readiness.md +5 -2
- package/templates/contract/review.md +5 -0
- package/templates/contract/validation.md +16 -2
package/src/viewer/public/app.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
normalizeRepoState,
|
|
20
20
|
restoreViewerState,
|
|
21
21
|
selectProject,
|
|
22
|
+
setDetailPresentation,
|
|
22
23
|
setOwnerFilter,
|
|
23
24
|
setRepo,
|
|
24
25
|
setSortKey,
|
|
@@ -36,7 +37,7 @@ import { boardStatuses, isVisible, passesTombstones } from './state.js';
|
|
|
36
37
|
import { html, render as litRender, nothing } from './templates.js';
|
|
37
38
|
import {
|
|
38
39
|
card,
|
|
39
|
-
|
|
40
|
+
detailToolbar,
|
|
40
41
|
sortIndicator,
|
|
41
42
|
specBody,
|
|
42
43
|
stageBlock,
|
|
@@ -50,6 +51,8 @@ export { cssIdent, esc, makeMermaidExpandable, safeHtml } from './security.js';
|
|
|
50
51
|
export { boardStatuses, isVisible, passesTombstones } from './state.js';
|
|
51
52
|
export {
|
|
52
53
|
card,
|
|
54
|
+
detailPresentationControls,
|
|
55
|
+
detailToolbar,
|
|
53
56
|
sortIndicator,
|
|
54
57
|
stageBlock,
|
|
55
58
|
statusSummary,
|
|
@@ -312,6 +315,73 @@ export async function runValidationSubmission({ root, request, onSuccess }) {
|
|
|
312
315
|
return true;
|
|
313
316
|
}
|
|
314
317
|
|
|
318
|
+
export function reopenPanel(status) {
|
|
319
|
+
if (status !== 'done') return nothing;
|
|
320
|
+
return html`<section class="validation-actions" aria-labelledby="reopen-title">
|
|
321
|
+
<div class="validation-copy">
|
|
322
|
+
<span class="validation-kicker">Lifecycle correction</span>
|
|
323
|
+
<h2 id="reopen-title">Reopen completed change</h2>
|
|
324
|
+
<p>Return this change to active work while preserving why its completion was reconsidered.</p>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="reopen-controls">
|
|
327
|
+
<div class="rejection-field">
|
|
328
|
+
<label for="reopen-reason">Reason for reopening</label>
|
|
329
|
+
<input
|
|
330
|
+
id="reopen-reason"
|
|
331
|
+
data-reopen-reason
|
|
332
|
+
type="text"
|
|
333
|
+
placeholder="What requires more work?"
|
|
334
|
+
/>
|
|
335
|
+
<p class="validation-error" role="alert" hidden></p>
|
|
336
|
+
</div>
|
|
337
|
+
<button type="button" class="button button-danger" data-reopen>Reopen change</button>
|
|
338
|
+
</div>
|
|
339
|
+
</section>`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function bindReopenAction({ root, request, onSuccess }) {
|
|
343
|
+
const button = root.querySelector('[data-reopen]');
|
|
344
|
+
if (!button) return;
|
|
345
|
+
button.onclick = async () => {
|
|
346
|
+
const input = root.querySelector('[data-reopen-reason]');
|
|
347
|
+
const reason = input?.value.trim();
|
|
348
|
+
if (!reason) {
|
|
349
|
+
showValidationError(root, 'A reopening reason is required.');
|
|
350
|
+
input?.focus();
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return runValidationSubmission({
|
|
354
|
+
root,
|
|
355
|
+
request: () => request(reason),
|
|
356
|
+
onSuccess,
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function applyDetailPresentation(root = document) {
|
|
362
|
+
const overlay = root.querySelector('#overlay');
|
|
363
|
+
const detail = root.querySelector('#detail');
|
|
364
|
+
if (!overlay || !detail) return;
|
|
365
|
+
overlay.dataset.detailMode = state.detailMode;
|
|
366
|
+
detail.dataset.detailSize = state.detailSize;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function bindDetailPresentation(root = document) {
|
|
370
|
+
root.querySelectorAll('[data-detail-setting]').forEach((button) => {
|
|
371
|
+
button.onclick = () => {
|
|
372
|
+
const setting = button.dataset.detailSetting;
|
|
373
|
+
setDetailPresentation(
|
|
374
|
+
setting === 'mode' ? button.dataset.detailValue : state.detailMode,
|
|
375
|
+
setting === 'size' ? button.dataset.detailValue : state.detailSize,
|
|
376
|
+
);
|
|
377
|
+
applyDetailPresentation(root);
|
|
378
|
+
root.querySelectorAll(`[data-detail-setting="${setting}"]`).forEach((option) => {
|
|
379
|
+
option.setAttribute('aria-pressed', String(option === button));
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
315
385
|
async function submitValidation(id, status, reason) {
|
|
316
386
|
const root = $('#detail');
|
|
317
387
|
await runValidationSubmission({
|
|
@@ -325,6 +395,28 @@ async function submitValidation(id, status, reason) {
|
|
|
325
395
|
});
|
|
326
396
|
}
|
|
327
397
|
|
|
398
|
+
export function resetDetailScroll(
|
|
399
|
+
detail,
|
|
400
|
+
schedule = globalThis.requestAnimationFrame?.bind(globalThis),
|
|
401
|
+
) {
|
|
402
|
+
const reset = () => {
|
|
403
|
+
detail.scrollTo?.({ top: 0, left: 0, behavior: 'instant' });
|
|
404
|
+
detail.scrollTop = 0;
|
|
405
|
+
};
|
|
406
|
+
reset();
|
|
407
|
+
schedule?.(reset);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function scrollToStage(stage) {
|
|
411
|
+
stage.scrollIntoView({ behavior: 'auto', block: 'start' });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function renderOpenedDetail(content) {
|
|
415
|
+
const detail = $('#detail');
|
|
416
|
+
litRender(content, detail);
|
|
417
|
+
resetDetailScroll(detail);
|
|
418
|
+
}
|
|
419
|
+
|
|
328
420
|
function openDetail(id) {
|
|
329
421
|
const c = state.repo.changes.find((x) => String(x.id) === String(id));
|
|
330
422
|
if (!c) return;
|
|
@@ -334,14 +426,11 @@ function openDetail(id) {
|
|
|
334
426
|
? html`<span class="pill ext" data-extdep=${d} style="cursor:pointer">depends on ${d}</span>`
|
|
335
427
|
: html`<span class="pill" data-dep=${d} style="cursor:pointer">depends on #${d}</span>`;
|
|
336
428
|
});
|
|
337
|
-
const pipeline = c.stages.map(
|
|
338
|
-
(s) => html`<span class="stage-chip" data-go=${`stage-${s.key}`}>${s.heading}</span>`,
|
|
339
|
-
);
|
|
340
429
|
const stages = c.stages.map((s) => stageBlock(c, s));
|
|
341
430
|
|
|
342
|
-
|
|
431
|
+
renderOpenedDetail(
|
|
343
432
|
html`
|
|
344
|
-
${
|
|
433
|
+
${detailToolbar(state.detailMode, state.detailSize, c.stages)}
|
|
345
434
|
<h1>${c.title}</h1>
|
|
346
435
|
<div class="detail-meta">
|
|
347
436
|
<span class="pill">#${c.id}</span>
|
|
@@ -352,16 +441,18 @@ function openDetail(id) {
|
|
|
352
441
|
${deps}
|
|
353
442
|
</div>
|
|
354
443
|
${c.status === 'in-validation' ? validationPanel() : nothing}
|
|
355
|
-
|
|
444
|
+
${reopenPanel(c.status)}
|
|
356
445
|
${stages}
|
|
357
446
|
<div id="git-section"></div>`,
|
|
358
|
-
$('#detail'),
|
|
359
447
|
);
|
|
360
448
|
|
|
361
449
|
resetValidationState($('#detail'));
|
|
362
450
|
|
|
363
451
|
const overlay = $('#overlay');
|
|
364
452
|
overlay.classList.remove('hidden');
|
|
453
|
+
document.documentElement.classList.add('detail-open');
|
|
454
|
+
applyDetailPresentation();
|
|
455
|
+
bindDetailPresentation();
|
|
365
456
|
$('#detail').querySelector('.close').onclick = closeDetail;
|
|
366
457
|
const accept = $('#detail').querySelector('[data-validation="pass"]');
|
|
367
458
|
if (accept) accept.onclick = () => submitValidation(c.id, 'done');
|
|
@@ -378,14 +469,22 @@ function openDetail(id) {
|
|
|
378
469
|
submitValidation(c.id, 'in-progress', reason);
|
|
379
470
|
};
|
|
380
471
|
}
|
|
472
|
+
bindReopenAction({
|
|
473
|
+
root: $('#detail'),
|
|
474
|
+
request: (reason) => postStatus(state.currentProject, c.id, 'in-progress', reason),
|
|
475
|
+
onSuccess: async () => {
|
|
476
|
+
invalidateCache();
|
|
477
|
+
await load();
|
|
478
|
+
openDetail(c.id);
|
|
479
|
+
},
|
|
480
|
+
});
|
|
381
481
|
overlay.onclick = (e) => {
|
|
382
482
|
if (e.target === overlay) closeDetail();
|
|
383
483
|
};
|
|
384
484
|
$('#detail')
|
|
385
485
|
.querySelectorAll('[data-go]')
|
|
386
486
|
.forEach((el) => {
|
|
387
|
-
el.onclick = () =>
|
|
388
|
-
$(`#${el.dataset.go}`).scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
487
|
+
el.onclick = () => scrollToStage($(`#${el.dataset.go}`));
|
|
389
488
|
});
|
|
390
489
|
$('#detail')
|
|
391
490
|
.querySelectorAll('[data-dep]')
|
|
@@ -441,6 +540,7 @@ async function loadGitRefs(id) {
|
|
|
441
540
|
|
|
442
541
|
function closeDetail() {
|
|
443
542
|
$('#overlay').classList.add('hidden');
|
|
543
|
+
document.documentElement.classList.remove('detail-open');
|
|
444
544
|
}
|
|
445
545
|
|
|
446
546
|
let diagramLightbox = null;
|
|
@@ -584,9 +684,9 @@ function renderSpecs() {
|
|
|
584
684
|
}
|
|
585
685
|
|
|
586
686
|
function openSpec(s) {
|
|
587
|
-
|
|
687
|
+
renderOpenedDetail(
|
|
588
688
|
html`
|
|
589
|
-
${
|
|
689
|
+
${detailToolbar(state.detailMode, state.detailSize)}
|
|
590
690
|
<h1>${s.title}</h1>
|
|
591
691
|
<div class="detail-meta">
|
|
592
692
|
<span class="pill">spec</span>
|
|
@@ -594,10 +694,12 @@ function openSpec(s) {
|
|
|
594
694
|
${(s.tags || []).map((t) => html`<span class="pill">${t}</span>`)}
|
|
595
695
|
</div>
|
|
596
696
|
${specBody(s.body)}`,
|
|
597
|
-
$('#detail'),
|
|
598
697
|
);
|
|
599
698
|
const overlay = $('#overlay');
|
|
600
699
|
overlay.classList.remove('hidden');
|
|
700
|
+
document.documentElement.classList.add('detail-open');
|
|
701
|
+
applyDetailPresentation();
|
|
702
|
+
bindDetailPresentation();
|
|
601
703
|
const detail = $('#detail');
|
|
602
704
|
detail.querySelector('.close').onclick = closeDetail;
|
|
603
705
|
overlay.onclick = (e) => {
|
|
@@ -23,12 +23,34 @@
|
|
|
23
23
|
--status-discarded: #9aa0aa;
|
|
24
24
|
--status-muted: #8b929e;
|
|
25
25
|
font-family: "Avenir Next", Avenir, ui-sans-serif, sans-serif;
|
|
26
|
+
color-scheme: dark;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
* {
|
|
29
30
|
box-sizing: border-box;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
html,
|
|
34
|
+
body {
|
|
35
|
+
scrollbar-width: thin;
|
|
36
|
+
scrollbar-color: #3a414e transparent;
|
|
37
|
+
}
|
|
38
|
+
html::-webkit-scrollbar,
|
|
39
|
+
body::-webkit-scrollbar {
|
|
40
|
+
width: 8px;
|
|
41
|
+
height: 8px;
|
|
42
|
+
}
|
|
43
|
+
html::-webkit-scrollbar-track,
|
|
44
|
+
body::-webkit-scrollbar-track {
|
|
45
|
+
background: transparent;
|
|
46
|
+
}
|
|
47
|
+
html::-webkit-scrollbar-thumb,
|
|
48
|
+
body::-webkit-scrollbar-thumb {
|
|
49
|
+
background: #3a414e;
|
|
50
|
+
border: 2px solid var(--bg);
|
|
51
|
+
border-radius: 999px;
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
body {
|
|
33
55
|
margin: 0;
|
|
34
56
|
background: var(--bg);
|
|
@@ -527,16 +549,31 @@ body {
|
|
|
527
549
|
padding: 16px;
|
|
528
550
|
overflow-x: auto;
|
|
529
551
|
align-items: flex-start;
|
|
552
|
+
}
|
|
553
|
+
.board,
|
|
554
|
+
.detail,
|
|
555
|
+
.detail pre,
|
|
556
|
+
.detail table {
|
|
530
557
|
scrollbar-width: thin;
|
|
531
558
|
scrollbar-color: #3a414e transparent;
|
|
532
559
|
}
|
|
533
|
-
.board::-webkit-scrollbar
|
|
560
|
+
.board::-webkit-scrollbar,
|
|
561
|
+
.detail::-webkit-scrollbar,
|
|
562
|
+
.detail pre::-webkit-scrollbar,
|
|
563
|
+
.detail table::-webkit-scrollbar {
|
|
564
|
+
width: 8px;
|
|
534
565
|
height: 8px;
|
|
535
566
|
}
|
|
536
|
-
.board::-webkit-scrollbar-track
|
|
567
|
+
.board::-webkit-scrollbar-track,
|
|
568
|
+
.detail::-webkit-scrollbar-track,
|
|
569
|
+
.detail pre::-webkit-scrollbar-track,
|
|
570
|
+
.detail table::-webkit-scrollbar-track {
|
|
537
571
|
background: transparent;
|
|
538
572
|
}
|
|
539
|
-
.board::-webkit-scrollbar-thumb
|
|
573
|
+
.board::-webkit-scrollbar-thumb,
|
|
574
|
+
.detail::-webkit-scrollbar-thumb,
|
|
575
|
+
.detail pre::-webkit-scrollbar-thumb,
|
|
576
|
+
.detail table::-webkit-scrollbar-thumb {
|
|
540
577
|
background: #3a414e;
|
|
541
578
|
border: 2px solid var(--bg);
|
|
542
579
|
border-radius: 999px;
|
|
@@ -545,10 +582,10 @@ body {
|
|
|
545
582
|
background: var(--panel);
|
|
546
583
|
border: 1px solid var(--line);
|
|
547
584
|
border-radius: 12px;
|
|
548
|
-
width: clamp(
|
|
549
|
-
min-width:
|
|
550
|
-
max-width:
|
|
551
|
-
flex: 0 0 clamp(
|
|
585
|
+
width: clamp(190px, calc((100vw - 140px) / 6), 400px);
|
|
586
|
+
min-width: 190px;
|
|
587
|
+
max-width: 400px;
|
|
588
|
+
flex: 0 0 clamp(190px, calc((100vw - 140px) / 6), 400px);
|
|
552
589
|
}
|
|
553
590
|
.column-head {
|
|
554
591
|
display: flex;
|
|
@@ -593,6 +630,8 @@ body {
|
|
|
593
630
|
font-family: ui-monospace, monospace;
|
|
594
631
|
color: var(--muted);
|
|
595
632
|
font-size: 12px;
|
|
633
|
+
min-width: 0;
|
|
634
|
+
overflow-wrap: anywhere;
|
|
596
635
|
}
|
|
597
636
|
.type-tag {
|
|
598
637
|
font-size: 10px;
|
|
@@ -625,6 +664,7 @@ body {
|
|
|
625
664
|
.card-title {
|
|
626
665
|
font-weight: 600;
|
|
627
666
|
line-height: 1.3;
|
|
667
|
+
overflow-wrap: anywhere;
|
|
628
668
|
}
|
|
629
669
|
.progress {
|
|
630
670
|
margin-top: 9px;
|
|
@@ -640,6 +680,7 @@ body {
|
|
|
640
680
|
}
|
|
641
681
|
.card-meta {
|
|
642
682
|
display: flex;
|
|
683
|
+
flex-wrap: wrap;
|
|
643
684
|
gap: 10px;
|
|
644
685
|
margin-top: 7px;
|
|
645
686
|
font-size: 11px;
|
|
@@ -689,6 +730,13 @@ body {
|
|
|
689
730
|
gap: 10px;
|
|
690
731
|
margin-top: 15px;
|
|
691
732
|
}
|
|
733
|
+
.reopen-controls {
|
|
734
|
+
display: grid;
|
|
735
|
+
grid-template-columns: minmax(190px, 1fr) auto;
|
|
736
|
+
align-items: end;
|
|
737
|
+
gap: 10px;
|
|
738
|
+
margin-top: 15px;
|
|
739
|
+
}
|
|
692
740
|
.rejection-field {
|
|
693
741
|
display: flex;
|
|
694
742
|
flex-direction: column;
|
|
@@ -750,6 +798,7 @@ body {
|
|
|
750
798
|
}
|
|
751
799
|
.owner {
|
|
752
800
|
font-weight: 600;
|
|
801
|
+
overflow-wrap: anywhere;
|
|
753
802
|
}
|
|
754
803
|
.card.archived {
|
|
755
804
|
opacity: 0.55;
|
|
@@ -847,21 +896,107 @@ body {
|
|
|
847
896
|
}
|
|
848
897
|
|
|
849
898
|
/* Overlay + detail */
|
|
899
|
+
.detail-open {
|
|
900
|
+
overflow: hidden;
|
|
901
|
+
}
|
|
850
902
|
.overlay {
|
|
851
903
|
position: fixed;
|
|
852
904
|
inset: 0;
|
|
853
905
|
background: rgba(0, 0, 0, 0.55);
|
|
854
906
|
display: flex;
|
|
855
907
|
justify-content: flex-end;
|
|
908
|
+
align-items: stretch;
|
|
856
909
|
z-index: 20;
|
|
857
910
|
}
|
|
858
911
|
.detail {
|
|
859
|
-
width:
|
|
912
|
+
--detail-width: 960px;
|
|
913
|
+
container-type: inline-size;
|
|
914
|
+
position: relative;
|
|
915
|
+
width: min(var(--detail-width), calc(100vw - 48px));
|
|
860
916
|
height: 100%;
|
|
861
917
|
background: var(--bg);
|
|
862
918
|
border-left: 1px solid var(--line);
|
|
863
919
|
overflow-y: auto;
|
|
920
|
+
overflow-anchor: none;
|
|
864
921
|
padding: 22px 26px 60px;
|
|
922
|
+
box-shadow: -24px 0 70px rgba(0, 0, 0, 0.28);
|
|
923
|
+
}
|
|
924
|
+
.detail[data-detail-size="compact"] {
|
|
925
|
+
--detail-width: 720px;
|
|
926
|
+
}
|
|
927
|
+
.detail[data-detail-size="wide"] {
|
|
928
|
+
--detail-width: 960px;
|
|
929
|
+
}
|
|
930
|
+
.detail[data-detail-size="full"] {
|
|
931
|
+
--detail-width: 1280px;
|
|
932
|
+
}
|
|
933
|
+
.overlay[data-detail-mode="floating"] {
|
|
934
|
+
align-items: center;
|
|
935
|
+
justify-content: center;
|
|
936
|
+
padding: 28px;
|
|
937
|
+
}
|
|
938
|
+
.overlay[data-detail-mode="floating"] .detail {
|
|
939
|
+
width: min(var(--detail-width), calc(100vw - 56px));
|
|
940
|
+
height: min(92vh, calc(100vh - 56px));
|
|
941
|
+
border: 1px solid var(--line);
|
|
942
|
+
border-radius: 16px;
|
|
943
|
+
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.48);
|
|
944
|
+
}
|
|
945
|
+
.detail-toolbar {
|
|
946
|
+
position: sticky;
|
|
947
|
+
top: -22px;
|
|
948
|
+
z-index: 3;
|
|
949
|
+
display: grid;
|
|
950
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
951
|
+
align-items: center;
|
|
952
|
+
gap: 12px;
|
|
953
|
+
margin: -22px -26px 20px;
|
|
954
|
+
padding: 14px 26px 10px;
|
|
955
|
+
border-bottom: 1px solid color-mix(in srgb, var(--line) 72%, transparent);
|
|
956
|
+
background: color-mix(in srgb, var(--bg) 92%, transparent);
|
|
957
|
+
backdrop-filter: blur(14px);
|
|
958
|
+
}
|
|
959
|
+
.detail-presentation {
|
|
960
|
+
display: flex;
|
|
961
|
+
flex-wrap: wrap;
|
|
962
|
+
gap: 8px;
|
|
963
|
+
width: fit-content;
|
|
964
|
+
}
|
|
965
|
+
.detail-choice {
|
|
966
|
+
display: inline-flex;
|
|
967
|
+
padding: 3px;
|
|
968
|
+
border: 1px solid var(--line);
|
|
969
|
+
border-radius: 9px;
|
|
970
|
+
background: var(--panel);
|
|
971
|
+
}
|
|
972
|
+
.detail-option {
|
|
973
|
+
min-height: 28px;
|
|
974
|
+
padding: 4px 9px;
|
|
975
|
+
border: 0;
|
|
976
|
+
border-radius: 6px;
|
|
977
|
+
background: transparent;
|
|
978
|
+
color: var(--muted);
|
|
979
|
+
font: inherit;
|
|
980
|
+
font-size: 11px;
|
|
981
|
+
cursor: pointer;
|
|
982
|
+
}
|
|
983
|
+
.detail-option[aria-pressed="true"] {
|
|
984
|
+
background: var(--panel-2);
|
|
985
|
+
color: var(--text);
|
|
986
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 42%, var(--line));
|
|
987
|
+
}
|
|
988
|
+
.detail-option:focus-visible {
|
|
989
|
+
outline: 2px solid var(--accent);
|
|
990
|
+
outline-offset: 2px;
|
|
991
|
+
}
|
|
992
|
+
.detail pre {
|
|
993
|
+
max-width: 100%;
|
|
994
|
+
overflow-x: auto;
|
|
995
|
+
}
|
|
996
|
+
.detail table {
|
|
997
|
+
display: block;
|
|
998
|
+
max-width: 100%;
|
|
999
|
+
overflow-x: auto;
|
|
865
1000
|
}
|
|
866
1001
|
.detail h1 {
|
|
867
1002
|
font-size: 22px;
|
|
@@ -909,19 +1044,27 @@ body {
|
|
|
909
1044
|
outline-offset: 3px;
|
|
910
1045
|
}
|
|
911
1046
|
.close {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1047
|
+
justify-self: end;
|
|
1048
|
+
}
|
|
1049
|
+
.detail-toolbar-close {
|
|
1050
|
+
grid-column: 3;
|
|
915
1051
|
}
|
|
916
1052
|
|
|
917
1053
|
.pipeline {
|
|
918
1054
|
display: flex;
|
|
919
|
-
|
|
1055
|
+
min-width: 0;
|
|
1056
|
+
flex-wrap: nowrap;
|
|
920
1057
|
gap: 6px;
|
|
921
|
-
margin:
|
|
1058
|
+
margin: 0;
|
|
1059
|
+
padding: 3px 2px;
|
|
1060
|
+
overflow-x: auto;
|
|
1061
|
+
overscroll-behavior-x: contain;
|
|
1062
|
+
scrollbar-width: thin;
|
|
922
1063
|
}
|
|
923
1064
|
.stage-chip {
|
|
1065
|
+
flex: 0 0 auto;
|
|
924
1066
|
font-size: 12px;
|
|
1067
|
+
font-family: inherit;
|
|
925
1068
|
padding: 5px 11px;
|
|
926
1069
|
border-radius: 999px;
|
|
927
1070
|
background: var(--panel);
|
|
@@ -937,6 +1080,7 @@ body {
|
|
|
937
1080
|
.stage {
|
|
938
1081
|
border-top: 1px solid var(--line);
|
|
939
1082
|
padding-top: 14px;
|
|
1083
|
+
scroll-margin-top: 72px;
|
|
940
1084
|
margin-top: 18px;
|
|
941
1085
|
}
|
|
942
1086
|
.stage h2 {
|
|
@@ -1235,7 +1379,8 @@ body {
|
|
|
1235
1379
|
width: 100%;
|
|
1236
1380
|
flex: 0 0 auto;
|
|
1237
1381
|
}
|
|
1238
|
-
.validation-controls
|
|
1382
|
+
.validation-controls,
|
|
1383
|
+
.reopen-controls {
|
|
1239
1384
|
grid-template-columns: 1fr;
|
|
1240
1385
|
align-items: stretch;
|
|
1241
1386
|
}
|
|
@@ -1255,7 +1400,32 @@ body {
|
|
|
1255
1400
|
}
|
|
1256
1401
|
.detail {
|
|
1257
1402
|
width: 100vw;
|
|
1403
|
+
max-width: none;
|
|
1404
|
+
height: 100vh;
|
|
1405
|
+
max-height: none;
|
|
1258
1406
|
padding: 18px 16px 60px;
|
|
1407
|
+
border: 0;
|
|
1408
|
+
border-radius: 0;
|
|
1409
|
+
}
|
|
1410
|
+
.overlay[data-detail-mode] {
|
|
1411
|
+
align-items: stretch;
|
|
1412
|
+
justify-content: stretch;
|
|
1413
|
+
padding: 0;
|
|
1414
|
+
}
|
|
1415
|
+
.overlay[data-detail-mode] .detail {
|
|
1416
|
+
width: 100vw;
|
|
1417
|
+
height: 100vh;
|
|
1418
|
+
max-height: none;
|
|
1419
|
+
border: 0;
|
|
1420
|
+
border-radius: 0;
|
|
1421
|
+
}
|
|
1422
|
+
.detail-toolbar {
|
|
1423
|
+
top: -18px;
|
|
1424
|
+
margin: -18px -16px 16px;
|
|
1425
|
+
padding: 10px 16px 8px;
|
|
1426
|
+
}
|
|
1427
|
+
.detail-option {
|
|
1428
|
+
padding-inline: 7px;
|
|
1259
1429
|
}
|
|
1260
1430
|
.grid th,
|
|
1261
1431
|
.grid td {
|
|
@@ -1263,6 +1433,27 @@ body {
|
|
|
1263
1433
|
}
|
|
1264
1434
|
}
|
|
1265
1435
|
|
|
1436
|
+
/* Detail toolbar stacks its stage navigation onto its own row whenever the
|
|
1437
|
+
detail panel itself lacks room, independent of the surrounding viewport —
|
|
1438
|
+
this also covers the "Compact" width preset, not just narrow screens. */
|
|
1439
|
+
@container (max-width: 680px) {
|
|
1440
|
+
.detail-toolbar {
|
|
1441
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
1442
|
+
gap: 8px;
|
|
1443
|
+
}
|
|
1444
|
+
.detail-toolbar-close {
|
|
1445
|
+
grid-column: 2;
|
|
1446
|
+
grid-row: 1;
|
|
1447
|
+
}
|
|
1448
|
+
.pipeline {
|
|
1449
|
+
grid-column: 1 / -1;
|
|
1450
|
+
grid-row: 2;
|
|
1451
|
+
}
|
|
1452
|
+
.stage {
|
|
1453
|
+
scroll-margin-top: 126px;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1266
1457
|
/* config editor tabs and form (113924) */
|
|
1267
1458
|
.config-section {
|
|
1268
1459
|
border-bottom: 1px solid var(--line);
|
|
@@ -36,6 +36,58 @@ export function closeButton(label = 'Close detail', extraClass = '') {
|
|
|
36
36
|
</button>`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function detailPresentationControls(mode = 'side', size = 'wide') {
|
|
40
|
+
const options = (values, selected, attr) =>
|
|
41
|
+
values.map(
|
|
42
|
+
([value, label]) => html`<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="detail-option"
|
|
45
|
+
data-detail-setting=${attr}
|
|
46
|
+
data-detail-value=${value}
|
|
47
|
+
aria-pressed=${String(selected === value)}
|
|
48
|
+
>${label}</button>`,
|
|
49
|
+
);
|
|
50
|
+
return html`<div class="detail-presentation" aria-label="Detail presentation">
|
|
51
|
+
<div class="detail-choice" role="group" aria-label="Layout">
|
|
52
|
+
${options(
|
|
53
|
+
[
|
|
54
|
+
['side', 'Side panel'],
|
|
55
|
+
['floating', 'Floating modal'],
|
|
56
|
+
],
|
|
57
|
+
mode,
|
|
58
|
+
'mode',
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
<div class="detail-choice" role="group" aria-label="Width">
|
|
62
|
+
${options(
|
|
63
|
+
[
|
|
64
|
+
['compact', 'Compact'],
|
|
65
|
+
['wide', 'Wide'],
|
|
66
|
+
['full', 'Full'],
|
|
67
|
+
],
|
|
68
|
+
size,
|
|
69
|
+
'size',
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function detailToolbar(mode = 'side', size = 'wide', stages = []) {
|
|
76
|
+
const navigation = stages.length
|
|
77
|
+
? html`<nav class="pipeline" aria-label="Change sections">
|
|
78
|
+
${stages.map(
|
|
79
|
+
(stage) =>
|
|
80
|
+
html`<button type="button" class="stage-chip" data-go=${`stage-${stage.key}`}>${stage.heading}</button>`,
|
|
81
|
+
)}
|
|
82
|
+
</nav>`
|
|
83
|
+
: nothing;
|
|
84
|
+
return html`<div class="detail-toolbar" aria-label="Detail tools">
|
|
85
|
+
${detailPresentationControls(mode, size)}
|
|
86
|
+
${navigation}
|
|
87
|
+
${closeButton('Close detail', 'detail-toolbar-close')}
|
|
88
|
+
</div>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
39
91
|
export function validationPanel() {
|
|
40
92
|
return html`<section class="validation-actions" aria-labelledby="validation-title">
|
|
41
93
|
<div class="validation-copy">
|
|
@@ -39,6 +39,12 @@ not necessarily that a spec was created.
|
|
|
39
39
|
The graduation link remains derivable from the Log marker `graduado a spec`,
|
|
40
40
|
which carries the spec link, rather than from the boolean flag.
|
|
41
41
|
|
|
42
|
+
After `--into` or `--skip`, create one final closure commit that coalesces any
|
|
43
|
+
pending `in-review → in-validation → done` ledger updates with the graduation
|
|
44
|
+
decision and durable spec edit. Do not create separate commits whose only
|
|
45
|
+
content is one of those transitions. If no lifecycle state is pending, the
|
|
46
|
+
graduation or skip itself remains the meaningful closure evidence.
|
|
47
|
+
|
|
42
48
|
Operational inspection and visibility:
|
|
43
49
|
|
|
44
50
|
- `changeledger list [--status S] [--type T] [--json]`
|
|
@@ -47,4 +53,4 @@ Operational inspection and visibility:
|
|
|
47
53
|
|
|
48
54
|
Use Mermaid where it communicates persistent relationships better than prose.
|
|
49
55
|
After closure, share a brief retrospective. New work needs a newly authorized
|
|
50
|
-
change;
|
|
56
|
+
change; graduated, skipped, archived or released work never reopens.
|