@vitrinka/web 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,111 +1,266 @@
1
1
  /**
2
- * The HUD's stylesheet the extension's pill/sheet/pick CSS, verbatim where
3
- * it applies. Lives inside the shadow root (a `<style>` element), so it never
4
- * touches host CSS and host CSS never touches it.
2
+ * The HUD's stylesheet. Lives inside the shadow root (a `<style>` element),
3
+ * so it never touches host CSS and host CSS never touches it.
4
+ *
5
+ * Shapes (recorder-hud-subtle): the 28px glass PUCK at idle, the dot+timer
6
+ * CAPSULE while recording that unfolds into the TRAY on intent, the 6px edge
7
+ * TAB when tucked. The `.dock` rests on one of six spots (`data-row` t|b ×
8
+ * `data-col` l|c|r) by viewport insets, so an unfolding tray always grows
9
+ * toward the centre; a drag moves it by `transform` only. Motion follows
10
+ * the transitions.dev token scale.
5
11
  */
6
12
  export const HUD_CSS_BASE = `
7
13
  * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
8
14
  svg { width:1em; height:1em; vertical-align:-.125em; }
9
- .stack { position:relative; z-index:4; display:flex; flex-direction:column; align-items:flex-end; gap:6px; }
10
- .pill { display:flex; align-items:center; gap:10px; padding:8px 10px 8px 14px;
11
- background:#1d1a1b; border:1px solid #292526; border-radius:999px;
12
- box-shadow:0 8px 30px rgba(0,0,0,.35); color:#f0eae4; }
13
- .dot { width:10px; height:10px; border-radius:50%; background:#ff3b57; animation:p 1.4s ease infinite; }
14
- .paused .dot { animation:none; background:#756e68; }
15
- @keyframes p { 50% { opacity:.35; } }
16
- .sync { font:600 11px/1 ui-monospace, Menlo, monospace; color:#5f7a5f; }
17
- .sync.warn { color:#e8a33d; }
18
- .sync.bad { color:#ff3b57; }
19
- .sync.busy { color:#a8a099; }
20
- .detail { max-width:320px; padding:6px 12px; border-radius:999px;
21
- background:#1d1a1b; border:1px solid #292526; color:#a8a099;
22
- font:500 10px/1.4 ui-monospace, Menlo, monospace;
23
- opacity:0; transform:translateY(-3px); transition:opacity .24s ease, transform .24s ease;
24
- pointer-events:none; }
15
+ .hud {
16
+ --ink: rgba(26,22,23,.72); --ink-pop: rgba(26,22,23,.88); --ink-solid: #1d1a1b;
17
+ --ink-2: rgba(255,255,255,.08); --edge: rgba(255,255,255,.11); --rim: rgba(0,0,0,.30);
18
+ --fg: #f4efea; --fg-2: rgba(244,239,234,.66); --fg-3: rgba(244,239,234,.44);
19
+ --rec: #ff3b57; --warn: #f0a63a;
20
+ --m: 16px; --h: 28px; --b: 24px;
21
+ --duration-quick: 150ms; --duration-fast: 250ms; --duration-resize: 300ms; --duration-medium: 350ms; --duration-snap: 500ms;
22
+ --ease-smooth-out: cubic-bezier(0.22, 1, 0.36, 1); --ease-in-out: ease-in-out; --ease-out: ease-out;
23
+ --ease-spring: var(--ease-smooth-out);
24
+ --scale-open: .97; --scale-closing: .99; --blur-small: 2px; --rise: 16px;
25
+ color: var(--fg); font-size: 12px;
26
+ }
27
+ @supports (transition-timing-function: linear(0, 1)) {
28
+ /* a damped spring, ~4% overshoot over 500ms — the settle after a throw */
29
+ .hud { --ease-spring: linear(0, 0.042, 0.143, 0.274, 0.414, 0.549, 0.67, 0.773, 0.856, 0.921, 0.968, 1.001, 1.021, 1.033, 1.038, 1.038, 1.035, 1.031, 1.025, 1.02, 1.015, 1.011, 1.007, 1.004, 1); }
30
+ }
31
+ @media (pointer: coarse) { .hud { --m: 12px; --h: 32px; --b: 30px; } }
32
+ /* scoped under .hud so a control's own \`all:unset\` reset cannot wipe the surface */
33
+ .hud .glass, .hud .menu, .hud .pop, .hud .detail, .hud .hint {
34
+ background: var(--ink);
35
+ -webkit-backdrop-filter: blur(16px) saturate(1.5); backdrop-filter: blur(16px) saturate(1.5);
36
+ box-shadow: inset 0 0 0 1px var(--edge), 0 0 0 .5px var(--rim), 0 10px 28px -10px rgba(0,0,0,.5), 0 2px 6px -2px rgba(0,0,0,.28);
37
+ color: var(--fg);
38
+ }
39
+ .hud .menu, .hud .pop { background: var(--ink-pop); }
40
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
41
+ .hud .glass, .hud .menu, .hud .pop, .hud .detail, .hud .hint { background: var(--ink-solid); }
42
+ }
43
+ .sr { position:absolute; width:1px; height:1px; margin:-1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; }
44
+
45
+ /* the dock: one of six spots, or tucked into a side edge */
46
+ .dock { position:fixed; z-index:4; display:flex; pointer-events:auto; }
47
+ .dock[data-row="b"] { bottom: calc(var(--m) + env(safe-area-inset-bottom, 0px)); }
48
+ .dock[data-row="t"] { top: calc(var(--m) + env(safe-area-inset-top, 0px)); }
49
+ .dock[data-col="l"] { left: calc(var(--m) + env(safe-area-inset-left, 0px)); }
50
+ .dock[data-col="r"] { right: calc(var(--m) + env(safe-area-inset-right, 0px)); }
51
+ .dock[data-col="c"] { left: calc(50% - var(--hw, 36px) / 2); }
52
+ .dock[data-tuck] { top: clamp(8px, calc(var(--ty, .5) * 100% - 28px), calc(100% - 64px)); }
53
+ .dock[data-tuck="left"] { left:0; }
54
+ .dock[data-tuck="right"] { right:0; }
55
+ .dock.settling { transition: transform var(--duration-snap) var(--ease-spring); }
56
+ .dock.dragging .glass { box-shadow: inset 0 0 0 1px var(--edge), 0 0 0 .5px var(--rim), 0 18px 40px -12px rgba(0,0,0,.55), 0 4px 10px -4px rgba(0,0,0,.3); }
57
+
58
+ /* the recording dot: a soft ripple while recording, still otherwise */
59
+ .dot { position:relative; width:8px; height:8px; flex:none; border-radius:50%; background:var(--rec); }
60
+ .dot::after { content:""; position:absolute; inset:0; border-radius:50%; box-shadow:0 0 0 0 rgba(255,59,87,.5); animation:ripple 2s var(--ease-out) infinite; }
61
+ @keyframes ripple { 0% { box-shadow:0 0 0 0 rgba(255,59,87,.45); } 70%, 100% { box-shadow:0 0 0 7px rgba(255,59,87,0); } }
62
+ .bar[data-state="paused"] .dot { background:var(--fg-3); }
63
+ .bar[data-state="bad"] .dot { background:var(--warn); }
64
+ .bar[data-state="paused"] .dot::after, .bar[data-state="bad"] .dot::after { animation:none; }
65
+
66
+ /* idle: the puck — ring = not linked, dot = ready; the label slides out on intent */
67
+ .puck { all:unset; box-sizing:border-box; display:inline-flex; align-items:center; height:var(--h); min-width:var(--h);
68
+ padding:0 calc((var(--h) - 10px) / 2); border-radius:999px; cursor:pointer; touch-action:none; user-select:none; -webkit-user-select:none;
69
+ font-weight:600; font-size:12px; color:var(--fg); }
70
+ .dock[data-col="r"] .puck { flex-direction:row-reverse; }
71
+ .ring { width:10px; height:10px; flex:none; border-radius:50%; box-shadow:inset 0 0 0 1.5px var(--fg-2);
72
+ transition: background-color var(--duration-quick) var(--ease-out), box-shadow var(--duration-quick) var(--ease-out); }
73
+ .puck[data-state="ready"] .ring { background:var(--fg-2); box-shadow:none; }
74
+ .puck[data-state="busy"] .ring { background:var(--fg-3); box-shadow:none; animation:breathe 1.2s var(--ease-in-out) infinite; }
75
+ @keyframes breathe { 50% { opacity:.35; } }
76
+ .lab { display:grid; grid-template-columns:0fr; opacity:0;
77
+ transition: grid-template-columns var(--duration-resize) var(--ease-smooth-out), opacity var(--duration-quick) var(--ease-out); }
78
+ /* the gap to the ring is a clipped pseudo-element: padding would keep a folded label 10px wide */
79
+ .lab > span { min-width:0; overflow:hidden; white-space:nowrap; }
80
+ .lab > span::before, .lab > span::after { content:""; display:inline-block; }
81
+ .lab > span::before { width:8px; }
82
+ .lab > span::after { width:2px; }
83
+ .dock[data-col="r"] .lab > span::before { width:2px; }
84
+ .dock[data-col="r"] .lab > span::after { width:8px; }
85
+ .puck:focus-visible .lab { grid-template-columns:1fr; opacity:1; }
86
+ .puck:focus-visible { outline:2px solid var(--rec); outline-offset:2px; }
87
+ @media (hover: hover) {
88
+ .puck:hover .lab { grid-template-columns:1fr; opacity:1; }
89
+ .puck[data-state="ready"]:hover .ring { background:var(--rec); }
90
+ .puck[data-state="unlinked"]:hover .ring { box-shadow:inset 0 0 0 1.5px var(--fg); }
91
+ }
92
+
93
+ /* recording: the capsule (handle) + the tray it unfolds */
94
+ .bar { position:relative; display:inline-flex; align-items:center; height:var(--h); border-radius:999px; }
95
+ .dock[data-col="r"] .bar, .dock[data-col="r"] .tray-in { flex-direction:row-reverse; }
96
+ .handle { all:unset; box-sizing:border-box; display:inline-flex; align-items:center; gap:7px; height:var(--h);
97
+ padding:0 11px 0 10px; border-radius:999px; cursor:grab; touch-action:none; user-select:none; -webkit-user-select:none; }
98
+ .dock.dragging .handle { cursor:grabbing; }
99
+ .handle:focus-visible { outline:2px solid var(--rec); outline-offset:2px; }
100
+ .time { font-weight:600; font-size:12px; line-height:1; font-variant-numeric:tabular-nums; letter-spacing:.01em; min-width:34px; }
101
+ .tray { display:grid; grid-template-columns:0fr; transition: grid-template-columns var(--duration-resize) var(--ease-smooth-out); }
102
+ .tray-in { min-width:0; overflow:hidden; display:flex; align-items:center; gap:1px; padding:34px 3px; margin:-34px 0;
103
+ pointer-events:none; opacity:0; filter:blur(var(--blur-small));
104
+ transition: opacity var(--duration-quick) var(--ease-in-out), filter var(--duration-quick) var(--ease-in-out); }
105
+ .tray-in > * { pointer-events:auto; }
106
+ .bar[data-open] .tray { grid-template-columns:1fr; }
107
+ .bar[data-open] .tray-in { opacity:1; filter:none; transition-duration: var(--duration-fast); transition-delay: 60ms; }
108
+ .sep { width:1px; height:14px; flex:none; margin:0 5px; background:var(--edge); }
109
+ .sync { display:inline-grid; place-items:center; width:18px; flex:none; font-size:11px; color:var(--fg-3); }
110
+ .sync.warn, .sync.bad { color:var(--warn); }
111
+ button.tb { all:unset; pointer-events:auto; box-sizing:border-box; position:relative; flex:none; display:inline-grid; place-items:center;
112
+ width:var(--b); height:var(--b); border-radius:999px; color:var(--fg-2); cursor:pointer; font-size:13px;
113
+ transition: background-color var(--duration-quick) var(--ease-out), color var(--duration-quick) var(--ease-out); }
114
+ button.tb.b-annotate { color:var(--rec); }
115
+ button.tb.on { background:var(--rec); color:#fff; }
116
+ button.tb:focus-visible { outline:2px solid var(--rec); outline-offset:-2px; }
117
+ @media (hover: hover) { button.tb:hover { background:var(--ink-2); color:var(--fg); } button.tb.on:hover { background:var(--rec); color:#fff; } }
118
+ kbd { position:absolute; left:50%; bottom:calc(100% + 8px); transform:translateX(-50%); padding:4px 6px; border-radius:6px;
119
+ background:var(--ink-pop); box-shadow:inset 0 0 0 1px var(--edge); color:var(--fg-2); font:600 10px/1 -apple-system, BlinkMacSystemFont, sans-serif;
120
+ letter-spacing:.02em; white-space:nowrap; opacity:0; pointer-events:none; transition:opacity var(--duration-quick) var(--ease-out); }
121
+ .dock[data-row="t"] kbd { bottom:auto; top:calc(100% + 8px); }
122
+ @media (hover: hover) { button.tb:hover kbd { opacity:1; transition-delay:400ms; } }
123
+ button.tb:focus-visible kbd { opacity:1; }
124
+ .bar.composing kbd { opacity:0 !important; }
125
+
126
+ /* the health line: only when something needs saying */
127
+ .detail { position:absolute; width:max-content; max-width:260px; padding:6px 11px; border-radius:999px; color:var(--fg-2); font-size:11px; line-height:1.35;
128
+ opacity:0; transform:translateY(4px); pointer-events:none;
129
+ transition: opacity var(--duration-fast) var(--ease-smooth-out), transform var(--duration-fast) var(--ease-smooth-out); }
130
+ .dock[data-row="b"] .detail { bottom:calc(100% + 6px); }
131
+ .dock[data-row="t"] .detail { top:calc(100% + 6px); transform:translateY(-4px); }
132
+ .dock[data-col="r"] .detail { right:0; }
133
+ .dock[data-col="l"] .detail, .dock[data-col="c"] .detail { left:0; }
25
134
  .detail.show { opacity:1; transform:none; }
26
- .detail.bad { border-color:#ff3b57; color:#f0eae4; }
27
- .time { font:600 11px/1 ui-monospace, Menlo, monospace; }
28
- .name { font:500 10px/1 ui-monospace, Menlo, monospace; color:#756e68;
29
- border-left:1px solid #292526; padding-left:10px; max-width:140px;
30
- overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
31
- button { all:unset; cursor:pointer; position:relative; width:28px; height:28px; border-radius:50%;
32
- border:1px solid #363132; color:#a8a099; font-size:12px; text-align:center; line-height:28px; }
33
- button:hover { color:#f0eae4; border-color:#756e68; }
34
- button:focus-visible, .sendb:focus-visible, .closeb:focus-visible { outline:2px solid #ff3b57; outline-offset:2px; }
35
- button.snap { background:#ff3b57; border-color:#ff3b57; color:#fff; }
36
- button.snap.on { background:#f0eae4; border-color:#f0eae4; color:#1d1a1b; }
37
- kbd { position:absolute; left:50%; bottom:calc(100% + 8px); transform:translateX(-50%);
38
- padding:4px 6px; border-radius:5px; background:#252122; border:1px solid #363132;
39
- color:#a8a099; font:600 9.5px/1 ui-monospace, Menlo, monospace; letter-spacing:.06em;
40
- white-space:nowrap; opacity:0; transition:opacity .16s ease; pointer-events:none; }
41
- .b-snap kbd, .b-more kbd { left:auto; right:0; transform:none; }
42
- button:hover kbd, button:focus-visible kbd { opacity:1; }
43
- .pill.composing kbd { opacity:0; }
44
- /* idle grip: a quiet dot until a recording starts */
45
- .grip { display:flex; align-items:center; justify-content:center; width:34px; height:34px; border-radius:50%;
46
- background:#1d1a1b; border:1px solid #292526; box-shadow:0 8px 30px rgba(0,0,0,.35); }
47
- .grip .dot { animation:none; opacity:.9; }
48
- .grip:hover .dot { animation:p 1.4s ease infinite; }
49
- .grip.busy .dot { background:#756e68; }
50
- /* menu (Open board · Stop) */
51
- .menu { position:absolute; right:0; bottom:44px; min-width:160px; padding:6px;
52
- background:#1d1a1b; border:1px solid #292526; border-radius:10px;
53
- box-shadow:0 8px 30px rgba(0,0,0,.35); display:flex; flex-direction:column; gap:2px; }
54
- .menu button { all:unset; cursor:pointer; padding:7px 10px; border-radius:6px; color:#d8d2cc;
55
- font:500 12px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display:flex; gap:8px; align-items:center; }
56
- .menu button:hover { background:#252122; color:#f0eae4; }
57
- .menu button.danger:hover { color:#ff3b57; }
58
- .menuwrap { position:relative; }
135
+ .detail.bad { color:var(--fg); box-shadow: inset 0 0 0 1px rgba(240,166,58,.55), 0 10px 28px -10px rgba(0,0,0,.5); }
136
+
137
+ /* the edge tab a tucked HUD becomes */
138
+ .tab { all:unset; box-sizing:border-box; position:relative; display:block; width:22px; height:56px; cursor:pointer; touch-action:none; }
139
+ .tab::before { content:""; position:absolute; top:0; bottom:0; width:6px; background:var(--ink);
140
+ -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
141
+ box-shadow: inset 0 0 0 1px var(--edge), 0 0 0 .5px var(--rim), 0 6px 18px -6px rgba(0,0,0,.5);
142
+ transition: width var(--duration-quick) var(--ease-out); }
143
+ .tab::after { content:""; position:absolute; top:16px; bottom:16px; width:2px; border-radius:1px; background:var(--fg-3); }
144
+ .dock[data-tuck="left"] .tab::before { left:0; border-radius:0 7px 7px 0; }
145
+ .dock[data-tuck="right"] .tab::before { right:0; border-radius:7px 0 0 7px; }
146
+ .dock[data-tuck="left"] .tab::after { left:2px; }
147
+ .dock[data-tuck="right"] .tab::after { right:2px; }
148
+ .tab[data-state="rec"]::after { background:var(--rec); }
149
+ .tab[data-state="bad"]::after { background:var(--warn); }
150
+ .tab:focus-visible::before { width:10px; outline:2px solid var(--rec); outline-offset:1px; }
151
+ @media (hover: hover) { .tab:hover::before { width:9px; } }
152
+
153
+ /* the menu grows from the bar toward the viewport centre */
154
+ .menu { position:absolute; min-width:188px; padding:5px; border-radius:12px; display:flex; flex-direction:column; gap:1px; z-index:5; }
155
+ .dock[data-row="b"] .menu { bottom:calc(100% + 8px); }
156
+ .dock[data-row="t"] .menu { top:calc(100% + 8px); }
157
+ .dock[data-col="r"] .menu { right:0; }
158
+ .dock[data-col="l"] .menu, .dock[data-col="c"] .menu { left:0; }
159
+ .menu > button { all:unset; box-sizing:border-box; cursor:pointer; padding:8px 10px; border-radius:7px; color:var(--fg);
160
+ font-size:12.5px; line-height:1; display:flex; gap:9px; align-items:center; }
161
+ .menu > button svg { color:var(--fg-2); }
162
+ .menu > button:focus-visible { outline:2px solid var(--rec); outline-offset:-2px; }
163
+ @media (hover: hover) { .menu > button:hover { background:var(--ink-2); } .menu > button.danger:hover, .menu > button.danger:hover svg { color:var(--rec); } }
164
+ .moveto { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:6px 6px 6px 10px; margin-top:3px;
165
+ border-top:1px solid var(--edge); color:var(--fg-2); font-size:12px; }
166
+ .spots { display:grid; grid-template-columns:repeat(3, 24px); grid-template-rows:repeat(2, 24px); gap:0 2px; }
167
+ .spots button { all:unset; box-sizing:border-box; position:relative; cursor:pointer; border-radius:5px; }
168
+ .spots button::before { content:""; position:absolute; inset:7px 5px; border-radius:3px; background:var(--ink-2); box-shadow:inset 0 0 0 1px var(--edge); }
169
+ .spots button[aria-checked="true"]::before { background:var(--rec); box-shadow:none; }
170
+ .spots button:focus-visible { outline:2px solid var(--rec); outline-offset:-2px; }
171
+ @media (hover: hover) { .spots button:hover::before { background:var(--fg-3); } .spots button[aria-checked="true"]:hover::before { background:var(--rec); } }
172
+
173
+ /* presence: dropdown-style grow from the anchor on open, a softer shrink on close */
174
+ .grow { transform:scale(var(--scale-open)); opacity:0;
175
+ transition: transform var(--duration-fast) var(--ease-smooth-out), opacity var(--duration-fast) var(--ease-smooth-out); }
176
+ .grow[data-origin="bottom-right"] { transform-origin:bottom right; }
177
+ .grow[data-origin="bottom-left"] { transform-origin:bottom left; }
178
+ .grow[data-origin="bottom-center"] { transform-origin:bottom center; }
179
+ .grow[data-origin="top-right"] { transform-origin:top right; }
180
+ .grow[data-origin="top-left"] { transform-origin:top left; }
181
+ .grow[data-origin="top-center"] { transform-origin:top center; }
182
+ .grow.is-open { transform:scale(1); opacity:1; }
183
+ .grow.is-closing { transform:scale(var(--scale-closing)); opacity:0; pointer-events:none;
184
+ transition-duration: var(--duration-quick); }
185
+ /* phone: the sheet rises from the bottom edge (toast motion) */
186
+ .rise { transform:translateY(var(--rise)) scale(var(--scale-open)); opacity:0; filter:blur(var(--blur-small));
187
+ transition: transform var(--duration-fast) var(--ease-smooth-out), opacity var(--duration-fast) var(--ease-smooth-out), filter var(--duration-fast) var(--ease-smooth-out); }
188
+ .rise.is-open { transform:none; opacity:1; filter:none; transition-duration: var(--duration-medium); }
189
+ .rise.is-closing { pointer-events:none; }
190
+
59
191
  /* pick chrome (annotate mode): dim, outline, hint — shadow-root children */
60
- .dim { position:fixed; inset:0; z-index:1; pointer-events:none; background:rgba(0,0,0,.22); }
61
- .outline { position:fixed; z-index:2; pointer-events:none; border:2px solid #ff3b57; border-radius:6px;
192
+ .dim { position:fixed; inset:0; z-index:1; pointer-events:none; background:rgba(0,0,0,.18); }
193
+ .outline { position:fixed; z-index:2; pointer-events:none; border:2px solid var(--rec); border-radius:6px;
62
194
  box-shadow:0 0 0 4px rgba(255,59,87,.15); }
63
- .hint { position:fixed; top:16px; left:50%; transform:translateX(-50%); z-index:3; padding:8px 16px;
64
- border-radius:999px; background:#1d1a1b; border:1px solid #ff3b57; box-shadow:0 8px 30px rgba(0,0,0,.4);
65
- font:600 12px/1 -apple-system,BlinkMacSystemFont,sans-serif; color:#f0eae4; white-space:nowrap; }
66
- /* the composer sheet (360px) */
67
- .pop { width:360px; padding:14px; background:#1d1a1b; border:1px solid #292526; border-radius:14px;
68
- box-shadow:0 8px 30px rgba(0,0,0,.35); color:#f0eae4; }
195
+ .hint { position:fixed; top:14px; left:50%; transform:translateX(-50%); z-index:3; padding:8px 14px;
196
+ border-radius:999px; font-size:12px; font-weight:600; line-height:1; white-space:nowrap; display:flex; gap:8px; align-items:center; }
197
+ .hint svg { color:var(--rec); }
198
+ .hint .keys { color:var(--fg-3); font-weight:500; margin-left:-4px; }
199
+ @media (pointer: coarse) { .hint .keys { display:none; } }
200
+ .hud[data-spot="tc"] .hint { top:auto; bottom:14px; }
201
+
202
+ /* sheets: a popover anchored to the dock, or a phone bottom sheet */
203
+ .anchor { position:fixed; z-index:6; pointer-events:none; }
204
+ .anchor > * { pointer-events:auto; }
205
+ .phone { position:fixed; z-index:6; display:flex; flex-direction:column; justify-content:flex-end;
206
+ padding:0 8px calc(8px + env(safe-area-inset-bottom, 0px)); pointer-events:none; }
207
+ .phone > * { pointer-events:auto; }
208
+ .pop { width:min(288px, calc(100vw - 24px)); padding:12px; border-radius:14px; }
209
+ .phone .pop { width:100%; border-radius:18px; padding:14px; }
69
210
  .pop-head { display:flex; align-items:center; justify-content:space-between; gap:8px; }
70
- label { display:flex; align-items:center; gap:7px; font:700 9.5px/1 ui-monospace, Menlo, monospace;
71
- letter-spacing:.18em; text-transform:uppercase; color:#756e68; }
72
- label i { width:3px; height:10px; background:#ff3b57; }
73
- .closeb { all:unset; cursor:pointer; width:24px; height:24px; border-radius:6px; color:#756e68; font-size:13px; text-align:center; line-height:24px; }
74
- .closeb:hover { color:#f0eae4; background:#252122; }
75
- textarea { width:100%; margin-top:10px; padding:10px 12px; min-height:72px; resize:vertical;
76
- background:#141213; border:1px solid #363132; border-radius:8px; color:#f0eae4;
77
- font:400 16px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
78
- textarea:focus { outline:none; border-color:#756e68; }
79
- .row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:12px; margin-top:10px; }
211
+ .title { display:flex; align-items:center; gap:7px; font-size:12.5px; font-weight:600; color:var(--fg); }
212
+ .title i { width:3px; height:11px; border-radius:1px; background:var(--rec); }
213
+ .closeb { all:unset; box-sizing:border-box; cursor:pointer; width:24px; height:24px; border-radius:7px; color:var(--fg-3);
214
+ display:inline-grid; place-items:center; font-size:12px; }
215
+ .closeb:focus-visible, .sendb:focus-visible, .retry:focus-visible, .dest button:focus-visible { outline:2px solid var(--rec); outline-offset:2px; }
216
+ @media (hover: hover) { .closeb:hover { color:var(--fg); background:var(--ink-2); } }
217
+ textarea { width:100%; margin-top:10px; padding:9px 11px; min-height:64px; resize:vertical; background:rgba(0,0,0,.28);
218
+ border:0; box-shadow:inset 0 0 0 1px var(--edge); border-radius:9px; color:var(--fg);
219
+ font:400 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
220
+ @media (pointer: coarse) { textarea { font-size:16px; } .hints { display:none; } }
221
+ textarea::placeholder { color:var(--fg-3); }
222
+ textarea:focus { outline:none; box-shadow:inset 0 0 0 1px var(--fg-3); }
223
+ .row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:10px; margin-top:10px; }
80
224
  .row.nodest { grid-template-columns:minmax(0,1fr) auto; }
81
- .ctx { font:500 10px/1.35 ui-monospace, Menlo, monospace; color:#756e68; text-align:center;
82
- overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
225
+ .ctx { font-size:11px; line-height:1.35; color:var(--fg-3); text-align:center; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
83
226
  .row.nodest .ctx { text-align:left; }
84
- .dest { display:flex; align-items:center; gap:9px; }
85
- .dest button { all:unset; cursor:pointer; padding:2px 0; color:#756e68; font:600 9px/1 ui-monospace, Menlo, monospace;
86
- letter-spacing:.12em; text-transform:uppercase; border-bottom:1px solid transparent; }
87
- .dest button[aria-pressed="true"] { color:#f0eae4; border-bottom-color:#ff3b57; }
88
- .sendb { all:unset; cursor:pointer; display:inline-flex; align-items:center; gap:6px; padding:7px 14px; border-radius:8px;
89
- background:#ff3b57; color:#fff; font-size:13px; font-weight:600; line-height:1; }
90
- .sendb:hover { background:#ff5670; }
91
- .hints { margin-top:8px; font:500 9.5px/1 ui-monospace, Menlo, monospace; letter-spacing:.06em; color:#756e68; }
227
+ .dest { display:inline-flex; padding:2px; border-radius:8px; background:rgba(0,0,0,.28); box-shadow:inset 0 0 0 1px var(--edge); }
228
+ .dest button { all:unset; box-sizing:border-box; cursor:pointer; padding:5px 8px; min-height:24px; border-radius:6px; color:var(--fg-3);
229
+ font-size:11.5px; font-weight:600; line-height:14px; transition: background-color var(--duration-quick) var(--ease-out), color var(--duration-quick) var(--ease-out); }
230
+ .dest button[aria-pressed="true"] { background:var(--ink-2); color:var(--fg); }
231
+ .sendb { all:unset; box-sizing:border-box; cursor:pointer; display:inline-flex; align-items:center; gap:6px; height:28px; padding:0 12px;
232
+ border-radius:8px; background:var(--rec); color:#fff; font-size:12.5px; font-weight:600; line-height:1; text-decoration:none; }
233
+ @media (hover: hover) { .sendb:hover { background:#ff5670; } }
234
+ .hints { margin-top:9px; font-size:11px; line-height:1; color:var(--fg-3); }
92
235
  `;
93
- /** Inline style of the HUD host — fixed bottom-right, above everything; overrides the [popover] UA sheet. */
94
- export const HOST_STYLE = 'all:initial;position:fixed;z-index:2147483647;inset:auto 20px 20px auto;margin:0;padding:0;border:0;' +
95
- 'background:transparent;overflow:visible;width:auto;height:auto;color-scheme:normal;';
96
- /** Inline style of a sheet host portaled into a dialog: the same corner, above the dialog's own chrome. */
97
- export const SHEET_HOST_STYLE = 'all:initial;position:fixed;z-index:2147483647;right:20px;bottom:76px;margin:0;padding:0;border:0;' +
98
- 'background:transparent;overflow:visible;width:auto;height:auto;color-scheme:normal;';
236
+ /**
237
+ * Inline style of the HUD host: a 0×0 fixed box at the viewport origin (the
238
+ * dock and sheets are fixed children of its shadow root). Zero-size keeps
239
+ * rrweb's placeholder for this blocked node empty in every replay, and
240
+ * `pointer-events:none` lets the page under the HUD's gaps take every
241
+ * click. Overrides the [popover] UA sheet.
242
+ */
243
+ export const HOST_STYLE = 'all:initial;position:fixed;z-index:2147483647;inset:0 auto auto 0;margin:0;padding:0;border:0;' +
244
+ 'background:transparent;overflow:visible;width:0;height:0;pointer-events:none;color-scheme:normal;';
245
+ /** Inline style of a sheet host portaled into a dialog — the same 0×0 origin box. */
246
+ export const SHEET_HOST_STYLE = HOST_STYLE;
99
247
  /** Link-sheet additions (appended to HUD_CSS). */
100
248
  export const LINK_CSS = `
101
- .pop.link .code { margin-top:12px; font:700 28px/1.2 ui-monospace, Menlo, monospace; letter-spacing:.14em; text-align:center; color:#f0eae4; }
102
- .linkrow { display:flex; align-items:center; justify-content:space-between; gap:14px; margin-top:12px; }
103
- .linkrow .sendb { text-decoration:none; }
104
- .qr { width:96px; height:96px; border-radius:8px; background:#fff; padding:4px; }
105
- .linkline { margin-top:10px; font:500 10px/1.4 ui-monospace, Menlo, monospace; color:#756e68; display:flex; gap:10px; align-items:center; }
106
- .linkline.bad { color:#ff3b57; }
107
- .retry { all:unset; cursor:pointer; padding:3px 8px; border-radius:6px; border:1px solid #363132; color:#a8a099; font:600 10px/1 ui-monospace, Menlo, monospace; }
108
- .retry:hover { color:#f0eae4; border-color:#756e68; }
109
- .grip.link { width:auto; border-radius:999px; padding:0 14px; gap:8px; color:#f0eae4; font:600 11px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
249
+ .linkgrid { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:4px 12px; align-items:center; margin-top:10px; }
250
+ .linkgrid.noqr { grid-template-columns:minmax(0,1fr); }
251
+ .code { font-size:22px; font-weight:700; line-height:1.2; letter-spacing:.08em; font-variant-numeric:tabular-nums; color:var(--fg); }
252
+ .linkgrid .sendb { justify-self:start; margin-top:6px; }
253
+ .qr { grid-column:2; grid-row:1 / span 3; width:76px; height:76px; border-radius:8px; background:#fff; padding:4px; }
254
+ .linkline { margin-top:8px; font-size:11px; line-height:1.4; color:var(--fg-2); display:flex; gap:10px; align-items:center; }
255
+ .linkline.bad { color:var(--warn); }
256
+ .retry { all:unset; box-sizing:border-box; cursor:pointer; min-height:24px; padding:4px 9px; border-radius:7px; box-shadow:inset 0 0 0 1px var(--edge);
257
+ color:var(--fg); font-size:11.5px; font-weight:600; line-height:1; }
258
+ @media (hover: hover) { .retry:hover { background:var(--ink-2); } }
259
+ `;
260
+ /** Reduced motion: no ripple, no springs, no slides — every state change is instant. */
261
+ export const MOTION_CSS = `
262
+ @media (prefers-reduced-motion: reduce) {
263
+ .hud *, .hud *::before, .hud *::after { transition-duration:0ms !important; transition-delay:0ms !important; animation:none !important; }
264
+ }
110
265
  `;
111
- export const HUD_CSS = HUD_CSS_BASE + LINK_CSS;
266
+ export const HUD_CSS = HUD_CSS_BASE + LINK_CSS + MOTION_CSS;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Where the HUD rests and how it moves (recorder-hud-subtle D2). The dock
3
+ * sits on one of six spots by CSS insets (see `.dock` in styles.ts), so a
4
+ * resize or rotation needs no JS; a drag moves it by `transform` only and a
5
+ * release settles through `@vitrinka/link/dock` — a flick lands where it is
6
+ * aimed, a push past a side edge tucks it into a tab. The move to the new
7
+ * spot is a FLIP: the dock re-anchors, then springs from where it was let
8
+ * go. Arrow keys on a focused handle are the non-drag alternative.
9
+ * The place is remembered per origin in the recorder's storage.
10
+ */
11
+ import { type Place, type Spot } from '@vitrinka/link/dock';
12
+ import { type KeyboardEvent, type MouseEvent, type PointerEvent, type RefObject } from 'react';
13
+ export interface DockHandle {
14
+ onPointerDown: (e: PointerEvent<HTMLElement>) => void;
15
+ onPointerMove: (e: PointerEvent<HTMLElement>) => void;
16
+ onPointerUp: (e: PointerEvent<HTMLElement>) => void;
17
+ onPointerCancel: (e: PointerEvent<HTMLElement>) => void;
18
+ onKeyDown: (e: KeyboardEvent<HTMLElement>) => void;
19
+ }
20
+ export interface Dock {
21
+ place: Place;
22
+ /** The spot the dock anchors by (a tucked dock: its side and half). */
23
+ spot: Spot;
24
+ ref: RefObject<HTMLDivElement | null>;
25
+ dragging: boolean;
26
+ /** Spread on every drag handle: the capsule handle, the puck, the tab. */
27
+ handle: DockHandle;
28
+ /** On the dock element: swallows the click that ends a drag. */
29
+ onClickCapture: (e: MouseEvent) => void;
30
+ moveTo: (next: Place) => void;
31
+ }
32
+ export declare function useDock(onMoveStart: () => void): Dock;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Where the HUD rests and how it moves (recorder-hud-subtle D2). The dock
3
+ * sits on one of six spots by CSS insets (see `.dock` in styles.ts), so a
4
+ * resize or rotation needs no JS; a drag moves it by `transform` only and a
5
+ * release settles through `@vitrinka/link/dock` — a flick lands where it is
6
+ * aimed, a push past a side edge tucks it into a tab. The move to the new
7
+ * spot is a FLIP: the dock re-anchors, then springs from where it was let
8
+ * go. Arrow keys on a focused handle are the non-drag alternative.
9
+ * The place is remembered per origin in the recorder's storage.
10
+ */
11
+ import { colOf, neighbour, parsePlace, rowOf, settle, untuck } from '@vitrinka/link/dock';
12
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
13
+ import { getRecorderStorage } from '../storage';
14
+ const DOCK_KEY = 'dock';
15
+ /** Travel before a press becomes a drag — below it a press stays a tap. */
16
+ const DRAG_SLOP = 4;
17
+ /** Velocity is read over the last this-many ms of the drag. */
18
+ const VELOCITY_WINDOW = 100;
19
+ const ARROWS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']);
20
+ function reducedMotion() {
21
+ return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
22
+ }
23
+ function margin() {
24
+ return globalThis.matchMedia?.('(pointer: coarse)').matches ? 12 : 16;
25
+ }
26
+ /** The point of `r` a place is anchored by: its corner, its middle on the c column, its mid-height when tucked. */
27
+ function anchorPoint(r, place) {
28
+ const spot = untuck(place);
29
+ const x = colOf(spot) === 'l' ? r.left : colOf(spot) === 'r' ? r.right : r.left + r.width / 2;
30
+ const y = 'tuck' in place ? r.top + r.height / 2 : rowOf(spot) === 't' ? r.top : r.bottom;
31
+ return { x, y };
32
+ }
33
+ export function useDock(onMoveStart) {
34
+ const [place, setPlace] = useState(() => parsePlace(getRecorderStorage().getString(DOCK_KEY)));
35
+ const [dragging, setDragging] = useState(false);
36
+ const ref = useRef(null);
37
+ const from = useRef(null);
38
+ const press = useRef(null);
39
+ const swallowClick = useRef(false);
40
+ const onMoveStartRef = useRef(onMoveStart);
41
+ onMoveStartRef.current = onMoveStart;
42
+ const moveTo = useCallback((next) => {
43
+ const el = ref.current;
44
+ if (el)
45
+ from.current = el.getBoundingClientRect();
46
+ setPlace(next);
47
+ try {
48
+ getRecorderStorage().set(DOCK_KEY, JSON.stringify(next));
49
+ }
50
+ catch (e) {
51
+ console.warn('vitrinka: could not remember the HUD position', e);
52
+ }
53
+ }, []);
54
+ // FLIP: the dock has re-anchored; spring it from where it was let go.
55
+ useLayoutEffect(() => {
56
+ const el = ref.current;
57
+ const prev = from.current;
58
+ from.current = null;
59
+ if (!el || !prev)
60
+ return;
61
+ el.classList.remove('settling');
62
+ el.style.transform = '';
63
+ if (reducedMotion())
64
+ return;
65
+ const next = el.getBoundingClientRect();
66
+ const a = anchorPoint(prev, place);
67
+ const b = anchorPoint(next, place);
68
+ const dx = a.x - b.x;
69
+ const dy = a.y - b.y;
70
+ if (Math.abs(dx) < 1 && Math.abs(dy) < 1)
71
+ return;
72
+ el.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
73
+ void el.offsetWidth;
74
+ el.classList.add('settling');
75
+ el.style.transform = '';
76
+ const done = () => el.classList.remove('settling');
77
+ el.addEventListener('transitionend', done, { once: true });
78
+ }, [place]);
79
+ // The centre column anchors the handle's middle: keep --hw = its width.
80
+ useEffect(() => {
81
+ const el = ref.current;
82
+ if (!el || typeof ResizeObserver !== 'function')
83
+ return;
84
+ const ro = new ResizeObserver(() => {
85
+ const h = el.querySelector('.handle, .puck, .tab');
86
+ if (h)
87
+ el.style.setProperty('--hw', `${h.offsetWidth}px`);
88
+ });
89
+ ro.observe(el);
90
+ return () => ro.disconnect();
91
+ }, []);
92
+ const release = useCallback((e, cancelled) => {
93
+ const p = press.current;
94
+ press.current = null;
95
+ if (!p || p.id !== e.pointerId)
96
+ return;
97
+ if (e.currentTarget.hasPointerCapture?.(e.pointerId))
98
+ e.currentTarget.releasePointerCapture(e.pointerId);
99
+ if (!p.moved)
100
+ return;
101
+ setDragging(false);
102
+ swallowClick.current = true;
103
+ setTimeout(() => (swallowClick.current = false), 0);
104
+ const el = ref.current;
105
+ if (!el)
106
+ return;
107
+ const r = el.getBoundingClientRect();
108
+ if (cancelled) {
109
+ moveTo(place);
110
+ return;
111
+ }
112
+ const s = p.samples;
113
+ const first = s[0];
114
+ const last = s[s.length - 1];
115
+ const dt = first && last ? (last.t - first.t) / 1000 : 0;
116
+ const v = first && last && dt > 0 ? { x: (last.x - first.x) / dt, y: (last.y - first.y) / dt } : { x: 0, y: 0 };
117
+ const m = margin();
118
+ moveTo(settle({ x: r.left, y: r.top, w: r.width, h: r.height }, v, { w: innerWidth, h: innerHeight }, { top: m, right: m, bottom: m, left: m }));
119
+ }, [moveTo, place]);
120
+ const handle = useMemo(() => ({
121
+ onPointerDown: (e) => {
122
+ if (e.button !== 0 || !e.isPrimary)
123
+ return;
124
+ press.current = { id: e.pointerId, x: e.clientX, y: e.clientY, moved: false, samples: [] };
125
+ // Capture at once: a fast first move would otherwise leave the handle
126
+ // before the drag begins and the page would get the rest.
127
+ e.currentTarget.setPointerCapture?.(e.pointerId);
128
+ },
129
+ onPointerMove: (e) => {
130
+ const p = press.current;
131
+ const el = ref.current;
132
+ if (!p || p.id !== e.pointerId || !el)
133
+ return;
134
+ const dx = e.clientX - p.x;
135
+ const dy = e.clientY - p.y;
136
+ if (!p.moved) {
137
+ if (Math.hypot(dx, dy) < DRAG_SLOP)
138
+ return;
139
+ p.moved = true;
140
+ el.classList.remove('settling');
141
+ setDragging(true);
142
+ onMoveStartRef.current();
143
+ }
144
+ el.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
145
+ const t = e.timeStamp;
146
+ p.samples.push({ x: e.clientX, y: e.clientY, t });
147
+ while (p.samples.length > 2 && t - p.samples[0].t > VELOCITY_WINDOW)
148
+ p.samples.shift();
149
+ },
150
+ onPointerUp: (e) => release(e, false),
151
+ onPointerCancel: (e) => release(e, true),
152
+ onKeyDown: (e) => {
153
+ if (!ARROWS.has(e.key) || e.altKey || e.metaKey || e.ctrlKey)
154
+ return;
155
+ e.preventDefault();
156
+ onMoveStartRef.current();
157
+ moveTo(neighbour(place, e.key));
158
+ },
159
+ }), [moveTo, place, release]);
160
+ const onClickCapture = useCallback((e) => {
161
+ if (!swallowClick.current)
162
+ return;
163
+ swallowClick.current = false;
164
+ e.preventDefault();
165
+ e.stopPropagation();
166
+ }, []);
167
+ return { place, spot: untuck(place), ref, dragging, handle, onClickCapture, moveTo };
168
+ }
@@ -6,11 +6,15 @@ import { type Linked, type LinkStart } from '@vitrinka/link';
6
6
  export { LinkExpired } from '@vitrinka/link';
7
7
  export interface DeviceLink {
8
8
  start: LinkStart;
9
- /** Resolves once the tester approved; rejects with LinkExpired / AbortError. */
9
+ /** Resolves once the tester approved; rejects with LinkExpired / LinkWorkspaceMismatch / AbortError. */
10
10
  linked: Promise<Linked>;
11
11
  cancel: () => void;
12
12
  }
13
- /** Ask for a code and poll until approved. The token is stored on success. */
13
+ /**
14
+ * Ask for a code and poll until approved. The token is stored on success.
15
+ * A `url` addressing `/w/<slug>` preselects that workspace on the approve
16
+ * page, and a token approved into any other one is discarded unstored.
17
+ */
14
18
  export declare function linkDevice(): Promise<DeviceLink>;
15
19
  /** Forget the stored link; a live session ends locally (its tail is dropped). */
16
20
  export declare function forgetLink(): void;