@tobq/boardclip 0.0.0-stage → 2.1.108
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/LICENSE +21 -0
- package/README.md +170 -2
- package/assets/boardclip-icon.png +0 -0
- package/bin/boardclip.js +43 -0
- package/editor-preload.js +71 -0
- package/editor.html +220 -0
- package/icon.png +0 -0
- package/icon@2x.png +0 -0
- package/iconTemplate.png +0 -0
- package/iconTemplate@2x.png +0 -0
- package/index.html +1339 -0
- package/lib/appearance.js +183 -0
- package/lib/auto-update.js +306 -0
- package/lib/backup-worker.js +46 -0
- package/lib/backup.js +275 -0
- package/lib/blob-field-store.js +275 -0
- package/lib/blob-store.js +68 -0
- package/lib/build-info.js +89 -0
- package/lib/clip-blob-store.js +161 -0
- package/lib/clip-revision.js +81 -0
- package/lib/clipboard-capture.js +95 -0
- package/lib/clipboard-follow.js +98 -0
- package/lib/clipboard-model.js +1235 -0
- package/lib/cloud-accounts.js +250 -0
- package/lib/conflict-model.js +186 -0
- package/lib/control-client.js +87 -0
- package/lib/control-server.js +149 -0
- package/lib/diagnostics.js +121 -0
- package/lib/drag-files.js +48 -0
- package/lib/ensure-directory.js +56 -0
- package/lib/fork-names.js +35 -0
- package/lib/fs-probe.js +132 -0
- package/lib/history-feed.js +48 -0
- package/lib/hmac-auth.js +78 -0
- package/lib/macos-hotkey.js +297 -0
- package/lib/macos-paste.js +112 -0
- package/lib/mcp-core.js +270 -0
- package/lib/mcp-installers.js +284 -0
- package/lib/mcp-paths.js +99 -0
- package/lib/p2p-crypto.js +54 -0
- package/lib/p2p-discovery.js +191 -0
- package/lib/quick-paste.js +187 -0
- package/lib/retention.js +43 -0
- package/lib/sync-delta.js +227 -0
- package/lib/sync-journal.js +181 -0
- package/lib/sync-paths.js +73 -0
- package/lib/tailscale.js +80 -0
- package/lib/text-blob-store.js +38 -0
- package/lib/windows-clipboard.js +213 -0
- package/lib/windows-console.js +63 -0
- package/lib/windows-dwm.js +183 -0
- package/lib/windows-hook-worker.js +236 -0
- package/lib/windows-hook.js +91 -0
- package/lib/windows-paste.js +185 -0
- package/main.js +7540 -0
- package/mcp/boardclip-mcp.js +255 -0
- package/mcp-approval-preload.js +22 -0
- package/mcp-approval.html +218 -0
- package/package.json +55 -4
- package/preload.js +106 -0
- package/site/index.html +1624 -0
- package/site/shared/clip-search.js +2330 -0
- package/site/shared/clipboard-popup.css +1046 -0
- package/site/shared/clipboard-tokens.css +326 -0
- package/site/shared/clipboard-ui-core.js +6921 -0
- package/site/shared/clipboard-window.css +36 -0
- package/site/shared/vendor/cm5/README.md +52 -0
- package/site/shared/vendor/cm5/codemirror.css +344 -0
- package/site/shared/vendor/cm5/codemirror.js +9884 -0
- package/site/shared/vendor/cm5/diff-match-patch.js +2228 -0
- package/site/shared/vendor/cm5/merge.css +119 -0
- package/site/shared/vendor/cm5/merge.js +1182 -0
- package/site/shared/vendor/fonts/README.md +19 -0
- package/site/shared/vendor/fonts/material-symbols-rounded.woff2 +0 -0
- package/viewer-preload.js +51 -0
- package/viewer.html +129 -0
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
@import url("clipboard-tokens.css");
|
|
2
|
+
|
|
3
|
+
/* The icon font, self-hosted (vendor/fonts/README.md): every app window and the
|
|
4
|
+
website load THIS sheet, so this is its one declaration. No window waits on
|
|
5
|
+
Google Fonts, and font-display: block means a reload never flashes the
|
|
6
|
+
ligature words ("settings", "close") while the font loads. */
|
|
7
|
+
@font-face {
|
|
8
|
+
font-family: "Material Symbols Rounded";
|
|
9
|
+
font-display: block;
|
|
10
|
+
src: url("vendor/fonts/material-symbols-rounded.woff2") format("woff2");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ONE icon spec: Material Symbols Rounded (the vendored variable font above),
|
|
14
|
+
axes pinned HERE and nowhere else. FILL 1 is a state signal only
|
|
15
|
+
(pinned star, active facet): set --icon-fill: 1 on the icon or a parent. */
|
|
16
|
+
.mi {
|
|
17
|
+
font-family: "Material Symbols Rounded";
|
|
18
|
+
font-size: var(--icon-md);
|
|
19
|
+
font-weight: var(--fw-regular);
|
|
20
|
+
font-style: normal;
|
|
21
|
+
font-variation-settings: "FILL" var(--icon-fill, 0), "wght" 400, "GRAD" 0, "opsz" 20;
|
|
22
|
+
line-height: 1;
|
|
23
|
+
letter-spacing: normal;
|
|
24
|
+
text-transform: none;
|
|
25
|
+
display: inline-block;
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
word-wrap: normal;
|
|
28
|
+
direction: ltr;
|
|
29
|
+
-webkit-font-smoothing: antialiased;
|
|
30
|
+
}
|
|
31
|
+
.mi.filled { --icon-fill: 1; }
|
|
32
|
+
.mi.sm { font-size: var(--icon-sm); }
|
|
33
|
+
.mi.lg { font-size: var(--icon-lg); }
|
|
34
|
+
.mi.mid { vertical-align: middle; }
|
|
35
|
+
|
|
36
|
+
/* ---------- Shared primitives (every window + the website demo) ---------- */
|
|
37
|
+
|
|
38
|
+
/* ONE motion switch: with prefers-reduced-motion every state transition (they
|
|
39
|
+
all run on the shared --dur, the reveal and the panel fold included) is instant. */
|
|
40
|
+
@media (prefers-reduced-motion: reduce) {
|
|
41
|
+
:root { --dur: 0ms; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ONE keyboard focus ring for every BoardClip control: a box-shadow, so no
|
|
45
|
+
outline and no layout shift. Text fields show focus on their own edge. */
|
|
46
|
+
:is(:root[data-theme], .bc-popup) :where(button, [role="button"], a[href], .filter-tag, .seg-btn, .np-btn, .bc-menu-item):focus-visible {
|
|
47
|
+
outline: none; box-shadow: var(--focus-ring);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ONE floating surface for every panel that floats over content: menus,
|
|
51
|
+
submenus, popovers (a row's keypad / group picker), suggest, dialogs, the
|
|
52
|
+
toast and the Newest pill. Flat in-window, this is the only elevation. */
|
|
53
|
+
.search-suggest, .dialog, .toast, .list-newest, .tag-submenu, .bc-menu { background: var(--menu); border-radius: var(--r-panel); padding: var(--sp-2);
|
|
54
|
+
box-shadow: 0 8px 24px var(--shadow), 0 0 0 1px var(--menu-edge); }
|
|
55
|
+
|
|
56
|
+
/* ONE button family: a fill, never a border. Default / primary / danger /
|
|
57
|
+
quiet, plus .sm (20px text affordance in bars and footers). */
|
|
58
|
+
.btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); height: var(--ctl-lg); padding: 0 var(--sp-6);
|
|
59
|
+
border: none; border-radius: var(--r-ctl); background: var(--ctl-fill); color: var(--text); font-family: inherit; font-size: var(--fs-ui);
|
|
60
|
+
font-weight: var(--fw-regular); line-height: var(--lh-ui); white-space: nowrap; cursor: pointer;
|
|
61
|
+
transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
|
|
62
|
+
.btn:hover { background: var(--ctl-fill-hover); }
|
|
63
|
+
.btn.primary { background: var(--accent); color: var(--active-fg); }
|
|
64
|
+
.btn.primary:hover { background: var(--accent-hover); }
|
|
65
|
+
.btn.danger { background: var(--red); color: var(--danger-fg); }
|
|
66
|
+
.btn.danger:hover { background: color-mix(in srgb, var(--red) 86%, var(--text)); }
|
|
67
|
+
.btn.quiet { background: transparent; color: var(--text-dim); }
|
|
68
|
+
.btn.quiet:hover { background: var(--hover); color: var(--text); }
|
|
69
|
+
.btn.sm { height: var(--ctl-sm); padding: 0 var(--sp-3); font-size: var(--fs-meta); }
|
|
70
|
+
/* An accent text action (Undo in a toast, "Did you mean", a pane head's
|
|
71
|
+
Accept, the merge note's "Show them"): a quiet button in the accent TEXT
|
|
72
|
+
colour, the accent tint on hover. */
|
|
73
|
+
.btn.quiet.accent, .btn.quiet.accent:hover { color: var(--accent-text); }
|
|
74
|
+
.btn.quiet.accent:hover { background: var(--accent-bg); }
|
|
75
|
+
/* A default button whose action throws something away (Remove conflict): the
|
|
76
|
+
menu's danger cue on hover / focus, never a second button family. */
|
|
77
|
+
.btn.discard:hover, .btn.discard:focus-visible { background: var(--red-bg); color: var(--red); }
|
|
78
|
+
.btn:disabled, .btn:disabled:hover { background: var(--ctl-fill); color: var(--text-dim); cursor: not-allowed; }
|
|
79
|
+
.btn.quiet:disabled, .btn.quiet:disabled:hover { background: transparent; }
|
|
80
|
+
|
|
81
|
+
/* ONE overline: the small uppercase heading over a SECTION (a settings
|
|
82
|
+
section, the approval prompt's clip card, the demo's note). Window titles
|
|
83
|
+
are NOT overlines (they are .bc-bar-title, sentence case). */
|
|
84
|
+
.overline, .settings-note, .settings-section h3 {
|
|
85
|
+
font-size: var(--fs-meta); font-weight: var(--fw-strong); line-height: var(--lh-ui); letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim);
|
|
86
|
+
}
|
|
87
|
+
/* ONE small label, sentence case: names a group of controls or a pane inside
|
|
88
|
+
a section (an options-panel row, a list in a settings section, a merge
|
|
89
|
+
pane head). Deliberately not an overline: it sits beside
|
|
90
|
+
controls, where an uppercase heading would shout. */
|
|
91
|
+
.bc-label, .opts-facet-label, .settings-list-title, .bc-head-label {
|
|
92
|
+
font-size: var(--fs-meta); font-weight: var(--fw-regular); line-height: var(--lh-ui); color: var(--text-dim);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ONE scrollbar for every BoardClip surface: the app windows (data-theme on
|
|
96
|
+
<html>) and the demo popup (.bc-popup). The thumb is inset by a transparent
|
|
97
|
+
border clipped to the padding box, so it works on any background. Chromium
|
|
98
|
+
ignores these pseudo-elements on an element that sets scrollbar-width or
|
|
99
|
+
scrollbar-color, so nothing else may set them (except to hide a bar). */
|
|
100
|
+
:is(:root[data-theme], .bc-popup) ::-webkit-scrollbar { width: var(--scrollbar-size); height: var(--scrollbar-size); }
|
|
101
|
+
:is(:root[data-theme], .bc-popup) ::-webkit-scrollbar-track,
|
|
102
|
+
:is(:root[data-theme], .bc-popup) ::-webkit-scrollbar-corner { background: transparent; }
|
|
103
|
+
:is(:root[data-theme], .bc-popup) ::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); background-clip: padding-box; border: 2px solid transparent; border-radius: var(--r-panel); }
|
|
104
|
+
:is(:root[data-theme], .bc-popup) ::-webkit-scrollbar-thumb:hover { background-color: var(--scrollbar-thumb-hover); }
|
|
105
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
106
|
+
:where(:root[data-theme], .bc-popup) * { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) transparent; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ONE text-selection colour (typed text, a find match left selected, any
|
|
110
|
+
selectable copy), in every window and the demo. */
|
|
111
|
+
:is(:root[data-theme], .bc-popup) ::selection { background: var(--accent-bg-strong); }
|
|
112
|
+
|
|
113
|
+
/* Filled text fields: a fill, never a border; focus (and a recording shortcut
|
|
114
|
+
field) draws an inset accent edge, which never shifts layout. The popup's
|
|
115
|
+
search field and the clip windows' title / find fields are the flat
|
|
116
|
+
exception (no fill, a hairline underline). */
|
|
117
|
+
.setting-row input:focus, .prompt-input:focus,
|
|
118
|
+
.shortcut-btn:hover, .shortcut-btn.recording { box-shadow: inset 0 0 0 1px var(--accent); }
|
|
119
|
+
|
|
120
|
+
/* ---------- Components ---------- */
|
|
121
|
+
|
|
122
|
+
.main-view { display: flex; flex-direction: column; flex: 1; min-height: 0; height: 100%; }
|
|
123
|
+
.main-view.hidden { display: none; }
|
|
124
|
+
/* TWO tones, the same in every window: the chrome (the popup's header with
|
|
125
|
+
the search, the options panel and the chips; the settings header; every
|
|
126
|
+
window bar and foot) is a band in --surface, the content (the results, the
|
|
127
|
+
settings body, the editor's text, the viewer) sits on --canvas, which the
|
|
128
|
+
window (body, or the demo's frame) paints; the band ends on the --line
|
|
129
|
+
hairline. Under glass --surface is a translucent lift over the one scrim
|
|
130
|
+
(no second blur). */
|
|
131
|
+
.sticky { flex-shrink: 0; background: var(--surface); padding-bottom: var(--sp-2); border-bottom: 1px solid var(--line); z-index: 10; position: relative; }
|
|
132
|
+
/* The header (and the settings header) are NOT -webkit-app-region: drag: that
|
|
133
|
+
swallowed every click and double-click-maximised. Core.attachWindowDrag owns
|
|
134
|
+
them: a click focuses the search, a press-and-move past 4 px moves the window. */
|
|
135
|
+
.sticky header { display: flex; align-items: center; padding: var(--sp-4) var(--edge-ctl) 0 var(--gutter); gap: var(--sp-3); }
|
|
136
|
+
.count { font-size: var(--fs-meta); color: var(--text-dim); flex: 1; }
|
|
137
|
+
/* .close-btn is only a hook now: every close is <span class="mi">close</span>
|
|
138
|
+
inside a plain .icon-btn. */
|
|
139
|
+
.icon-btn { background: none; border: none; border-radius: var(--r-ctl); color: var(--text-dim); cursor: pointer;
|
|
140
|
+
width: var(--ctl-md); height: var(--ctl-md); display: flex; align-items: center; justify-content: center;
|
|
141
|
+
transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); padding: 0; }
|
|
142
|
+
.icon-btn:hover { color: var(--text); background: var(--hover); }
|
|
143
|
+
.icon-btn.danger:hover, .icon-btn.danger:focus-visible { color: var(--red); background: var(--red-bg); }
|
|
144
|
+
.icon-btn:disabled { opacity: .45; cursor: not-allowed; }
|
|
145
|
+
.icon-btn:disabled:hover { color: var(--text-dim); background: none; }
|
|
146
|
+
.icon-btn .mi { font-size: var(--icon-md); }
|
|
147
|
+
/* ONE reveal: a control that slides in and out sits in a 0fr -> 1fr width
|
|
148
|
+
track (Forge's QueueStrip collapser), so the text beside it takes the full
|
|
149
|
+
width while it is hidden and the reveal animates instead of snapping. Two
|
|
150
|
+
belts, both unconditional: `min-width: 0` on the track's child (the
|
|
151
|
+
0fr track is minmax(auto, 0fr), and auto = the child's min-content width,
|
|
152
|
+
i.e. no collapse) and a padding-less inner wrapper (a border box cannot be
|
|
153
|
+
crushed below its own padding). Open = .open on the track (or a consumer's
|
|
154
|
+
own selector, e.g. a row hover). Closed controls go invisible once the
|
|
155
|
+
track has shut, so Tab never lands on them. The space before a revealed
|
|
156
|
+
control is its own (--reveal-gap, a margin INSIDE the track), never the
|
|
157
|
+
container's flex gap: a gap stays between closed, 0 px tracks. Used by the
|
|
158
|
+
search field's buttons and, next, the row actions. Markup:
|
|
159
|
+
<span class="bc-reveal"><span class="bc-reveal-inner">...</span></span> */
|
|
160
|
+
.bc-reveal { display: grid; grid-template-columns: 0fr; flex-shrink: 0; overflow: clip; overflow-clip-margin: var(--sp-2);
|
|
161
|
+
transition: grid-template-columns var(--dur) var(--ease); }
|
|
162
|
+
.bc-reveal > * { min-width: 0; }
|
|
163
|
+
/* visibility interpolates as "visible" for the whole transition when either end is
|
|
164
|
+
visible: shown at once on open, hidden only once the track has shut.
|
|
165
|
+
content-visibility does the same (allow-discrete) and also skips laying out a
|
|
166
|
+
closed reveal's contents: every list row carries two closed reveals, and
|
|
167
|
+
laying out their buttons cost ~8 ms of each keystroke's rebuild (measured,
|
|
168
|
+
owner scale). Every open selector sets both. */
|
|
169
|
+
.bc-reveal-inner { display: flex; align-items: center; gap: var(--sp-1); padding: 0; visibility: hidden; content-visibility: hidden;
|
|
170
|
+
transition: visibility var(--dur) var(--ease), content-visibility var(--dur) var(--ease) allow-discrete; }
|
|
171
|
+
.bc-reveal-inner > :first-child { margin-inline-start: var(--reveal-gap, 0); }
|
|
172
|
+
.bc-reveal.open { grid-template-columns: 1fr; }
|
|
173
|
+
.bc-reveal.open > .bc-reveal-inner { visibility: visible; content-visibility: visible; }
|
|
174
|
+
|
|
175
|
+
/* The search field is FLAT: no fill, no border, a hairline underline (an inset
|
|
176
|
+
shadow, so focus never shifts layout) that turns accent while focused. */
|
|
177
|
+
.search-row { display: flex; align-items: center; gap: 0; --reveal-gap: var(--sp-1); margin: var(--sp-3) var(--gutter) var(--sp-2); background: transparent; border: none; border-radius: 0;
|
|
178
|
+
box-shadow: inset 0 -1px 0 var(--line); transition: box-shadow var(--dur) var(--ease); }
|
|
179
|
+
/* Focused = ONE state, set by Core.attachSearchBox: the field (or its buttons)
|
|
180
|
+
has the focus AND the window does - the same state that swaps the
|
|
181
|
+
placeholder, so an unfocused popup never shows an accent line beside
|
|
182
|
+
"Click here to search...". */
|
|
183
|
+
.search-row.is-focused { box-shadow: inset 0 -1px 0 var(--accent); }
|
|
184
|
+
.search-field { position: relative; flex: 1; min-width: 0; display: flex; }
|
|
185
|
+
.search { flex: 1; background: transparent; border: none; padding: var(--sp-3) var(--sp-2) var(--sp-3) 0; color: var(--text); font-size: var(--fs-text); outline: none; font-family: inherit; min-width: 0; }
|
|
186
|
+
.search::placeholder { color: var(--text-dim); }
|
|
187
|
+
/* The live-highlight input paints its text transparent (the mirror shows it);
|
|
188
|
+
::placeholder inherits that fill, so it must set its own. */
|
|
189
|
+
.search-live::placeholder { -webkit-text-fill-color: var(--text-dim); }
|
|
190
|
+
.search-row .icon-btn { flex-shrink: 0; }
|
|
191
|
+
/* The last in-field button's glyph (tune, in the tools reveal: open whenever
|
|
192
|
+
any in-field button is) ends on the gutter with the header's gear and the
|
|
193
|
+
selection bar's close, the underline under it. */
|
|
194
|
+
.search-row > .bc-reveal[data-reveal="tools"] { transition: grid-template-columns var(--dur) var(--ease), margin-right var(--dur) var(--ease); }
|
|
195
|
+
.search-row > .bc-reveal[data-reveal="tools"].open { margin-right: calc(var(--edge-ctl) - var(--gutter)); } /* closed, the field keeps the whole row */
|
|
196
|
+
/* A toggled in-field control: Regex (the search field's and the find bar's),
|
|
197
|
+
match case, Recent order, the options panel open (or a filter it owns in
|
|
198
|
+
the query). */
|
|
199
|
+
.rx-btn.active, .case-btn.active, .sort-btn.active, .opts-btn.active { color: var(--accent); background: var(--accent-bg); }
|
|
200
|
+
.rx-btn.active:hover, .case-btn.active:hover, .sort-btn.active:hover, .opts-btn.active:hover { color: var(--accent); background: var(--accent-bg-strong); }
|
|
201
|
+
|
|
202
|
+
/* Live query-syntax highlight: a colored backdrop mirror UNDER a transparent input. The
|
|
203
|
+
two must share identical text metrics (font/size/padding/line-height) so glyphs align. */
|
|
204
|
+
.search-hl { position: absolute; inset: 0; padding: var(--sp-3) var(--sp-2) var(--sp-3) 0;
|
|
205
|
+
font-size: var(--fs-text); font-family: inherit; line-height: normal; color: var(--text);
|
|
206
|
+
white-space: pre; overflow: hidden; pointer-events: none; z-index: 0; border: none;
|
|
207
|
+
display: flex; align-items: center; }
|
|
208
|
+
.search-live { position: relative; z-index: 1; background: transparent; -webkit-text-fill-color: transparent; caret-color: var(--text); }
|
|
209
|
+
.qh-prefix { color: var(--accent-text); font-weight: var(--fw-strong); }
|
|
210
|
+
.qh-neg { color: var(--red); font-weight: var(--fw-strong); }
|
|
211
|
+
.qh-value { color: var(--text); }
|
|
212
|
+
.qh-regex { color: var(--green); }
|
|
213
|
+
.qh-quote { color: var(--text-dim); }
|
|
214
|
+
/* OR and the brackets of a group: the language's operators, like a key. */
|
|
215
|
+
.qh-op { color: var(--accent-text); font-weight: var(--fw-strong); }
|
|
216
|
+
/* Exactly the invalid key or value (Search.validateQuery), nothing around it. */
|
|
217
|
+
.qh-unknown { color: var(--red); text-decoration: underline wavy var(--red); text-decoration-thickness: 1px; text-underline-offset: 3px; }
|
|
218
|
+
.qh-ws { color: transparent; }
|
|
219
|
+
/* The top suggestion's rest, after the typed text (Tab or Right at the end takes it). */
|
|
220
|
+
.qh-ghost { color: var(--text-dim); opacity: .7; }
|
|
221
|
+
|
|
222
|
+
/* ONE hint line under the field (an invalid token, a broken regex): a rows
|
|
223
|
+
fold, 0 px and nothing reserved while there is nothing to say. */
|
|
224
|
+
.search-hint { display: grid; grid-template-rows: 0fr; opacity: 0; margin: 0 var(--gutter);
|
|
225
|
+
transition: grid-template-rows var(--dur) var(--ease), opacity var(--dur) var(--ease); }
|
|
226
|
+
.search-hint.show { grid-template-rows: 1fr; opacity: 1; }
|
|
227
|
+
.search-hint-clip { min-height: 0; overflow: hidden; }
|
|
228
|
+
.search-hint-row { display: flex; align-items: baseline; gap: var(--sp-2); padding: 0 0 var(--sp-2); font-size: var(--fs-meta); line-height: var(--lh-ui); color: var(--text-dim); }
|
|
229
|
+
.search-hint-row > .mi { font-size: var(--icon-sm); color: var(--red); align-self: center; }
|
|
230
|
+
.search-hint-fix { margin-left: var(--sp-1); } /* the shared .btn.quiet.sm.accent */
|
|
231
|
+
.search-hint-fix code { font-family: var(--font-mono); }
|
|
232
|
+
|
|
233
|
+
/* Autocomplete dropdown for the token being typed. */
|
|
234
|
+
/* .search-suggest shares the floating-surface bg/shadow/radius rule (top of file); only its
|
|
235
|
+
positioning + scroll live here. Its rows ARE menu rows (.bc-menu-item): the
|
|
236
|
+
token in mono, its own hint dim at the end. */
|
|
237
|
+
.search-suggest { position: absolute; top: calc(100% + var(--sp-2)); left: 0; right: 0; z-index: 40;
|
|
238
|
+
overflow: hidden; max-height: 240px; overflow-y: auto; }
|
|
239
|
+
.search-suggest.hidden { display: none; }
|
|
240
|
+
.search-suggest-item .ss-text { color: var(--text); font-family: var(--font-mono); }
|
|
241
|
+
.search-suggest-item .ss-hint { flex-shrink: 0; color: var(--text-dim); font-size: var(--fs-meta); }
|
|
242
|
+
.search-suggest-item.active { background: var(--menu-hover); }
|
|
243
|
+
|
|
244
|
+
/* Search options panel (the "tune" toggle): an in-flow fold under the search
|
|
245
|
+
field, inside the sticky header. Closed = grid row 0fr + opacity 0 + inert
|
|
246
|
+
(Forge's SearchFacets fold); open = 1fr. Its scroller is capped by
|
|
247
|
+
--opts-h (Core.attachSearchBox: ~40 % of the popup by default, dragged on
|
|
248
|
+
the bottom handle, kept per machine) and fades its edges (.bc-scroll-fade). */
|
|
249
|
+
.search-opts { display: grid; grid-template-rows: 0fr; opacity: 0;
|
|
250
|
+
transition: grid-template-rows var(--dur) var(--ease), opacity var(--dur) var(--ease); }
|
|
251
|
+
.search-opts.open { grid-template-rows: 1fr; opacity: 1; }
|
|
252
|
+
.search-opts-clip { min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
253
|
+
.search-opts-scroll { max-height: var(--opts-h, 240px); overflow-y: auto; padding: var(--sp-2) var(--gutter) var(--sp-4); }
|
|
254
|
+
.search-opts.resizing .search-opts-scroll { max-height: none; height: var(--opts-h, 240px); }
|
|
255
|
+
/* Filter units flow and wrap (Forge's FacetRows): as many per line as fit.
|
|
256
|
+
Every label takes the same width, so units stacked in a narrow popup still
|
|
257
|
+
line their chips up in one column. */
|
|
258
|
+
.opts-facets { --opts-label-w: 3.2em; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-10); }
|
|
259
|
+
.opts-unit { display: flex; align-items: center; gap: var(--sp-3); flex: 0 0 auto; max-width: 100%; } /* whole units move to the next line; only one wider than the panel wraps its chips */
|
|
260
|
+
/* A real width, not just a flex-basis: Chromium sizes the unit from the
|
|
261
|
+
label's TEXT width, then lays the label out at its basis, taking the
|
|
262
|
+
difference from the chips (the last chip of a unit that fit wrapped). */
|
|
263
|
+
.opts-unit > .opts-facet-label { flex: 0 0 auto; width: var(--opts-label-w); }
|
|
264
|
+
.opts-facet-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); min-width: 0; }
|
|
265
|
+
/* A key chip paints its prefix as the field will (.qh-prefix), and the notes
|
|
266
|
+
their tokens (.qh-neg, .qh-quote): what the panel shows is what you type. */
|
|
267
|
+
.opts-notes { flex: 1 0 100%; margin: 0; padding-left: calc(var(--opts-label-w) + var(--sp-3) + var(--sp-3)); font-size: var(--fs-meta); line-height: var(--lh-ui); color: var(--text-dim); }
|
|
268
|
+
.opts-notes-sep { margin: 0 var(--sp-2); }
|
|
269
|
+
/* Bottom edge: the resize handle (Forge's ResizeHandle: a chrome-less strip,
|
|
270
|
+
accent while hovered or dragged) over the panel's divider line. */
|
|
271
|
+
.search-opts-resize { position: relative; flex-shrink: 0; height: var(--sp-3); cursor: row-resize; touch-action: none; }
|
|
272
|
+
.search-opts-resize::after { content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: var(--line);
|
|
273
|
+
transition: background-color var(--dur) var(--ease), height var(--dur) var(--ease); }
|
|
274
|
+
.search-opts-resize:hover::after, .search-opts.resizing .search-opts-resize::after { height: 2px; background: color-mix(in srgb, var(--accent) 55%, transparent); }
|
|
275
|
+
|
|
276
|
+
/* ONE scroll-edge fade (Forge's useScrollFade): a real alpha mask, so content
|
|
277
|
+
dissolves into whatever is behind it, glass included. Core.attachScrollFade
|
|
278
|
+
sets --fade-top / --fade-bottom to the preset size only while content is
|
|
279
|
+
hidden past that edge; @property makes them animatable, so the fade eases in
|
|
280
|
+
and out as you reach an end. */
|
|
281
|
+
@property --fade-top { syntax: "<length>"; inherits: false; initial-value: 0px; }
|
|
282
|
+
@property --fade-bottom { syntax: "<length>"; inherits: false; initial-value: 0px; }
|
|
283
|
+
.bc-scroll-fade {
|
|
284
|
+
-webkit-mask-image: linear-gradient(to bottom, transparent 0, black var(--fade-top), black calc(100% - var(--fade-bottom)), transparent 100%);
|
|
285
|
+
mask-image: linear-gradient(to bottom, transparent 0, black var(--fade-top), black calc(100% - var(--fade-bottom)), transparent 100%);
|
|
286
|
+
transition: --fade-top var(--dur) var(--ease), --fade-bottom var(--dur) var(--ease);
|
|
287
|
+
}
|
|
288
|
+
/* The same fade sideways, for a one-line strip that scrolls (the chip row, a
|
|
289
|
+
window bar's keys): Core.attachScrollFade(el, preset, { axis: 'x' }). */
|
|
290
|
+
@property --fade-left { syntax: "<length>"; inherits: false; initial-value: 0px; }
|
|
291
|
+
@property --fade-right { syntax: "<length>"; inherits: false; initial-value: 0px; }
|
|
292
|
+
.bc-scroll-fade-x {
|
|
293
|
+
-webkit-mask-image: linear-gradient(to right, transparent 0, black var(--fade-left), black calc(100% - var(--fade-right)), transparent 100%);
|
|
294
|
+
mask-image: linear-gradient(to right, transparent 0, black var(--fade-left), black calc(100% - var(--fade-right)), transparent 100%);
|
|
295
|
+
transition: --fade-left var(--dur) var(--ease), --fade-right var(--dur) var(--ease);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* The chip row (Core.attachChipStrip): ONE line that scrolls sideways under
|
|
299
|
+
the mask fade (.bc-scroll-fade-x), every chip once the search options panel
|
|
300
|
+
is open (.expanded, wrapped). .strip-moving is the switch: the row's height
|
|
301
|
+
follows while each chip glides to its new place (FLIP). */
|
|
302
|
+
.group-filters { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-1) var(--gutter) var(--sp-1) calc(var(--gutter) - var(--sp-2)); box-sizing: border-box;
|
|
303
|
+
flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; position: relative; z-index: 30; }
|
|
304
|
+
.group-filters::-webkit-scrollbar { display: none; }
|
|
305
|
+
.group-filters > * { flex-shrink: 0; }
|
|
306
|
+
.group-filters.expanded { flex-wrap: wrap; overflow: visible; }
|
|
307
|
+
.group-filters.strip-moving { overflow: hidden; }
|
|
308
|
+
/* ONE chip (Core.renderChip): the chip bar's icon facets and group chips and
|
|
309
|
+
the options panel's facet chips. Idle = dim text, no fill, no border (a
|
|
310
|
+
group chip: its name in the accent, so the groups read at a glance; owner
|
|
311
|
+
2026-10-09: "too hard to see before selected"); hover = the hover overlay;
|
|
312
|
+
active (include) = the whole chip lit, a solid accent fill with the accent's
|
|
313
|
+
ink and a filled glyph; excluded = struck through. One shape: the pill. */
|
|
314
|
+
.filter-tag { min-height: var(--ctl-sm); max-width: 170px; box-sizing: border-box; font-size: var(--fs-ui); padding: 0 var(--sp-3); border-radius: var(--r-chip); border: 0;
|
|
315
|
+
background: transparent; color: var(--text-dim); cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); line-height: var(--lh-ui); position: relative;
|
|
316
|
+
display: inline-flex; align-items: center; gap: var(--sp-1); vertical-align: top; font-family: inherit; }
|
|
317
|
+
.filter-tag.icon-filter { min-width: var(--ctl-sm); height: var(--ctl-sm); justify-content: center; padding: 0 var(--sp-2); }
|
|
318
|
+
.filter-tag:hover { background: var(--hover); color: var(--text); }
|
|
319
|
+
.filter-tag.group-tag, .filter-tag.group-tag:hover { color: var(--accent-text); }
|
|
320
|
+
.filter-tag.active, .filter-tag.active:hover { background: var(--accent); color: var(--active-fg); --icon-fill: 1; }
|
|
321
|
+
.filter-tag.active:hover { background: var(--accent-hover); }
|
|
322
|
+
.filter-tag.excluded { color: var(--text-dim); text-decoration: line-through; text-decoration-thickness: 1px; opacity: .6; }
|
|
323
|
+
/* A greyed chip (the availability census, Forge's disabledTier): it would show
|
|
324
|
+
nothing under the other filters (.4), or can never match with them
|
|
325
|
+
(.dis-structural, .25). Inert (the controller ignores aria-disabled; panel
|
|
326
|
+
chips are disabled buttons), the reason is its tooltip, it never moves. */
|
|
327
|
+
.filter-tag:disabled, .filter-tag.is-disabled { opacity: .4; cursor: default; }
|
|
328
|
+
.filter-tag.is-disabled.dis-structural { opacity: .25; }
|
|
329
|
+
.filter-tag:disabled:hover, .filter-tag.is-disabled:hover { background: transparent; color: var(--text-dim); }
|
|
330
|
+
/* The selection at the front of the chip row (Core.renderFilterBar): the
|
|
331
|
+
selected groups (then the excluded ones), led by their or / and when two or
|
|
332
|
+
more are joined, a hairline either side. The panel's Type row leads with
|
|
333
|
+
the same toggle. Chips glide in and out (Core.flipChildren). */
|
|
334
|
+
.cluster-sep { flex: 0 0 1px; align-self: stretch; margin: var(--sp-1) 0; background: var(--line); }
|
|
335
|
+
.conn-toggle { flex-shrink: 0; min-height: var(--ctl-sm); padding: 0 var(--sp-2); border: 0; border-radius: var(--r-chip); background: transparent;
|
|
336
|
+
color: var(--accent-text); font: inherit; font-size: var(--fs-ui); font-weight: var(--fw-strong); cursor: pointer; }
|
|
337
|
+
.conn-toggle:hover { background: var(--accent-bg); }
|
|
338
|
+
|
|
339
|
+
/* The list sits in a wrapper so the "Newest" pill can float over its top edge
|
|
340
|
+
without being part of the scrolled rows (Core.createClipList owns both). */
|
|
341
|
+
.list-wrap { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
342
|
+
.list-newest { position: absolute; top: var(--sp-4); left: 50%; z-index: 30; display: inline-flex; align-items: center; gap: var(--sp-2);
|
|
343
|
+
height: var(--ctl-md); padding: 0 var(--sp-5) 0 var(--sp-4); border: none; border-radius: var(--r-chip); color: var(--text);
|
|
344
|
+
font: inherit; font-size: var(--fs-meta); cursor: pointer;
|
|
345
|
+
opacity: 0; pointer-events: none; transform: translate(-50%, -6px); transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); }
|
|
346
|
+
.list-newest.show { opacity: 1; pointer-events: auto; transform: translate(-50%, 0); }
|
|
347
|
+
.list-newest:hover { color: var(--accent-text); }
|
|
348
|
+
.list-newest .mi.sm { font-size: var(--icon-sm); }
|
|
349
|
+
.list-newest-dot { display: none; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
|
|
350
|
+
.list-newest.has-new .list-newest-dot { display: inline-block; }
|
|
351
|
+
/* The results fade out at an edge while more is past it (the shared
|
|
352
|
+
.bc-scroll-fade, Core.createClipList); scroll-padding keeps the keyboard
|
|
353
|
+
cursor out of the fades when it is scrolled into view. */
|
|
354
|
+
.list { flex: 1; overflow-y: auto; padding: 0 0 var(--sp-4); min-height: 0; scroll-padding-block: 36px; }
|
|
355
|
+
.item { padding: var(--sp-4) var(--gutter); cursor: pointer; transition: background-color var(--dur) var(--ease); position: relative; border-bottom: 1px solid var(--line-faint); color: inherit; }
|
|
356
|
+
/* Row states: hover = the --hover overlay; the keyboard cursor (the row Enter
|
|
357
|
+
pastes) = --accent-bg; a candidate duplicate of the hovered or cursor row
|
|
358
|
+
(.similar, Core.similarClipIds) = a NEUTRAL wash under --hover plus a dotted
|
|
359
|
+
dim edge, never the selection hue, so it can't read as the cursor or a
|
|
360
|
+
checked row. ONE 2px left edge, an overlay so it never shifts the row's
|
|
361
|
+
content: similar = dotted --text-dim, pin = --pin (wins over similar),
|
|
362
|
+
multi-select = --accent (wins over both). No state changes a row's size. */
|
|
363
|
+
.item::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: var(--row-edge, transparent); pointer-events: none; }
|
|
364
|
+
.item.similar { background: color-mix(in srgb, var(--hover) var(--similar-mix), transparent); --row-edge: repeating-linear-gradient(to bottom, var(--text-dim) 0 3px, transparent 3px 6px); }
|
|
365
|
+
.item.has-pin { --row-edge: var(--pin); }
|
|
366
|
+
.item:hover { background: var(--hover); }
|
|
367
|
+
.item.selected { background: var(--accent-bg); }
|
|
368
|
+
/* No flex gap: the space after the star is its own margin, so a closed
|
|
369
|
+
actions track at the end of the row takes exactly 0 px. */
|
|
370
|
+
.item-row { display: flex; align-items: flex-start; }
|
|
371
|
+
.pin-area { position: relative; flex-shrink: 0; display: flex; align-items: center; margin-right: var(--sp-4); margin-top: calc((var(--fs-ui) * var(--lh-ui) - var(--ctl-md)) / 2); }
|
|
372
|
+
.star { display: flex; align-items: center; justify-content: center; width: var(--ctl-md); height: var(--ctl-md); background: none; border: none; color: var(--text-dim); cursor: pointer; padding: 0;
|
|
373
|
+
transition: color var(--dur) var(--ease); line-height: 1; }
|
|
374
|
+
.star.active, .star:hover { color: var(--pin); }
|
|
375
|
+
/* The keypad (renderKeypadMenu) as a block inside a floating surface: the
|
|
376
|
+
clip menu's Numpad submenu and a row's # popover. Real numpad formation:
|
|
377
|
+
7 8 9 / 4 5 6 / 1 2 3 (the 3-column grid lays it out like the keypad), keys
|
|
378
|
+
aligned with the menu rows' icon column. Neutral keys; the clip's own key is
|
|
379
|
+
the accent (the selection colour); a key another clip holds carries a dot. */
|
|
380
|
+
.numpad-picker { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-1) var(--sp-3); }
|
|
381
|
+
/* The keypad alone (a clip with no key yet): the same inset on all four
|
|
382
|
+
sides; the icon-column alignment only matters beside menu rows. */
|
|
383
|
+
.numpad-picker:only-child { padding: var(--sp-1); }
|
|
384
|
+
.np-row { display: grid; grid-template-columns: repeat(3, var(--ctl-md)); gap: var(--sp-1); justify-content: start; }
|
|
385
|
+
.np-btn { position: relative; width: var(--ctl-md); height: var(--ctl-md); display: flex; align-items: center; justify-content: center; padding: 0 0 var(--sp-1); border: none; border-radius: var(--r-ctl);
|
|
386
|
+
background: var(--ctl-fill); color: var(--text); font-family: inherit; font-size: var(--fs-ui); line-height: 1; font-variant-numeric: tabular-nums;
|
|
387
|
+
cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
|
|
388
|
+
.np-btn:hover { background: var(--ctl-fill-hover); }
|
|
389
|
+
/* A key another clip holds: a dot under the digit (every digit sits a step
|
|
390
|
+
high, so the dot keeps its clearance). */
|
|
391
|
+
.np-btn.taken::after { content: ""; position: absolute; left: 0; right: 0; bottom: 1px; width: var(--sp-2); height: var(--sp-2); margin: 0 auto;
|
|
392
|
+
border-radius: var(--r-chip); background: var(--text-dim); }
|
|
393
|
+
.np-btn.current { background: var(--accent-bg); color: var(--accent-text); font-weight: var(--fw-strong); }
|
|
394
|
+
.np-btn.current:hover { background: var(--accent-bg-strong); }
|
|
395
|
+
.bc-menu.bc-keypad, .bc-menu .tag-submenu.bc-keypad { min-width: 0; }
|
|
396
|
+
.filter-tag.virtual { opacity: .82; font-style: italic; }
|
|
397
|
+
.tag-caret { flex: 0 0 auto; font-size: var(--icon-md); line-height: 1; opacity: .72; margin-left: auto; }
|
|
398
|
+
.filter-tag .tag-caret { margin-left: var(--sp-1); margin-right: calc(-1 * var(--sp-1)); }
|
|
399
|
+
.tag-label { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: var(--lh-ui); }
|
|
400
|
+
.tag-menu-node { position: relative; display: block; }
|
|
401
|
+
.group-filters > .tag-menu-node { display: inline-flex; }
|
|
402
|
+
.tag-submenu { display: none; position: absolute; top: 100%; left: 0; min-width: 150px; max-width: 240px; z-index: 70; }
|
|
403
|
+
.tag-menu-node:hover > .tag-submenu, .tag-menu-node:focus-within > .tag-submenu { display: block; }
|
|
404
|
+
.tag-submenu .tag-submenu { top: calc(-1 * var(--sp-2)); left: calc(100% - 1px); }
|
|
405
|
+
/* A chip's submenu in the top layer (popover="manual", placed and shown by
|
|
406
|
+
installSubmenuAutoflip): its own box, and shown only while it is open (the
|
|
407
|
+
:hover rule above would otherwise show it in place inside the strip). */
|
|
408
|
+
.group-filters .tag-menu-node > .tag-submenu[popover] { position: fixed; inset: auto; margin: 0; border: 0; overflow: visible; color: inherit; }
|
|
409
|
+
.group-filters .tag-menu-node > .tag-submenu[popover]:not(:popover-open) { display: none; }
|
|
410
|
+
.group-filters .tag-menu-node > .tag-submenu[popover]:popover-open { display: block; }
|
|
411
|
+
/* A group chip's dropdown holds menu rows (renderTagTreeMenuNode, depth > 0):
|
|
412
|
+
an excluded group is struck through like its chip, a greyed one dims in the
|
|
413
|
+
chips' two tiers, and a parent row stays lit while its submenu is out. */
|
|
414
|
+
.bc-menu-item.excluded > .bc-menu-label { text-decoration: line-through; }
|
|
415
|
+
.bc-menu-item.is-disabled { opacity: .4; cursor: default; }
|
|
416
|
+
.bc-menu-item.is-disabled.dis-structural { opacity: .25; }
|
|
417
|
+
.bc-menu-item.is-disabled:hover { background: none; }
|
|
418
|
+
.group-filters .tag-menu-node:hover > .bc-menu-item { background: var(--menu-hover); }
|
|
419
|
+
.content { flex: 1; min-width: 0; }
|
|
420
|
+
/* Row anatomy (Core.renderClipItem): the primary line (a real title at the
|
|
421
|
+
strong weight, or an untitled clip's first line at the regular weight), the
|
|
422
|
+
dim mono preview of the rest, then the meta line. Every line is ONE line, so
|
|
423
|
+
a row's height depends only on which lines it has, never on its width. */
|
|
424
|
+
.clip-title { font-size: var(--fs-ui); line-height: var(--lh-ui); font-weight: var(--fw-strong); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
425
|
+
.clip-title.derived { font-weight: var(--fw-regular); }
|
|
426
|
+
.clip-title + .preview { margin-top: var(--sp-1); }
|
|
427
|
+
.preview { font-size: var(--fs-ui); line-height: var(--lh-text); color: var(--text-dim); font-family: var(--font-mono); }
|
|
428
|
+
.preview.collapsed { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
429
|
+
.preview.expanded { white-space: pre-wrap; word-break: break-word; max-height: 300px; overflow-y: auto; background: var(--surface); border-radius: var(--r-2); padding: var(--sp-4); margin: var(--sp-1) 0; }
|
|
430
|
+
/* Image previews render at --clip-img-h (Ctrl+wheel / Settings, default 60px),
|
|
431
|
+
capped by --clip-img-cap (the list's visible height, set by Core.createImageZoom)
|
|
432
|
+
so one preview never outgrows the list. The wrapper is the picture's box (so
|
|
433
|
+
the row's buttons can float over its corner): with known dims (--ar/--nw from
|
|
434
|
+
the row markup) its width is the smallest of the row, height x aspect ratio
|
|
435
|
+
and the image's real width - so the ratio holds, nothing overflows sideways
|
|
436
|
+
and nothing is enlarged past its real size. Legacy clips without dims fall
|
|
437
|
+
back to max-width/max-height, which also keeps the ratio. */
|
|
438
|
+
.preview-img { --img-h: min(var(--clip-img-h, 60px), var(--clip-img-cap, 100000px));
|
|
439
|
+
position: relative; display: block; width: fit-content; max-width: 100%; }
|
|
440
|
+
.preview-img[style*="--ar"] { width: min(100%, calc(var(--img-h) * var(--ar)), var(--nw)); }
|
|
441
|
+
.preview-img img { display: block; max-width: 100%; max-height: var(--img-h); height: auto; border-radius: var(--r-1); }
|
|
442
|
+
.preview-img[style*="--ar"] img { width: 100%; max-height: none; }
|
|
443
|
+
/* An image row's buttons sit where a text row's do (the row's top right, on
|
|
444
|
+
the first line), floating so the picture never resizes and the row never
|
|
445
|
+
grows. Plain like a text row's; only while the picture reaches under them
|
|
446
|
+
(.img-under, Core.markImageUnder) do they get a frosted chip in the opaque
|
|
447
|
+
menu colour, which reads on any picture in both themes and every accent.
|
|
448
|
+
The chip's padding is cancelled by its margin, so the buttons never move. */
|
|
449
|
+
.item-row { position: relative; }
|
|
450
|
+
.img-actions { position: absolute; top: calc((var(--fs-ui) * var(--lh-ui) - var(--ctl-md)) / 2); right: 0; display: flex; align-items: center; gap: var(--sp-1);
|
|
451
|
+
border-radius: var(--r-panel); opacity: 0; visibility: hidden;
|
|
452
|
+
transition: opacity var(--dur) var(--ease), visibility var(--dur) var(--ease); }
|
|
453
|
+
.img-under .img-actions { padding: var(--sp-1); margin: calc(-1 * var(--sp-1));
|
|
454
|
+
background: color-mix(in srgb, var(--menu) 78%, transparent); -webkit-backdrop-filter: blur(10px) saturate(1.3); backdrop-filter: blur(10px) saturate(1.3);
|
|
455
|
+
box-shadow: 0 1px 6px var(--shadow); }
|
|
456
|
+
.img-under .img-actions .icon-btn { color: var(--text); }
|
|
457
|
+
/* A text row's buttons ride the shared reveal at the row's end: closed = 0 px
|
|
458
|
+
(the text runs the full width), open while the row is hovered, holds focus,
|
|
459
|
+
has its menu open or is being dragged (.actions-held); the text's ellipsis
|
|
460
|
+
moves with them. The meta line's ghost # / + open the same way. */
|
|
461
|
+
.row-actions { --reveal-gap: var(--sp-2); margin-top: calc((var(--fs-ui) * var(--lh-ui) - var(--ctl-md)) / 2); }
|
|
462
|
+
.item:hover .bc-reveal.row-actions, .item:focus-within .bc-reveal.row-actions, .item.actions-held .bc-reveal.row-actions,
|
|
463
|
+
.item:hover .bc-reveal.meta-reveal, .item:focus-within .bc-reveal.meta-reveal, .item.actions-held .bc-reveal.meta-reveal { grid-template-columns: 1fr; }
|
|
464
|
+
.item:hover .bc-reveal.row-actions > .bc-reveal-inner, .item:focus-within .bc-reveal.row-actions > .bc-reveal-inner, .item.actions-held .bc-reveal.row-actions > .bc-reveal-inner,
|
|
465
|
+
.item:hover .bc-reveal.meta-reveal > .bc-reveal-inner, .item:focus-within .bc-reveal.meta-reveal > .bc-reveal-inner, .item.actions-held .bc-reveal.meta-reveal > .bc-reveal-inner,
|
|
466
|
+
.item:hover .img-actions, .item:focus-within .img-actions, .item.actions-held .img-actions { visibility: visible; content-visibility: visible; }
|
|
467
|
+
.item:hover .img-actions, .item:focus-within .img-actions, .item.actions-held .img-actions { opacity: 1; }
|
|
468
|
+
/* Meta line (Core.renderClipMeta): time, size, the numpad badge and the clip's
|
|
469
|
+
groups as plain text (each a filter: click includes, right-click excludes),
|
|
470
|
+
then the hover-only ghost # and + on the reveal. ONE line that never wraps:
|
|
471
|
+
as the ghosts slide in, group names that no longer fit drop out whole (.meta-tags), so no row changes height
|
|
472
|
+
and nothing is reserved while they are hidden (the reveal's negative margin
|
|
473
|
+
cancels the flex gap before a closed track). */
|
|
474
|
+
.meta { --meta-h: calc(var(--fs-meta) * var(--lh-text)); margin-top: var(--sp-1); display: flex; align-items: center; gap: var(--sp-3); min-width: 0; height: var(--meta-h);
|
|
475
|
+
font-size: var(--fs-meta); line-height: 1; color: var(--text-dim); white-space: nowrap; overflow: clip; overflow-clip-margin: var(--sp-1); }
|
|
476
|
+
.meta-time, .meta-size, .meta-np { flex-shrink: 0; }
|
|
477
|
+
.meta-np, .meta-tag, .meta-ghost { height: var(--meta-h); padding: 0 var(--sp-1); border: 0; border-radius: var(--r-1); background: transparent; color: inherit;
|
|
478
|
+
font: inherit; line-height: 1; cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
|
|
479
|
+
/* The badge and the names pad their hover fill but pull it back out of the
|
|
480
|
+
flow, so every text-to-text gap on the line is the one --sp-3. */
|
|
481
|
+
.meta-np, .meta-tag { margin-inline: calc(-1 * var(--sp-1)); }
|
|
482
|
+
.meta-np { color: var(--text); }
|
|
483
|
+
/* The group names show whole or not at all, never as fragments: they wrap onto
|
|
484
|
+
a second line the fixed-height box clips away, so a name that does not fit
|
|
485
|
+
is left out whole (each name's own title still names it on hover); only a
|
|
486
|
+
name alone on the line ellipsizes. The + picker lists every membership. */
|
|
487
|
+
.meta-tags { display: flex; flex-wrap: wrap; align-content: flex-start; column-gap: var(--sp-3); flex: 0 1 auto; min-width: 0; height: var(--meta-h); overflow: hidden;
|
|
488
|
+
padding-inline: var(--sp-1); margin-inline: calc(-1 * var(--sp-1)); /* room for the names' hover fill */ }
|
|
489
|
+
/* Hover (or focus, or a held row): the names that would not fit beside the
|
|
490
|
+
ghosts are set aside whole (Core.fitMetaTags marks them), so the box ends
|
|
491
|
+
at the last name and the + follows it instead of the box's far edge. */
|
|
492
|
+
.item:hover .meta-tag.meta-cut, .item:focus-within .meta-tag.meta-cut, .item.actions-held .meta-tag.meta-cut { display: none; }
|
|
493
|
+
.meta-tag { display: block; flex: 0 1 auto; min-width: 0; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
|
|
494
|
+
.meta-np:hover, .meta-tag:hover, .meta-ghost:hover { color: var(--text); background: var(--hover); }
|
|
495
|
+
.meta-ghost { display: inline-flex; align-items: center; justify-content: center; width: var(--meta-h); padding: 0; }
|
|
496
|
+
.meta-ghost .mi { font-size: var(--icon-sm); }
|
|
497
|
+
.meta > .meta-reveal, .bc-tag-strip > .meta-reveal { margin-left: calc(-1 * var(--sp-3)); --reveal-gap: var(--sp-3); }
|
|
498
|
+
.meta-reveal .bc-reveal-inner { gap: var(--sp-1); }
|
|
499
|
+
/* Empty list (Core.renderEmptyState): a quiet icon, what is empty and why, the
|
|
500
|
+
way out; .empty-nudge is the slot for the search behaviour's suggestion. */
|
|
501
|
+
.list-empty { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); padding: var(--sp-12) var(--gutter); text-align: center; color: var(--text-dim); }
|
|
502
|
+
.list-empty .empty-icon { font-size: var(--icon-lg); color: var(--text-dim); margin-bottom: var(--sp-1); }
|
|
503
|
+
.empty-title { margin: 0; max-width: 100%; font-size: var(--fs-ui); line-height: var(--lh-ui); font-weight: var(--fw-strong); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
504
|
+
.empty-hint { margin: 0; font-size: var(--fs-meta); line-height: var(--lh-ui); color: var(--text-dim); }
|
|
505
|
+
.empty-action { margin-top: var(--sp-2); }
|
|
506
|
+
.empty-nudge[hidden] { display: none; }
|
|
507
|
+
/* The relax nudge (Core.renderRelaxNudge): one quiet line, the blocking token
|
|
508
|
+
in mono, the count, an arrow; a click drops that filter. */
|
|
509
|
+
.empty-nudge-btn { display: inline-flex; align-items: center; gap: var(--sp-2); min-height: var(--ctl-md); padding: 0 var(--sp-3); border: 0; border-radius: var(--r-ctl);
|
|
510
|
+
background: transparent; color: var(--text-dim); font: inherit; font-size: var(--fs-ui); cursor: pointer;
|
|
511
|
+
transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
|
|
512
|
+
.empty-nudge-btn code { font-family: var(--font-mono); color: var(--text); }
|
|
513
|
+
.empty-nudge-btn .mi { font-size: var(--icon-sm); color: var(--accent); }
|
|
514
|
+
.empty-nudge-btn:hover { background: var(--hover); color: var(--text); }
|
|
515
|
+
|
|
516
|
+
.settings-view { display: none; flex-direction: column; height: 100%; color: var(--text); background: transparent; }
|
|
517
|
+
.settings-view.show { display: flex; }
|
|
518
|
+
.settings-hdr { display: flex; align-items: center; flex-shrink: 0; height: var(--bar-h); padding: 0 var(--edge-ctl); gap: var(--sp-3);
|
|
519
|
+
border-bottom: 1px solid var(--line); background: var(--surface); }
|
|
520
|
+
.settings-hdr h2 { font-size: var(--fs-ui); font-weight: var(--fw-strong); flex: 1; margin: 0; color: var(--text); }
|
|
521
|
+
.settings-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-6) var(--gutter); }
|
|
522
|
+
/* Settings (Core.renderSettingsBody, Core.renderSettingsItem): ONE row grid and
|
|
523
|
+
ONE list style on the same three columns, label | control | reset, so every
|
|
524
|
+
control and every row action (a reset, a remove x) ends on one right edge;
|
|
525
|
+
the third column is reserved even when empty. Rows are spaced, never boxed;
|
|
526
|
+
list rows are split by --line-faint hairlines only. */
|
|
527
|
+
.setting-row, .settings-item, .settings-list-cols { display: grid; grid-template-columns: minmax(0, 1fr) auto var(--ctl-md); column-gap: var(--sp-4); align-items: center;
|
|
528
|
+
color: var(--text); font-size: var(--fs-ui); line-height: var(--lh-ui); }
|
|
529
|
+
.setting-row { min-height: 36px; padding: var(--sp-3) 0; }
|
|
530
|
+
.setting-row.note { min-height: 0; }
|
|
531
|
+
.setting-text, .si-text { grid-column: 1; display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
|
|
532
|
+
.setting-control, .setting-row > .switch, .si-control { grid-column: 2; justify-self: end; display: inline-flex; align-items: center; gap: var(--sp-2); min-width: 0; }
|
|
533
|
+
.setting-reset, .si-action { grid-column: 3; display: inline-flex; justify-content: center; }
|
|
534
|
+
.setting-label { color: var(--text); }
|
|
535
|
+
/* One-line help / status under a label; an empty one takes no space. */
|
|
536
|
+
.setting-help { font-size: var(--fs-meta); color: var(--text-dim); overflow-wrap: anywhere; }
|
|
537
|
+
.setting-help:empty { display: none; }
|
|
538
|
+
.setting-help.mono { font-family: var(--font-mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; overflow-wrap: normal; }
|
|
539
|
+
.setting-help.error { color: var(--red); }
|
|
540
|
+
/* A status that reports something running carries the green success dot. */
|
|
541
|
+
.setting-help.active::before { content: ""; display: inline-block; width: 6px; height: 6px; margin-right: var(--sp-2); border-radius: 50%; background: var(--green); vertical-align: 1px; }
|
|
542
|
+
/* Free text takes the full width under its label (pinned peers), up to the
|
|
543
|
+
shared control edge. */
|
|
544
|
+
.setting-row.stack > .setting-control { grid-column: 1 / 3; justify-self: stretch; margin-top: var(--sp-3); }
|
|
545
|
+
.setting-row.stack input { flex: 1; }
|
|
546
|
+
/* Filled fields: a fill, never a border. A number field carries its unit
|
|
547
|
+
inside (.input-affix: the fill and the focus edge sit on the wrapper). */
|
|
548
|
+
.setting-row input[type="text"], .input-affix { height: var(--ctl-lg); background: var(--input); border: none; border-radius: var(--r-ctl); color: var(--text); font: inherit; font-size: var(--fs-ui); }
|
|
549
|
+
.setting-row input[type="text"] { min-width: 0; padding: 0 var(--sp-4); outline: none; }
|
|
550
|
+
.input-affix { display: inline-flex; align-items: center; gap: var(--sp-2); width: 104px; padding: 0 var(--sp-4); color: var(--text-dim); cursor: text; }
|
|
551
|
+
.input-affix:focus-within { box-shadow: inset 0 0 0 1px var(--accent); }
|
|
552
|
+
.input-affix input { flex: 1; width: 0; min-width: 0; height: 100%; padding: 0; border: none; background: transparent; color: var(--text); font: inherit; text-align: right; outline: none; }
|
|
553
|
+
.input-affix input:focus { box-shadow: none; }
|
|
554
|
+
/* ONE switch (a row's, or one inside a list row via .switch-wrap). */
|
|
555
|
+
.switch-row, .switch-wrap { position: relative; cursor: pointer; }
|
|
556
|
+
.switch-wrap { display: inline-flex; }
|
|
557
|
+
.switch-row input, .switch-wrap input { position: absolute; opacity: 0; pointer-events: none; }
|
|
558
|
+
.switch { width: 32px; height: 18px; position: relative; flex-shrink: 0; border-radius: var(--r-chip); background: var(--line-strong); transition: background-color var(--dur) var(--ease); }
|
|
559
|
+
.switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--text); transition: transform var(--dur) var(--ease), background-color var(--dur) var(--ease); }
|
|
560
|
+
input:checked + .switch { background: var(--accent); }
|
|
561
|
+
input:checked + .switch::after { transform: translateX(14px); background: var(--active-fg); }
|
|
562
|
+
input:focus-visible + .switch { box-shadow: var(--focus-ring); }
|
|
563
|
+
input:disabled + .switch { opacity: .55; }
|
|
564
|
+
.switch-wrap.is-disabled { cursor: default; }
|
|
565
|
+
/* Lists: one row primitive for numpad slots, groups, sync providers, peers,
|
|
566
|
+
AI clients, grants and conflicts. */
|
|
567
|
+
.settings-list { display: flex; flex-direction: column; }
|
|
568
|
+
.settings-item { min-height: 34px; padding: var(--sp-2) 0; border-bottom: 1px solid var(--line-faint); }
|
|
569
|
+
.settings-item:last-child { border-bottom: 0; }
|
|
570
|
+
.si-title { display: flex; align-items: baseline; gap: var(--sp-3); min-width: 0; }
|
|
571
|
+
.si-key { flex-shrink: 0; color: var(--text); font-variant-numeric: tabular-nums; }
|
|
572
|
+
.si-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
|
|
573
|
+
.si-meta { flex-shrink: 0; font-size: var(--fs-meta); color: var(--text-dim); }
|
|
574
|
+
.si-sub { font-size: var(--fs-meta); color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
575
|
+
.si-name.mono, .si-sub.mono { font-family: var(--font-mono); }
|
|
576
|
+
.settings-item.dim .si-name { color: var(--text-dim); }
|
|
577
|
+
/* A row that acts on a click (a numpad slot copies its clip): the hover
|
|
578
|
+
overlay reaches a little past the text (two side shadows, so neither the
|
|
579
|
+
columns nor the hairline move). */
|
|
580
|
+
.settings-item.clickable { cursor: pointer; }
|
|
581
|
+
.settings-item.clickable:hover { background: var(--hover); box-shadow: calc(-1 * var(--sp-3)) 0 0 var(--hover), var(--sp-3) 0 0 var(--hover); }
|
|
582
|
+
.settings-list-title { padding: var(--sp-4) 0 var(--sp-1); } /* type from the shared .bc-label rule */
|
|
583
|
+
/* A quiet text button under a list ("Show all clients"): its label lines up
|
|
584
|
+
with the rows' text. */
|
|
585
|
+
.settings-more { margin-top: var(--sp-2); margin-left: calc(-1 * var(--sp-3)); }
|
|
586
|
+
/* Accent: one swatch per choice, each dot (--dot) previewing the accent that
|
|
587
|
+
choice paints in THIS theme: the presets' steps mirror clipboard-tokens.css
|
|
588
|
+
(a step darker in light), System and Custom get their colour's per-theme
|
|
589
|
+
shade inline (--dot-dark / --dot-light + data-own, Core.mountSettings), a
|
|
590
|
+
Custom with no colour yet shows the wheel. The chosen one carries its ink
|
|
591
|
+
check. System, while not chosen, is a RING of its colour: "follows the
|
|
592
|
+
system", never a twin of Blue when the OS gives no colour. */
|
|
593
|
+
.accent-swatches { display: inline-flex; gap: var(--sp-1); }
|
|
594
|
+
.accent-swatch { display: inline-flex; align-items: center; justify-content: center; width: var(--ctl-md); height: var(--ctl-md); padding: 0; border: none; border-radius: var(--r-chip);
|
|
595
|
+
background: transparent; cursor: pointer; transition: background-color var(--dur) var(--ease); --dot: var(--dot-dark, var(--blue-500)); }
|
|
596
|
+
.accent-swatch[data-accent-mode="teal"] { --dot: var(--teal-500); }
|
|
597
|
+
.accent-swatch[data-accent-mode="mono"] { --dot: var(--g-300); }
|
|
598
|
+
:where(:root, .bc-popup)[data-theme="light"] .accent-swatch { --dot: var(--dot-light, var(--blue-600)); }
|
|
599
|
+
:where(:root, .bc-popup)[data-theme="light"] .accent-swatch[data-accent-mode="teal"] { --dot: var(--teal-600); }
|
|
600
|
+
:where(:root, .bc-popup)[data-theme="light"] .accent-swatch[data-accent-mode="mono"] { --dot: var(--g-700); }
|
|
601
|
+
.accent-swatch:hover { background: var(--hover); }
|
|
602
|
+
.accent-dot { display: inline-flex; align-items: center; justify-content: center; width: var(--icon-md); height: var(--icon-md); border-radius: 50%; background: var(--dot); }
|
|
603
|
+
.accent-swatch[data-accent-mode="custom"]:not([data-own]) .accent-dot { background: conic-gradient(var(--red-500), var(--amber-500), var(--green-500), var(--teal-500), var(--blue-500), var(--red-500)); }
|
|
604
|
+
.accent-swatch[data-accent-mode="system"]:not([aria-checked="true"]) .accent-dot { background: transparent; box-shadow: inset 0 0 0 3px var(--dot); }
|
|
605
|
+
.accent-dot .mi { font-size: var(--icon-sm); color: var(--active-fg); visibility: hidden; }
|
|
606
|
+
.accent-swatch[aria-checked="true"] .accent-dot .mi { visibility: visible; }
|
|
607
|
+
/* Custom colour: a hue slider (its rainbow track is painted inline from the
|
|
608
|
+
same hue maths) plus a #rrggbb field. */
|
|
609
|
+
.accent-hue { -webkit-appearance: none; appearance: none; width: 120px; height: var(--sp-3); margin: 0; border-radius: var(--r-chip); cursor: pointer; outline: none; }
|
|
610
|
+
.accent-hue::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: var(--icon-sm); height: var(--icon-sm); border-radius: 50%; background: var(--text); box-shadow: 0 0 0 2px var(--bg); }
|
|
611
|
+
.accent-hue::-moz-range-thumb { width: var(--icon-sm); height: var(--icon-sm); border: none; border-radius: 50%; background: var(--text); box-shadow: 0 0 0 2px var(--bg); }
|
|
612
|
+
.accent-hue:focus-visible { box-shadow: var(--focus-ring); }
|
|
613
|
+
.setting-row input.accent-hex { width: 88px; font-family: var(--font-mono); }
|
|
614
|
+
.seg { display: inline-flex; height: var(--ctl-lg); padding: var(--sp-1); gap: var(--sp-1); background: var(--input); border: none; border-radius: var(--r-ctl); }
|
|
615
|
+
.seg-btn { height: calc(var(--ctl-lg) - 2 * var(--sp-1)); border: none; background: transparent; color: var(--text-dim); font-size: var(--fs-ui); font-family: inherit; padding: 0 var(--sp-4); border-radius: var(--r-1); cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
|
|
616
|
+
.seg-btn:hover { color: var(--text); }
|
|
617
|
+
.seg-btn.active { background: var(--accent); color: var(--active-fg); }
|
|
618
|
+
/* Sections: the shared .overline heading, --sp-12 above each one but the first
|
|
619
|
+
shown (the conflicts section on top is hidden while there are none). */
|
|
620
|
+
.settings-section { margin-top: var(--sp-12); }
|
|
621
|
+
.settings-section:first-child, .settings-section.hidden + .settings-section { margin-top: 0; }
|
|
622
|
+
.settings-section h3 { margin: 0 0 var(--sp-2); } /* type from the shared .overline rule */
|
|
623
|
+
|
|
624
|
+
@media (max-width: 760px) {
|
|
625
|
+
.item { padding-inline: var(--sp-5); }
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/* Popup theme variables + design tokens live in clipboard-tokens.css
|
|
629
|
+
(imported at the top of this file), so the app and demo render from
|
|
630
|
+
one palette and cannot drift. Only component rules live here. */
|
|
631
|
+
|
|
632
|
+
/* The popup root carries the base text color so popup content never inherits a
|
|
633
|
+
host page's color (the website embeds the popup inside the marketing
|
|
634
|
+
page; the desktop app sets this on <body>). Without it, unstyled text like
|
|
635
|
+
the dialog title rendered dark-on-dark. */
|
|
636
|
+
.bc-popup { color: var(--text); }
|
|
637
|
+
.hidden { display: none; }
|
|
638
|
+
mark { background: var(--mark-bg); color: var(--mark-fg); border-radius: var(--r-1); padding: 0 var(--sp-1); }
|
|
639
|
+
|
|
640
|
+
/* Toast: a neutral floating surface (shared rule at the top), never a status
|
|
641
|
+
colour; an action (Undo) is an accent text button. */
|
|
642
|
+
.toast { position: fixed; bottom: var(--sp-8); left: 50%; transform: translateX(-50%) translateY(16px); display: flex; align-items: center; gap: var(--sp-4);
|
|
643
|
+
width: max-content; max-width: calc(100% - 2 * var(--gutter)); padding: var(--sp-3) var(--sp-5); color: var(--text); font-size: var(--fs-ui); line-height: var(--lh-ui);
|
|
644
|
+
opacity: 0; transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); pointer-events: none; z-index: 50; }
|
|
645
|
+
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
646
|
+
|
|
647
|
+
/* Confirm / prompt dialog */
|
|
648
|
+
.overlay { display: none; pointer-events: none; position: fixed; inset: 0; background: var(--scrim); z-index: 30; align-items: center; justify-content: center; }
|
|
649
|
+
.overlay.show { display: flex; pointer-events: auto; }
|
|
650
|
+
/* Dialog = the shared floating surface (bg, --r-panel, shadow at the top) with
|
|
651
|
+
room to breathe; sans body copy, an optional mono preview of clip text, and
|
|
652
|
+
the shared .btn set (confirm is .primary, or .danger for destructive ones). */
|
|
653
|
+
.dialog { padding: var(--sp-8); max-width: 320px; width: 90%; color: var(--text); }
|
|
654
|
+
.dialog h3 { font-size: var(--fs-ui); font-weight: var(--fw-strong); line-height: var(--lh-ui); color: var(--text); margin-bottom: var(--sp-3); }
|
|
655
|
+
.dialog p { font-size: var(--fs-ui); line-height: var(--lh-text); color: var(--text-dim); margin-bottom: var(--sp-6); word-break: break-word; }
|
|
656
|
+
.dialog p:empty { display: none; }
|
|
657
|
+
.dialog-preview { font-family: var(--font-mono); font-size: var(--fs-ui); line-height: var(--lh-text); color: var(--text); background: var(--input); border-radius: var(--r-ctl);
|
|
658
|
+
padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-6); white-space: pre-wrap; word-break: break-word;
|
|
659
|
+
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
660
|
+
.dialog-preview:empty { display: none; }
|
|
661
|
+
.dialog-btns { display: flex; gap: var(--sp-4); justify-content: flex-end; }
|
|
662
|
+
.prompt-input { width: 100%; height: var(--ctl-lg); background: var(--input); border: none; border-radius: var(--r-ctl); padding: 0 var(--sp-4); color: var(--text); font-size: var(--fs-text); outline: none; margin-bottom: var(--sp-6); font-family: inherit; }
|
|
663
|
+
|
|
664
|
+
/* Settings: the shortcut fields (sans key names; "Not set" dim) and the sync
|
|
665
|
+
button's live spin. Buttons are the shared .btn / .icon-btn. */
|
|
666
|
+
.shortcut-btn { min-width: 130px; max-width: 190px; height: var(--ctl-lg); background: var(--input); border: none; border-radius: var(--r-ctl); color: var(--text);
|
|
667
|
+
padding: 0 var(--sp-4); font-size: var(--fs-ui); font-family: inherit; white-space: nowrap; overflow: hidden;
|
|
668
|
+
text-overflow: ellipsis; cursor: pointer; text-align: center; }
|
|
669
|
+
.shortcut-btn.unset { color: var(--text-dim); }
|
|
670
|
+
.shortcut-btn.recording { color: var(--accent-text); background: var(--accent-bg); }
|
|
671
|
+
/* Live sync feedback: the icon itself spins while a merge is in flight. */
|
|
672
|
+
@keyframes bc-sync-spin { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } }
|
|
673
|
+
.sync-btn.syncing .mi { display: inline-block; animation: bc-sync-spin 1.1s linear infinite; }
|
|
674
|
+
|
|
675
|
+
/* --- Shared plain-text editor (Core.createEditor): app window + demo clip window --- */
|
|
676
|
+
/* Clip windows (editor, viewer, unify + conflict; the demo's clip window too):
|
|
677
|
+
ONE surface per window. The root paints --surface, every band in it (bar,
|
|
678
|
+
find, pane heads, merge gap, footer) is transparent and split by one
|
|
679
|
+
--line hairline, so the writing surface is never grey under white chrome.
|
|
680
|
+
Under glass the root goes transparent too and the window's single scrim
|
|
681
|
+
(clipboard-window.css) is the only frost. */
|
|
682
|
+
.bc-editor { display: flex; flex-direction: column; height: 100%; width: 100%; background: transparent; color: var(--text); min-height: 0; }
|
|
683
|
+
/* ONE window bar (.bc-bar, Core.renderWindowBar): [lead] title [context]
|
|
684
|
+
[tags] spacer actions. It is the window's drag region (a double-click
|
|
685
|
+
maximises on Windows, zooms or minimises per the macOS setting); its
|
|
686
|
+
controls are not. With the OS's own window controls (data-window-controls
|
|
687
|
+
on <html>, Core.attachWindowControls) it reserves their room: the traffic
|
|
688
|
+
lights' inset on the left (macOS, --wc-left from main), the caption
|
|
689
|
+
buttons' width on the right (Windows, the Window Controls Overlay env()). */
|
|
690
|
+
.bc-bar { -webkit-app-region: drag; display: flex; align-items: center; gap: var(--sp-3); height: var(--bar-h); flex-shrink: 0;
|
|
691
|
+
padding: 0 var(--edge-ctl) 0 var(--gutter); border-bottom: 1px solid var(--line); background: var(--surface); color: var(--text); }
|
|
692
|
+
:root[data-window-controls="left"] .bc-bar { padding-left: var(--wc-left, var(--gutter)); }
|
|
693
|
+
:root[data-window-controls="right"] { --wc-right: calc(100vw - env(titlebar-area-x, 0px) - env(titlebar-area-width, 100vw)); }
|
|
694
|
+
:root[data-window-controls="right"] .bc-bar { padding-right: max(var(--edge-ctl), calc(var(--wc-right) + var(--sp-2))); }
|
|
695
|
+
.bc-bar-title { flex: 0 1 auto; min-width: 0; font-size: var(--fs-ui); font-weight: var(--fw-strong); line-height: var(--lh-ui); color: var(--text);
|
|
696
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
697
|
+
/* The editable title (editor, merge): a flat field sized to its text (the
|
|
698
|
+
rest of the bar stays draggable), underline on hover / focus like the
|
|
699
|
+
search field. An untitled clip's placeholder is its first line, dim and
|
|
700
|
+
regular weight, as an untitled row reads in the list. */
|
|
701
|
+
input.bc-bar-title { -webkit-app-region: no-drag; field-sizing: content; min-width: 6ch; max-width: 60%; height: var(--ctl-md); margin-left: calc(-1 * var(--sp-2)); text-overflow: ellipsis;
|
|
702
|
+
padding: 0 var(--sp-2); background: transparent; border: none; border-radius: 0; font-family: inherit; outline: none;
|
|
703
|
+
box-shadow: inset 0 -1px 0 transparent; transition: box-shadow var(--dur) var(--ease); }
|
|
704
|
+
input.bc-bar-title:hover { box-shadow: inset 0 -1px 0 var(--line); }
|
|
705
|
+
input.bc-bar-title:focus { box-shadow: inset 0 -1px 0 var(--accent); }
|
|
706
|
+
input.bc-bar-title::placeholder { color: var(--text-dim); font-weight: var(--fw-regular); }
|
|
707
|
+
.bc-bar-context { flex: 0 1000 auto; min-width: 0; font-size: var(--fs-meta); /* gives way before the title in a narrow window */ color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
708
|
+
.bc-bar-spacer { flex: 1 1 0; min-width: var(--sp-4); align-self: stretch; }
|
|
709
|
+
.bc-bar-actions { -webkit-app-region: no-drag; display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; }
|
|
710
|
+
.bc-bar-actions .icon-btn.active { color: var(--accent); background: var(--accent-bg); }
|
|
711
|
+
/* A window on a page (the website demo, Core.attachWebWindow) draws the
|
|
712
|
+
visitor's OS controls in its bar (renderWindowBar webControls): macOS
|
|
713
|
+
traffic lights first (glyphs on hover; grey while the window is not the
|
|
714
|
+
active one), Windows caption buttons last, flush with the window's corner
|
|
715
|
+
and the bar's full height, close going red. */
|
|
716
|
+
.bc-wc { display: flex; flex-shrink: 0; align-self: stretch; align-items: center; -webkit-app-region: no-drag; }
|
|
717
|
+
.bc-wc-btn { display: grid; place-items: center; padding: 0; border: 0; cursor: default; }
|
|
718
|
+
.bc-wc-btn svg { display: block; fill: none; stroke: currentColor; }
|
|
719
|
+
.bc-wc-btn .wc-max, .bc-ww[data-max] .bc-wc-btn .wc-norm { display: none; }
|
|
720
|
+
.bc-ww[data-max] .bc-wc-btn .wc-max { display: block; }
|
|
721
|
+
.bc-wc[data-os="mac"] { gap: var(--sp-4); margin-right: var(--sp-5); }
|
|
722
|
+
.bc-wc[data-os="mac"] .bc-wc-btn { width: 12px; height: 12px; border-radius: var(--r-pill); color: var(--os-mac-ink); box-shadow: inset 0 0 0 .5px var(--os-mac-ink); }
|
|
723
|
+
.bc-wc[data-os="mac"] .bc-wc-btn[data-wc="close"] { background: var(--os-mac-close); }
|
|
724
|
+
.bc-wc[data-os="mac"] .bc-wc-btn[data-wc="min"] { background: var(--os-mac-min); }
|
|
725
|
+
.bc-wc[data-os="mac"] .bc-wc-btn[data-wc="max"] { background: var(--os-mac-max); }
|
|
726
|
+
.bc-wc[data-os="mac"] .bc-wc-btn svg { width: 12px; height: 12px; stroke-width: 1.2; opacity: 0; }
|
|
727
|
+
.bc-wc[data-os="mac"] .bc-wc-btn .wc-fill { fill: currentColor; stroke: none; }
|
|
728
|
+
.bc-wc[data-os="mac"]:hover .bc-wc-btn svg { opacity: 1; }
|
|
729
|
+
.bc-wc[data-os="mac"] .bc-wc-btn:active { filter: brightness(.82); }
|
|
730
|
+
.bc-ww:not([data-active]) .bc-wc[data-os="mac"]:not(:hover) .bc-wc-btn { background: var(--line); box-shadow: inset 0 0 0 .5px var(--line); }
|
|
731
|
+
.bc-wc[data-os="win"] { margin: 0 calc(-1 * var(--edge-ctl)) 0 var(--sp-2); }
|
|
732
|
+
.bc-wc[data-os="win"] .bc-wc-btn { width: 46px; height: 100%; background: transparent; color: var(--text); transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
|
|
733
|
+
.bc-wc[data-os="win"] .bc-wc-btn svg { width: 10px; height: 10px; stroke-width: 1; }
|
|
734
|
+
.bc-wc[data-os="win"] .bc-wc-btn:hover { background: var(--hover); }
|
|
735
|
+
.bc-wc[data-os="win"] .bc-wc-btn:active { background: var(--line-faint); }
|
|
736
|
+
.bc-wc[data-os="win"] .bc-wc-btn[data-wc="close"]:hover { background: var(--os-win-close); color: var(--white); }
|
|
737
|
+
.bc-wc[data-os="win"] .bc-wc-btn[data-wc="close"]:active { background: color-mix(in srgb, var(--os-win-close) 85%, var(--white)); color: var(--white); }
|
|
738
|
+
.bc-ww:not([data-active]) .bc-wc[data-os="win"] .bc-wc-btn:not(:hover) { color: var(--text-dim); }
|
|
739
|
+
/* The web window itself: placed in the page, raised by z-index, resized from
|
|
740
|
+
eight invisible grips on its edges and corners (none while maximised). */
|
|
741
|
+
.bc-ww { position: absolute; }
|
|
742
|
+
.bc-ww[hidden] { display: none !important; }
|
|
743
|
+
.bc-ww[data-max] { border-radius: 0; box-shadow: none; }
|
|
744
|
+
.bc-ww-moving, .bc-ww-sizing { user-select: none; }
|
|
745
|
+
.bc-ww-rs { position: absolute; z-index: 60; }
|
|
746
|
+
.bc-ww-rs[data-rs="n"], .bc-ww-rs[data-rs="s"] { left: var(--sp-6); right: var(--sp-6); height: var(--sp-3); cursor: ns-resize; }
|
|
747
|
+
.bc-ww-rs[data-rs="e"], .bc-ww-rs[data-rs="w"] { top: var(--sp-6); bottom: var(--sp-6); width: var(--sp-3); cursor: ew-resize; }
|
|
748
|
+
.bc-ww-rs[data-rs="n"], .bc-ww-rs[data-rs="ne"], .bc-ww-rs[data-rs="nw"] { top: 0; }
|
|
749
|
+
.bc-ww-rs[data-rs="s"], .bc-ww-rs[data-rs="se"], .bc-ww-rs[data-rs="sw"] { bottom: 0; }
|
|
750
|
+
.bc-ww-rs[data-rs="e"], .bc-ww-rs[data-rs="ne"], .bc-ww-rs[data-rs="se"] { right: 0; }
|
|
751
|
+
.bc-ww-rs[data-rs="w"], .bc-ww-rs[data-rs="nw"], .bc-ww-rs[data-rs="sw"] { left: 0; }
|
|
752
|
+
.bc-ww-rs[data-rs="ne"], .bc-ww-rs[data-rs="nw"], .bc-ww-rs[data-rs="se"], .bc-ww-rs[data-rs="sw"] { width: var(--sp-6); height: var(--sp-6); }
|
|
753
|
+
.bc-ww-rs[data-rs="ne"], .bc-ww-rs[data-rs="sw"] { cursor: nesw-resize; }
|
|
754
|
+
.bc-ww-rs[data-rs="nw"], .bc-ww-rs[data-rs="se"] { cursor: nwse-resize; }
|
|
755
|
+
.bc-ww[data-max] .bc-ww-rs { display: none; }
|
|
756
|
+
/* The viewer's leading image glyph is the file itself (a title-bar proxy
|
|
757
|
+
icon): drag it out into another app or a folder. */
|
|
758
|
+
.bc-drag-handle { -webkit-app-region: no-drag; flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: var(--ctl-md); height: var(--ctl-md);
|
|
759
|
+
margin-left: calc(-1 * var(--sp-2)); border-radius: var(--r-ctl); color: var(--text-dim); cursor: grab;
|
|
760
|
+
transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
|
|
761
|
+
.bc-drag-handle:hover { color: var(--text); background: var(--hover); }
|
|
762
|
+
.bc-drag-handle:active { cursor: grabbing; }
|
|
763
|
+
/* A clip window's star and keys (Core.updateTagStrip): the row's own star and
|
|
764
|
+
meta keys (renderClipKeys: the numpad badge, the group names, the ghosts #
|
|
765
|
+
and + on the reveal), so pin, key and groups read and work as in the list.
|
|
766
|
+
The star's glyph lands on the gutter when it leads the bar. The strip is
|
|
767
|
+
ONE line that scrolls sideways under the mask fade (attachScrollFade x);
|
|
768
|
+
the ghosts slide in while the bar is pointed at or holds the focus. */
|
|
769
|
+
.bc-bar > .star { -webkit-app-region: no-drag; flex-shrink: 0; }
|
|
770
|
+
.bc-bar > .star:first-child { margin-left: calc(var(--edge-ctl) - var(--gutter)); }
|
|
771
|
+
.bc-bar > .star[hidden] { display: none; }
|
|
772
|
+
.bc-tag-strip { -webkit-app-region: no-drag; --meta-h: calc(var(--fs-meta) * var(--lh-text)); display: flex; align-items: center; gap: var(--sp-3); min-width: 0; max-width: 50%;
|
|
773
|
+
padding-inline: var(--sp-1); overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
|
|
774
|
+
font-size: var(--fs-meta); line-height: 1; color: var(--text-dim); white-space: nowrap; }
|
|
775
|
+
.bc-tag-strip::-webkit-scrollbar { display: none; }
|
|
776
|
+
.bc-tag-strip[hidden] { display: none; }
|
|
777
|
+
.bc-tag-strip .meta-tags { flex-wrap: nowrap; flex: 0 0 auto; height: auto; overflow: visible; }
|
|
778
|
+
.bc-tag-strip .meta-tag { flex: 0 0 auto; }
|
|
779
|
+
.bc-bar:hover .bc-tag-strip .bc-reveal.meta-reveal, .bc-bar:focus-within .bc-tag-strip .bc-reveal.meta-reveal { grid-template-columns: 1fr; }
|
|
780
|
+
.bc-bar:hover .bc-tag-strip .meta-reveal > .bc-reveal-inner, .bc-bar:focus-within .bc-tag-strip .meta-reveal > .bc-reveal-inner { visibility: visible; content-visibility: visible; }
|
|
781
|
+
/* Find: chrome, so a band under the window bar like the merge pane heads
|
|
782
|
+
(--surface, ending on the --line hairline). The band's hairline IS the
|
|
783
|
+
field's underline: accent while anything in it has the focus (one line,
|
|
784
|
+
never a field underline 4 px above a band edge). The close's glyph ends on
|
|
785
|
+
the gutter. */
|
|
786
|
+
.bc-find { -webkit-app-region: no-drag; display: flex; align-items: center; gap: var(--sp-1); margin: 0; padding: var(--sp-1) var(--gutter); flex-shrink: 0;
|
|
787
|
+
background: var(--surface); border-bottom: 1px solid var(--line); transition: border-color var(--dur) var(--ease); }
|
|
788
|
+
.bc-find:focus-within { border-bottom-color: var(--accent); }
|
|
789
|
+
.bc-find[hidden] { display: none; }
|
|
790
|
+
.bc-find > :last-child { margin-right: calc(var(--edge-ctl) - var(--gutter)); }
|
|
791
|
+
.bc-find-input { flex: 1; min-width: 0; height: var(--ctl-md); background: transparent; border: none; border-radius: 0; color: var(--text); font: inherit; font-size: var(--fs-text);
|
|
792
|
+
padding: 0; margin-right: var(--sp-2); outline: none; }
|
|
793
|
+
.bc-find-input::placeholder { color: var(--text-dim); }
|
|
794
|
+
.bc-find-count { font-size: var(--fs-meta); color: var(--text-dim); min-width: 34px; text-align: right; font-variant-numeric: tabular-nums; margin-right: var(--sp-2); }
|
|
795
|
+
.bc-find.bad-pattern .bc-find-count { color: var(--red); }
|
|
796
|
+
.bc-editor-area { flex: 1; min-height: 0; width: 100%; resize: none; border: 0; outline: none; background: transparent; color: var(--text); }
|
|
797
|
+
.bc-editor-area, .bc-editor-hl {
|
|
798
|
+
font-family: var(--font-mono); font-size: var(--fs-text); line-height: var(--lh-text); padding: var(--sp-6) var(--gutter); tab-size: 4; white-space: pre-wrap; word-break: break-word; }
|
|
799
|
+
.bc-editor-area::placeholder { color: var(--text-dim); }
|
|
800
|
+
/* Find-match highlight backdrop: mirrors the textarea's text (transparent) so
|
|
801
|
+
<mark> backgrounds line up under the real text. Textarea and wrap are
|
|
802
|
+
transparent so the marks show through; the window's one surface (.bc-editor)
|
|
803
|
+
is the writing surface. */
|
|
804
|
+
.bc-editor-area-wrap { position: relative; flex: 1; min-height: 0; display: flex; background: transparent; }
|
|
805
|
+
.bc-editor-area-wrap .bc-editor-area { background: transparent; position: relative; z-index: 1; overflow-y: scroll; }
|
|
806
|
+
/* Width-only override of the shared scrollbar: the highlight mirror reserves
|
|
807
|
+
exactly this gutter (inset 0 10px 0 0 below), so it must stay fixed. */
|
|
808
|
+
.bc-editor-area-wrap .bc-editor-area::-webkit-scrollbar { width: 10px; }
|
|
809
|
+
.bc-editor-hl { position: absolute; inset: 0 10px 0 0; overflow: hidden; pointer-events: none; color: transparent; margin: 0; box-sizing: border-box; }
|
|
810
|
+
.bc-editor-hl mark { color: transparent; background: var(--mark-bg); border-radius: var(--r-1); padding: 0; }
|
|
811
|
+
.bc-editor-hl mark.cur { background: var(--mark-bg-current); }
|
|
812
|
+
/* Footer: stats left; the clipboard follow state, the live save state (or the
|
|
813
|
+
viewer's zoom controls) right. */
|
|
814
|
+
.bc-editor-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-5); padding: 0 var(--gutter); height: var(--ctl-lg); flex-shrink: 0;
|
|
815
|
+
border-top: 1px solid var(--line); background: var(--surface); font-size: var(--fs-meta); color: var(--text-dim); }
|
|
816
|
+
.bc-editor-foot > [data-x="stats"], .bc-editor-foot > [data-x="dims"] { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
817
|
+
.bc-editor-foot-end { display: flex; align-items: center; gap: var(--sp-5); flex-shrink: 0; }
|
|
818
|
+
/* Text ends on the gutter: a trailing quiet button (the viewer's 100%) pulls
|
|
819
|
+
in by its own padding. */
|
|
820
|
+
.bc-editor-foot-end > .btn.sm:last-child { margin-right: calc(-1 * var(--sp-3)); }
|
|
821
|
+
.bc-save-state { white-space: nowrap; }
|
|
822
|
+
.bc-save-state.failed { color: var(--red); }
|
|
823
|
+
/* Clipboard follow status: "On clipboard" (accent) / a Copy button / a brief notice. */
|
|
824
|
+
.bc-editor-clip { display: inline-flex; align-items: center; gap: var(--sp-2); white-space: nowrap; }
|
|
825
|
+
.bc-editor-clip.on { color: var(--accent-text); }
|
|
826
|
+
.bc-editor-clip.notice { color: var(--text); }
|
|
827
|
+
.bc-editor-clip .mi.sm { font-size: var(--icon-sm); }
|
|
828
|
+
/* The footer Copy is the shared quiet .btn.sm (20px text affordance). */
|
|
829
|
+
@media (max-width: 480px) { .bc-editor-clip.on > span:last-child { display: none; } }
|
|
830
|
+
/* Viewer zoom: zoom out | percentage | zoom in | Fit | 100%, quiet 20px
|
|
831
|
+
controls; the view that is on (Fit / 100%) reads in the accent. */
|
|
832
|
+
.bc-zoom { gap: var(--sp-1); }
|
|
833
|
+
.bc-zoom .icon-btn { width: var(--ctl-sm); height: var(--ctl-sm); }
|
|
834
|
+
.bc-zoom .icon-btn .mi { font-size: var(--icon-sm); }
|
|
835
|
+
.bc-zoom-pct { min-width: 4ch; text-align: center; font-variant-numeric: tabular-nums; }
|
|
836
|
+
.bc-zoom .btn.quiet.active { color: var(--accent-text); background: var(--accent-bg); }
|
|
837
|
+
/* Glass: the window roots go transparent so the OS acrylic + the one tint
|
|
838
|
+
scrim show through; every band in them is already transparent. */
|
|
839
|
+
:root[data-surface="glass"] .bc-editor,
|
|
840
|
+
:root[data-surface="glass"] .bc-reconcile,
|
|
841
|
+
.bc-popup[data-surface="glass"][data-glass-scope="all"] .bc-editor,
|
|
842
|
+
.bc-popup[data-surface="glass"][data-glass-scope="all"] .bc-reconcile { background: transparent; }
|
|
843
|
+
/* In-app image viewer: same window chrome as the editor (the .bc-bar + foot on
|
|
844
|
+
the one surface); the stage is the image's matte, centres the image and
|
|
845
|
+
hosts zoom/pan (transform-positioned <img>). */
|
|
846
|
+
.bc-viewer { display: flex; flex-direction: column; height: 100%; width: 100%; background: transparent; color: var(--text); min-height: 0; outline: none; }
|
|
847
|
+
.bc-viewer-stage { position: relative; flex: 1; min-height: 0; overflow: hidden; background: var(--canvas); }
|
|
848
|
+
.bc-viewer-img { position: absolute; left: 0; top: 0; transform-origin: 0 0; user-select: none; image-rendering: auto; }
|
|
849
|
+
:root[data-surface="glass"] .bc-viewer, :root[data-surface="glass"] .bc-viewer-stage { background: transparent; }
|
|
850
|
+
/* Demo overlay host: the editor floats over the demo popup, standing in for
|
|
851
|
+
the app's editor WINDOW, so it follows "Glass on" (data-glass-scope on the
|
|
852
|
+
popup, Core.applyVariants) like the app does: Popup only = a solid window
|
|
853
|
+
(it re-takes the solid surface tokens the popup's glass overrides, so it
|
|
854
|
+
paints exactly the app editor's --surface); All windows = one frosted
|
|
855
|
+
layer over the list (the window roots inside go transparent above). */
|
|
856
|
+
/* IntelliJ-style merge (CodeMirror 5 merge addon, skinned with our tokens):
|
|
857
|
+
Current (read-only) | Result (editable) | Incoming (read-only); 2-pane =
|
|
858
|
+
Current (edited in place into the result) | Incoming. One plane: the root
|
|
859
|
+
paints the window surface, heads / panes / gap / footer are transparent. */
|
|
860
|
+
.bc-reconcile { display: flex; flex-direction: column; height: 100%; width: 100%; background: transparent; color: var(--text); min-height: 0; }
|
|
861
|
+
/* Pane heads aligned to the addon's pane widths (2-pane 47%/6%, 3-pane
|
|
862
|
+
31%/3.5%): the pane's name, its title as a one-click pick when the two
|
|
863
|
+
titles differ (the result's title is the bar's field), and the whole-side
|
|
864
|
+
accept at the end of the head of the pane it takes. */
|
|
865
|
+
.bc-merge-heads { flex-shrink: 0; display: flex; align-items: center; height: var(--ctl-lg); border-bottom: 1px solid var(--line); background: var(--surface); }
|
|
866
|
+
.bc-merge-head { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; height: 100%; padding: 0 var(--sp-2) 0 var(--sp-4); container: bc-head / inline-size; }
|
|
867
|
+
/* The first head starts on the window's gutter, like the bar title above it
|
|
868
|
+
and the first pane's text below it (one left edge). */
|
|
869
|
+
.bc-merge-head:first-child { padding-left: var(--gutter); }
|
|
870
|
+
.bc-merge-heads.bc-heads-2 > .bc-merge-head { width: 47%; }
|
|
871
|
+
.bc-merge-heads.bc-heads-2 > .bc-merge-heads-gap { width: 6%; }
|
|
872
|
+
.bc-merge-heads.bc-heads-3 > .bc-merge-head { width: 31%; }
|
|
873
|
+
.bc-merge-heads.bc-heads-3 > .bc-merge-heads-gap { width: 3.5%; }
|
|
874
|
+
.bc-head-label { flex-shrink: 0; white-space: nowrap; } /* type from the shared .bc-label rule */
|
|
875
|
+
.bc-head-title { flex: 0 1 auto; min-width: 6ch; height: var(--ctl-sm); padding: 0 var(--sp-2); border: none; border-radius: var(--r-ctl); background: transparent; color: var(--text);
|
|
876
|
+
font-family: inherit; font-size: var(--fs-meta); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;
|
|
877
|
+
transition: background-color var(--dur) var(--ease); }
|
|
878
|
+
.bc-head-title:hover { background: var(--hover); }
|
|
879
|
+
.bc-merge-head .bc-head-accept { margin-left: auto; flex-shrink: 0; } /* the shared .btn.quiet.sm.accent */
|
|
880
|
+
/* The accept says only "Accept" (its head names the side; the tooltip and
|
|
881
|
+
aria-label the whole action), and a title pick ellipsizes first, so the
|
|
882
|
+
default 3-pane window keeps the word. Only a head too narrow for label +
|
|
883
|
+
pick + "Accept" drops to the glyph. */
|
|
884
|
+
@container bc-head (max-width: 179px) {
|
|
885
|
+
.bc-merge-head:has(.bc-head-title) .bc-accept-label { display: none; }
|
|
886
|
+
}
|
|
887
|
+
@container bc-head (max-width: 139px) {
|
|
888
|
+
.bc-accept-label { display: none; }
|
|
889
|
+
}
|
|
890
|
+
/* ONE counter in the bar ("2 of 5 resolved", a text button to the next open
|
|
891
|
+
change) and a red conflict chip only while conflicts remain. */
|
|
892
|
+
.bc-chg-count { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-meta); color: var(--text-dim); margin-right: var(--sp-2); white-space: nowrap; }
|
|
893
|
+
.bc-chg-progress { height: var(--ctl-sm); padding: 0 var(--sp-2); border: none; border-radius: var(--r-ctl); background: transparent; color: var(--text-dim);
|
|
894
|
+
font-family: inherit; font-size: var(--fs-meta); cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
|
|
895
|
+
.bc-chg-progress:hover { color: var(--text); background: var(--hover); }
|
|
896
|
+
.bc-conflict-chip { border: none; background: var(--red-bg); color: var(--red); font-size: var(--fs-meta); font-family: inherit; height: var(--ctl-sm); padding: 0 var(--sp-3); border-radius: var(--r-chip); cursor: pointer; }
|
|
897
|
+
/* --- CodeMirror skin: tokens only, no stock palette. --- */
|
|
898
|
+
.bc-merge-host { flex: 1; min-height: 0; position: relative; }
|
|
899
|
+
/* Full-height chain: the addon's default is a fixed 350px and its panes have
|
|
900
|
+
AUTO height (so CodeMirror height:100% would resolve against auto and grow to
|
|
901
|
+
content — no internal scroll, the user's "can't scroll" bug). Pin every level
|
|
902
|
+
to 100% so each pane scrolls internally. */
|
|
903
|
+
.bc-merge-host .CodeMirror-merge,
|
|
904
|
+
.bc-merge-host .CodeMirror-merge-pane,
|
|
905
|
+
.bc-merge-host .CodeMirror-merge .CodeMirror { height: 100%; }
|
|
906
|
+
.bc-merge-host .CodeMirror-merge { border: 0; }
|
|
907
|
+
/* Every pane is the same plane (no read-only dimming: that changed the text,
|
|
908
|
+
the background and every highlight at once); the caret and the pane heads
|
|
909
|
+
say which pane takes edits. */
|
|
910
|
+
.bc-merge-host .CodeMirror { background: transparent; color: var(--text); font-family: var(--font-mono); font-size: var(--fs-text); line-height: var(--lh-text); }
|
|
911
|
+
.bc-merge-host .CodeMirror-cursor { border-left: 1.5px solid var(--text); }
|
|
912
|
+
/* Text edges: each pane's text starts where its head's label does (--sp-4),
|
|
913
|
+
the first pane's on the window gutter with the bar title and its head.
|
|
914
|
+
The -like rule is CodeMirror's measuring copy, so wrapping and the caret
|
|
915
|
+
follow the padding. */
|
|
916
|
+
.bc-merge-host .CodeMirror pre.CodeMirror-line, .bc-merge-host .CodeMirror pre.CodeMirror-line-like { padding-left: var(--sp-4); }
|
|
917
|
+
.bc-merge-host .CodeMirror-merge-pane:first-child .CodeMirror pre.CodeMirror-line,
|
|
918
|
+
.bc-merge-host .CodeMirror-merge-pane:first-child .CodeMirror pre.CodeMirror-line-like { padding-left: var(--gutter); }
|
|
919
|
+
.bc-merge-host .CodeMirror-selected { background: var(--accent-bg); }
|
|
920
|
+
/* The gap between panes holds the connectors and the gutter buttons: no fill,
|
|
921
|
+
no box, ONE hairline on the side that faces the read-only pane. */
|
|
922
|
+
.bc-merge-host .CodeMirror-merge-gap { background: transparent; border: 0; }
|
|
923
|
+
.bc-merge-host .CodeMirror-merge-gap:has(+ .CodeMirror-merge-pane-rightmost) { box-shadow: inset -1px 0 0 var(--line); }
|
|
924
|
+
.bc-merge-host .CodeMirror-merge-3pane .CodeMirror-merge-pane:first-child + .CodeMirror-merge-gap { box-shadow: inset 1px 0 0 var(--line); }
|
|
925
|
+
.bc-merge-host .CodeMirror-merge-scrolllock-wrap { display: none; } /* scroll stays locked; the toggle was an unexplained glyph */
|
|
926
|
+
/* Chunks: ONE green layer per change (the wash; no start/end hairlines, the
|
|
927
|
+
connector the same wash with no stroke). Inside it the word-level marks
|
|
928
|
+
(vendored bcWordDiff; a whole-line edit gets none) are a step stronger.
|
|
929
|
+
Conflicts go red on every pane that holds them, their marks too. */
|
|
930
|
+
.bc-merge-host .CodeMirror-merge-l-chunk, .bc-merge-host .CodeMirror-merge-r-chunk { background: var(--green-bg); }
|
|
931
|
+
.bc-merge-host .CodeMirror-merge-l-chunk-start, .bc-merge-host .CodeMirror-merge-r-chunk-start { border-top: 0; }
|
|
932
|
+
.bc-merge-host .CodeMirror-merge-l-chunk-end, .bc-merge-host .CodeMirror-merge-r-chunk-end { border-bottom: 0; }
|
|
933
|
+
.bc-merge-host .CodeMirror-merge-l-inserted, .bc-merge-host .CodeMirror-merge-r-inserted,
|
|
934
|
+
.bc-merge-host .CodeMirror-merge-l-deleted, .bc-merge-host .CodeMirror-merge-r-deleted {
|
|
935
|
+
background: color-mix(in srgb, var(--green) 28%, transparent); border-radius: var(--r-1); -webkit-box-decoration-break: clone; box-decoration-break: clone; text-decoration: none; }
|
|
936
|
+
.bc-merge-host .CodeMirror-merge-l-connect, .bc-merge-host .CodeMirror-merge-r-connect { fill: var(--green-bg); stroke: none; }
|
|
937
|
+
.bc-merge-host .CodeMirror-merge-l-connect.bc-conflict-connect, .bc-merge-host .CodeMirror-merge-r-connect.bc-conflict-connect { fill: var(--red-bg); }
|
|
938
|
+
.bc-merge-host .bc-conflict { background: var(--red-bg); }
|
|
939
|
+
.bc-merge-host .bc-conflict-line .CodeMirror-merge-l-inserted, .bc-merge-host .bc-conflict-line .CodeMirror-merge-r-inserted,
|
|
940
|
+
.bc-merge-host .bc-conflict-line .CodeMirror-merge-l-deleted, .bc-merge-host .bc-conflict-line .CodeMirror-merge-r-deleted { background: color-mix(in srgb, var(--red) 28%, transparent); }
|
|
941
|
+
/* Chunk-state paint from the wrapper: 'quiet' (whitespace-only) chunks lose the
|
|
942
|
+
green wash entirely; declined chunks dim their read-only text and get
|
|
943
|
+
a dashed hollow connector. */
|
|
944
|
+
.bc-merge-host .bc-quiet.CodeMirror-merge-l-chunk, .bc-merge-host .bc-quiet.CodeMirror-merge-r-chunk { background: transparent; }
|
|
945
|
+
.bc-merge-host .bc-dim-line { opacity: .45; }
|
|
946
|
+
.bc-merge-host .CodeMirror-merge-l-connect.bc-declined,
|
|
947
|
+
.bc-merge-host .CodeMirror-merge-r-connect.bc-declined { fill: transparent; stroke: var(--line-strong); stroke-dasharray: 3 3; }
|
|
948
|
+
/* The gutter buttons (vendored merge.js: Material glyphs): the arrow pulls a
|
|
949
|
+
chunk into the result, the x below it dismisses it. Quiet 20px icon
|
|
950
|
+
buttons, centred in the gap (a 3-pane gap is about one button wide). */
|
|
951
|
+
.bc-merge-host .CodeMirror-merge-gap .CodeMirror-merge-copy, .bc-merge-host .CodeMirror-merge-gap .CodeMirror-merge-copy-reverse {
|
|
952
|
+
display: flex; align-items: center; justify-content: center; width: var(--ctl-sm); height: var(--ctl-sm); left: 50%; right: auto;
|
|
953
|
+
margin-left: calc(var(--ctl-sm) / -2); border-radius: var(--r-ctl); color: var(--text-dim); font-size: var(--icon-sm);
|
|
954
|
+
transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
|
|
955
|
+
.bc-merge-host .CodeMirror-merge-gap .CodeMirror-merge-copy:hover { color: var(--accent); background: var(--hover); }
|
|
956
|
+
.bc-merge-host .CodeMirror-merge-gap .bc-decline { margin-top: var(--ctl-sm); }
|
|
957
|
+
.bc-merge-host .CodeMirror-merge-gap .bc-decline:hover { color: var(--red); }
|
|
958
|
+
/* Collapsed identical stretch ("12 unchanged lines"): quiet text, no box;
|
|
959
|
+
a hover fill only, like an icon button. */
|
|
960
|
+
.bc-merge-host .CodeMirror-merge-collapsed-widget { background: transparent; border: none; border-radius: var(--r-ctl); color: var(--text-dim); font-family: var(--font-sans); font-size: var(--fs-meta); padding: 0 var(--sp-3); cursor: pointer; }
|
|
961
|
+
.bc-merge-host .CodeMirror-merge-collapsed-widget:hover { color: var(--text); background: var(--hover); }
|
|
962
|
+
.bc-merge-host .CodeMirror-merge-collapsed-widget:before { content: "\22EF\00A0"; }
|
|
963
|
+
.bc-merge-note { flex-shrink: 0; display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--gutter); background: var(--accent-bg); color: var(--text); font-size: var(--fs-ui); border-bottom: 1px solid var(--line); }
|
|
964
|
+
.bc-merge-note[hidden] { display: none; }
|
|
965
|
+
.bc-merge-note .btn { flex-shrink: 0; } /* the shared .btn.quiet.sm.accent */
|
|
966
|
+
/* Footer: ONE row in every mode (unify, conflict, 2- or 3-pane), centred:
|
|
967
|
+
Keep both, Remove conflict (conflicts only), the primary. The whole-side
|
|
968
|
+
accepts live in the pane heads, so nothing here jumps between modes.
|
|
969
|
+
Footer buttons are the shared .btn set; the primary label changes on a
|
|
970
|
+
Unify's last step, and a fixed width keeps it from reflowing under a cursor
|
|
971
|
+
that is about to click. */
|
|
972
|
+
.bc-reconcile-actions { position: relative; flex-shrink: 0; display: flex; align-items: center; justify-content: center; gap: var(--sp-3); padding: var(--sp-3) var(--gutter);
|
|
973
|
+
border-top: 1px solid var(--line); background: var(--surface); }
|
|
974
|
+
.bc-reconcile-actions .btn.primary { min-width: 10.5em; }
|
|
975
|
+
/* A multi-step Unify's "Step 2 of 3": on the footer's left gutter, out of the
|
|
976
|
+
flow, so the buttons stay centred and never move between steps. */
|
|
977
|
+
.bc-step { position: absolute; left: var(--gutter); top: 50%; transform: translateY(-50%); font-size: var(--fs-meta); color: var(--text-dim); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
978
|
+
|
|
979
|
+
/* --- Multi-select: checked-row tint + slim contextual toolbar --- */
|
|
980
|
+
/* `.selected` is the keyboard cursor (--accent-bg, defined with the row); a
|
|
981
|
+
checked row (Ctrl/Shift-click) adds the accent edge so it reads as part of
|
|
982
|
+
the bulk set even when it isn't the cursor; the cursor inside the set is a
|
|
983
|
+
step stronger. */
|
|
984
|
+
.item.multi-selected { background: var(--accent-bg); --row-edge: var(--accent); }
|
|
985
|
+
.item.multi-selected:hover, .item.selected.multi-selected { background: var(--accent-bg-strong); }
|
|
986
|
+
/* The selection bar takes the chip bar's place while 2+ clips are selected:
|
|
987
|
+
both sit in ONE grid cell (.chip-row), the chips go invisible, the cell's
|
|
988
|
+
height is the taller of the two, so nothing below moves; no band, no tint,
|
|
989
|
+
no border (the checked rows carry the only accent). The cell is never lower
|
|
990
|
+
than a populated chip bar, so an empty one (no pins, keys, images or groups
|
|
991
|
+
yet) does not grow when the selection bar or the first chip shows. */
|
|
992
|
+
.chip-row { display: grid; min-height: max(var(--ctl-md), calc(var(--ctl-sm) + 2 * var(--sp-1))); }
|
|
993
|
+
.chip-row > .group-filters, .chip-row > .selection-bar { grid-area: 1 / 1; }
|
|
994
|
+
.chip-row:has(> .selection-bar:not(.hidden)) > .group-filters { visibility: hidden; }
|
|
995
|
+
.selection-bar { display: flex; align-items: center; align-self: start; gap: var(--sp-3); min-height: var(--ctl-md); padding: 0 var(--edge-ctl) 0 var(--gutter); z-index: 31; }
|
|
996
|
+
.selection-bar.hidden { display: none; }
|
|
997
|
+
.selection-count { flex: 1; font-size: var(--fs-ui); font-weight: var(--fw-strong); color: var(--text); }
|
|
998
|
+
.selection-actions { display: flex; align-items: center; gap: var(--sp-1); }
|
|
999
|
+
|
|
1000
|
+
/* Rows are click-to-act (click pastes, shift/ctrl-click selects) — native text
|
|
1001
|
+
selection inside them is never meaningful and shift-click was smearing a text
|
|
1002
|
+
highlight across rows. The editor is the copy-a-fragment surface. */
|
|
1003
|
+
.list .item { user-select: none; -webkit-user-select: none; }
|
|
1004
|
+
|
|
1005
|
+
/* Submenu auto-flip (set by installSubmenuAutoflip when a submenu would leave
|
|
1006
|
+
the window): side-opening submenus open leftward. */
|
|
1007
|
+
.tag-menu-node > .tag-submenu.flip-x { left: auto; right: calc(100% - 1px); }
|
|
1008
|
+
|
|
1009
|
+
/* --- Shared popover menu: the per-clip "..." menu, the bulk menu, a row's
|
|
1010
|
+
keypad / group popovers, the editor + viewer menus. ONE row primitive
|
|
1011
|
+
(Core.menuRowHtml): icon column, label, optional hint / caret, --ctl-md tall,
|
|
1012
|
+
contiguous. Every submenu and group checklist uses the same row, so labels
|
|
1013
|
+
line up across levels. Membership = the accent check glyph in the icon
|
|
1014
|
+
column (a dash for "some of the selection"), never a row fill; --text-dim is
|
|
1015
|
+
for disabled rows only. Keyboard focus paints like hover (a menu has one
|
|
1016
|
+
active row), danger rows turn red on hover / focus. --- */
|
|
1017
|
+
.bc-menu { position: absolute; z-index: 200; min-width: 176px; max-width: 260px; } /* surface look comes from the shared floating-surface rule */
|
|
1018
|
+
.bc-menu-list { display: flex; flex-direction: column; }
|
|
1019
|
+
.bc-menu-item { display: flex; align-items: center; gap: var(--sp-3); width: 100%; min-height: var(--ctl-md); box-sizing: border-box; padding: 0 var(--sp-3);
|
|
1020
|
+
background: none; border: none; border-radius: var(--r-1); color: var(--text); font-family: inherit; font-size: var(--fs-ui); line-height: var(--lh-ui);
|
|
1021
|
+
text-align: left; cursor: pointer; position: relative; }
|
|
1022
|
+
.bc-menu-item:hover, .bc-menu .bc-menu-item:focus-visible { background: var(--menu-hover); box-shadow: none; }
|
|
1023
|
+
.bc-menu-item > .mi { width: 1em; font-size: var(--icon-md); color: var(--text-dim); flex-shrink: 0; }
|
|
1024
|
+
.bc-menu-item > .bc-menu-check { color: var(--accent); }
|
|
1025
|
+
.bc-menu-item.danger:hover, .bc-menu-item.danger:focus-visible { background: var(--red-bg); }
|
|
1026
|
+
.bc-menu-item.danger:hover, .bc-menu-item.danger:hover > .mi, .bc-menu-item.danger:focus-visible, .bc-menu-item.danger:focus-visible > .mi { color: var(--red); }
|
|
1027
|
+
.bc-menu-item:disabled { color: var(--text-dim); cursor: default; }
|
|
1028
|
+
.bc-menu-item:disabled:hover { background: none; }
|
|
1029
|
+
.bc-menu-label { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1030
|
+
.bc-menu-hint { flex-shrink: 0; color: var(--text-dim); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }
|
|
1031
|
+
.bc-menu-item > .tag-caret { margin-left: 0; font-size: var(--icon-sm); color: var(--text-dim); opacity: 1; }
|
|
1032
|
+
.bc-menu-sep { height: 1px; margin: var(--sp-2) 0; background: var(--line); }
|
|
1033
|
+
/* A submenu opens beside its row (the row and its .tag-submenu share one
|
|
1034
|
+
.tag-menu-node): on hover, or .open from the keyboard (Right / Enter). Focus
|
|
1035
|
+
on a parent row alone does not open it. */
|
|
1036
|
+
.bc-menu .tag-submenu { top: -4px; left: calc(100% - 1px); min-width: 150px; }
|
|
1037
|
+
.bc-menu .tag-menu-node.open > .tag-submenu { display: block; }
|
|
1038
|
+
.bc-menu .tag-menu-node:hover > .bc-menu-item, .bc-menu .tag-menu-node.open > .bc-menu-item { background: var(--menu-hover); } /* the row whose submenu is out */
|
|
1039
|
+
.bc-menu .tag-menu-node:not(.open):not(:hover) > .tag-submenu { display: none; }
|
|
1040
|
+
|
|
1041
|
+
/* --- Undo toast action: the shared accent text action (.btn.quiet.sm.accent),
|
|
1042
|
+
pulled in by its own padding. The toast itself ignores the pointer (it
|
|
1043
|
+
floats over the last row); the action opts back in only while the toast is
|
|
1044
|
+
shown (hidden, it still sits over the last row). --- */
|
|
1045
|
+
.toast-action { margin-right: calc(-1 * var(--sp-3)); }
|
|
1046
|
+
.toast.show .toast-action { pointer-events: auto; }
|