design-playbook 0.7.0 → 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/mcp/preview/browser.py +16 -8
- package/mcp/preview/confirm.py +4 -76
- package/mcp/preview/control.css +482 -0
- package/mcp/preview/control.html +67 -0
- package/mcp/preview/control.js +669 -0
- package/mcp/preview/control.py +29 -1146
- package/mcp/preview/test_browser_control.py +36 -32
- package/package.json +1 -1
package/mcp/preview/browser.py
CHANGED
|
@@ -593,14 +593,22 @@ def _collect_via_browser(
|
|
|
593
593
|
validated = session.validate(posted_round, posted_token)
|
|
594
594
|
if not validated:
|
|
595
595
|
# Fail closed: missing / reused / mismatched token -> NOT confirmed.
|
|
596
|
-
result
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
596
|
+
# First-decision-wins also guards the shared ``result`` slot:
|
|
597
|
+
# once a valid POST owns the result, a later rejected POST (replay,
|
|
598
|
+
# mismatch) must NOT clobber it — otherwise a double-click or
|
|
599
|
+
# browser retry after a valid confirm overwrites the confirmed
|
|
600
|
+
# result with an aborted/rejected one and G5 fails despite the
|
|
601
|
+
# user's confirm. Only mutate ``result`` before any valid
|
|
602
|
+
# decision has landed (session not yet locked).
|
|
603
|
+
if not session.locked:
|
|
604
|
+
result = with_prototype_hash({
|
|
605
|
+
"choice": "",
|
|
606
|
+
"feedback": feedback,
|
|
607
|
+
"aborted": True,
|
|
608
|
+
"anchors": anchors,
|
|
609
|
+
"rejected": True,
|
|
610
|
+
"rejection": session.last_rejection,
|
|
611
|
+
})
|
|
604
612
|
else:
|
|
605
613
|
result = with_prototype_hash({
|
|
606
614
|
"choice": choice,
|
package/mcp/preview/confirm.py
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
"""ADR-0008 floor logic
|
|
1
|
+
"""ADR-0008 floor logic, G5 trust token, and prototype target resolution.
|
|
2
2
|
|
|
3
3
|
Sibling module split from server.py; behavior unchanged. Holds the
|
|
4
|
-
feedback-floor check,
|
|
5
|
-
and the ``--self-check``
|
|
4
|
+
feedback-floor check, the G5 one-time decision token + first-decision-wins
|
|
5
|
+
session, the prototype html digest primitive, and the ``--self-check``
|
|
6
|
+
floor cases.
|
|
6
7
|
"""
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import hashlib
|
|
10
|
-
import json
|
|
11
11
|
import secrets
|
|
12
12
|
import sys
|
|
13
13
|
import threading
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
|
-
from util import _now_iso
|
|
18
|
-
|
|
19
17
|
|
|
20
18
|
def prototype_html_digest(raw: bytes) -> str:
|
|
21
19
|
"""SHA-256 of prototype bytes with newlines normalized to LF.
|
|
@@ -56,76 +54,6 @@ def _ensure_prototype(path_arg: str | None, html: str | None, round_n: int,
|
|
|
56
54
|
return target
|
|
57
55
|
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
def _append_log(preview_dir: Path, *, round_n: int, report_ref: str,
|
|
61
|
-
feedback: str, aborted: bool, selected: list[str],
|
|
62
|
-
anchors: list[dict[str, Any]] | None = None,
|
|
63
|
-
floor_pass: bool | None = None,
|
|
64
|
-
floor_failure: str = "",
|
|
65
|
-
rejected: bool = False,
|
|
66
|
-
rejection: str = "") -> None:
|
|
67
|
-
preview_dir.mkdir(parents=True, exist_ok=True)
|
|
68
|
-
log_path = preview_dir / "log.md"
|
|
69
|
-
if not log_path.is_file():
|
|
70
|
-
log_path.write_text("# preview log\n", encoding="utf-8")
|
|
71
|
-
block = (
|
|
72
|
-
f"\n## round {round_n}\n"
|
|
73
|
-
f"- report_ref: {report_ref}\n"
|
|
74
|
-
f"- timestamp: {_now_iso()}\n"
|
|
75
|
-
f"- feedback: {feedback or ''}\n"
|
|
76
|
-
f"- selected: {', '.join(selected) if selected else ''}\n"
|
|
77
|
-
f"- aborted: {str(aborted).lower()}\n"
|
|
78
|
-
f"- anchors: {len(anchors or [])}\n"
|
|
79
|
-
)
|
|
80
|
-
if floor_pass is not None:
|
|
81
|
-
block += f"- floor_pass: {str(floor_pass).lower()}\n"
|
|
82
|
-
if floor_failure:
|
|
83
|
-
block += f"- floor_failure: {floor_failure}\n"
|
|
84
|
-
# LOW-4 (secure-ship-0.4.4): persist G5 fail-closed rejections (forged
|
|
85
|
-
# token / replay / round mismatch) to log.md so the event is auditable
|
|
86
|
-
# on disk, not just in the ephemeral MCP payload. Only emitted when a
|
|
87
|
-
# decision was actually rejected — a normal confirm/revise/abort leaves
|
|
88
|
-
# no rejection line.
|
|
89
|
-
if rejected:
|
|
90
|
-
block += "- rejected: true\n"
|
|
91
|
-
if rejection:
|
|
92
|
-
block += f"- rejection: {rejection}\n"
|
|
93
|
-
if anchors:
|
|
94
|
-
for i, a in enumerate(anchors, 1):
|
|
95
|
-
sel = a.get("selector") or ""
|
|
96
|
-
note = a.get("comment") or ""
|
|
97
|
-
label = a.get("label") or ""
|
|
98
|
-
block += f" - [{i}] {sel} | {label} | {note}\n"
|
|
99
|
-
with log_path.open("a", encoding="utf-8") as fh:
|
|
100
|
-
fh.write(block)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _write_confirm(preview_dir: Path, *, round_n: int, report_ref: str,
|
|
105
|
-
selected: list[str], feedback: str,
|
|
106
|
-
confirmed: bool, floor_pass: bool,
|
|
107
|
-
prototype_html_hash: str,
|
|
108
|
-
floor_failure: str = "") -> Path:
|
|
109
|
-
record = {
|
|
110
|
-
"round": round_n,
|
|
111
|
-
"report_ref": report_ref,
|
|
112
|
-
"confirmed": confirmed,
|
|
113
|
-
"floor_pass": floor_pass,
|
|
114
|
-
"selected_options": selected,
|
|
115
|
-
"feedback": feedback,
|
|
116
|
-
"timestamp": _now_iso(),
|
|
117
|
-
"prototype_path": f"preview/round-{round_n}.html",
|
|
118
|
-
"prototype_html_hash": prototype_html_hash,
|
|
119
|
-
}
|
|
120
|
-
if floor_failure:
|
|
121
|
-
record["floor_failure"] = floor_failure
|
|
122
|
-
out = preview_dir / f"confirm-round-{round_n}.json"
|
|
123
|
-
out.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n",
|
|
124
|
-
encoding="utf-8")
|
|
125
|
-
return out
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
57
|
def _check_feedback_floor(feedback: str,
|
|
130
58
|
anchors: list[dict[str, Any]]) -> tuple[bool, str]:
|
|
131
59
|
"""ADR-0008 preview feedback floor (structural, machine-checkable).
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
#dpb-preview-bar,
|
|
2
|
+
#dpb-float-root {
|
|
3
|
+
--dpb-bg: #1e2330;
|
|
4
|
+
--dpb-surface: #252b38;
|
|
5
|
+
--dpb-elev: #2a3140;
|
|
6
|
+
--dpb-line: #2c3444;
|
|
7
|
+
--dpb-line-strong: #3d475a;
|
|
8
|
+
--dpb-ink: #f3f4f6;
|
|
9
|
+
--dpb-muted: #9aa3b2;
|
|
10
|
+
--dpb-soft: #c5cbd6;
|
|
11
|
+
--dpb-accent: #14b8a6;
|
|
12
|
+
--dpb-accent-ink: #ffffff;
|
|
13
|
+
--dpb-accent-deep: #0f766e;
|
|
14
|
+
--dpb-danger: #f87171;
|
|
15
|
+
--dpb-chrome: #131722;
|
|
16
|
+
--dpb-hover: #3a4150;
|
|
17
|
+
--dpb-secondary-hover: #252b39;
|
|
18
|
+
--dpb-border-hover: #5b6578;
|
|
19
|
+
--dpb-border-hover-strong: #6b7588;
|
|
20
|
+
--dpb-accent-hover: #2dd4bf;
|
|
21
|
+
--dpb-accent-contrast: #042f2e;
|
|
22
|
+
--dpb-accent-soft: #99f6e4;
|
|
23
|
+
--dpb-ready: #5eead4;
|
|
24
|
+
--dpb-placeholder: #7d8698;
|
|
25
|
+
--dpb-input-focus: #12161e;
|
|
26
|
+
--dpb-quiet-hover: rgba(255,255,255,.04);
|
|
27
|
+
--dpb-note-bg: #1f2430;
|
|
28
|
+
--dpb-note-ink: #f3f4f6;
|
|
29
|
+
--dpb-radius: 10px;
|
|
30
|
+
--dpb-control-h: 40px;
|
|
31
|
+
}
|
|
32
|
+
#dpb-preview-bar {
|
|
33
|
+
font: 13px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
34
|
+
color: var(--dpb-ink);
|
|
35
|
+
}
|
|
36
|
+
#dpb-preview-bar[data-theme="light"],
|
|
37
|
+
#dpb-float-root[data-theme="light"] {
|
|
38
|
+
--dpb-bg: #ffffff;
|
|
39
|
+
--dpb-surface: #f9fafb;
|
|
40
|
+
--dpb-elev: #f3f4f6;
|
|
41
|
+
--dpb-line: #e5e7eb;
|
|
42
|
+
--dpb-line-strong: #d1d5db;
|
|
43
|
+
--dpb-ink: #111827;
|
|
44
|
+
--dpb-muted: #6b7280;
|
|
45
|
+
--dpb-soft: #4b5563;
|
|
46
|
+
--dpb-accent: #0f766e;
|
|
47
|
+
--dpb-accent-ink: #ffffff;
|
|
48
|
+
--dpb-accent-deep: #134e4a;
|
|
49
|
+
--dpb-danger: #ef4444;
|
|
50
|
+
--dpb-chrome: #f3f4f6;
|
|
51
|
+
--dpb-hover: #e5e7eb;
|
|
52
|
+
--dpb-secondary-hover: #e5e7eb;
|
|
53
|
+
--dpb-border-hover: #9ca3af;
|
|
54
|
+
--dpb-border-hover-strong: #6b7280;
|
|
55
|
+
--dpb-accent-hover: #115e59;
|
|
56
|
+
--dpb-accent-contrast: #ffffff;
|
|
57
|
+
--dpb-accent-soft: #115e59;
|
|
58
|
+
--dpb-ready: #0f766e;
|
|
59
|
+
--dpb-placeholder: #6b7280;
|
|
60
|
+
--dpb-input-focus: #ffffff;
|
|
61
|
+
--dpb-quiet-hover: rgba(17,24,39,.05);
|
|
62
|
+
--dpb-note-bg: #ffffff;
|
|
63
|
+
--dpb-note-ink: #111827;
|
|
64
|
+
}
|
|
65
|
+
/* ---- floating pill (default state) ---- */
|
|
66
|
+
#dpb-preview-bar {
|
|
67
|
+
position: fixed;
|
|
68
|
+
right: 24px;
|
|
69
|
+
bottom: 24px;
|
|
70
|
+
left: auto;
|
|
71
|
+
top: auto;
|
|
72
|
+
z-index: 1000;
|
|
73
|
+
background: transparent;
|
|
74
|
+
border: 0;
|
|
75
|
+
padding: 0;
|
|
76
|
+
border-radius: 0;
|
|
77
|
+
border-top: 0;
|
|
78
|
+
}
|
|
79
|
+
#dpb-preview-bar .dpb-pill {
|
|
80
|
+
display: inline-flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 8px;
|
|
83
|
+
padding: 8px 8px 8px 16px;
|
|
84
|
+
background: var(--dpb-bg);
|
|
85
|
+
border: 1px solid var(--dpb-line);
|
|
86
|
+
border-radius: 999px;
|
|
87
|
+
box-shadow: 0 8px 28px rgba(0,0,0,.15), 0 2px 6px rgba(0,0,0,.1);
|
|
88
|
+
}
|
|
89
|
+
#dpb-preview-bar .dpb-pill .dpb-pill-info {
|
|
90
|
+
display: inline-flex; align-items: center; gap: 8px; max-width: 260px;
|
|
91
|
+
}
|
|
92
|
+
#dpb-preview-bar .dpb-round {
|
|
93
|
+
flex: 0 0 auto; display: inline-flex; align-items: center; height: 22px;
|
|
94
|
+
padding: 0 8px; border-radius: 999px; font-size: 11px; font-weight: 700;
|
|
95
|
+
font-variant-numeric: tabular-nums; color: var(--dpb-accent-soft);
|
|
96
|
+
background: rgba(20,184,166,.14); border: 1px solid rgba(20,184,166,.35);
|
|
97
|
+
}
|
|
98
|
+
#dpb-preview-bar .dpb-pill .dpb-summary {
|
|
99
|
+
margin: 0; min-width: 0; color: var(--dpb-soft);
|
|
100
|
+
font-size: 12.5px; font-weight: 500; overflow: hidden;
|
|
101
|
+
text-overflow: ellipsis; white-space: nowrap;
|
|
102
|
+
}
|
|
103
|
+
#dpb-preview-bar .dpb-pill .dpb-pill-actions {
|
|
104
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
105
|
+
}
|
|
106
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-ghost {
|
|
107
|
+
appearance: none; cursor: pointer; height: 32px; padding: 0 12px;
|
|
108
|
+
border-radius: 999px; border: 1px solid var(--dpb-line-strong);
|
|
109
|
+
background: var(--dpb-surface); color: var(--dpb-soft);
|
|
110
|
+
font: 650 12.5px/1 system-ui, sans-serif;
|
|
111
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
112
|
+
transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
|
|
113
|
+
}
|
|
114
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-ghost:hover {
|
|
115
|
+
background: var(--dpb-elev); border-color: var(--dpb-border-hover); color: var(--dpb-ink);
|
|
116
|
+
}
|
|
117
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-ghost:focus-visible {
|
|
118
|
+
outline: 2px solid var(--dpb-accent); outline-offset: 2px;
|
|
119
|
+
}
|
|
120
|
+
#dpb-preview-bar .dpb-pill .dpb-badge-dot {
|
|
121
|
+
display: none; min-width: 18px; height: 18px; padding: 0 5px;
|
|
122
|
+
align-items: center; justify-content: center; border-radius: 999px;
|
|
123
|
+
font-size: 11px; font-weight: 700; color: var(--dpb-accent-contrast);
|
|
124
|
+
background: var(--dpb-accent);
|
|
125
|
+
}
|
|
126
|
+
#dpb-preview-bar .dpb-pill .dpb-badge-dot.is-on { display: inline-flex; }
|
|
127
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-primary {
|
|
128
|
+
appearance: none; cursor: pointer; height: 32px; padding: 0 16px;
|
|
129
|
+
border-radius: 999px; border: 1px solid var(--dpb-accent-deep);
|
|
130
|
+
background: var(--dpb-accent); color: var(--dpb-accent-ink);
|
|
131
|
+
font: 650 12.5px/1 system-ui, sans-serif;
|
|
132
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,.18);
|
|
133
|
+
transition: background 140ms ease, transform 100ms ease;
|
|
134
|
+
}
|
|
135
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-primary:hover { background: var(--dpb-accent-hover); color: var(--dpb-accent-contrast); }
|
|
136
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-primary:active { transform: translateY(1px); }
|
|
137
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-primary:focus-visible {
|
|
138
|
+
outline: 2px solid var(--dpb-accent); outline-offset: 2px;
|
|
139
|
+
}
|
|
140
|
+
/* Direct confirm state on pill primary when readiness allows immediate action */
|
|
141
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-primary.is-direct-confirm {
|
|
142
|
+
box-shadow: 0 0 0 3px rgba(20,184,166,0.25);
|
|
143
|
+
font-weight: 700;
|
|
144
|
+
}
|
|
145
|
+
/* Pill direct-confirm arm (二级保护): first click arms, second submits */
|
|
146
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-primary.is-armed {
|
|
147
|
+
font-weight: 700;
|
|
148
|
+
animation: dpb-confirm-pulse 1s ease-in-out infinite;
|
|
149
|
+
}
|
|
150
|
+
@keyframes dpb-confirm-pulse {
|
|
151
|
+
0%,100% { box-shadow: 0 0 0 3px rgba(20,184,166,0.35); }
|
|
152
|
+
50% { box-shadow: 0 0 0 7px rgba(20,184,166,0.12); }
|
|
153
|
+
}
|
|
154
|
+
/* secondary action surfaced on the pill (e.g. the revise label) */
|
|
155
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-pill-secondary {
|
|
156
|
+
appearance: none; cursor: pointer; height: 32px; padding: 0 14px;
|
|
157
|
+
border-radius: 999px; border: 1px solid var(--dpb-line-strong);
|
|
158
|
+
background: var(--dpb-elev); color: var(--dpb-soft);
|
|
159
|
+
font: 650 12.5px/1 system-ui, sans-serif;
|
|
160
|
+
display: inline-flex; align-items: center;
|
|
161
|
+
transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
|
|
162
|
+
}
|
|
163
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-pill-secondary:hover {
|
|
164
|
+
background: var(--dpb-hover); border-color: var(--dpb-border-hover-strong); color: var(--dpb-ink);
|
|
165
|
+
}
|
|
166
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-pill-secondary:focus-visible {
|
|
167
|
+
outline: 2px solid var(--dpb-accent); outline-offset: 2px;
|
|
168
|
+
}
|
|
169
|
+
@media (max-width: 720px) {
|
|
170
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-pill-secondary { display: inline-flex; font-size: 11px; padding: 0 8px; }
|
|
171
|
+
/* Per debate: do not hide revise on mobile; allow stacking if needed */
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ---- drawer (expanded state) ---- */
|
|
175
|
+
#dpb-preview-bar dialog.dpb-drawer {
|
|
176
|
+
position: fixed;
|
|
177
|
+
right: 24px;
|
|
178
|
+
bottom: 24px;
|
|
179
|
+
margin: 0;
|
|
180
|
+
width: 380px;
|
|
181
|
+
max-width: calc(100vw - 48px);
|
|
182
|
+
max-height: calc(100vh - 48px);
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
background: var(--dpb-bg);
|
|
185
|
+
border: 1px solid var(--dpb-line);
|
|
186
|
+
border-radius: 16px;
|
|
187
|
+
box-shadow: 0 16px 48px rgba(0,0,0,.22), 0 4px 12px rgba(0,0,0,.12);
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
color: var(--dpb-ink);
|
|
190
|
+
padding: 0;
|
|
191
|
+
}
|
|
192
|
+
/* non-modal <dialog>.show() is not in the top layer, so we keep the hand-rolled
|
|
193
|
+
scrim (stronger than before per UX V4) so the drawer reads as a focused layer. */
|
|
194
|
+
#dpb-preview-bar dialog.dpb-drawer[open] { display: flex; }
|
|
195
|
+
/* show() failure / no-dialog fallback: is-open alone must reveal the drawer */
|
|
196
|
+
#dpb-preview-bar.is-open dialog.dpb-drawer { display: flex; }
|
|
197
|
+
#dpb-preview-bar.is-open::before {
|
|
198
|
+
content: "";
|
|
199
|
+
position: fixed; inset: 0; z-index: -1;
|
|
200
|
+
background: rgba(15,23,42,.5);
|
|
201
|
+
pointer-events: auto; /* P1.5: intercept clicks outside drawer (close on scrim) */
|
|
202
|
+
}
|
|
203
|
+
body.dpb-pin-mode #dpb-preview-bar.is-open::before {
|
|
204
|
+
pointer-events: none; /* P1.5: let pin-mode clicks reach prototype elements */
|
|
205
|
+
}
|
|
206
|
+
#dpb-preview-bar.is-open .dpb-pill { display: none; }
|
|
207
|
+
|
|
208
|
+
#dpb-preview-bar .dpb-drawer-head {
|
|
209
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
210
|
+
gap: 10px; padding: 12px 14px;
|
|
211
|
+
border-bottom: 1px solid var(--dpb-line);
|
|
212
|
+
background: linear-gradient(180deg, var(--dpb-chrome), var(--dpb-bg));
|
|
213
|
+
}
|
|
214
|
+
#dpb-preview-bar .dpb-drawer-head .dpb-head-left {
|
|
215
|
+
display: inline-flex; align-items: center; gap: 8px; min-width: 0;
|
|
216
|
+
}
|
|
217
|
+
#dpb-preview-bar .dpb-drawer-head .dpb-title {
|
|
218
|
+
margin: 0; font-size: 13px; font-weight: 650; color: var(--dpb-ink);
|
|
219
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
220
|
+
}
|
|
221
|
+
#dpb-preview-bar .dpb-drawer-head .dpb-icon-btn {
|
|
222
|
+
appearance: none; cursor: pointer; width: 28px; height: 28px;
|
|
223
|
+
border-radius: 8px; border: 1px solid transparent;
|
|
224
|
+
background: transparent; color: var(--dpb-muted);
|
|
225
|
+
font: 650 16px/1 system-ui, sans-serif;
|
|
226
|
+
display: inline-grid; place-items: center;
|
|
227
|
+
transition: background 140ms ease, color 140ms ease;
|
|
228
|
+
}
|
|
229
|
+
#dpb-preview-bar .dpb-drawer-head .dpb-icon-btn:hover {
|
|
230
|
+
background: var(--dpb-elev); color: var(--dpb-ink);
|
|
231
|
+
}
|
|
232
|
+
#dpb-preview-bar .dpb-drawer-head .dpb-icon-btn:focus-visible {
|
|
233
|
+
outline: 2px solid var(--dpb-accent); outline-offset: 1px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#dpb-preview-bar .dpb-drawer-body {
|
|
237
|
+
display: flex; flex-direction: column; min-height: 0;
|
|
238
|
+
overflow: auto; padding: 10px 14px 14px; gap: 14px;
|
|
239
|
+
}
|
|
240
|
+
#dpb-preview-bar .dpb-drawer-body .dpb-subhead {
|
|
241
|
+
margin: 0; font-size: 11px; font-weight: 700; letter-spacing: 0.05em;
|
|
242
|
+
color: var(--dpb-muted); text-transform: uppercase;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* pin toggle in drawer */
|
|
246
|
+
#dpb-preview-bar .dpb-pin-toggle {
|
|
247
|
+
appearance: none; cursor: pointer; height: 34px; padding: 0 12px;
|
|
248
|
+
border-radius: 8px; border: 1px solid var(--dpb-line-strong);
|
|
249
|
+
background: var(--dpb-surface); color: var(--dpb-soft);
|
|
250
|
+
font: 650 12.5px/1 system-ui, sans-serif;
|
|
251
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
252
|
+
transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
|
|
253
|
+
}
|
|
254
|
+
#dpb-preview-bar .dpb-pin-toggle:hover {
|
|
255
|
+
background: var(--dpb-elev); border-color: var(--dpb-border-hover); color: var(--dpb-ink);
|
|
256
|
+
}
|
|
257
|
+
#dpb-preview-bar .dpb-pin-toggle.is-on {
|
|
258
|
+
color: var(--dpb-accent-contrast); background: var(--dpb-accent); border-color: var(--dpb-accent-deep);
|
|
259
|
+
}
|
|
260
|
+
#dpb-preview-bar .dpb-pin-toggle:focus-visible {
|
|
261
|
+
outline: 2px solid var(--dpb-accent); outline-offset: 2px;
|
|
262
|
+
}
|
|
263
|
+
#dpb-preview-bar .dpb-pin-row {
|
|
264
|
+
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
|
265
|
+
}
|
|
266
|
+
#dpb-preview-bar .dpb-pin-count {
|
|
267
|
+
font-size: 11.5px; color: var(--dpb-muted); font-variant-numeric: tabular-nums;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* anchor list */
|
|
271
|
+
#dpb-preview-bar .dpb-anchors {
|
|
272
|
+
display: none; flex-direction: column; gap: 6px;
|
|
273
|
+
max-height: 180px; overflow: auto; padding: 8px;
|
|
274
|
+
border-radius: 8px; background: var(--dpb-surface);
|
|
275
|
+
border: 1px solid var(--dpb-line);
|
|
276
|
+
}
|
|
277
|
+
#dpb-preview-bar .dpb-anchors.has-items { display: flex; }
|
|
278
|
+
#dpb-preview-bar .dpb-empty {
|
|
279
|
+
display: none; padding: 10px 4px; font-size: 12px; color: var(--dpb-muted);
|
|
280
|
+
text-align: center;
|
|
281
|
+
}
|
|
282
|
+
#dpb-preview-bar .dpb-anchors:not(.has-items) + .dpb-empty { display: block; }
|
|
283
|
+
#dpb-preview-bar .dpb-anchor {
|
|
284
|
+
display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; gap: 8px;
|
|
285
|
+
align-items: start; padding: 6px 8px; border-radius: 8px;
|
|
286
|
+
background: var(--dpb-elev); border: 1px solid var(--dpb-line);
|
|
287
|
+
}
|
|
288
|
+
#dpb-preview-bar .dpb-anchor .n {
|
|
289
|
+
font-size: 11px; font-weight: 700; color: var(--dpb-accent); padding-top: 4px;
|
|
290
|
+
}
|
|
291
|
+
#dpb-preview-bar .dpb-anchor .meta {
|
|
292
|
+
min-width: 0; display: grid; gap: 4px;
|
|
293
|
+
}
|
|
294
|
+
#dpb-preview-bar .dpb-anchor .label {
|
|
295
|
+
font-size: 12px; font-weight: 650; color: var(--dpb-ink);
|
|
296
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
297
|
+
appearance: none; border: 0; background: transparent; padding: 0;
|
|
298
|
+
text-align: left; cursor: pointer; width: 100%;
|
|
299
|
+
display: block; min-width: 0;
|
|
300
|
+
}
|
|
301
|
+
#dpb-preview-bar .dpb-anchor .label:hover { color: var(--dpb-accent); }
|
|
302
|
+
#dpb-preview-bar .dpb-anchor .label:focus-visible {
|
|
303
|
+
outline: 2px solid var(--dpb-accent); outline-offset: 1px; border-radius: 4px;
|
|
304
|
+
}
|
|
305
|
+
#dpb-preview-bar .dpb-anchor .sel {
|
|
306
|
+
font-size: 11px; color: var(--dpb-muted);
|
|
307
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
308
|
+
}
|
|
309
|
+
#dpb-preview-bar .dpb-anchor input {
|
|
310
|
+
width: 100%; box-sizing: border-box; height: 28px; margin: 0; padding: 0 8px;
|
|
311
|
+
border-radius: 6px; border: 1px solid var(--dpb-line-strong);
|
|
312
|
+
background: var(--dpb-surface); color: var(--dpb-ink); font: inherit; outline: none;
|
|
313
|
+
}
|
|
314
|
+
#dpb-preview-bar .dpb-anchor input:focus {
|
|
315
|
+
border-color: var(--dpb-accent); box-shadow: 0 0 0 2px rgba(20,184,166,.2);
|
|
316
|
+
}
|
|
317
|
+
#dpb-preview-bar .dpb-anchor .rm {
|
|
318
|
+
appearance: none; cursor: pointer; height: 28px; padding: 0 8px;
|
|
319
|
+
border: 0; border-radius: 6px; background: transparent; color: var(--dpb-muted);
|
|
320
|
+
font: 650 12px/1 system-ui, sans-serif;
|
|
321
|
+
}
|
|
322
|
+
#dpb-preview-bar .dpb-anchor .rm:hover { color: var(--dpb-danger); background: rgba(248,113,113,.08); }
|
|
323
|
+
|
|
324
|
+
/* overall comment */
|
|
325
|
+
#dpb-preview-bar .dpb-field { display: grid; gap: 6px; min-width: 0; }
|
|
326
|
+
#dpb-preview-bar .dpb-label-row {
|
|
327
|
+
display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
|
|
328
|
+
}
|
|
329
|
+
#dpb-preview-bar .dpb-label { font-size: 12px; font-weight: 650; color: var(--dpb-muted); }
|
|
330
|
+
/* I4: neutral hint up-front; .is-on only adds emphasis (was red-danger-only). */
|
|
331
|
+
#dpb-preview-bar .dpb-hint { margin: 0; font-size: 12px; color: var(--dpb-muted); display: block; }
|
|
332
|
+
#dpb-preview-bar .dpb-hint.is-on { color: var(--dpb-danger); font-weight: 600; }
|
|
333
|
+
/* I4: pill readiness indicator (live status on the resting pill). */
|
|
334
|
+
#dpb-preview-bar .dpb-pill .dpb-pill-ready {
|
|
335
|
+
font-size: 11px; color: var(--dpb-muted); white-space: nowrap;
|
|
336
|
+
}
|
|
337
|
+
#dpb-preview-bar .dpb-pill .dpb-pill-ready.is-ready {
|
|
338
|
+
color: var(--dpb-ready); font-weight: 600;
|
|
339
|
+
}
|
|
340
|
+
#dpb-preview-bar textarea[name="feedback"] {
|
|
341
|
+
width: 100%; box-sizing: border-box; min-height: 56px; max-height: 120px;
|
|
342
|
+
resize: vertical; margin: 0; padding: 8px 12px; color: var(--dpb-ink);
|
|
343
|
+
background: var(--dpb-surface); border: 1px solid var(--dpb-line-strong);
|
|
344
|
+
border-radius: var(--dpb-radius); font: inherit; line-height: 1.45; outline: none;
|
|
345
|
+
transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease;
|
|
346
|
+
}
|
|
347
|
+
#dpb-preview-bar textarea[name="feedback"]::placeholder { color: var(--dpb-placeholder); }
|
|
348
|
+
#dpb-preview-bar textarea[name="feedback"]:hover { border-color: var(--dpb-border-hover); background: var(--dpb-elev); }
|
|
349
|
+
#dpb-preview-bar textarea[name="feedback"]:focus {
|
|
350
|
+
border-color: var(--dpb-accent); box-shadow: 0 0 0 3px rgba(20,184,166,.22); background: var(--dpb-input-focus);
|
|
351
|
+
}
|
|
352
|
+
#dpb-preview-bar textarea[name="feedback"][aria-invalid="true"] {
|
|
353
|
+
border-color: var(--dpb-danger); box-shadow: 0 0 0 3px rgba(248,113,113,.18);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* footer actions */
|
|
357
|
+
#dpb-preview-bar .dpb-drawer-foot {
|
|
358
|
+
display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between;
|
|
359
|
+
align-items: center; padding: 12px 14px;
|
|
360
|
+
border-top: 1px solid var(--dpb-line);
|
|
361
|
+
background: linear-gradient(180deg, var(--dpb-bg), var(--dpb-chrome));
|
|
362
|
+
}
|
|
363
|
+
#dpb-preview-bar .dpb-btns {
|
|
364
|
+
display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; align-items: center;
|
|
365
|
+
}
|
|
366
|
+
#dpb-preview-bar .dpb-btn {
|
|
367
|
+
appearance: none; cursor: pointer; box-sizing: border-box; height: var(--dpb-control-h);
|
|
368
|
+
min-width: 96px; padding: 0 16px; border-radius: var(--dpb-radius);
|
|
369
|
+
font: 650 13px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
370
|
+
border: 1px solid transparent;
|
|
371
|
+
transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 100ms ease;
|
|
372
|
+
}
|
|
373
|
+
#dpb-preview-bar .dpb-btn:active { transform: translateY(1px); }
|
|
374
|
+
#dpb-preview-bar .dpb-btn:focus-visible { outline: 2px solid var(--dpb-accent); outline-offset: 2px; }
|
|
375
|
+
#dpb-preview-bar .dpb-btn-primary {
|
|
376
|
+
background: var(--dpb-accent); color: var(--dpb-accent-ink); border-color: var(--dpb-accent-deep);
|
|
377
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,.18);
|
|
378
|
+
}
|
|
379
|
+
#dpb-preview-bar .dpb-btn-primary:hover { background: var(--dpb-accent-hover); color: var(--dpb-accent-contrast); }
|
|
380
|
+
#dpb-preview-bar .dpb-btn-secondary {
|
|
381
|
+
background: var(--dpb-elev); color: var(--dpb-ink); border-color: var(--dpb-line-strong);
|
|
382
|
+
}
|
|
383
|
+
#dpb-preview-bar .dpb-btn-secondary:hover { background: var(--dpb-secondary-hover); border-color: var(--dpb-border-hover); }
|
|
384
|
+
#dpb-preview-bar .dpb-btn-quiet {
|
|
385
|
+
min-width: 72px; background: transparent; color: var(--dpb-muted); border-color: transparent;
|
|
386
|
+
}
|
|
387
|
+
#dpb-preview-bar .dpb-btn-quiet:hover { color: var(--dpb-ink); background: var(--dpb-quiet-hover); }
|
|
388
|
+
#dpb-preview-bar .dpb-btn-danger {
|
|
389
|
+
min-width: 72px; background: transparent; color: var(--dpb-danger); border-color: transparent;
|
|
390
|
+
}
|
|
391
|
+
#dpb-preview-bar .dpb-btn-danger:hover { color: #fff; background: var(--dpb-danger); border-color: var(--dpb-danger); }
|
|
392
|
+
#dpb-preview-bar .dpb-btn-danger.is-armed { color: #fff; background: var(--dpb-danger); border-color: var(--dpb-danger); font-weight: 700; animation: dpb-abort-pulse 1s ease-in-out infinite; }
|
|
393
|
+
/* ring pulse (not opacity) so the armed label stays at full contrast */
|
|
394
|
+
@keyframes dpb-abort-pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(248,113,113,.55); } 50% { box-shadow: 0 0 0 7px rgba(248,113,113,.12); } }
|
|
395
|
+
/* visually hidden live region (screen-reader announcements, e.g. abort arming) */
|
|
396
|
+
#dpb-preview-bar .dpb-sr-only {
|
|
397
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden;
|
|
398
|
+
clip-path: inset(50%); white-space: nowrap;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* floating annotation bubbles pinned to targeted elements.
|
|
402
|
+
z-index 999 < #dpb-preview-bar (1000): float-note sits above the prototype
|
|
403
|
+
page content but BELOW the bar/drawer, so pin bubbles never cover the
|
|
404
|
+
confirm controls (was 1001 -> covered drawer, d46682d follow-up). */
|
|
405
|
+
.dpb-float-note {
|
|
406
|
+
position: absolute;
|
|
407
|
+
z-index: 999;
|
|
408
|
+
max-width: 240px;
|
|
409
|
+
min-width: 120px;
|
|
410
|
+
padding: 6px 10px 6px 8px;
|
|
411
|
+
border-radius: 10px;
|
|
412
|
+
background: var(--dpb-note-bg);
|
|
413
|
+
color: var(--dpb-note-ink);
|
|
414
|
+
border: 1px solid var(--dpb-line);
|
|
415
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.32), 0 2px 6px rgba(0,0,0,.18);
|
|
416
|
+
font: 12px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
417
|
+
pointer-events: none;
|
|
418
|
+
transform: translateY(4px);
|
|
419
|
+
opacity: 0;
|
|
420
|
+
transition: opacity 160ms ease, transform 160ms ease;
|
|
421
|
+
}
|
|
422
|
+
.dpb-float-note.is-visible { opacity: 1; transform: translateY(0); }
|
|
423
|
+
.dpb-float-note .dpb-float-n {
|
|
424
|
+
position: absolute; top: -7px; left: -7px;
|
|
425
|
+
width: 18px; height: 18px; border-radius: 999px;
|
|
426
|
+
background: var(--dpb-accent); color: var(--dpb-accent-contrast);
|
|
427
|
+
font-size: 11px; font-weight: 700;
|
|
428
|
+
display: inline-grid; place-items: center;
|
|
429
|
+
box-shadow: 0 1px 3px rgba(0,0,0,.3);
|
|
430
|
+
}
|
|
431
|
+
.dpb-float-note .dpb-float-label {
|
|
432
|
+
display: block; font-weight: 650; color: var(--dpb-accent-soft);
|
|
433
|
+
font-size: 11px; margin-bottom: 2px; word-break: break-word;
|
|
434
|
+
}
|
|
435
|
+
.dpb-float-note .dpb-float-comment {
|
|
436
|
+
color: var(--dpb-soft); word-break: break-word; white-space: pre-wrap;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.dpb-pin-target {
|
|
440
|
+
outline: 1.5px solid rgba(20,184,166,.9) !important;
|
|
441
|
+
outline-offset: 1px !important;
|
|
442
|
+
background-color: rgba(20,184,166,.06) !important;
|
|
443
|
+
cursor: crosshair !important;
|
|
444
|
+
}
|
|
445
|
+
.dpb-pin-hover {
|
|
446
|
+
outline: 1px dashed rgba(20,184,166,.45) !important;
|
|
447
|
+
outline-offset: 1px !important;
|
|
448
|
+
}
|
|
449
|
+
/* brief flash when an anchor is located from the list */
|
|
450
|
+
.dpb-pin-flash {
|
|
451
|
+
animation: dpb-flash 900ms ease-out 1;
|
|
452
|
+
}
|
|
453
|
+
@keyframes dpb-flash {
|
|
454
|
+
0% { box-shadow: 0 0 0 0 rgba(20,184,166,.55); outline-color: rgba(20,184,166,.9) !important; }
|
|
455
|
+
50% { box-shadow: 0 0 0 6px rgba(20,184,166,.25); }
|
|
456
|
+
100% { box-shadow: 0 0 0 0 rgba(20,184,166,0); }
|
|
457
|
+
}
|
|
458
|
+
body.dpb-pin-mode, body.dpb-pin-mode * { cursor: crosshair !important; }
|
|
459
|
+
body.dpb-pin-mode #dpb-preview-bar, body.dpb-pin-mode #dpb-preview-bar * {
|
|
460
|
+
cursor: default !important;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
@media (max-width: 720px) {
|
|
464
|
+
#dpb-preview-bar { right: 12px; bottom: 12px; }
|
|
465
|
+
#dpb-preview-bar .dpb-drawer { width: calc(100vw - 24px); }
|
|
466
|
+
#dpb-preview-bar .dpb-pill .dpb-pill-info { display: none; }
|
|
467
|
+
#dpb-preview-bar .dpb-drawer-body { padding: 10px 10px 14px; }
|
|
468
|
+
#dpb-preview-bar .dpb-drawer-foot { justify-content: flex-start; flex-wrap: wrap; }
|
|
469
|
+
#dpb-preview-bar .dpb-btn.dpb-btn-danger { flex: 0 0 auto; }
|
|
470
|
+
#dpb-preview-bar .dpb-btns { flex: 1 1 auto; }
|
|
471
|
+
#dpb-preview-bar .dpb-btns .dpb-btn { flex: 1 1 auto; min-width: 0; }
|
|
472
|
+
}
|
|
473
|
+
@media (prefers-reduced-motion: reduce) {
|
|
474
|
+
#dpb-preview-bar .dpb-btn,
|
|
475
|
+
#dpb-preview-bar textarea[name="feedback"],
|
|
476
|
+
.dpb-float-note { transition: none; }
|
|
477
|
+
#dpb-preview-bar .dpb-btn:active { transform: none; }
|
|
478
|
+
.dpb-pin-flash { animation: none; }
|
|
479
|
+
/* armed state still reads via solid danger fill + bold label, no pulse */
|
|
480
|
+
#dpb-preview-bar .dpb-btn-danger.is-armed { animation: none; }
|
|
481
|
+
#dpb-preview-bar .dpb-pill .dpb-btn-primary.is-armed { animation: none; }
|
|
482
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div id="dpb-preview-bar" role="region" aria-label="{t_region}">
|
|
2
|
+
<form method="POST" action="/decide" id="dpb-decide-form">
|
|
3
|
+
<input type="hidden" name="anchors_json" id="dpb-anchors-json" value="[]" />
|
|
4
|
+
|
|
5
|
+
<!-- floating pill (default) -->
|
|
6
|
+
<div class="dpb-pill">
|
|
7
|
+
<span class="dpb-pill-info">
|
|
8
|
+
<span class="dpb-round">{t_round}</span>
|
|
9
|
+
<p class="dpb-summary" title="{summary_safe}">{summary_safe}</p>
|
|
10
|
+
</span>
|
|
11
|
+
<span class="dpb-pill-ready" id="dpb-pill-ready" role="status">{t_not_ready}</span>
|
|
12
|
+
<span class="dpb-pill-actions">
|
|
13
|
+
{pill_secondary_html}
|
|
14
|
+
<button type="button" class="dpb-btn-ghost" id="dpb-open-drawer" aria-haspopup="dialog">
|
|
15
|
+
{t_annotate}<span class="dpb-badge-dot" id="dpb-pill-count">0</span>
|
|
16
|
+
</button>
|
|
17
|
+
<button type="button" class="dpb-btn-primary" id="dpb-open-primary" aria-haspopup="dialog">{t_pill_open}</button>
|
|
18
|
+
</span>
|
|
19
|
+
<span class="dpb-sr-only" id="dpb-pill-arm-status" role="alert"></span>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- drawer (expanded) -->
|
|
23
|
+
<dialog class="dpb-drawer" id="dpb-drawer" aria-label="{t_drawer_aria}">
|
|
24
|
+
<div class="dpb-drawer-head">
|
|
25
|
+
<span class="dpb-head-left">
|
|
26
|
+
<span class="dpb-round">{t_round}</span>
|
|
27
|
+
<h2 class="dpb-title">{summary_safe}</h2>
|
|
28
|
+
</span>
|
|
29
|
+
<button type="button" class="dpb-icon-btn" id="dpb-close-drawer" aria-label="{t_collapse}">−</button>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="dpb-drawer-body">
|
|
32
|
+
<div class="dpb-pin-row">
|
|
33
|
+
<button type="button" class="dpb-pin-toggle" id="dpb-pin-toggle" aria-pressed="false">
|
|
34
|
+
<span aria-hidden="true">🎯</span><span class="dpb-pin-label">{t_pin_toggle}</span>
|
|
35
|
+
</button>
|
|
36
|
+
<span class="dpb-pin-count" id="dpb-pin-count">{t_pin_count}</span>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div>
|
|
40
|
+
<p class="dpb-subhead">{t_anchors_head}</p>
|
|
41
|
+
<div class="dpb-anchors" id="dpb-anchors" aria-live="polite"></div>
|
|
42
|
+
<p class="dpb-empty">{t_anchors_empty}</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="dpb-field">
|
|
46
|
+
<span class="dpb-label-row">
|
|
47
|
+
<span class="dpb-label">{t_field_label}</span>
|
|
48
|
+
<span class="dpb-hint" id="dpb-feedback-hint" role="alert">{t_field_hint}</span>
|
|
49
|
+
</span>
|
|
50
|
+
<textarea name="feedback" rows="2"
|
|
51
|
+
placeholder="{t_field_placeholder}"
|
|
52
|
+
autocomplete="off"></textarea>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="dpb-drawer-foot">
|
|
56
|
+
<button type="submit" name="choice" value="__abort__" class="dpb-btn dpb-btn-danger" id="dpb-abort" title="{t_terminate_desc}" aria-description="{t_terminate_desc}">{t_terminate}</button>
|
|
57
|
+
<span class="dpb-sr-only" id="dpb-abort-status" role="alert"></span>
|
|
58
|
+
<div class="dpb-btns">
|
|
59
|
+
<button type="button" class="dpb-btn dpb-btn-quiet" id="dpb-draft" title="{t_draft_desc}" aria-description="{t_draft_desc}">{t_draft}</button>
|
|
60
|
+
{secondary_html}
|
|
61
|
+
<button type="submit" name="choice" value="{primary_val}" class="dpb-btn dpb-btn-primary" title="{t_confirm_desc}" aria-description="{t_confirm_desc}">{primary_label}</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</dialog>
|
|
65
|
+
</form>
|
|
66
|
+
</div>
|
|
67
|
+
<div id="dpb-preview-spacer" aria-hidden="true"></div>
|