tablewalk 0.0.1
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 +553 -0
- package/dist/adapters/adapter.js +372 -0
- package/dist/adapters/connect.js +33 -0
- package/dist/adapters/mysql.js +951 -0
- package/dist/adapters/postgres.js +1000 -0
- package/dist/adapters/sqlite.js +781 -0
- package/dist/client/agent.js +262 -0
- package/dist/client/app.js +973 -0
- package/dist/client/arrange.js +254 -0
- package/dist/client/ask.js +133 -0
- package/dist/client/breakdown.js +317 -0
- package/dist/client/clauses.js +390 -0
- package/dist/client/columns.js +98 -0
- package/dist/client/complete.js +437 -0
- package/dist/client/compose.js +166 -0
- package/dist/client/composer.css +495 -0
- package/dist/client/composer.js +1972 -0
- package/dist/client/connections.js +234 -0
- package/dist/client/connmanager.js +962 -0
- package/dist/client/connurl.js +188 -0
- package/dist/client/core.js +893 -0
- package/dist/client/deeplink.js +270 -0
- package/dist/client/delete.js +144 -0
- package/dist/client/diagram.js +885 -0
- package/dist/client/dropdown.js +279 -0
- package/dist/client/export.js +456 -0
- package/dist/client/features.css +524 -0
- package/dist/client/findvalue.js +169 -0
- package/dist/client/grid.js +205 -0
- package/dist/client/handoff.js +153 -0
- package/dist/client/help.css +145 -0
- package/dist/client/help.js +881 -0
- package/dist/client/history.js +222 -0
- package/dist/client/index.html +116 -0
- package/dist/client/insert.js +151 -0
- package/dist/client/menu.js +160 -0
- package/dist/client/nested.js +255 -0
- package/dist/client/page.css +713 -0
- package/dist/client/page.js +1345 -0
- package/dist/client/pagebuilder.js +1222 -0
- package/dist/client/pagemarks.js +95 -0
- package/dist/client/palette.js +374 -0
- package/dist/client/peek.js +254 -0
- package/dist/client/picker.js +139 -0
- package/dist/client/pins.js +140 -0
- package/dist/client/prompt.js +129 -0
- package/dist/client/record.js +707 -0
- package/dist/client/schemaexport.js +242 -0
- package/dist/client/schematext.js +125 -0
- package/dist/client/shape.js +178 -0
- package/dist/client/shapecheck.js +129 -0
- package/dist/client/skeleton.js +139 -0
- package/dist/client/sql.css +126 -0
- package/dist/client/sql.js +398 -0
- package/dist/client/sqlcomplete.js +163 -0
- package/dist/client/sqlsaved.js +107 -0
- package/dist/client/style.css +2711 -0
- package/dist/client/summary.js +259 -0
- package/dist/client/table.js +1035 -0
- package/dist/client/template.js +539 -0
- package/dist/client/theme.js +74 -0
- package/dist/client/tour.js +324 -0
- package/dist/client/undo.js +105 -0
- package/dist/client/url.js +166 -0
- package/dist/client/value.js +223 -0
- package/dist/client/views.js +215 -0
- package/dist/client/virtual.js +176 -0
- package/dist/client/welcome.js +170 -0
- package/dist/client/write.js +414 -0
- package/dist/server/changeimpact.js +195 -0
- package/dist/server/connections.js +615 -0
- package/dist/server/constraints.js +62 -0
- package/dist/server/credentials.js +230 -0
- package/dist/server/fixture.js +199 -0
- package/dist/server/graph.js +194 -0
- package/dist/server/impact.js +48 -0
- package/dist/server/index.js +2204 -0
- package/dist/server/journal.js +173 -0
- package/dist/server/layouts.js +128 -0
- package/dist/server/mcp.js +2840 -0
- package/dist/server/shapeonly.js +91 -0
- package/dist/shared/breakdown.js +231 -0
- package/dist/shared/breakdowntext.js +257 -0
- package/dist/shared/diff.js +130 -0
- package/dist/shared/like.js +29 -0
- package/dist/shared/lint.js +149 -0
- package/dist/shared/order.js +133 -0
- package/dist/shared/page.js +932 -0
- package/dist/shared/query.js +831 -0
- package/dist/shared/recordview.js +343 -0
- package/dist/shared/schema.js +377 -0
- package/dist/shared/sqlsaved.js +67 -0
- package/dist/shared/view.js +981 -0
- package/dist/shared/viewtext.js +273 -0
- package/dist/shared/vocabulary.js +164 -0
- package/package.json +57 -0
|
@@ -0,0 +1,2711 @@
|
|
|
1
|
+
/* Tokens. Light is the bare :root; dark is redefined twice — once for the
|
|
2
|
+
OS preference (guarded so an explicit light choice still wins) and once for
|
|
3
|
+
the explicit stamp. Nothing outside these blocks names a colour directly. */
|
|
4
|
+
:root {
|
|
5
|
+
--bg: #fbfaf8;
|
|
6
|
+
--surface: #ffffff;
|
|
7
|
+
--surface-2: #f3f1ed;
|
|
8
|
+
--border: #e2ded6;
|
|
9
|
+
--border-strong: #cbc5b9;
|
|
10
|
+
--text: #24211c;
|
|
11
|
+
--text-dim: #6d675d;
|
|
12
|
+
--text-faint: #98918480;
|
|
13
|
+
--accent: #1d6a5a;
|
|
14
|
+
--accent-soft: #1d6a5a14;
|
|
15
|
+
--link: #17594c;
|
|
16
|
+
--warn: #9a4b12;
|
|
17
|
+
--danger: #a12d2d;
|
|
18
|
+
--key: #7a5cc0;
|
|
19
|
+
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
|
20
|
+
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
21
|
+
--radius: 6px;
|
|
22
|
+
|
|
23
|
+
/* Motion.
|
|
24
|
+
|
|
25
|
+
The built-in CSS easings are too weak to read as intentional, so the
|
|
26
|
+
curves are explicit. `ease-in` appears nowhere: it starts slow, delaying
|
|
27
|
+
the exact moment the user is watching, and an ease-out at 200ms feels
|
|
28
|
+
faster than an ease-in at 200ms.
|
|
29
|
+
|
|
30
|
+
Durations are short because this is a tool, not a presentation. A
|
|
31
|
+
dropdown at 160ms feels responsive; the same dropdown at 400ms feels
|
|
32
|
+
like it is thinking. Nothing here exceeds 220ms.
|
|
33
|
+
|
|
34
|
+
What is deliberately NOT animated: anything reached by keyboard, and
|
|
35
|
+
anything you see a hundred times a day. Arrowing through the table list
|
|
36
|
+
and paging through rows are instant, because animating a repeated action
|
|
37
|
+
makes the tool feel slower every single time. */
|
|
38
|
+
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
39
|
+
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
|
40
|
+
--t-press: 140ms;
|
|
41
|
+
--t-hover: 120ms;
|
|
42
|
+
--t-popover: 160ms;
|
|
43
|
+
--t-panel: 200ms;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@media (prefers-color-scheme: dark) {
|
|
47
|
+
:root:not([data-theme="light"]) {
|
|
48
|
+
--bg: #16181a;
|
|
49
|
+
--surface: #1c1f22;
|
|
50
|
+
--surface-2: #23272b;
|
|
51
|
+
--border: #2f343a;
|
|
52
|
+
--border-strong: #454c54;
|
|
53
|
+
--text: #e4e2dd;
|
|
54
|
+
--text-dim: #9aa0a6;
|
|
55
|
+
--text-faint: #6b727980;
|
|
56
|
+
--accent: #5fc9ad;
|
|
57
|
+
--accent-soft: #5fc9ad1a;
|
|
58
|
+
--link: #6fd4b8;
|
|
59
|
+
--warn: #e0a05c;
|
|
60
|
+
--danger: #e8756f;
|
|
61
|
+
--key: #b39ae8;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
:root[data-theme="dark"] {
|
|
66
|
+
--bg: #16181a;
|
|
67
|
+
--surface: #1c1f22;
|
|
68
|
+
--surface-2: #23272b;
|
|
69
|
+
--border: #2f343a;
|
|
70
|
+
--border-strong: #454c54;
|
|
71
|
+
--text: #e4e2dd;
|
|
72
|
+
--text-dim: #9aa0a6;
|
|
73
|
+
--text-faint: #6b727980;
|
|
74
|
+
--accent: #5fc9ad;
|
|
75
|
+
--accent-soft: #5fc9ad1a;
|
|
76
|
+
--link: #6fd4b8;
|
|
77
|
+
--warn: #e0a05c;
|
|
78
|
+
--danger: #e8756f;
|
|
79
|
+
--key: #b39ae8;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
* { box-sizing: border-box; }
|
|
83
|
+
|
|
84
|
+
body {
|
|
85
|
+
margin: 0;
|
|
86
|
+
background: var(--bg);
|
|
87
|
+
color: var(--text);
|
|
88
|
+
font: 14px/1.5 var(--sans);
|
|
89
|
+
height: 100vh;
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ---------- top bar ---------- */
|
|
95
|
+
|
|
96
|
+
.topbar {
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 16px;
|
|
100
|
+
padding: 8px 14px;
|
|
101
|
+
background: var(--surface);
|
|
102
|
+
border-bottom: 1px solid var(--border);
|
|
103
|
+
flex: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* The wordmark is a home link, so it has to look pressable without becoming
|
|
107
|
+
a button in the visual sense — no border, no fill, just a hit area. */
|
|
108
|
+
.brand {
|
|
109
|
+
display: flex; align-items: center; gap: 8px; flex: none;
|
|
110
|
+
/* Colour reset explicitly, not just the background. The base button rule
|
|
111
|
+
sets `color: var(--bg)` to sit on an accent fill; unsetting only the fill
|
|
112
|
+
left near-white text on a white bar, visible solely on hover. */
|
|
113
|
+
background: none; border: none; border-radius: var(--radius);
|
|
114
|
+
color: var(--text);
|
|
115
|
+
padding: 4px 6px; margin-left: -6px; cursor: pointer;
|
|
116
|
+
font: inherit;
|
|
117
|
+
}
|
|
118
|
+
.brand:hover .brand-name { color: var(--accent); }
|
|
119
|
+
.brand:hover .brand-mark { border-color: var(--accent); }
|
|
120
|
+
|
|
121
|
+
/* Two linked nodes, which is the whole idea of the tool in 14 pixels. */
|
|
122
|
+
.brand-mark {
|
|
123
|
+
width: 14px; height: 14px; border-radius: 50%;
|
|
124
|
+
border: 2px solid var(--accent);
|
|
125
|
+
position: relative;
|
|
126
|
+
}
|
|
127
|
+
.brand-mark::after {
|
|
128
|
+
content: ""; position: absolute; left: 12px; top: 3px;
|
|
129
|
+
width: 10px; height: 2px; background: var(--accent);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.brand-name { font-weight: 620; letter-spacing: -0.01em; color: var(--text); font-size: 14px; }
|
|
133
|
+
/* The connection name, which is also the button that opens Connections.
|
|
134
|
+
Quiet by default — it reads as the label it is — and only takes a box on
|
|
135
|
+
hover, because a permanent button here would compete with the query bar
|
|
136
|
+
for the eye every time the page is looked at. */
|
|
137
|
+
.brand-db {
|
|
138
|
+
color: var(--text-dim);
|
|
139
|
+
font-family: var(--mono);
|
|
140
|
+
font-size: 12px;
|
|
141
|
+
padding: 4px 6px;
|
|
142
|
+
border: 1px solid transparent;
|
|
143
|
+
border-radius: var(--radius);
|
|
144
|
+
background: none;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
}
|
|
148
|
+
.brand-db:hover { color: var(--text); border-color: var(--border-strong); }
|
|
149
|
+
.brand-db:focus-visible { color: var(--text); border-color: var(--border-strong); }
|
|
150
|
+
/* Before a connection is named there is nothing to click, and an empty box
|
|
151
|
+
beside the wordmark reads as a rendering fault. */
|
|
152
|
+
.brand-db:empty { display: none; }
|
|
153
|
+
|
|
154
|
+
.querybar { display: flex; gap: 6px; flex: 1; min-width: 0; }
|
|
155
|
+
|
|
156
|
+
#query {
|
|
157
|
+
flex: 1; min-width: 0;
|
|
158
|
+
padding: 7px 10px;
|
|
159
|
+
font: 13px/1.4 var(--mono);
|
|
160
|
+
color: var(--text);
|
|
161
|
+
background: var(--bg);
|
|
162
|
+
border: 1px solid var(--border-strong);
|
|
163
|
+
border-radius: var(--radius);
|
|
164
|
+
}
|
|
165
|
+
#query:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }
|
|
166
|
+
|
|
167
|
+
button {
|
|
168
|
+
font: inherit; font-size: 13px;
|
|
169
|
+
padding: 7px 12px;
|
|
170
|
+
border-radius: var(--radius);
|
|
171
|
+
border: 1px solid transparent;
|
|
172
|
+
background: var(--accent); color: var(--bg);
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
}
|
|
175
|
+
button.ghost { background: transparent; color: var(--text-dim); border-color: var(--border-strong); }
|
|
176
|
+
button.ghost[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
|
|
177
|
+
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
178
|
+
@media (prefers-reduced-motion: no-preference) and (hover: hover) and (pointer: fine) {
|
|
179
|
+
/* Only transform and opacity, which skip layout and paint. Confined to
|
|
180
|
+
deliberate controls — a row you click a hundred times a day should not
|
|
181
|
+
spring. */
|
|
182
|
+
.topbar button, dialog.confirm button, .view-save {
|
|
183
|
+
transition: transform var(--t-press) var(--ease-out),
|
|
184
|
+
background var(--t-hover) ease,
|
|
185
|
+
border-color var(--t-hover) ease,
|
|
186
|
+
color var(--t-hover) ease;
|
|
187
|
+
}
|
|
188
|
+
.topbar button:active, dialog.confirm button:active { transform: scale(0.97); }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* The pane, so the control that edits the statement sits in its corner rather
|
|
192
|
+
than on a row of its own. A persistent full-width row for one button is a
|
|
193
|
+
line of chrome between the query bar and the results, on every page, for an
|
|
194
|
+
action taken occasionally. */
|
|
195
|
+
.explain-pane { position: relative; flex: none; }
|
|
196
|
+
.explain-pane[hidden] { display: none; }
|
|
197
|
+
.explain-edit {
|
|
198
|
+
position: absolute; top: 6px; right: 12px;
|
|
199
|
+
padding: 2px 9px; border-radius: 5px;
|
|
200
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
201
|
+
color: var(--text-dim); cursor: pointer;
|
|
202
|
+
font: 10.5px var(--mono);
|
|
203
|
+
opacity: 0;
|
|
204
|
+
}
|
|
205
|
+
/* Revealed on approach, and always for the keyboard — an affordance nobody can
|
|
206
|
+
tab to is one that does not exist for anyone not using a mouse. */
|
|
207
|
+
.explain-pane:hover .explain-edit,
|
|
208
|
+
.explain-edit:focus-visible { opacity: 1; }
|
|
209
|
+
.explain-edit:hover { color: var(--accent); border-color: var(--accent); }
|
|
210
|
+
|
|
211
|
+
.explain {
|
|
212
|
+
margin: 0; padding: 10px 14px;
|
|
213
|
+
font: 12px/1.55 var(--mono);
|
|
214
|
+
background: var(--surface-2);
|
|
215
|
+
border-bottom: 1px solid var(--border);
|
|
216
|
+
color: var(--text-dim);
|
|
217
|
+
white-space: pre; overflow-x: auto;
|
|
218
|
+
flex: none;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.errors {
|
|
222
|
+
padding: 8px 14px; flex: none;
|
|
223
|
+
background: color-mix(in srgb, var(--danger) 10%, var(--surface));
|
|
224
|
+
border-bottom: 1px solid var(--border);
|
|
225
|
+
color: var(--danger);
|
|
226
|
+
font-size: 13px;
|
|
227
|
+
}
|
|
228
|
+
.errors p { margin: 2px 0; }
|
|
229
|
+
|
|
230
|
+
/* ---------- layout ---------- */
|
|
231
|
+
|
|
232
|
+
.layout { display: flex; flex: 1; min-height: 0; }
|
|
233
|
+
|
|
234
|
+
.sidebar {
|
|
235
|
+
width: 232px; flex: none;
|
|
236
|
+
border-right: 1px solid var(--border);
|
|
237
|
+
background: var(--surface);
|
|
238
|
+
display: flex; flex-direction: column;
|
|
239
|
+
min-height: 0;
|
|
240
|
+
}
|
|
241
|
+
#table-filter {
|
|
242
|
+
margin: 8px; padding: 6px 8px;
|
|
243
|
+
font: 12px var(--sans); color: var(--text);
|
|
244
|
+
background: var(--bg);
|
|
245
|
+
border: 1px solid var(--border); border-radius: var(--radius);
|
|
246
|
+
}
|
|
247
|
+
#table-list { list-style: none; margin: 0; padding: 0 6px 12px; overflow-y: auto; flex: 1; }
|
|
248
|
+
#table-list li { margin-bottom: 1px; }
|
|
249
|
+
#table-list button {
|
|
250
|
+
width: 100%; text-align: left;
|
|
251
|
+
background: transparent; color: var(--text);
|
|
252
|
+
border: none; border-radius: var(--radius);
|
|
253
|
+
padding: 5px 8px;
|
|
254
|
+
font: 12.5px/1.35 var(--mono);
|
|
255
|
+
display: flex; justify-content: space-between; gap: 8px; align-items: baseline;
|
|
256
|
+
}
|
|
257
|
+
#table-list button:hover { background: var(--surface-2); }
|
|
258
|
+
#table-list button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
259
|
+
#table-list .rows { color: var(--text-dim); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
260
|
+
#table-list .view-tag { color: var(--key); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
261
|
+
|
|
262
|
+
/* `scrollbar-gutter: stable` reserves the scrollbar's width whether or not
|
|
263
|
+
it is showing, so content keeps the same right-hand gutter either way —
|
|
264
|
+
without it the bar sits *in* the padding and the page reads as flush
|
|
265
|
+
against the window on that side only. */
|
|
266
|
+
.content {
|
|
267
|
+
flex: 1; min-width: 0; overflow: auto; display: flex; flex-direction: column;
|
|
268
|
+
scrollbar-gutter: stable;
|
|
269
|
+
}
|
|
270
|
+
/* The record grid fills the height so its divider runs the full way down,
|
|
271
|
+
rather than stopping wherever the shorter column happens to end. */
|
|
272
|
+
.content > .record { flex: 1; }
|
|
273
|
+
|
|
274
|
+
.empty { margin: auto; max-width: 30rem; padding: 32px; text-align: center; }
|
|
275
|
+
.empty h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.015em; margin: 0 0 8px; }
|
|
276
|
+
.empty p { color: var(--text-dim); margin: 0; }
|
|
277
|
+
|
|
278
|
+
/* ---------- breadcrumb ---------- */
|
|
279
|
+
|
|
280
|
+
.crumbs {
|
|
281
|
+
display: flex; align-items: baseline; gap: 1px; flex-wrap: wrap;
|
|
282
|
+
padding: 8px 14px;
|
|
283
|
+
border-bottom: 1px solid var(--border);
|
|
284
|
+
background: var(--surface);
|
|
285
|
+
font-size: 12.5px;
|
|
286
|
+
position: sticky; top: 0; z-index: 2;
|
|
287
|
+
}
|
|
288
|
+
.crumbs button {
|
|
289
|
+
background: none; border: none; padding: 2px 5px;
|
|
290
|
+
color: var(--link); font: inherit; font-family: var(--mono);
|
|
291
|
+
cursor: pointer; border-radius: 3px;
|
|
292
|
+
}
|
|
293
|
+
.crumbs button:hover { background: var(--surface-2); }
|
|
294
|
+
/* The chevron is the only separator, and it is set lighter and roomier than
|
|
295
|
+
the crumbs so the eye reads it as structure rather than content. */
|
|
296
|
+
.crumbs .sep {
|
|
297
|
+
color: var(--text-faint);
|
|
298
|
+
font-size: 11px;
|
|
299
|
+
padding: 0 3px;
|
|
300
|
+
}
|
|
301
|
+
.crumbs .current {
|
|
302
|
+
color: var(--text); font-weight: 500;
|
|
303
|
+
font-family: var(--mono); padding: 2px 5px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* ---------- results table ---------- */
|
|
307
|
+
|
|
308
|
+
/* ---------- tabs ---------- */
|
|
309
|
+
|
|
310
|
+
.tabs { display: flex; gap: 2px; padding: 8px 14px 0; border-bottom: 1px solid var(--border); }
|
|
311
|
+
.tab {
|
|
312
|
+
background: none; border: none; border-bottom: 2px solid transparent;
|
|
313
|
+
color: var(--text-dim); padding: 5px 10px 7px;
|
|
314
|
+
font: 12.5px var(--sans); border-radius: 0; cursor: pointer;
|
|
315
|
+
}
|
|
316
|
+
.tab:hover { color: var(--text); }
|
|
317
|
+
.tab[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); }
|
|
318
|
+
|
|
319
|
+
/* Wider than it was: with the sidebar on the left, 14px of gutter on the
|
|
320
|
+
right reads as none at all beside a panel edge. */
|
|
321
|
+
.panel-pad { padding: 16px 20px; }
|
|
322
|
+
|
|
323
|
+
.ddl-note { margin: 12px 14px 0; color: var(--text-dim); font-size: 12px; }
|
|
324
|
+
.ddl-note strong { color: var(--text); font-weight: 600; }
|
|
325
|
+
.ddl {
|
|
326
|
+
margin: 8px 14px 16px; padding: 12px 14px;
|
|
327
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
328
|
+
font: 12.5px/1.6 var(--mono); color: var(--text);
|
|
329
|
+
white-space: pre; overflow-x: auto;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* ---------- diagram ---------- */
|
|
333
|
+
|
|
334
|
+
/* ---------- diagram: viewport, zoom, focus ---------- */
|
|
335
|
+
|
|
336
|
+
/* No width cap: the diagram is a viewport now, and every pixel of panel it
|
|
337
|
+
does not use is a pixel of schema the reader has to pan to instead. */
|
|
338
|
+
.diagram { max-width: none; }
|
|
339
|
+
|
|
340
|
+
/* A window onto a drawing larger than the window.
|
|
341
|
+
|
|
342
|
+
Given a height rather than left to size to its content: a viewport that
|
|
343
|
+
grows with its content is not a viewport, and the whole point is that the
|
|
344
|
+
picture can be bigger than the space it is read in. */
|
|
345
|
+
.diagram-viewport {
|
|
346
|
+
position: relative;
|
|
347
|
+
height: min(72vh, 760px);
|
|
348
|
+
border: 1px solid var(--border);
|
|
349
|
+
border-radius: 8px;
|
|
350
|
+
background: var(--bg);
|
|
351
|
+
overflow: hidden;
|
|
352
|
+
cursor: grab;
|
|
353
|
+
touch-action: none;
|
|
354
|
+
}
|
|
355
|
+
.diagram-viewport.dragging { cursor: grabbing; }
|
|
356
|
+
.diagram-viewport svg { display: block; }
|
|
357
|
+
|
|
358
|
+
.zoom-controls {
|
|
359
|
+
position: absolute; right: 10px; bottom: 10px;
|
|
360
|
+
display: flex; align-items: center; gap: 4px;
|
|
361
|
+
padding: 4px; border-radius: 7px;
|
|
362
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
363
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.06);
|
|
364
|
+
}
|
|
365
|
+
.zoom-controls button { padding: 2px 8px; font-size: 12px; }
|
|
366
|
+
.zoom-readout {
|
|
367
|
+
min-width: 3.4rem; text-align: center;
|
|
368
|
+
font: 11px var(--mono); color: var(--text-dim);
|
|
369
|
+
font-variant-numeric: tabular-nums;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.diagram-head {
|
|
373
|
+
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
374
|
+
margin-bottom: 12px;
|
|
375
|
+
}
|
|
376
|
+
.diagram-head .diagram-title { margin: 0; }
|
|
377
|
+
|
|
378
|
+
.diagram-focus {
|
|
379
|
+
display: flex; align-items: center; gap: 6px;
|
|
380
|
+
margin-left: auto; justify-content: flex-end;
|
|
381
|
+
/* wrap-reverse so an overflowing row of chips grows upwards. Plain `wrap`
|
|
382
|
+
would push the field itself onto a second line, which is the same
|
|
383
|
+
moving-target problem in the other axis. */
|
|
384
|
+
flex-wrap: wrap-reverse;
|
|
385
|
+
}
|
|
386
|
+
.diagram-focus .picker { flex: none; }
|
|
387
|
+
|
|
388
|
+
/* A field with a menu we draw ourselves. See picker.js for why. */
|
|
389
|
+
.picker { position: relative; }
|
|
390
|
+
.picker-input {
|
|
391
|
+
width: 15rem; max-width: 100%; padding: 5px 9px;
|
|
392
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
393
|
+
background: var(--bg); color: var(--text);
|
|
394
|
+
font: 12px var(--mono);
|
|
395
|
+
}
|
|
396
|
+
.picker-input:focus { outline: none; border-color: var(--accent); }
|
|
397
|
+
.picker-list {
|
|
398
|
+
z-index: 40; max-height: 17rem; overflow-y: auto;
|
|
399
|
+
padding: 4px;
|
|
400
|
+
background: var(--surface);
|
|
401
|
+
border: 1px solid var(--border-strong); border-radius: 8px;
|
|
402
|
+
box-shadow: 0 8px 28px rgb(0 0 0 / 0.16);
|
|
403
|
+
}
|
|
404
|
+
.picker-option {
|
|
405
|
+
display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
|
|
406
|
+
padding: 6px 9px; border-radius: 5px; cursor: pointer;
|
|
407
|
+
}
|
|
408
|
+
.picker-option.active { background: var(--accent-soft); }
|
|
409
|
+
.picker-label { font: 12.5px var(--mono); color: var(--text); }
|
|
410
|
+
.picker-option.active .picker-label { color: var(--accent); }
|
|
411
|
+
.picker-detail { font: 10.5px var(--mono); color: var(--text-faint); }
|
|
412
|
+
.picker-empty { padding: 10px; margin: 0; font-size: 12px; color: var(--text-dim); }
|
|
413
|
+
.diagram-chip {
|
|
414
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
415
|
+
padding: 4px 8px; border-radius: 999px;
|
|
416
|
+
border: 1px solid var(--accent); background: var(--accent-soft);
|
|
417
|
+
color: var(--accent); font: 11.5px var(--mono); cursor: pointer;
|
|
418
|
+
}
|
|
419
|
+
.diagram-chip .chip-x { opacity: 0.6; }
|
|
420
|
+
.diagram-chip:hover .chip-x { opacity: 1; }
|
|
421
|
+
.diagram-clear { font-size: 11.5px; }
|
|
422
|
+
|
|
423
|
+
/* ---------- command palette ---------- */
|
|
424
|
+
|
|
425
|
+
.palette-backdrop {
|
|
426
|
+
/* Above everything it can be opened from.
|
|
427
|
+
|
|
428
|
+
⌘K is bound at the document with capture, so it fires whatever is on
|
|
429
|
+
screen — and at 60 it opened *underneath* the SQL panel (75), the
|
|
430
|
+
connection manager (80) and the page builder (85). The result was a
|
|
431
|
+
focused text field nobody could see, swallowing keystrokes meant for the
|
|
432
|
+
surface in front of it. Whatever the palette can open over, it opens
|
|
433
|
+
above. */
|
|
434
|
+
position: fixed; inset: 0; z-index: 100;
|
|
435
|
+
background: rgb(0 0 0 / 0.28);
|
|
436
|
+
display: flex; justify-content: center;
|
|
437
|
+
padding-top: 12vh;
|
|
438
|
+
}
|
|
439
|
+
.palette {
|
|
440
|
+
width: min(620px, 92vw); max-height: 62vh;
|
|
441
|
+
display: flex; flex-direction: column;
|
|
442
|
+
background: var(--surface);
|
|
443
|
+
border: 1px solid var(--border-strong);
|
|
444
|
+
border-radius: 10px;
|
|
445
|
+
box-shadow: 0 18px 60px rgb(0 0 0 / 0.28);
|
|
446
|
+
overflow: hidden;
|
|
447
|
+
}
|
|
448
|
+
.palette-input {
|
|
449
|
+
padding: 14px 16px;
|
|
450
|
+
border: none; border-bottom: 1px solid var(--border);
|
|
451
|
+
background: none; color: var(--text);
|
|
452
|
+
font: 14px var(--mono);
|
|
453
|
+
}
|
|
454
|
+
/* The outline is dropped because the palette's own frame is the focus
|
|
455
|
+
indicator while it is the only thing focusable — but Tab can move to a
|
|
456
|
+
result and back, and coming back to an input with no indicator at all is
|
|
457
|
+
the caret being somewhere nobody can see. */
|
|
458
|
+
.palette-input:focus { outline: none; border-bottom-color: var(--accent); }
|
|
459
|
+
.palette-list { overflow-y: auto; padding: 6px; }
|
|
460
|
+
.palette-item {
|
|
461
|
+
display: grid; grid-template-columns: 4.6rem 1fr auto;
|
|
462
|
+
align-items: baseline; gap: 10px;
|
|
463
|
+
padding: 7px 10px; border-radius: 6px; cursor: pointer;
|
|
464
|
+
}
|
|
465
|
+
.palette-item.active { background: var(--accent-soft); }
|
|
466
|
+
.palette-kind {
|
|
467
|
+
font: 10px var(--mono); text-transform: uppercase; letter-spacing: 0.06em;
|
|
468
|
+
color: var(--text-faint);
|
|
469
|
+
}
|
|
470
|
+
.palette-kind.kind-view, .palette-kind.kind-filter { color: var(--link); }
|
|
471
|
+
.palette-kind.kind-recent { color: var(--text-faint); }
|
|
472
|
+
.palette-kind.kind-action { color: var(--accent); }
|
|
473
|
+
.palette-kind.kind-page { color: var(--accent); }
|
|
474
|
+
.palette-kind.kind-sql { color: var(--key); }
|
|
475
|
+
.palette-label {
|
|
476
|
+
font: 13px var(--mono); color: var(--text);
|
|
477
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
478
|
+
}
|
|
479
|
+
.palette-item.active .palette-label { color: var(--accent); }
|
|
480
|
+
.palette-detail { font: 11px var(--mono); color: var(--text-faint); }
|
|
481
|
+
.palette-empty { padding: 16px; margin: 0; font-size: 12.5px; color: var(--text-dim); }
|
|
482
|
+
|
|
483
|
+
/* ---------- the sidebar toggle ---------- */
|
|
484
|
+
|
|
485
|
+
/* Header controls that are not the primary action: an icon's worth of space
|
|
486
|
+
each, no outline until you reach for them. The outlined-rectangle treatment
|
|
487
|
+
is reserved for Run and for the mode, which are the two things in the header
|
|
488
|
+
worth reading every time. */
|
|
489
|
+
.icon-button {
|
|
490
|
+
padding: 5px 9px; border-radius: 6px;
|
|
491
|
+
background: none; border: 1px solid transparent;
|
|
492
|
+
color: var(--text-dim); cursor: pointer;
|
|
493
|
+
font: 11px var(--mono);
|
|
494
|
+
}
|
|
495
|
+
.icon-button:hover { color: var(--accent); border-color: var(--border); background: var(--surface); }
|
|
496
|
+
.icon-button[aria-expanded="true"],
|
|
497
|
+
.icon-button[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
|
|
498
|
+
|
|
499
|
+
.more-list {
|
|
500
|
+
z-index: 70; min-width: 15rem; padding: 5px;
|
|
501
|
+
background: var(--surface);
|
|
502
|
+
border: 1px solid var(--border-strong); border-radius: 8px;
|
|
503
|
+
box-shadow: 0 10px 34px rgb(0 0 0 / 0.18);
|
|
504
|
+
}
|
|
505
|
+
.more-item {
|
|
506
|
+
display: grid; grid-template-columns: 1.1rem 1fr auto;
|
|
507
|
+
align-items: baseline; gap: 8px;
|
|
508
|
+
padding: 7px 9px; border-radius: 5px; cursor: pointer;
|
|
509
|
+
font: 12.5px var(--mono); color: var(--text);
|
|
510
|
+
}
|
|
511
|
+
.more-item:hover, .more-item:focus { background: var(--accent-soft); outline: none; }
|
|
512
|
+
.more-item:focus .more-label { color: var(--accent); }
|
|
513
|
+
.more-tick { color: var(--accent); font-size: 11px; }
|
|
514
|
+
.more-detail { font-size: 10.5px; color: var(--text-faint); }
|
|
515
|
+
|
|
516
|
+
/* The collapse control sits in the sidebar, on the filter field's row. */
|
|
517
|
+
.sidebar-filter-row { display: flex; align-items: center; gap: 6px; margin: 8px; }
|
|
518
|
+
.sidebar-filter-row #table-filter { margin: 0; flex: 1; min-width: 0; }
|
|
519
|
+
/* An icon inside a flex button is a flex item, and shrinks: the gear went to
|
|
520
|
+
two pixels wide while keeping its full height, which reads as a stray
|
|
521
|
+
vertical line rather than a control. The `width` attribute on an `<svg>` is
|
|
522
|
+
only a hint once flexbox is doing the sizing. */
|
|
523
|
+
.conn-manage svg,
|
|
524
|
+
.sidebar-collapse svg,
|
|
525
|
+
.sidebar-reveal svg { flex: none; display: block; }
|
|
526
|
+
|
|
527
|
+
/* `--text-faint` is a mid-grey at half alpha — the token for a rule between
|
|
528
|
+
things, not for a control somebody has to find. These are the two controls
|
|
529
|
+
a first-time reader is least likely to notice and most likely to want, so
|
|
530
|
+
they are drawn at the same strength as the text they sit beside, with a hit
|
|
531
|
+
area big enough to be one. */
|
|
532
|
+
.sidebar-collapse {
|
|
533
|
+
flex: none; display: inline-flex; align-items: center; justify-content: center;
|
|
534
|
+
width: 28px; height: 28px; line-height: 1;
|
|
535
|
+
background: none; border: 1px solid transparent; border-radius: 6px;
|
|
536
|
+
color: var(--text-dim); cursor: pointer;
|
|
537
|
+
}
|
|
538
|
+
.sidebar-collapse:hover { color: var(--text); border-color: var(--border); background: var(--surface-2); }
|
|
539
|
+
|
|
540
|
+
/* And the way back sits where the sidebar was. */
|
|
541
|
+
.sidebar-reveal {
|
|
542
|
+
display: none;
|
|
543
|
+
align-self: flex-start; flex: none;
|
|
544
|
+
/* Room on every side: it sat hard against the window edge with its focus
|
|
545
|
+
ring clipped by it, and hard against the content on the other. */
|
|
546
|
+
margin: 10px 10px 0; line-height: 1;
|
|
547
|
+
/* No `display` here: the rule above says `none`, and the one that reveals
|
|
548
|
+
this button is the one that decides how it is laid out. Setting
|
|
549
|
+
`inline-flex` in the base rule overrode that `none` and left the button
|
|
550
|
+
on screen beside the sidebar it exists to bring back. */
|
|
551
|
+
align-items: center; justify-content: center;
|
|
552
|
+
width: 28px; height: 28px;
|
|
553
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
|
|
554
|
+
color: var(--text-dim); cursor: pointer;
|
|
555
|
+
}
|
|
556
|
+
.sidebar-reveal:hover { color: var(--text); border-color: var(--accent); }
|
|
557
|
+
/* Drawn inside its own box, so the ring cannot be cut off by the edge of the
|
|
558
|
+
window the button is pinned to. */
|
|
559
|
+
.sidebar-reveal:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
560
|
+
|
|
561
|
+
body.sidebar-hidden .sidebar { display: none; }
|
|
562
|
+
body.sidebar-hidden .sidebar-reveal { display: inline-flex; }
|
|
563
|
+
|
|
564
|
+
.diagram svg { display: block; overflow: visible; }
|
|
565
|
+
/* Opened nodes make the picture wider than the panel; it scrolls rather than
|
|
566
|
+
shrinking the type to fit. */
|
|
567
|
+
.node > rect { fill: var(--surface); stroke: var(--border-strong); stroke-width: 1; }
|
|
568
|
+
.node text { font: 12px var(--mono); fill: var(--text); }
|
|
569
|
+
.node-head { cursor: pointer; }
|
|
570
|
+
.node-head-hit { fill: transparent; stroke: none; }
|
|
571
|
+
.node:hover > rect { stroke: var(--accent); }
|
|
572
|
+
.node-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
573
|
+
.node-focus > rect { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 1.5; }
|
|
574
|
+
.node-focus .node-head text { fill: var(--accent); }
|
|
575
|
+
.node-head text { font-weight: 600; }
|
|
576
|
+
|
|
577
|
+
/* The column list. Quieter than the table name it sits under — it is there to
|
|
578
|
+
be scanned for a name you already have in mind, not read top to bottom. */
|
|
579
|
+
.node-rule { stroke: var(--border); stroke-width: 1; }
|
|
580
|
+
.node-field { font-size: 11px; fill: var(--text-dim); }
|
|
581
|
+
.node-field.is-key { fill: var(--text); }
|
|
582
|
+
.node-field.is-ref { fill: var(--accent); }
|
|
583
|
+
.node-type { font-size: 10px; fill: var(--text-faint); }
|
|
584
|
+
.node-more { cursor: pointer; }
|
|
585
|
+
.node-more-hit { fill: transparent; stroke: none; }
|
|
586
|
+
.node-more text { font-size: 10.5px; fill: var(--text-faint); }
|
|
587
|
+
.node-more:hover text { fill: var(--accent); }
|
|
588
|
+
.node-more:hover .node-more-hit { fill: var(--accent-soft); }
|
|
589
|
+
.node-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
590
|
+
.edge { fill: none; stroke: var(--border-strong); stroke-width: 1.25; }
|
|
591
|
+
.edge-head { fill: var(--border-strong); }
|
|
592
|
+
.edge-label { font: 10.5px var(--mono); fill: var(--text-dim); }
|
|
593
|
+
.diagram-legend { margin: 12px 0 0; font-size: 11.5px; color: var(--text-faint); font-family: var(--mono); }
|
|
594
|
+
.diagram-legend span { color: var(--text-dim); }
|
|
595
|
+
|
|
596
|
+
.result-head {
|
|
597
|
+
display: flex; align-items: baseline; gap: 12px;
|
|
598
|
+
padding: 10px 14px 6px;
|
|
599
|
+
color: var(--text-dim); font-size: 12px;
|
|
600
|
+
}
|
|
601
|
+
.result-head h2 { margin: 0; font: 600 15px var(--mono); color: var(--text); letter-spacing: -0.01em; }
|
|
602
|
+
.result-head .stat { font-variant-numeric: tabular-nums; }
|
|
603
|
+
|
|
604
|
+
/* No horizontal padding on the scrollport.
|
|
605
|
+
|
|
606
|
+
`position: sticky; left: 0` pins to the *padding* edge, so a scrollport with
|
|
607
|
+
14px of left padding leaves a 14px strip that pinned cells do not cover and
|
|
608
|
+
scrolled content passes under — the tail of a column showing through beside
|
|
609
|
+
the frozen one. The inset moves into the edge cells, where it is the same
|
|
610
|
+
14px and covers nothing. */
|
|
611
|
+
.table-wrap { overflow-x: auto; padding: 0 0 14px; }
|
|
612
|
+
th:first-child, td:first-child { padding-left: 14px; }
|
|
613
|
+
th:last-child, td:last-child { padding-right: 14px; }
|
|
614
|
+
th:first-child .th-sort { padding-left: 0; }
|
|
615
|
+
|
|
616
|
+
/* Columns frozen against a sideways scroll. The `left` on each is measured
|
|
617
|
+
after layout — see applyPins — because it is the width of everything before
|
|
618
|
+
it, which the browser knows and this file cannot. */
|
|
619
|
+
.pinned {
|
|
620
|
+
position: sticky;
|
|
621
|
+
z-index: 1;
|
|
622
|
+
background: var(--bg);
|
|
623
|
+
}
|
|
624
|
+
thead .pinned { z-index: 3; background: var(--surface); }
|
|
625
|
+
tbody tr:hover .pinned { background: var(--accent-soft); }
|
|
626
|
+
/* One seam, on the rightmost frozen column, rather than a line per column. */
|
|
627
|
+
.pinned-edge { box-shadow: 1px 0 0 var(--border-strong); }
|
|
628
|
+
.table-wrap:not([data-scrolled]) .pinned-edge { box-shadow: none; }
|
|
629
|
+
table { border-collapse: collapse; width: 100%; font: 12.5px var(--mono); }
|
|
630
|
+
th, td {
|
|
631
|
+
text-align: left; padding: 5px 10px;
|
|
632
|
+
border-bottom: 1px solid var(--border);
|
|
633
|
+
white-space: nowrap; max-width: 30rem;
|
|
634
|
+
overflow: hidden; text-overflow: ellipsis;
|
|
635
|
+
}
|
|
636
|
+
th {
|
|
637
|
+
position: sticky; top: 0;
|
|
638
|
+
/* Above the pinned body cells, which carry `z-index: 1` so they float over
|
|
639
|
+
the columns they freeze past. Without this, scrolling down slid the
|
|
640
|
+
frozen `id` column over its own heading — two sticky things, both
|
|
641
|
+
stacked, and the one that should win by being the header did not. */
|
|
642
|
+
z-index: 2;
|
|
643
|
+
background: var(--surface); color: var(--text-dim);
|
|
644
|
+
font-weight: 600; font-size: 11px;
|
|
645
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
646
|
+
border-bottom: 1px solid var(--border-strong);
|
|
647
|
+
padding: 0;
|
|
648
|
+
}
|
|
649
|
+
th.sorted { color: var(--accent); border-bottom-color: var(--accent); }
|
|
650
|
+
|
|
651
|
+
/* The header splits into two controls: the label sorts, the caret filters.
|
|
652
|
+
One click target doing both would make every attempt to filter a sort. */
|
|
653
|
+
.th-sort {
|
|
654
|
+
background: none; border: none; border-radius: 0;
|
|
655
|
+
color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit;
|
|
656
|
+
padding: 5px 4px 5px 10px; cursor: pointer; white-space: nowrap;
|
|
657
|
+
}
|
|
658
|
+
.th-sort:hover { color: var(--accent); }
|
|
659
|
+
.th-arrow { color: var(--accent); }
|
|
660
|
+
/* Not disabled-looking, just not offered: the column reads normally, it simply
|
|
661
|
+
does not respond to a click. Greying the label out would say the column
|
|
662
|
+
itself is unavailable, which is not what is being said. */
|
|
663
|
+
.th-sort.not-sortable { cursor: default; opacity: 1; }
|
|
664
|
+
.th-sort.not-sortable:hover { color: inherit; }
|
|
665
|
+
/* Marked unavailable rather than disabled, so the sentence saying why is
|
|
666
|
+
reachable by keyboard — see the header in table.js. The browser no longer
|
|
667
|
+
greys it for us, so it is greyed here. */
|
|
668
|
+
.th-sort[aria-disabled="true"] { cursor: default; color: var(--text-faint); }
|
|
669
|
+
.th-sort[aria-disabled="true"]:hover { color: var(--text-faint); }
|
|
670
|
+
/* The default order is shown, not announced: an arrow the same shape as a
|
|
671
|
+
chosen sort, drawn faintly enough to read as "this happened" rather than
|
|
672
|
+
"you did this". */
|
|
673
|
+
.sorted-default .th-arrow { color: var(--text-faint); }
|
|
674
|
+
/* An estimate is a button, because there is something to do about it. */
|
|
675
|
+
.stat-estimate {
|
|
676
|
+
background: none; border: 1px solid transparent; border-radius: 5px;
|
|
677
|
+
padding: 1px 6px; cursor: pointer;
|
|
678
|
+
font: inherit; color: var(--key);
|
|
679
|
+
}
|
|
680
|
+
.stat-estimate:hover { border-color: var(--key); background: color-mix(in srgb, var(--key) 10%, transparent); }
|
|
681
|
+
|
|
682
|
+
.stat-order {
|
|
683
|
+
background: none; border: 1px solid transparent; border-radius: 5px;
|
|
684
|
+
padding: 1px 6px; cursor: pointer;
|
|
685
|
+
font: inherit; color: var(--text-faint);
|
|
686
|
+
}
|
|
687
|
+
.stat-order:hover { color: var(--accent); border-color: var(--border); }
|
|
688
|
+
.th-filter {
|
|
689
|
+
background: none; border: none; border-radius: 3px;
|
|
690
|
+
color: var(--text-faint); cursor: pointer;
|
|
691
|
+
padding: 4px 8px 4px 2px; font-size: 11px; line-height: 1;
|
|
692
|
+
}
|
|
693
|
+
.th-filter:hover { color: var(--accent); background: var(--accent-soft); }
|
|
694
|
+
.align-right .th-sort { padding-left: 4px; }
|
|
695
|
+
|
|
696
|
+
.th-menu {
|
|
697
|
+
position: fixed; z-index: 40;
|
|
698
|
+
min-width: 15rem; max-width: 24rem; max-height: 20rem; overflow-y: auto;
|
|
699
|
+
background: var(--surface);
|
|
700
|
+
border: 1px solid var(--border-strong); border-radius: 8px;
|
|
701
|
+
box-shadow: 0 6px 20px rgb(0 0 0 / 0.16);
|
|
702
|
+
padding: 4px;
|
|
703
|
+
}
|
|
704
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
705
|
+
.th-menu {
|
|
706
|
+
transform-origin: top center;
|
|
707
|
+
transition: opacity var(--t-popover) var(--ease-out), transform var(--t-popover) var(--ease-out);
|
|
708
|
+
@starting-style { opacity: 0; transform: scale(0.97) translateY(-4px); }
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
.th-menu-item {
|
|
712
|
+
width: 100%; display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
|
|
713
|
+
background: none; border: none; border-radius: 5px;
|
|
714
|
+
padding: 5px 8px; cursor: pointer; text-align: left;
|
|
715
|
+
text-transform: none; letter-spacing: 0;
|
|
716
|
+
}
|
|
717
|
+
.th-menu-item:hover { background: var(--surface-2); }
|
|
718
|
+
.th-menu-label { font: 12.5px var(--mono); color: var(--text); }
|
|
719
|
+
.th-menu-typed .th-menu-label { color: var(--link); }
|
|
720
|
+
.th-menu-sep { height: 1px; background: var(--border); margin: 4px 2px; }
|
|
721
|
+
.th-menu-cond { font: 10.5px var(--mono); color: var(--text-faint); flex: none; }
|
|
722
|
+
th .pk { color: var(--key); }
|
|
723
|
+
th .fk { color: var(--accent); }
|
|
724
|
+
/* Only where a click goes somewhere. A pointer over a row that does nothing
|
|
725
|
+
advertises an action the row does not have. */
|
|
726
|
+
tbody tr { cursor: default; }
|
|
727
|
+
tbody tr.clickable { cursor: pointer; }
|
|
728
|
+
tbody tr.clickable:hover { background: var(--accent-soft); }
|
|
729
|
+
|
|
730
|
+
/* Alignment is set per column by the renderer and applied to the header too,
|
|
731
|
+
so a label always sits over its own values. */
|
|
732
|
+
.align-left { text-align: left; }
|
|
733
|
+
.align-right { text-align: right; }
|
|
734
|
+
.align-center { text-align: center; }
|
|
735
|
+
|
|
736
|
+
td.mono { font-family: var(--mono); }
|
|
737
|
+
td.num { font-variant-numeric: tabular-nums; }
|
|
738
|
+
td.null { color: var(--text-faint); font-style: italic; }
|
|
739
|
+
td.bool .yes { color: var(--accent); }
|
|
740
|
+
/* `false` is a value, and was drawn in the token for absence — the same
|
|
741
|
+
`--text-faint` as `null`, a mid-grey at half alpha, with only an italic to
|
|
742
|
+
tell them apart. In a database browser those are two very different facts
|
|
743
|
+
about a row, and the negative one was the harder of the two to see at all.
|
|
744
|
+
It reads as present-and-negative now; `null` keeps the faint italic,
|
|
745
|
+
because absence is the thing that should recede. */
|
|
746
|
+
td.bool .no { color: var(--text-dim); }
|
|
747
|
+
td.reflink { color: var(--link); }
|
|
748
|
+
td .ref { color: var(--link); text-decoration: underline; text-decoration-style: dotted; }
|
|
749
|
+
|
|
750
|
+
/* Paging sits on the right, under the end of the table, where the eye
|
|
751
|
+
already is after reading a row across. */
|
|
752
|
+
.pager {
|
|
753
|
+
display: flex; gap: 12px; align-items: center; justify-content: space-between;
|
|
754
|
+
padding: 0 14px 16px; color: var(--text-dim); font-size: 12px;
|
|
755
|
+
flex-wrap: wrap;
|
|
756
|
+
}
|
|
757
|
+
/* How you are reading the table, on the left; where you are in it, on the
|
|
758
|
+
right. The size is not a step through the result, so it does not sit among
|
|
759
|
+
the buttons that are. */
|
|
760
|
+
.pager-size { display: flex; gap: 6px; align-items: center; }
|
|
761
|
+
/* Narrow: the list is four two-or-three-digit numbers, and the shared control
|
|
762
|
+
is sized for connection names. */
|
|
763
|
+
.pager-size .dd { min-width: 0; }
|
|
764
|
+
.pager-size .dd-button { padding: 3px 6px; }
|
|
765
|
+
.pager-size .dd-list { min-width: 0; }
|
|
766
|
+
.pager-steps { display: flex; gap: 8px; align-items: center; }
|
|
767
|
+
.pager .page-label { display: flex; gap: 6px; align-items: center; }
|
|
768
|
+
/* Wide enough for four digits without growing as the number does — a control
|
|
769
|
+
that resizes while you read it is a control that moves under the cursor. */
|
|
770
|
+
.page-input {
|
|
771
|
+
width: 5.5ch; text-align: right;
|
|
772
|
+
font: inherit; font-family: var(--mono);
|
|
773
|
+
color: var(--text); background: var(--bg);
|
|
774
|
+
border: 1px solid var(--border-strong); border-radius: var(--radius);
|
|
775
|
+
padding: 3px 4px;
|
|
776
|
+
}
|
|
777
|
+
/* A step that cannot be taken says so by looking unavailable rather than by
|
|
778
|
+
vanishing: buttons that come and go make the row jump as you page. */
|
|
779
|
+
.pager .ghost[disabled] { opacity: 0.4; cursor: default; }
|
|
780
|
+
|
|
781
|
+
/* ---------- record view ---------- */
|
|
782
|
+
|
|
783
|
+
/* The record is one surface, not a tray of cards.
|
|
784
|
+
|
|
785
|
+
Rounded panels floating in padding read as app chrome; the result grid
|
|
786
|
+
beside them is edge-to-edge with a hairline header, and the two views of
|
|
787
|
+
the same database should not look like they came from different tools. The
|
|
788
|
+
panels now sit flush, divided by a rule, with the same sticky header
|
|
789
|
+
treatment the grid uses. */
|
|
790
|
+
.record {
|
|
791
|
+
display: grid; gap: 0;
|
|
792
|
+
/* The reference panel needs a fixed-ish column, not a proportional one: its
|
|
793
|
+
content is a handful of short rows however wide the window is, so giving
|
|
794
|
+
it a third of a large screen leaves a column of nothing beside a field
|
|
795
|
+
list that has to scroll. */
|
|
796
|
+
grid-template-columns: minmax(0, 1fr) minmax(19rem, 24rem);
|
|
797
|
+
align-items: stretch;
|
|
798
|
+
}
|
|
799
|
+
@media (max-width: 900px) { .record { grid-template-columns: minmax(0, 1fr); } }
|
|
800
|
+
|
|
801
|
+
.panel { background: var(--surface); border-right: 1px solid var(--border); min-width: 0; }
|
|
802
|
+
.panel:last-child { border-right: none; }
|
|
803
|
+
@media (max-width: 900px) {
|
|
804
|
+
.panel { border-right: none; border-bottom: 1px solid var(--border); }
|
|
805
|
+
}
|
|
806
|
+
.panel > h3 {
|
|
807
|
+
margin: 0; padding: 10px 14px;
|
|
808
|
+
font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.09em;
|
|
809
|
+
color: var(--text-dim);
|
|
810
|
+
background: var(--surface);
|
|
811
|
+
border-bottom: 1px solid var(--border);
|
|
812
|
+
position: sticky; top: 0; z-index: 1;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/* ---------- record fields ----------
|
|
816
|
+
|
|
817
|
+
Grouped into sections, because a twenty-seven column table as one flat list
|
|
818
|
+
is technically complete and practically unreadable: identity, links, values
|
|
819
|
+
and bookkeeping all carry equal weight, so nothing carries any. */
|
|
820
|
+
|
|
821
|
+
.field-groups { padding: 12px 0 18px; }
|
|
822
|
+
.field-group-plain { padding-top: 4px; }
|
|
823
|
+
/* Divided by space, not by rules.
|
|
824
|
+
|
|
825
|
+
Every section already announces itself with a heading, so a border on top
|
|
826
|
+
of that is the same boundary drawn twice — and with five sections the
|
|
827
|
+
panel reads as a stack of boxes rather than one record. Whitespace is the
|
|
828
|
+
quieter separator and the heading does the work. */
|
|
829
|
+
.field-group + .field-group { margin-top: 24px; }
|
|
830
|
+
.field-group-head {
|
|
831
|
+
width: 100%; display: flex; align-items: baseline; gap: 7px;
|
|
832
|
+
background: none; border: none; border-radius: 0;
|
|
833
|
+
padding: 2px 14px 8px;
|
|
834
|
+
font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.09em;
|
|
835
|
+
color: var(--text-dim); cursor: pointer; text-align: left;
|
|
836
|
+
}
|
|
837
|
+
.field-group-head:hover { color: var(--text); }
|
|
838
|
+
.field-group-caret { font-size: 9px; color: var(--text-faint); }
|
|
839
|
+
.field-group-count {
|
|
840
|
+
margin-left: auto; font-variant-numeric: tabular-nums;
|
|
841
|
+
color: var(--text-faint); font-size: 10.5px; letter-spacing: 0;
|
|
842
|
+
}
|
|
843
|
+
.field-group.collapsed .fields { display: none; }
|
|
844
|
+
.field-group.collapsed .field-group-caret { transform: rotate(-90deg); }
|
|
845
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
846
|
+
.field-group-caret { transition: transform 140ms ease; display: inline-block; }
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* Fields flow into as many columns as fit.
|
|
850
|
+
|
|
851
|
+
A twenty-seven column record in one narrow column is a scroll with empty
|
|
852
|
+
space beside it. `auto-fill` with a minimum means the layout answers the
|
|
853
|
+
question itself: wide window, two or three columns; narrow window, one.
|
|
854
|
+
No breakpoint to keep in step with the panel's own width. */
|
|
855
|
+
.fields {
|
|
856
|
+
margin: 0; padding: 0 0 2px;
|
|
857
|
+
display: grid;
|
|
858
|
+
grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr));
|
|
859
|
+
/* Columns need a real gutter, not a hairline. At 4px the right-hand value
|
|
860
|
+
of one column sat almost against the left-hand label of the next, and
|
|
861
|
+
the eye could not tell which label a value belonged to. */
|
|
862
|
+
column-gap: 28px;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/* At most two columns, however wide the window.
|
|
866
|
+
|
|
867
|
+
`max(24rem, 45%)` does both jobs in one expression: the percentage means
|
|
868
|
+
only two tracks can ever fit, and the absolute minimum means a narrow panel
|
|
869
|
+
drops to one rather than to two unreadably thin ones. Three columns looked
|
|
870
|
+
efficient on a wide screen and was not — a four-field table spread its
|
|
871
|
+
fields across the full width, so reading one row meant crossing the whole
|
|
872
|
+
monitor to pair a label with its value. */
|
|
873
|
+
.fields { grid-template-columns: repeat(auto-fill, minmax(max(21rem, 45%), 1fr)); }
|
|
874
|
+
.field {
|
|
875
|
+
display: grid; grid-template-columns: minmax(8rem, 38%) 1fr; gap: 16px;
|
|
876
|
+
padding: 6px 14px; align-items: baseline;
|
|
877
|
+
border-left: 2px solid transparent;
|
|
878
|
+
min-width: 0;
|
|
879
|
+
}
|
|
880
|
+
/* Anything that needs room takes the full width rather than being squeezed
|
|
881
|
+
into a column and wrapping to six lines. */
|
|
882
|
+
.field:has(.long-value) { grid-column: 1 / -1; }
|
|
883
|
+
.field:hover { background: var(--surface-2); border-left-color: var(--border-strong); }
|
|
884
|
+
.field-ref:hover { border-left-color: var(--accent); }
|
|
885
|
+
/* Three levels, and they have to be distinguishable at a glance:
|
|
886
|
+
|
|
887
|
+
value 13px, --text what you came to read
|
|
888
|
+
label 12px, --text-dim what it is called
|
|
889
|
+
type 10px, --text-faint what it is
|
|
890
|
+
|
|
891
|
+
Before this the label was 12px and the value 12.5px in the same colour, so
|
|
892
|
+
a field read as one undifferentiated string and the eye had to parse it.
|
|
893
|
+
Half a pixel is not a hierarchy. */
|
|
894
|
+
/* Labels wrap rather than truncate. `work_order_id integer` clipped to
|
|
895
|
+
`work_order_id integer…` hides the one thing the annotation was there to
|
|
896
|
+
say, and a column name is not something the reader can guess the rest of. */
|
|
897
|
+
.field dt {
|
|
898
|
+
color: var(--text-dim); font: 12px/1.5 var(--mono);
|
|
899
|
+
letter-spacing: -0.005em;
|
|
900
|
+
min-width: 0; overflow-wrap: anywhere;
|
|
901
|
+
}
|
|
902
|
+
.field dd {
|
|
903
|
+
margin: 0; font: 13px/1.5 var(--mono); color: var(--text);
|
|
904
|
+
word-break: break-word;
|
|
905
|
+
}
|
|
906
|
+
/* Prose escapes the monospace. Names read better in the interface face, and
|
|
907
|
+
nothing is lost — there is no column of them to line up. */
|
|
908
|
+
.field dd .prose { font-family: var(--sans); font-size: 13.5px; letter-spacing: -0.003em; }
|
|
909
|
+
.long-value.prose { font-family: var(--sans); font-size: 13px; }
|
|
910
|
+
.field dd.null { color: var(--text-faint); font-style: italic; }
|
|
911
|
+
.field .type { color: var(--text-faint); font-size: 10px; margin-left: 7px; }
|
|
912
|
+
.field .pk {
|
|
913
|
+
color: var(--key); font-size: 9.5px; margin-left: 5px;
|
|
914
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
915
|
+
}
|
|
916
|
+
.field .req {
|
|
917
|
+
color: var(--text-faint); font-size: 9.5px; margin-left: 5px;
|
|
918
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/* Values render as what they are, matching the grid. Two views of the same
|
|
922
|
+
value should not disagree about its type. */
|
|
923
|
+
.bool-value.yes { color: var(--accent); }
|
|
924
|
+
.bool-value.no { color: var(--text-dim); }
|
|
925
|
+
.num-value { font-variant-numeric: tabular-nums; }
|
|
926
|
+
.date-value { color: var(--text); }
|
|
927
|
+
.long-value {
|
|
928
|
+
white-space: pre-wrap; line-height: 1.55;
|
|
929
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
|
930
|
+
padding: 8px 10px; max-height: 14rem; overflow-y: auto;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/* A foreign key reads as one control: the value it holds, and where it goes. */
|
|
934
|
+
.ref-jump {
|
|
935
|
+
display: inline-flex; align-items: baseline; gap: 8px;
|
|
936
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 999px;
|
|
937
|
+
padding: 2px 4px 2px 10px; cursor: pointer;
|
|
938
|
+
font: 12.5px var(--mono); color: var(--text);
|
|
939
|
+
}
|
|
940
|
+
.ref-jump:hover { border-color: var(--accent); background: var(--accent-soft); }
|
|
941
|
+
.ref-jump-value { font-variant-numeric: tabular-nums; }
|
|
942
|
+
.ref-jump-table {
|
|
943
|
+
color: var(--text-dim); font-size: 11px;
|
|
944
|
+
background: var(--surface-2); border-radius: 999px; padding: 1px 8px;
|
|
945
|
+
}
|
|
946
|
+
.ref-jump:hover .ref-jump-table { color: var(--accent); }
|
|
947
|
+
|
|
948
|
+
/* The walk, unfolded in place: a caret beside a reference pins the row it
|
|
949
|
+
points at as an indented card, whose own references carry carets in turn. */
|
|
950
|
+
.nest-wrap { display: inline; }
|
|
951
|
+
.nest-caret {
|
|
952
|
+
border: none; background: none; cursor: pointer; padding: 2px 5px;
|
|
953
|
+
color: var(--text-faint); font-size: 11px; border-radius: 4px;
|
|
954
|
+
}
|
|
955
|
+
.nest-caret:hover { color: var(--accent); background: var(--accent-soft); }
|
|
956
|
+
.nest-card {
|
|
957
|
+
margin: 6px 0 2px; padding: 8px 12px;
|
|
958
|
+
border-left: 2px solid var(--border-strong); border-radius: 0 6px 6px 0;
|
|
959
|
+
background: var(--surface-2);
|
|
960
|
+
font-size: 12.5px;
|
|
961
|
+
}
|
|
962
|
+
.nest-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
|
|
963
|
+
.nest-table { font: 600 12px var(--mono); }
|
|
964
|
+
.nest-label { color: var(--text-dim); font-family: var(--mono); font-size: 12px; }
|
|
965
|
+
.nest-open { margin-left: auto; font-size: 11px; padding: 1px 7px; }
|
|
966
|
+
.nest-line { display: grid; grid-template-columns: minmax(8rem, 12rem) 1fr; gap: 10px; padding: 1px 0; }
|
|
967
|
+
.nest-name { color: var(--text-faint); font: 11.5px var(--mono); }
|
|
968
|
+
.nest-value { font-family: var(--mono); font-size: 12px; overflow-wrap: anywhere; }
|
|
969
|
+
.nest-value.null { color: var(--text-faint); font-style: italic; }
|
|
970
|
+
.nest-missing, .nest-more { margin: 4px 0 0; color: var(--text-faint); font-size: 11.5px; }
|
|
971
|
+
|
|
972
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
973
|
+
.ref-jump, .field { transition: border-color 120ms ease, background 120ms ease; }
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/* The Referenced-by panel is gone: the relationship tabs on the record carry
|
|
977
|
+
its counts, its navigation and the rows themselves, so keeping it meant
|
|
978
|
+
saying the same thing twice in two places. Its one unique job — showing a
|
|
979
|
+
relationship that has no rows — moved to a disabled tab. */
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
.loading { padding: 10px 12px; color: var(--text-dim); font-size: 12px; }
|
|
983
|
+
|
|
984
|
+
/* ---------- placeholders ---------- */
|
|
985
|
+
|
|
986
|
+
/* One bar, one sweep.
|
|
987
|
+
|
|
988
|
+
The animation is a single moving highlight rather than a pulse of the whole
|
|
989
|
+
block: a pulse makes a page of placeholders throb in unison, which draws
|
|
990
|
+
more attention than the content it is standing in for. Every bar shares one
|
|
991
|
+
timeline, so the sweep reads as one surface being revealed rather than
|
|
992
|
+
forty independent flickers. */
|
|
993
|
+
.sk-bar {
|
|
994
|
+
display: block;
|
|
995
|
+
border-radius: 3px;
|
|
996
|
+
background: var(--surface-2);
|
|
997
|
+
background-image: linear-gradient(
|
|
998
|
+
90deg,
|
|
999
|
+
transparent 0%,
|
|
1000
|
+
color-mix(in srgb, var(--text-faint) 12%, transparent) 50%,
|
|
1001
|
+
transparent 100%
|
|
1002
|
+
);
|
|
1003
|
+
background-size: 240px 100%;
|
|
1004
|
+
background-repeat: no-repeat;
|
|
1005
|
+
animation: sk-sweep 1.25s linear infinite;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
@keyframes sk-sweep {
|
|
1009
|
+
from { background-position: -240px 0; }
|
|
1010
|
+
to { background-position: calc(100% + 240px) 0; }
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/* Motion is the decoration here, not the information — the shapes already say
|
|
1014
|
+
what is coming, so removing the sweep costs nothing. */
|
|
1015
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1016
|
+
.sk-bar { animation: none; background-image: none; }
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
.sk-grid { padding: 4px 12px 12px; }
|
|
1020
|
+
.sk-row {
|
|
1021
|
+
display: grid;
|
|
1022
|
+
grid-template-columns: repeat(var(--sk-cols), minmax(0, 1fr));
|
|
1023
|
+
gap: 16px; align-items: center;
|
|
1024
|
+
padding: 9px 2px;
|
|
1025
|
+
border-bottom: 1px solid var(--border);
|
|
1026
|
+
}
|
|
1027
|
+
.sk-head { border-bottom-color: var(--border-strong); }
|
|
1028
|
+
/* Placeholder rows sit at the same rhythm as real ones, so the grid does not
|
|
1029
|
+
change height when the data arrives. */
|
|
1030
|
+
.sk-row .sk-bar { justify-self: stretch; }
|
|
1031
|
+
|
|
1032
|
+
.sk-record { padding: 4px 22px 22px; }
|
|
1033
|
+
.sk-section { padding: 14px 0 4px; }
|
|
1034
|
+
.sk-section > .sk-bar { margin-bottom: 12px; }
|
|
1035
|
+
.sk-fields {
|
|
1036
|
+
display: grid;
|
|
1037
|
+
grid-template-columns: repeat(auto-fit, minmax(max(21rem, 45%), 1fr));
|
|
1038
|
+
gap: 2px 40px;
|
|
1039
|
+
}
|
|
1040
|
+
.sk-field {
|
|
1041
|
+
display: grid; grid-template-columns: 11rem minmax(0, 1fr);
|
|
1042
|
+
align-items: center; gap: 12px;
|
|
1043
|
+
padding: 9px 0;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
.sk-related { padding: 6px 0; }
|
|
1047
|
+
.sk-related-item {
|
|
1048
|
+
display: grid; grid-template-columns: minmax(2.4rem, auto) 1fr;
|
|
1049
|
+
align-items: center; gap: 12px;
|
|
1050
|
+
padding: 9px 14px;
|
|
1051
|
+
}
|
|
1052
|
+
.sk-related-names { display: flex; flex-direction: column; gap: 5px; }
|
|
1053
|
+
.note { padding: 10px 12px; color: var(--text-dim); font-size: 12px; margin: 0; }
|
|
1054
|
+
|
|
1055
|
+
/* Deliberately no transition on table rows. A hover you trigger continuously
|
|
1056
|
+
while reading should be instant; a fade makes the pointer feel like it is
|
|
1057
|
+
dragging the highlight behind it. */
|
|
1058
|
+
|
|
1059
|
+
/* ---------- write mode ----------
|
|
1060
|
+
|
|
1061
|
+
The indicator has to be visible without being looked for, so it rings the
|
|
1062
|
+
whole window rather than sitting quietly in a corner. */
|
|
1063
|
+
|
|
1064
|
+
.topbar-actions { display: flex; gap: 6px; flex: none; }
|
|
1065
|
+
|
|
1066
|
+
/* An icon's footprint, a mode's weight.
|
|
1067
|
+
|
|
1068
|
+
Read only is the resting state and gets no chrome at all; write mode gets a
|
|
1069
|
+
ring and a tint in the danger colour. The difference between them has to be
|
|
1070
|
+
readable at a glance from the corner of the eye — this is the control that
|
|
1071
|
+
says whether a keystroke can change the database. */
|
|
1072
|
+
button.mode {
|
|
1073
|
+
padding: 5px 9px; border-radius: 6px; line-height: 1;
|
|
1074
|
+
background: transparent; color: var(--text-dim);
|
|
1075
|
+
border: 1px solid transparent;
|
|
1076
|
+
font-size: 13px; cursor: pointer;
|
|
1077
|
+
}
|
|
1078
|
+
button.mode:hover { border-color: var(--border); background: var(--surface); }
|
|
1079
|
+
button.mode[aria-pressed="true"] {
|
|
1080
|
+
background: color-mix(in srgb, var(--danger) 14%, transparent);
|
|
1081
|
+
border-color: var(--danger);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
body.write-mode { box-shadow: inset 0 0 0 3px var(--danger); }
|
|
1085
|
+
body.write-mode .topbar { border-bottom-color: var(--danger); }
|
|
1086
|
+
|
|
1087
|
+
button.danger { background: var(--danger); color: var(--surface); border-color: var(--danger); }
|
|
1088
|
+
|
|
1089
|
+
/* ---------- dialogs ---------- */
|
|
1090
|
+
|
|
1091
|
+
dialog.confirm {
|
|
1092
|
+
border: 1px solid var(--border-strong); border-radius: 10px;
|
|
1093
|
+
background: var(--surface); color: var(--text);
|
|
1094
|
+
max-width: 34rem; padding: 20px 22px;
|
|
1095
|
+
font: 14px/1.55 var(--sans);
|
|
1096
|
+
}
|
|
1097
|
+
dialog.confirm::backdrop { background: rgb(0 0 0 / 0.45); }
|
|
1098
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1099
|
+
dialog.confirm {
|
|
1100
|
+
/* A modal appears in the middle of the viewport rather than out of a
|
|
1101
|
+
trigger, so it is the one popover that keeps a centre origin. */
|
|
1102
|
+
transition: opacity var(--t-panel) var(--ease-out), transform var(--t-panel) var(--ease-out);
|
|
1103
|
+
@starting-style { opacity: 0; transform: scale(0.97); }
|
|
1104
|
+
}
|
|
1105
|
+
dialog.confirm::backdrop {
|
|
1106
|
+
transition: background var(--t-panel) var(--ease-out);
|
|
1107
|
+
@starting-style { background: rgb(0 0 0 / 0); }
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
dialog.confirm h2 { margin: 0 0 8px; font-size: 16px; letter-spacing: -0.01em; }
|
|
1111
|
+
dialog.confirm p { margin: 0 0 10px; color: var(--text-dim); }
|
|
1112
|
+
dialog.confirm strong { color: var(--text); font-family: var(--mono); font-size: 13px; }
|
|
1113
|
+
.confirm-terms { margin: 0 0 14px; padding-left: 18px; color: var(--text-dim); font-size: 13px; }
|
|
1114
|
+
.confirm-terms li { margin-bottom: 3px; }
|
|
1115
|
+
.confirm-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
|
1116
|
+
.confirm-diff { width: 100%; margin-bottom: 12px; font: 12.5px var(--mono); }
|
|
1117
|
+
.confirm-diff th { text-align: left; color: var(--text-dim); font-weight: 500; width: 5rem; text-transform: none; letter-spacing: 0; font-size: 12px; position: static; background: none; border: none; }
|
|
1118
|
+
.confirm-diff td { border: none; }
|
|
1119
|
+
.confirm-diff td.null { color: var(--text-faint); font-style: italic; }
|
|
1120
|
+
.confirm-params { font: 11.5px var(--mono); color: var(--text-faint); margin: 6px 0 0; }
|
|
1121
|
+
dialog.confirm .ddl { margin: 0; }
|
|
1122
|
+
|
|
1123
|
+
/* ---------- cell editor ---------- */
|
|
1124
|
+
|
|
1125
|
+
.cell-edit {
|
|
1126
|
+
width: 100%; padding: 3px 6px;
|
|
1127
|
+
font: 12.5px var(--mono); color: var(--text);
|
|
1128
|
+
background: var(--bg);
|
|
1129
|
+
border: 1px solid var(--border-strong); border-radius: 4px;
|
|
1130
|
+
}
|
|
1131
|
+
.cell-edit:focus { outline: 2px solid var(--danger); outline-offset: -1px; border-color: transparent; }
|
|
1132
|
+
.cell-edit[data-null="true"] { font-style: italic; }
|
|
1133
|
+
|
|
1134
|
+
/* ---------- toast ---------- */
|
|
1135
|
+
|
|
1136
|
+
.toast {
|
|
1137
|
+
padding: 8px 14px; flex: none;
|
|
1138
|
+
font-size: 13px;
|
|
1139
|
+
border-bottom: 1px solid var(--border);
|
|
1140
|
+
background: var(--surface-2); color: var(--text-dim);
|
|
1141
|
+
}
|
|
1142
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1143
|
+
.toast {
|
|
1144
|
+
/* A transition rather than a keyframe: transitions retarget mid-flight,
|
|
1145
|
+
keyframes restart from zero, and a second toast arriving during the
|
|
1146
|
+
first would otherwise jump. translateY in percent so the distance is
|
|
1147
|
+
the element's own height whatever it contains. */
|
|
1148
|
+
transition: opacity var(--t-panel) var(--ease-out), transform var(--t-panel) var(--ease-out);
|
|
1149
|
+
@starting-style { opacity: 0; transform: translateY(-100%); }
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
.toast-ok { background: color-mix(in srgb, var(--accent) 12%, var(--surface)); color: var(--accent); }
|
|
1153
|
+
.toast-error { background: color-mix(in srgb, var(--danger) 12%, var(--surface)); color: var(--danger); }
|
|
1154
|
+
|
|
1155
|
+
/* ---------- schema diagram ---------- */
|
|
1156
|
+
|
|
1157
|
+
/* ---------- landing ---------- */
|
|
1158
|
+
|
|
1159
|
+
/* Kept for the query view's own saved-filter bar, which is not this card. */
|
|
1160
|
+
.landing-saved { margin-bottom: 26px; }
|
|
1161
|
+
.landing-heading {
|
|
1162
|
+
margin: 0 0 10px;
|
|
1163
|
+
font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.07em;
|
|
1164
|
+
color: var(--text-dim);
|
|
1165
|
+
}
|
|
1166
|
+
.landing-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1167
|
+
/* One line, not two: the name and what makes it worth starting from read
|
|
1168
|
+
as a sentence, and stacking them doubled the height of the one block that
|
|
1169
|
+
pushed this card past its cap. */
|
|
1170
|
+
.landing-chip {
|
|
1171
|
+
display: flex; align-items: baseline; gap: 8px;
|
|
1172
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
1173
|
+
border-radius: 3px; padding: 5px 10px; cursor: pointer; text-align: left;
|
|
1174
|
+
}
|
|
1175
|
+
.landing-chip:hover { border-color: var(--accent); background: var(--accent-soft); }
|
|
1176
|
+
.landing-chip-name { font: 12.5px var(--sans); color: var(--text); }
|
|
1177
|
+
.landing-chip-table { font: 10.5px var(--mono); color: var(--text-faint); }
|
|
1178
|
+
.landing-chip:hover .landing-chip-name { color: var(--accent); }
|
|
1179
|
+
|
|
1180
|
+
/* Where to start: the tables this database is actually built around, with
|
|
1181
|
+
their sizes. Clicking one runs it, which is also the first syntax lesson. */
|
|
1182
|
+
.landing-starts { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
|
|
1183
|
+
.landing-starts-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 10px; }
|
|
1184
|
+
.landing-starts-head .shape-label { margin: 0; }
|
|
1185
|
+
.start-chip .landing-chip-table { display: flex; gap: 6px; align-items: baseline; }
|
|
1186
|
+
.start-rows { color: var(--text-dim); }
|
|
1187
|
+
|
|
1188
|
+
/* ---------- find a value ---------- */
|
|
1189
|
+
|
|
1190
|
+
.find-value .conn-inner { max-width: 44rem; }
|
|
1191
|
+
.find-note { margin: 0 0 12px; font: 11.5px var(--sans); color: var(--text-faint); max-width: 46ch; }
|
|
1192
|
+
.find-input {
|
|
1193
|
+
width: 100%; padding: 8px 12px;
|
|
1194
|
+
font: 13px var(--mono); color: var(--text);
|
|
1195
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
|
|
1196
|
+
}
|
|
1197
|
+
.find-input:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
|
|
1198
|
+
.find-results { margin-top: 14px; max-height: 26rem; overflow-y: auto; }
|
|
1199
|
+
.find-group + .find-group { margin-top: 14px; }
|
|
1200
|
+
.find-group-head {
|
|
1201
|
+
display: flex; align-items: baseline; gap: 8px; margin: 0 0 6px;
|
|
1202
|
+
font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: 0.07em;
|
|
1203
|
+
color: var(--text-dim);
|
|
1204
|
+
}
|
|
1205
|
+
.find-count { color: var(--text-faint); }
|
|
1206
|
+
.find-hit {
|
|
1207
|
+
display: flex; align-items: baseline; gap: 10px; width: 100%;
|
|
1208
|
+
padding: 5px 10px; text-align: left; cursor: pointer;
|
|
1209
|
+
background: none; border: 1px solid transparent; border-radius: var(--radius);
|
|
1210
|
+
}
|
|
1211
|
+
.find-hit:hover:not(:disabled) { background: var(--accent-soft); border-color: var(--border); }
|
|
1212
|
+
.find-hit:disabled { cursor: default; opacity: 0.7; }
|
|
1213
|
+
.find-hit-columns { flex: 0 0 9rem; font: 11px var(--mono); color: var(--text-faint); }
|
|
1214
|
+
.find-hit-value { font: 12.5px var(--mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1215
|
+
/* What the sweep could not do, in the colour of a caveat rather than a result. */
|
|
1216
|
+
.find-warn { color: var(--warn); }
|
|
1217
|
+
|
|
1218
|
+
/* ---------- check the shape ---------- */
|
|
1219
|
+
|
|
1220
|
+
.shape-check .conn-inner { max-width: 46rem; }
|
|
1221
|
+
.shape-check-group + .shape-check-group { margin-top: 16px; }
|
|
1222
|
+
.shape-check-table {
|
|
1223
|
+
display: flex; align-items: baseline; gap: 8px;
|
|
1224
|
+
padding: 3px 6px; margin: 0 0 6px -6px; cursor: pointer;
|
|
1225
|
+
background: none; border: 1px solid transparent; border-radius: var(--radius);
|
|
1226
|
+
font: 600 11px var(--mono); text-transform: uppercase; letter-spacing: 0.06em;
|
|
1227
|
+
color: var(--text);
|
|
1228
|
+
}
|
|
1229
|
+
.shape-check-table:hover { border-color: var(--border); background: var(--surface-2); color: var(--accent); }
|
|
1230
|
+
.shape-check-count { color: var(--text-faint); font-weight: 400; }
|
|
1231
|
+
.shape-check-rule-group {
|
|
1232
|
+
margin: 0 0 10px; padding-left: 10px;
|
|
1233
|
+
border-left: 2px solid var(--border);
|
|
1234
|
+
}
|
|
1235
|
+
.shape-check-rule-group.tone-high { border-left-color: var(--danger); }
|
|
1236
|
+
.shape-check-rule-group.tone-medium { border-left-color: var(--warn); }
|
|
1237
|
+
/* The cost, once per rule; then the columns it applies to, indented under
|
|
1238
|
+
it in the tool's own voice — a column and an arrow, nothing else. */
|
|
1239
|
+
.shape-check-why {
|
|
1240
|
+
display: flex; gap: 8px; align-items: baseline; margin: 0 0 3px;
|
|
1241
|
+
font: 12px var(--sans); color: var(--text-dim);
|
|
1242
|
+
}
|
|
1243
|
+
.shape-check-rule { flex: 0 0 10.5rem; font: 11px var(--mono); color: var(--text-faint); }
|
|
1244
|
+
.shape-check-item {
|
|
1245
|
+
margin: 0 0 2px 10.5rem; padding-left: 8px;
|
|
1246
|
+
font: 12px var(--mono); color: var(--text);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/* ---------- pages, on the home screen ---------- */
|
|
1250
|
+
|
|
1251
|
+
/* Two columns, and one when there is only one card to show.
|
|
1252
|
+
|
|
1253
|
+
`:only-child` is the whole first-run rule: a database nobody has worked in
|
|
1254
|
+
yet has no work to resume, `workPanel` returns nothing rather than an empty
|
|
1255
|
+
card, and the schema takes the full width without anything having to decide
|
|
1256
|
+
that it should. */
|
|
1257
|
+
/* The work column takes the width it needs, up to a share of the page; the
|
|
1258
|
+
schema takes the rest. Equal halves stranded a column on a fresh browser,
|
|
1259
|
+
where "your work" is one short list and the summary beside it is six lines
|
|
1260
|
+
of prose — half a screen of nothing next to something that wanted the
|
|
1261
|
+
room. */
|
|
1262
|
+
/* `stretch`, not `start`: two cards of different heights side by side read
|
|
1263
|
+
as one card and one offcut. Equal height costs the shorter one some space
|
|
1264
|
+
at the bottom, which is what a column of a row looks like. */
|
|
1265
|
+
.landing-top {
|
|
1266
|
+
display: grid; gap: 16px; align-items: stretch;
|
|
1267
|
+
/* The database on the left, where reading starts and where the answer for
|
|
1268
|
+
everyone is; your work beside it, in a bounded column — every line in
|
|
1269
|
+
that card wraps, so `fit-content` resolves to min-content and collapses
|
|
1270
|
+
it to a sliver, while an equal half strands a screen of nothing. */
|
|
1271
|
+
grid-template-columns: minmax(0, 1fr) minmax(17rem, 27rem);
|
|
1272
|
+
margin-bottom: 16px;
|
|
1273
|
+
}
|
|
1274
|
+
.landing-top > :only-child { grid-column: 1 / -1; }
|
|
1275
|
+
.landing-top > .shape { max-width: none; margin: 0; }
|
|
1276
|
+
/* Under a third of the window, whatever is in them.
|
|
1277
|
+
|
|
1278
|
+
Both cards grow with their content — a wide schema has more to say, and
|
|
1279
|
+
"your work" gains a line every time you open a record — so a row that
|
|
1280
|
+
sizes to its content is a row that eventually owns the screen. The cap is
|
|
1281
|
+
on the row, and each card scrolls its own overflow: the diagram below is
|
|
1282
|
+
what the page is for, and it should never be pushed off by a list of
|
|
1283
|
+
things you did yesterday.
|
|
1284
|
+
|
|
1285
|
+
30vh rather than a quarter: a quarter cut the schema summary's own lines
|
|
1286
|
+
on a laptop, and the summary is the part a first-time reader needs whole. */
|
|
1287
|
+
.landing-top > .shape {
|
|
1288
|
+
max-height: 30vh;
|
|
1289
|
+
overflow-y: auto;
|
|
1290
|
+
/* A scroll that reaches the end stops there rather than carrying on down
|
|
1291
|
+
the page, which is what makes a scrolling panel usable inside one. */
|
|
1292
|
+
overscroll-behavior: contain;
|
|
1293
|
+
/* A shadow at whichever edge has more behind it.
|
|
1294
|
+
|
|
1295
|
+
A capped panel clips, and a clipped row reads as broken rather than as
|
|
1296
|
+
"there is more" — the cut lands mid-chip and looks like a rendering
|
|
1297
|
+
fault. The two `local` gradients scroll with the content and cover the
|
|
1298
|
+
shadows when the content is at that end; the two fixed ones show
|
|
1299
|
+
through when it is not. No script, and nothing to show when the card
|
|
1300
|
+
does not scroll at all, which is the case this has to get right. */
|
|
1301
|
+
background:
|
|
1302
|
+
linear-gradient(var(--surface) 30%, rgb(0 0 0 / 0)) top / 100% 14px no-repeat local,
|
|
1303
|
+
linear-gradient(rgb(0 0 0 / 0), var(--surface) 70%) bottom / 100% 14px no-repeat local,
|
|
1304
|
+
radial-gradient(farthest-side at 50% 0, rgb(0 0 0 / 0.09), rgb(0 0 0 / 0)) top / 100% 6px no-repeat,
|
|
1305
|
+
radial-gradient(farthest-side at 50% 100%, rgb(0 0 0 / 0.09), rgb(0 0 0 / 0)) bottom / 100% 6px no-repeat,
|
|
1306
|
+
var(--surface);
|
|
1307
|
+
}
|
|
1308
|
+
/* Each card answers to its own width, not the window's.
|
|
1309
|
+
|
|
1310
|
+
The label gutter is 9.5rem, which is a quarter of a column and a tenth of
|
|
1311
|
+
a page — so the same rule that reads well full-width squeezes the content
|
|
1312
|
+
into a ribbon beside it. A container query lets the card stack its labels
|
|
1313
|
+
when *it* is narrow, whatever the window is doing. */
|
|
1314
|
+
.landing-top > .shape { container-type: inline-size; }
|
|
1315
|
+
/* Scoped through `.landing-top` so these beat the base rules whatever order
|
|
1316
|
+
the file happens to put them in — the plain `.shape-label` is defined a
|
|
1317
|
+
thousand lines further down, and would otherwise win on cascade order. */
|
|
1318
|
+
@container (max-width: 34rem) {
|
|
1319
|
+
.landing-top .shape-line { grid-template-columns: minmax(0, 1fr); gap: 2px; }
|
|
1320
|
+
.landing-top .shape-label { text-align: left; }
|
|
1321
|
+
}
|
|
1322
|
+
/* Two columns of a schema summary and a work list stop being readable well
|
|
1323
|
+
before the window does — the label column alone is 9.5rem. */
|
|
1324
|
+
@media (max-width: 1180px) { .landing-top { grid-template-columns: minmax(0, 1fr); } }
|
|
1325
|
+
|
|
1326
|
+
/* The same card as the schema summary beside it — including no margin of its
|
|
1327
|
+
own. It carried `margin-bottom: 16px` from when it was a full-width band
|
|
1328
|
+
below the schema, and that rule sits after the `.landing-top > .shape`
|
|
1329
|
+
reset at equal specificity, so it won: the row stretched both cards to the
|
|
1330
|
+
same box, then this card gave 16px of its box back and ended short. The
|
|
1331
|
+
gap and the row's own margin do the spacing now. */
|
|
1332
|
+
.landing-pages .landing-heading { margin-bottom: 12px; }
|
|
1333
|
+
|
|
1334
|
+
/* Links on a line, not cards in a grid. Wrapping, so a long list of recent
|
|
1335
|
+
records fills the width it has rather than forcing a scroll. */
|
|
1336
|
+
.landing-links { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: baseline; }
|
|
1337
|
+
.landing-link {
|
|
1338
|
+
display: inline-flex; align-items: baseline; gap: 6px;
|
|
1339
|
+
padding: 1px 4px; margin: 0 -4px; cursor: pointer;
|
|
1340
|
+
background: none; border: none; border-radius: 2px;
|
|
1341
|
+
font: 12.5px var(--mono); color: var(--link);
|
|
1342
|
+
text-align: left;
|
|
1343
|
+
}
|
|
1344
|
+
.landing-link:hover { background: var(--accent-soft); color: var(--accent); }
|
|
1345
|
+
.landing-link:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
|
|
1346
|
+
.landing-link-detail { color: var(--text-faint); font-size: 11px; }
|
|
1347
|
+
.landing-all .landing-page-filter { margin-bottom: 6px; }
|
|
1348
|
+
.landing-group + .landing-group { margin-top: 16px; }
|
|
1349
|
+
.landing-sub {
|
|
1350
|
+
margin: 0 0 8px;
|
|
1351
|
+
font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: 0.07em;
|
|
1352
|
+
color: var(--text-faint);
|
|
1353
|
+
}
|
|
1354
|
+
.landing-sub-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
|
|
1355
|
+
.landing-sub-row .landing-sub { margin-bottom: 8px; }
|
|
1356
|
+
.landing-page-filter {
|
|
1357
|
+
flex: 0 1 14rem; min-width: 8rem;
|
|
1358
|
+
padding: 4px 10px; font: 11.5px var(--mono);
|
|
1359
|
+
background: var(--surface); color: var(--text);
|
|
1360
|
+
border: 1px solid var(--border); border-radius: var(--radius);
|
|
1361
|
+
}
|
|
1362
|
+
.landing-page-filter:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
|
|
1363
|
+
|
|
1364
|
+
/* Scrollable rather than capped: forty pages is where finding one matters
|
|
1365
|
+
more than seeing all of them, and a "+31 more" is a dead end. */
|
|
1366
|
+
/* Rows, not a panel: a grey block inside a card is a second card, and the
|
|
1367
|
+
list is short enough that its own border adds nothing but an edge. No
|
|
1368
|
+
height of its own either — the card is the scroller now, and a scroller
|
|
1369
|
+
inside a scroller is two things to fight with. */
|
|
1370
|
+
.landing-page-list { max-width: 38rem; }
|
|
1371
|
+
.landing-page {
|
|
1372
|
+
display: flex; align-items: baseline; gap: 10px; width: 100%;
|
|
1373
|
+
padding: 6px; margin-left: -6px; text-align: left; cursor: pointer;
|
|
1374
|
+
background: none; border: none; border-bottom: 1px solid var(--border);
|
|
1375
|
+
}
|
|
1376
|
+
.landing-page:last-child { border-bottom: none; }
|
|
1377
|
+
.landing-page:hover { background: var(--accent-soft); }
|
|
1378
|
+
.landing-page:hover .landing-page-name { color: var(--accent); }
|
|
1379
|
+
.landing-page-name { flex: 1 1 auto; font: 12.5px var(--sans); color: var(--text); }
|
|
1380
|
+
.landing-page-base { font: 11px var(--mono); color: var(--text-dim); }
|
|
1381
|
+
.landing-page-source { font: 10px var(--mono); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
1382
|
+
.landing-page-list .note { margin: 0; padding: 8px 12px; }
|
|
1383
|
+
|
|
1384
|
+
/* The star beside a page's name. Quiet until it means something. */
|
|
1385
|
+
.page-pin {
|
|
1386
|
+
padding: 2px 6px; font-size: 14px; line-height: 1;
|
|
1387
|
+
color: var(--text-faint); border-color: transparent; background: none;
|
|
1388
|
+
}
|
|
1389
|
+
.page-pin:hover { color: var(--text); border-color: var(--border); }
|
|
1390
|
+
.page-pin.is-pinned { color: var(--warn); }
|
|
1391
|
+
|
|
1392
|
+
.diagram-title { margin: 0 0 14px; font: 600 16px var(--mono); letter-spacing: -0.01em; }
|
|
1393
|
+
|
|
1394
|
+
.node-view rect { stroke-dasharray: 4 3; }
|
|
1395
|
+
.node.dim { opacity: 0.22; }
|
|
1396
|
+
.node.lit rect { stroke: var(--accent); stroke-width: 2; fill: var(--accent-soft); }
|
|
1397
|
+
.node.lit text { fill: var(--accent); font-weight: 600; }
|
|
1398
|
+
.schema-edges path.dim { opacity: 0.1; }
|
|
1399
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1400
|
+
.node, .schema-edges path { transition: opacity 120ms ease; }
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/* With no referring tables there is no second panel, so the fields should
|
|
1404
|
+
use the width rather than leaving a column of nothing beside them. */
|
|
1405
|
+
.record-solo { grid-template-columns: minmax(0, 1fr); }
|
|
1406
|
+
|
|
1407
|
+
/* ---------- connection picker ---------- */
|
|
1408
|
+
|
|
1409
|
+
/* In the top bar now, so it sizes to its content rather than filling a
|
|
1410
|
+
sidebar column: a dropdown stretched across the header would read as a
|
|
1411
|
+
second query bar. Capped, because a connection named after a URL should
|
|
1412
|
+
not push the query bar off the screen. */
|
|
1413
|
+
.conn-picker { display: flex; gap: 6px; align-items: center; flex: none; }
|
|
1414
|
+
.conn-picker .dd { min-width: 0; max-width: 200px; }
|
|
1415
|
+
.conn-picker[hidden] { display: none; }
|
|
1416
|
+
.conn-label {
|
|
1417
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em;
|
|
1418
|
+
color: var(--text-faint); flex: none;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.field-stack { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
|
|
1422
|
+
.field-stack span { font-size: 12px; color: var(--text-dim); }
|
|
1423
|
+
|
|
1424
|
+
/* ---------- saved filters ---------- */
|
|
1425
|
+
|
|
1426
|
+
.views {
|
|
1427
|
+
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
|
1428
|
+
padding: 8px 14px 0;
|
|
1429
|
+
}
|
|
1430
|
+
/* Saved filters on the left, what the query currently says on the right, with
|
|
1431
|
+
room between them: one is a thing you chose to keep, the other is the state
|
|
1432
|
+
of this moment, and running them together reads as one list of eight things. */
|
|
1433
|
+
.views .clause:first-of-type { margin-left: 10px; }
|
|
1434
|
+
.view-tab-wrap {
|
|
1435
|
+
display: inline-flex; align-items: stretch;
|
|
1436
|
+
border: 1px solid var(--border); border-radius: 999px;
|
|
1437
|
+
overflow: hidden;
|
|
1438
|
+
}
|
|
1439
|
+
.view-tab-wrap.active { border-color: var(--accent); background: var(--accent-soft); }
|
|
1440
|
+
.view-tab {
|
|
1441
|
+
background: none; border: none; border-radius: 0;
|
|
1442
|
+
color: var(--text-dim); padding: 3px 10px;
|
|
1443
|
+
font: 12px var(--sans); cursor: pointer;
|
|
1444
|
+
}
|
|
1445
|
+
.views > .view-tab {
|
|
1446
|
+
border: 1px solid var(--border); border-radius: 999px;
|
|
1447
|
+
}
|
|
1448
|
+
.view-tab[aria-selected="true"] { color: var(--accent); font-weight: 600; }
|
|
1449
|
+
.view-tab:hover { color: var(--text); }
|
|
1450
|
+
.view-remove {
|
|
1451
|
+
background: none; border: none; border-radius: 0;
|
|
1452
|
+
color: var(--text-faint); cursor: pointer;
|
|
1453
|
+
padding: 3px 7px 3px 2px; font-size: 13px; line-height: 1;
|
|
1454
|
+
}
|
|
1455
|
+
.view-remove:hover { color: var(--danger); }
|
|
1456
|
+
.view-save {
|
|
1457
|
+
background: none; border: 1px dashed var(--border-strong); border-radius: 999px;
|
|
1458
|
+
color: var(--text-dim); padding: 3px 10px; font: 12px var(--sans); cursor: pointer;
|
|
1459
|
+
}
|
|
1460
|
+
.view-save:hover { border-color: var(--accent); color: var(--accent); border-style: solid; }
|
|
1461
|
+
|
|
1462
|
+
/* What the query currently says, one chip per clause.
|
|
1463
|
+
|
|
1464
|
+
These had no rules at all: the label and the value ran together as
|
|
1465
|
+
"showingname, country_code", and the remove button fell back to the app's
|
|
1466
|
+
default button — a filled accent square twice the height of the chip it
|
|
1467
|
+
was supposed to sit inside.
|
|
1468
|
+
|
|
1469
|
+
Deliberately quieter than a saved view beside it. A saved filter is
|
|
1470
|
+
something you chose to keep; a clause chip is the state of this moment,
|
|
1471
|
+
and it should read as a description of the result with a way off rather
|
|
1472
|
+
than as a control competing with the tabs. */
|
|
1473
|
+
.clause {
|
|
1474
|
+
display: inline-flex; align-items: baseline; gap: 5px;
|
|
1475
|
+
padding: 2px 4px 2px 9px;
|
|
1476
|
+
border: 1px solid var(--border); border-radius: 999px;
|
|
1477
|
+
background: var(--surface-2);
|
|
1478
|
+
max-width: 22rem;
|
|
1479
|
+
}
|
|
1480
|
+
.clause-label {
|
|
1481
|
+
font: 10px var(--mono); text-transform: uppercase; letter-spacing: 0.05em;
|
|
1482
|
+
color: var(--text-faint); flex: none;
|
|
1483
|
+
}
|
|
1484
|
+
/* The value is the part worth reading, so it gets the room and the ellipsis:
|
|
1485
|
+
a `where` chip on a long `in (...)` list must not push the rest off the
|
|
1486
|
+
row. The full text is on the title. */
|
|
1487
|
+
.clause-text {
|
|
1488
|
+
font: 11.5px var(--mono); color: var(--text-dim);
|
|
1489
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1490
|
+
}
|
|
1491
|
+
.clause-remove {
|
|
1492
|
+
flex: none;
|
|
1493
|
+
background: none; border: none;
|
|
1494
|
+
padding: 0 5px; border-radius: 999px;
|
|
1495
|
+
font-size: 13px; line-height: 1; cursor: pointer;
|
|
1496
|
+
color: var(--text-faint);
|
|
1497
|
+
}
|
|
1498
|
+
.clause-remove:hover { color: var(--danger); }
|
|
1499
|
+
.clause-remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
1500
|
+
|
|
1501
|
+
/* ---------- themed listbox ---------- */
|
|
1502
|
+
|
|
1503
|
+
.dd { position: relative; }
|
|
1504
|
+
|
|
1505
|
+
.dd-button {
|
|
1506
|
+
width: 100%;
|
|
1507
|
+
display: flex; align-items: center; justify-content: space-between; gap: 6px;
|
|
1508
|
+
padding: 5px 8px;
|
|
1509
|
+
font: 12px var(--mono); color: var(--text);
|
|
1510
|
+
background: var(--bg);
|
|
1511
|
+
border: 1px solid var(--border); border-radius: var(--radius);
|
|
1512
|
+
cursor: pointer;
|
|
1513
|
+
}
|
|
1514
|
+
.dd-button:hover { border-color: var(--border-strong); }
|
|
1515
|
+
.dd-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1516
|
+
.dd-caret { color: var(--text-faint); font-size: 10px; flex: none; }
|
|
1517
|
+
|
|
1518
|
+
.dd-list {
|
|
1519
|
+
/* Positioned in the viewport by dropdown.js on open; these are the
|
|
1520
|
+
fallback values for the moment before that runs. */
|
|
1521
|
+
position: fixed; z-index: 40; top: 0; left: 0;
|
|
1522
|
+
min-width: 100%; max-width: 22rem; max-height: 18rem; overflow-y: auto;
|
|
1523
|
+
background: var(--surface);
|
|
1524
|
+
border: 1px solid var(--border-strong); border-radius: 8px;
|
|
1525
|
+
/* The one place a shadow earns its keep: a popover has to read as floating
|
|
1526
|
+
above the page rather than punched into it. */
|
|
1527
|
+
box-shadow: 0 6px 20px rgb(0 0 0 / 0.16);
|
|
1528
|
+
padding: 4px;
|
|
1529
|
+
}
|
|
1530
|
+
.dd-list[hidden] { display: none; }
|
|
1531
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1532
|
+
.dd-list {
|
|
1533
|
+
/* From the trigger, not the centre, so the menu reads as coming out of
|
|
1534
|
+
the thing that opened it. Never scale(0) — nothing in the real world
|
|
1535
|
+
appears from nothing. */
|
|
1536
|
+
transform-origin: top left;
|
|
1537
|
+
transition: opacity var(--t-popover) var(--ease-out), transform var(--t-popover) var(--ease-out);
|
|
1538
|
+
@starting-style { opacity: 0; transform: scale(0.96) translateY(-4px); }
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
.dd-option {
|
|
1543
|
+
display: flex; align-items: flex-start; gap: 6px;
|
|
1544
|
+
padding: 5px 7px; border-radius: 5px;
|
|
1545
|
+
cursor: pointer;
|
|
1546
|
+
}
|
|
1547
|
+
.dd-option.active { background: var(--surface-2); }
|
|
1548
|
+
.dd-option.selected .dd-label { color: var(--accent); font-weight: 600; }
|
|
1549
|
+
.dd-option.disabled { opacity: 0.5; cursor: default; }
|
|
1550
|
+
.dd-check { width: 0.9em; flex: none; color: var(--accent); font-size: 11px; line-height: 1.5; }
|
|
1551
|
+
.dd-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
|
1552
|
+
.dd-label { font: 12.5px var(--mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1553
|
+
.dd-detail { font: 11px var(--mono); color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1554
|
+
.dd-badge {
|
|
1555
|
+
flex: none; align-self: center;
|
|
1556
|
+
font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.05em;
|
|
1557
|
+
color: var(--text-dim); background: var(--surface-2);
|
|
1558
|
+
border-radius: 3px; padding: 1px 5px;
|
|
1559
|
+
}
|
|
1560
|
+
.dd-option.tone-error .dd-label { color: var(--danger); }
|
|
1561
|
+
.dd-option.tone-error .dd-detail::after { content: " · unreachable"; color: var(--danger); }
|
|
1562
|
+
.dd-divider { height: 1px; background: var(--border); margin: 4px 2px; }
|
|
1563
|
+
.dd-action .dd-label { color: var(--text-dim); font-family: var(--sans); font-size: 12px; }
|
|
1564
|
+
.dd-action:hover .dd-label { color: var(--accent); }
|
|
1565
|
+
|
|
1566
|
+
/* ---------- pending edits ---------- */
|
|
1567
|
+
|
|
1568
|
+
.pending {
|
|
1569
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
1570
|
+
padding: 8px 12px;
|
|
1571
|
+
background: color-mix(in srgb, var(--danger) 10%, var(--surface));
|
|
1572
|
+
border-bottom: 1px solid var(--border);
|
|
1573
|
+
}
|
|
1574
|
+
.pending-count { font-size: 12.5px; font-weight: 600; color: var(--danger); }
|
|
1575
|
+
.pending-cols {
|
|
1576
|
+
flex: 1; min-width: 0;
|
|
1577
|
+
font: 11.5px var(--mono); color: var(--text-dim);
|
|
1578
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1579
|
+
}
|
|
1580
|
+
.pending button { padding: 4px 10px; font-size: 12px; }
|
|
1581
|
+
|
|
1582
|
+
.cell-edit.dirty {
|
|
1583
|
+
border-color: var(--danger);
|
|
1584
|
+
background: color-mix(in srgb, var(--danger) 7%, var(--bg));
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
.confirm-wide { max-width: 42rem; }
|
|
1588
|
+
.confirm-diff td { padding: 3px 8px 3px 0; vertical-align: baseline; }
|
|
1589
|
+
.diff-col { font: 12.5px var(--mono); color: var(--text); }
|
|
1590
|
+
.diff-from { color: var(--text-dim); text-decoration: line-through; }
|
|
1591
|
+
.diff-arrow { color: var(--text-faint); padding: 0 4px; }
|
|
1592
|
+
.diff-to { color: var(--text); font-weight: 600; }
|
|
1593
|
+
.confirm-diff .null { font-style: italic; text-decoration: none; color: var(--text-faint); }
|
|
1594
|
+
.diff-type { color: var(--text-faint); font: 10.5px var(--mono); margin-left: 7px; }
|
|
1595
|
+
|
|
1596
|
+
/* The table a row crumb belongs to, shown only when the walk changed table.
|
|
1597
|
+
|
|
1598
|
+
No punctuation between the table and the label. The trail already uses a
|
|
1599
|
+
chevron to separate crumbs, and a second separator inside a crumb competes
|
|
1600
|
+
with it — `work_order › WO-100002 › customer · Blackwood Joinery` reads as
|
|
1601
|
+
four things at one level rather than three, one of which is qualified.
|
|
1602
|
+
Size and colour do the binding instead: the table is smaller, lighter and
|
|
1603
|
+
sits on the baseline as a qualifier, the label carries the weight. */
|
|
1604
|
+
.crumb-table {
|
|
1605
|
+
/* --text-dim, not --text-faint: faint carries 50% alpha, which at this
|
|
1606
|
+
size disappears rather than recedes. The qualifier has to be readable —
|
|
1607
|
+
it is the answer to "what am I looking at" — it just should not compete
|
|
1608
|
+
with the label. Size and weight do that; opacity was doing too much. */
|
|
1609
|
+
color: var(--text-dim);
|
|
1610
|
+
font-size: 11.5px;
|
|
1611
|
+
margin-right: 5px;
|
|
1612
|
+
}
|
|
1613
|
+
.crumbs button:hover .crumb-table { color: var(--text); }
|
|
1614
|
+
|
|
1615
|
+
/* Two states can apply to the same row and they must not look alike: the
|
|
1616
|
+
table you are *on*, and the row Enter would open. Before this they were a
|
|
1617
|
+
soft tint and a slightly different soft tint, so arrowing through the list
|
|
1618
|
+
left you unable to tell which one was armed.
|
|
1619
|
+
|
|
1620
|
+
The keyboard highlight wins outright wherever both apply — it describes
|
|
1621
|
+
what is about to happen, which matters more than what already has. */
|
|
1622
|
+
/* One indicator, used for both "this is the table you are looking at" and
|
|
1623
|
+
"this is where the keyboard is". They are the same row almost all the time,
|
|
1624
|
+
and giving the current table a fainter version of the same treatment made
|
|
1625
|
+
the strong one read as a different kind of thing rather than as the same
|
|
1626
|
+
thing with focus. */
|
|
1627
|
+
#table-list button[aria-current="true"],
|
|
1628
|
+
#table-list button.active {
|
|
1629
|
+
background: var(--accent-soft);
|
|
1630
|
+
color: var(--accent);
|
|
1631
|
+
box-shadow: inset 3px 0 0 var(--accent);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/* The highlight *is* the focus indicator here.
|
|
1635
|
+
|
|
1636
|
+
Now that arrowing moves real focus, the browser's ring was drawn on top of
|
|
1637
|
+
a row that already said it was the current one — two selection treatments
|
|
1638
|
+
on one row, one of them a heavy outlined rectangle. The lightweight
|
|
1639
|
+
indicator was doing the job; the ring only appears on a row that has focus
|
|
1640
|
+
without being the highlighted one, which cannot happen. */
|
|
1641
|
+
#table-list button:focus-visible {
|
|
1642
|
+
outline: none;
|
|
1643
|
+
background: var(--accent-soft);
|
|
1644
|
+
color: var(--accent);
|
|
1645
|
+
box-shadow: inset 3px 0 0 var(--accent);
|
|
1646
|
+
}
|
|
1647
|
+
/* While the cursor is away from the table on screen, the on-screen table's
|
|
1648
|
+
marker steps back to a whisper. One "you are here" at a time: during a
|
|
1649
|
+
quick run down the list the debounced preview lags the cursor, and both
|
|
1650
|
+
rows wearing the full treatment reads as two tables selected. */
|
|
1651
|
+
#table-list.skimming button[aria-current="true"]:not(.active) {
|
|
1652
|
+
background: none;
|
|
1653
|
+
color: inherit;
|
|
1654
|
+
box-shadow: inset 3px 0 0 var(--border-strong);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/* Names what the highlight means, so the affordance does not have to be
|
|
1658
|
+
inferred from a colour. */
|
|
1659
|
+
/* Only on a row you have not opened yet. On the table already on screen,
|
|
1660
|
+
"press Enter" would be an instruction to do what has been done. */
|
|
1661
|
+
#table-list button.active:not([aria-current="true"])::after {
|
|
1662
|
+
content: "↵";
|
|
1663
|
+
margin-left: auto;
|
|
1664
|
+
padding-left: 8px;
|
|
1665
|
+
color: var(--accent);
|
|
1666
|
+
font-size: 11px;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/* The new-row form: the schema as a column of labelled boxes. */
|
|
1670
|
+
.insert-dialog { max-width: 34rem; width: calc(100vw - 48px); }
|
|
1671
|
+
.insert-lede { margin: 4px 0 12px; font-size: 12.5px; color: var(--text-dim); }
|
|
1672
|
+
.insert-fields {
|
|
1673
|
+
display: grid; gap: 8px; max-height: 55vh; overflow-y: auto; padding-right: 4px;
|
|
1674
|
+
}
|
|
1675
|
+
.insert-field { display: grid; grid-template-columns: minmax(9rem, 14rem) 1fr; gap: 10px; align-items: center; }
|
|
1676
|
+
.insert-field label { font: 12.5px var(--mono); color: var(--text); overflow-wrap: anywhere; }
|
|
1677
|
+
.insert-field .type { margin-left: 6px; color: var(--text-faint); font-size: 10.5px; }
|
|
1678
|
+
.insert-input {
|
|
1679
|
+
padding: 5px 8px; font: 13px var(--mono);
|
|
1680
|
+
background: var(--surface); color: var(--text);
|
|
1681
|
+
border: 1px solid var(--border); border-radius: 6px; min-width: 0;
|
|
1682
|
+
}
|
|
1683
|
+
.insert-input:focus { outline: none; border-color: var(--accent); }
|
|
1684
|
+
.insert-problem { margin: 10px 0 0; color: var(--danger); font-size: 12.5px; }
|
|
1685
|
+
@media (max-width: 700px) {
|
|
1686
|
+
.insert-field { grid-template-columns: 1fr; gap: 2px; }
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/* A field that just landed. Green for a couple of seconds, then gone — a
|
|
1690
|
+
confirmation you have to dismiss is a confirmation you learn to ignore. */
|
|
1691
|
+
.cell-edit.saved {
|
|
1692
|
+
border-color: var(--accent);
|
|
1693
|
+
background: var(--accent-soft);
|
|
1694
|
+
}
|
|
1695
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1696
|
+
.cell-edit { transition: border-color 160ms ease, background 160ms ease; }
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/* ---------- query autocomplete ---------- */
|
|
1700
|
+
|
|
1701
|
+
.querybar { position: relative; }
|
|
1702
|
+
.ac {
|
|
1703
|
+
position: absolute; z-index: 30; top: calc(100% + 4px); left: 0;
|
|
1704
|
+
min-width: 22rem; max-width: 34rem; max-height: 19rem; overflow-y: auto;
|
|
1705
|
+
background: var(--surface);
|
|
1706
|
+
border: 1px solid var(--border-strong); border-radius: 8px;
|
|
1707
|
+
box-shadow: 0 6px 20px rgb(0 0 0 / 0.16);
|
|
1708
|
+
padding: 4px;
|
|
1709
|
+
}
|
|
1710
|
+
.ac[hidden] { display: none; }
|
|
1711
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1712
|
+
.ac {
|
|
1713
|
+
transform-origin: top left;
|
|
1714
|
+
transition: opacity var(--t-popover) var(--ease-out), transform var(--t-popover) var(--ease-out);
|
|
1715
|
+
@starting-style { opacity: 0; transform: scale(0.97) translateY(-4px); }
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
.ac-item {
|
|
1719
|
+
display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
|
|
1720
|
+
padding: 4px 8px; border-radius: 5px; cursor: pointer;
|
|
1721
|
+
}
|
|
1722
|
+
.ac-item:hover, .ac-item.active { background: var(--surface-2); }
|
|
1723
|
+
/* A Shift-extended run, where the clause takes a list. Accented rather than
|
|
1724
|
+
grey so the range is distinguishable from the cursor sitting on one item —
|
|
1725
|
+
the two states look identical if both are just "shaded". */
|
|
1726
|
+
.ac-item.picked { background: var(--accent-soft); color: var(--accent); }
|
|
1727
|
+
.ac-item.picked .ac-detail { color: var(--accent); opacity: 0.7; }
|
|
1728
|
+
.ac-item.picked.active { box-shadow: inset 2px 0 0 var(--accent); }
|
|
1729
|
+
.ac-label { font: 12.5px var(--mono); color: var(--text); }
|
|
1730
|
+
.ac-detail { font: 11px var(--mono); color: var(--text-dim); flex: none; }
|
|
1731
|
+
/* Kind is carried by colour on the label, so the list scans by category
|
|
1732
|
+
without needing a column of icons. */
|
|
1733
|
+
.kind-table .ac-label { color: var(--accent); }
|
|
1734
|
+
.kind-op .ac-label { color: var(--key); }
|
|
1735
|
+
.kind-kw .ac-label { color: var(--warn); }
|
|
1736
|
+
.kind-value .ac-label { color: var(--link); }
|
|
1737
|
+
|
|
1738
|
+
/* Virtualised rows: the spacers are layout only and must never be styled as
|
|
1739
|
+
content, or the striping and hover follow them. */
|
|
1740
|
+
.virtual-spacer, .virtual-spacer:hover { background: none; cursor: default; }
|
|
1741
|
+
.virtual-note { color: var(--text-faint); font-size: 11.5px; padding: 8px 14px 0; }
|
|
1742
|
+
|
|
1743
|
+
/* ---------- children on the record page ----------
|
|
1744
|
+
|
|
1745
|
+
The Referenced-by panel is the index — counts, and a way to jump. These are
|
|
1746
|
+
the data. A record that says "5 parts" and makes you click to see them is
|
|
1747
|
+
answering a question nobody asked; reading them is usually why the record
|
|
1748
|
+
was opened. */
|
|
1749
|
+
|
|
1750
|
+
.record-children {
|
|
1751
|
+
grid-column: 1 / -1;
|
|
1752
|
+
border-top: 1px solid var(--border);
|
|
1753
|
+
}
|
|
1754
|
+
/* The relationship index, in its own column.
|
|
1755
|
+
|
|
1756
|
+
It spent a while as a horizontal strip under the record, which put the list
|
|
1757
|
+
of what a row connects to below a screenful of fields — so finding out what
|
|
1758
|
+
a record relates to meant scrolling past the record. A column keeps it in
|
|
1759
|
+
view, and a vertical list has room for a dozen relationships where a tab
|
|
1760
|
+
strip wraps to three rows. */
|
|
1761
|
+
|
|
1762
|
+
/* Stays in view while the record scrolls.
|
|
1763
|
+
|
|
1764
|
+
It is a navigator, not part of the record, and a navigator that scrolls
|
|
1765
|
+
away leaves you paging back up to change what you are looking at. Anchored
|
|
1766
|
+
to the top of the grid rather than stretched, so `position: sticky` has a
|
|
1767
|
+
box shorter than the scroller to stick within. */
|
|
1768
|
+
.related-panel {
|
|
1769
|
+
display: flex; flex-direction: column;
|
|
1770
|
+
position: sticky; top: 0; align-self: start;
|
|
1771
|
+
max-height: 100vh; overflow-y: auto;
|
|
1772
|
+
}
|
|
1773
|
+
.related-list { display: flex; flex-direction: column; padding: 6px 0; }
|
|
1774
|
+
/* The rail's own header, which is the panel heading plus one control.
|
|
1775
|
+
|
|
1776
|
+
`.panel > h3` styles a heading that *is* the header; here the heading is
|
|
1777
|
+
inside one, so the sticky, the ground and the rule move out to the wrapper
|
|
1778
|
+
and the h3 keeps only its type. Without that the header scrolls away and
|
|
1779
|
+
the list runs under the trail above it. */
|
|
1780
|
+
.related-head {
|
|
1781
|
+
display: flex; align-items: center; gap: 8px;
|
|
1782
|
+
padding: 4px 14px 4px;
|
|
1783
|
+
background: var(--surface);
|
|
1784
|
+
border-bottom: 1px solid var(--border);
|
|
1785
|
+
position: sticky; top: 0; z-index: 1;
|
|
1786
|
+
}
|
|
1787
|
+
.related-head > h3 {
|
|
1788
|
+
margin: 0; padding: 6px 0;
|
|
1789
|
+
font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.09em;
|
|
1790
|
+
color: var(--text-dim);
|
|
1791
|
+
}
|
|
1792
|
+
/* Quiet until wanted: the list is what the rail is for, and the control that
|
|
1793
|
+
shortens it should not compete with the first row of it. */
|
|
1794
|
+
.related-empties {
|
|
1795
|
+
margin-left: auto; flex: none;
|
|
1796
|
+
padding: 2px 7px; font: 10px var(--mono);
|
|
1797
|
+
color: var(--text-faint); border-color: transparent; background: none;
|
|
1798
|
+
}
|
|
1799
|
+
.related-empties:hover { color: var(--accent); border-color: var(--border); }
|
|
1800
|
+
.related-list.hide-empty .related-item.empty { display: none; }
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
|
|
1804
|
+
/* Pushed to the far end of the trail, where it is out of the way of reading
|
|
1805
|
+
the trail itself but lands under the cursor after a click on the last
|
|
1806
|
+
crumb. */
|
|
1807
|
+
.crumbs .crumb-back {
|
|
1808
|
+
margin-left: auto;
|
|
1809
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
1810
|
+
padding: 3px 10px; border-radius: 5px;
|
|
1811
|
+
font: 11.5px var(--mono); color: var(--text-dim);
|
|
1812
|
+
background: none; border: 1px solid var(--border); cursor: pointer;
|
|
1813
|
+
}
|
|
1814
|
+
.crumbs .crumb-back:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
|
|
1815
|
+
|
|
1816
|
+
.related-item {
|
|
1817
|
+
display: grid; grid-template-columns: minmax(2.4rem, auto) 1fr;
|
|
1818
|
+
align-items: center; gap: 12px;
|
|
1819
|
+
/* A grid inside a <button> inherits the UA stylesheet's centring, which
|
|
1820
|
+
sizes each row to its own content and then centres it — so a row with a
|
|
1821
|
+
shorter table name sat further right than the one under it. Every row
|
|
1822
|
+
starts at the same edge whether it has rows behind it or not. */
|
|
1823
|
+
width: 100%; justify-content: start;
|
|
1824
|
+
background: none; border: none; border-radius: 0;
|
|
1825
|
+
border-left: 2px solid transparent;
|
|
1826
|
+
padding: 9px 14px; text-align: left; cursor: pointer;
|
|
1827
|
+
}
|
|
1828
|
+
.related-item:hover:not([disabled]) { background: var(--accent-soft); }
|
|
1829
|
+
.related-item[aria-selected="true"] {
|
|
1830
|
+
background: var(--accent-soft);
|
|
1831
|
+
border-left-color: var(--accent);
|
|
1832
|
+
}
|
|
1833
|
+
.related-item[disabled] { cursor: default; opacity: 0.55; }
|
|
1834
|
+
|
|
1835
|
+
.related-count {
|
|
1836
|
+
font: 600 16px var(--mono); color: var(--accent);
|
|
1837
|
+
font-variant-numeric: tabular-nums; text-align: right;
|
|
1838
|
+
letter-spacing: -0.02em;
|
|
1839
|
+
}
|
|
1840
|
+
.related-item.empty .related-count { color: var(--text-faint); font-weight: 400; }
|
|
1841
|
+
.related-names { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
1842
|
+
.related-table {
|
|
1843
|
+
font: 12.5px var(--mono); color: var(--text);
|
|
1844
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1845
|
+
}
|
|
1846
|
+
.related-item.empty .related-table { color: var(--text-dim); }
|
|
1847
|
+
.related-via {
|
|
1848
|
+
font: 10.5px var(--mono); color: var(--text-faint);
|
|
1849
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/* Separated by space and its own heading rather than a rule. On a
|
|
1853
|
+
four-field record a border here was the third horizontal line in a page
|
|
1854
|
+
with almost nothing in it, which reads as fragments rather than a record. */
|
|
1855
|
+
.record-children { grid-column: 1 / -1; margin-top: 10px; }
|
|
1856
|
+
|
|
1857
|
+
/* Room to breathe at the end. Content ending flush against the bottom edge
|
|
1858
|
+
reads as cut off rather than finished, and on a scrolled page there is no
|
|
1859
|
+
signal that you have reached the end of it. */
|
|
1860
|
+
.record { padding-bottom: 64px; }
|
|
1861
|
+
.record .panel { padding-bottom: 12px; }
|
|
1862
|
+
|
|
1863
|
+
/* A record has a measure; a table does not.
|
|
1864
|
+
|
|
1865
|
+
On a wide window with the table list hidden, a four-field record was spread
|
|
1866
|
+
across two thousand pixels — the label at one edge and its value at the
|
|
1867
|
+
other, with the eye having to travel the whole way to pair them. Capped and
|
|
1868
|
+
centred, the pairs stay together. Tables are left alone: columns of data
|
|
1869
|
+
genuinely want every pixel, and a table is scanned down rather than read
|
|
1870
|
+
across. */
|
|
1871
|
+
.record {
|
|
1872
|
+
width: 100%;
|
|
1873
|
+
max-width: 1500px;
|
|
1874
|
+
margin-inline: auto;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
.record-child-head {
|
|
1878
|
+
display: flex; align-items: baseline; gap: 10px;
|
|
1879
|
+
padding: 12px 14px 6px;
|
|
1880
|
+
}
|
|
1881
|
+
.record-child-head h3 {
|
|
1882
|
+
margin: 0;
|
|
1883
|
+
font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.09em;
|
|
1884
|
+
color: var(--text-dim);
|
|
1885
|
+
}
|
|
1886
|
+
.record-child-via { font: 10.5px var(--mono); color: var(--text-faint); }
|
|
1887
|
+
.record-child-head button { margin-left: auto; padding: 3px 10px; font-size: 11.5px; }
|
|
1888
|
+
|
|
1889
|
+
/* Embedded grids sit tighter than the main one: they are a supporting detail
|
|
1890
|
+
on a page, not the thing being read. */
|
|
1891
|
+
/* Embedded tables read at the same size as the ones on their own page.
|
|
1892
|
+
|
|
1893
|
+
They were shrunk on the theory that a table inside a record is secondary.
|
|
1894
|
+
It is not — a work order's parts are the reason the record was opened, and
|
|
1895
|
+
setting them two points smaller than the same rows in a table view says the
|
|
1896
|
+
opposite. The only thing an embedded table gives up is the surrounding
|
|
1897
|
+
chrome. */
|
|
1898
|
+
.grid-compact table { font-size: inherit; }
|
|
1899
|
+
|
|
1900
|
+
.child-pager {
|
|
1901
|
+
display: flex; align-items: center; gap: 8px;
|
|
1902
|
+
padding: 10px 2px 0;
|
|
1903
|
+
}
|
|
1904
|
+
.child-range {
|
|
1905
|
+
margin-right: auto;
|
|
1906
|
+
font-size: 11px; color: var(--text-dim);
|
|
1907
|
+
font-variant-numeric: tabular-nums;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/* ---------- record layout builder ---------- */
|
|
1911
|
+
|
|
1912
|
+
.record-heading { display: flex; align-items: center; gap: 10px; }
|
|
1913
|
+
.record-layout {
|
|
1914
|
+
margin-left: auto; padding: 3px 10px; font-size: 11px;
|
|
1915
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
1916
|
+
}
|
|
1917
|
+
.record-layout.is-custom { border-color: var(--accent); color: var(--accent); }
|
|
1918
|
+
.record-layout-name {
|
|
1919
|
+
padding-left: 7px; border-left: 1px solid var(--border);
|
|
1920
|
+
color: var(--text-dim); max-width: 12rem;
|
|
1921
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/* The layout's own name, edited in place in the builder's header. */
|
|
1925
|
+
.tpl-title { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
|
1926
|
+
.tpl-eyebrow {
|
|
1927
|
+
font: 10px var(--mono); text-transform: uppercase; letter-spacing: 0.07em;
|
|
1928
|
+
color: var(--text-faint);
|
|
1929
|
+
}
|
|
1930
|
+
.tpl-name {
|
|
1931
|
+
flex: 1 1 8rem; min-width: 6rem;
|
|
1932
|
+
padding: 2px 6px; margin: 0 -6px;
|
|
1933
|
+
border: 1px solid transparent; border-radius: 5px;
|
|
1934
|
+
background: none; color: var(--text);
|
|
1935
|
+
font: 600 15px var(--mono); letter-spacing: -0.01em;
|
|
1936
|
+
}
|
|
1937
|
+
.tpl-name:hover { border-color: var(--border); }
|
|
1938
|
+
.tpl-name:focus { outline: none; border-color: var(--accent); background: var(--bg); }
|
|
1939
|
+
.tpl-for { font: 11px var(--mono); color: var(--text-faint); }
|
|
1940
|
+
|
|
1941
|
+
/* A drawer, not an overlay.
|
|
1942
|
+
|
|
1943
|
+
The builder used to cover the screen, which meant configuring a record
|
|
1944
|
+
layout while unable to see the record. Every change had to be committed
|
|
1945
|
+
before it could be judged. As a drawer the record stays beside it and
|
|
1946
|
+
re-renders on every toggle, so the preview is the actual page rather than
|
|
1947
|
+
a representation of it. */
|
|
1948
|
+
.tpl {
|
|
1949
|
+
position: fixed; top: 0; right: 0; bottom: 0; z-index: 60;
|
|
1950
|
+
width: min(420px, 42vw);
|
|
1951
|
+
background: var(--surface);
|
|
1952
|
+
border-left: 1px solid var(--border-strong);
|
|
1953
|
+
box-shadow: -8px 0 24px rgb(0 0 0 / 0.10);
|
|
1954
|
+
display: flex; flex-direction: column;
|
|
1955
|
+
}
|
|
1956
|
+
/* The page gives up the width rather than being covered by it. */
|
|
1957
|
+
body.editing-layout .layout { padding-right: min(420px, 42vw); }
|
|
1958
|
+
|
|
1959
|
+
.tpl-head {
|
|
1960
|
+
padding: 14px 16px 12px;
|
|
1961
|
+
border-bottom: 1px solid var(--border);
|
|
1962
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
1963
|
+
}
|
|
1964
|
+
.tpl-head h2 { margin: 0; font: 600 13.5px var(--sans); letter-spacing: -0.01em; }
|
|
1965
|
+
.tpl-lede { margin: 0; color: var(--text-dim); font-size: 12px; line-height: 1.45; }
|
|
1966
|
+
.tpl-head-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 2px; }
|
|
1967
|
+
.tpl-head-actions button { padding: 5px 10px; font-size: 12px; }
|
|
1968
|
+
|
|
1969
|
+
.tpl-body { flex: 1; overflow-y: auto; padding: 12px 14px 32px; }
|
|
1970
|
+
|
|
1971
|
+
.tpl-section { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; background: var(--bg); }
|
|
1972
|
+
.tpl-section.off { opacity: 0.55; }
|
|
1973
|
+
.tpl-section-head {
|
|
1974
|
+
display: flex; align-items: center; gap: 10px;
|
|
1975
|
+
padding: 10px 12px;
|
|
1976
|
+
border-bottom: 1px solid var(--border);
|
|
1977
|
+
}
|
|
1978
|
+
.tpl-section.off .tpl-section-head { border-bottom: none; }
|
|
1979
|
+
.tpl-toggle { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
|
1980
|
+
.tpl-section-name { font: 600 12.5px var(--sans); color: var(--text); }
|
|
1981
|
+
.tpl-section-kind { font: 10.5px var(--mono); color: var(--text-faint); }
|
|
1982
|
+
.tpl-section-actions { margin-left: auto; display: flex; gap: 2px; }
|
|
1983
|
+
.tpl-move {
|
|
1984
|
+
background: none; border: none; border-radius: 4px;
|
|
1985
|
+
color: var(--text-dim); cursor: pointer; padding: 3px 7px; font-size: 11px;
|
|
1986
|
+
}
|
|
1987
|
+
.tpl-move:hover:not([disabled]) { color: var(--accent); background: var(--accent-soft); }
|
|
1988
|
+
.tpl-move[disabled] { color: var(--text-faint); cursor: default; }
|
|
1989
|
+
|
|
1990
|
+
/* The shared arranger (page.css, `.pb-*`) inside a layout card.
|
|
1991
|
+
|
|
1992
|
+
It is drawn there to hang off a builder row — no top border, its own tint,
|
|
1993
|
+
rounded at the bottom only — and a card already provides all three. So it
|
|
1994
|
+
keeps its contents and gives up its box. */
|
|
1995
|
+
.tpl-section .pb-panel {
|
|
1996
|
+
margin: 0; padding: 8px 10px 10px;
|
|
1997
|
+
background: none; border: none; border-radius: 0;
|
|
1998
|
+
}
|
|
1999
|
+
.tpl-section .pb-arrange { max-height: none; }
|
|
2000
|
+
|
|
2001
|
+
.tpl-rows {
|
|
2002
|
+
display: flex; align-items: center; gap: 8px;
|
|
2003
|
+
padding: 4px 12px 12px; font-size: 12px; color: var(--text-dim);
|
|
2004
|
+
}
|
|
2005
|
+
.tpl-rows input {
|
|
2006
|
+
width: 4.5rem; padding: 4px 8px;
|
|
2007
|
+
font: 12px var(--mono); color: var(--text);
|
|
2008
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/* ---------- agent console ---------- */
|
|
2012
|
+
|
|
2013
|
+
.agent-body { display: grid; grid-template-columns: 15rem 1fr; gap: 26px; padding-top: 18px; }
|
|
2014
|
+
.agent-tools { display: flex; flex-direction: column; gap: 2px; align-self: start; }
|
|
2015
|
+
.agent-tool {
|
|
2016
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
2017
|
+
padding: 6px 10px; border: none; background: none; border-radius: 6px;
|
|
2018
|
+
font: 13px var(--mono); color: var(--text); cursor: pointer; text-align: left;
|
|
2019
|
+
}
|
|
2020
|
+
.agent-tool:hover { background: var(--surface-2); }
|
|
2021
|
+
.agent-tool.active { background: var(--accent-soft); color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }
|
|
2022
|
+
.agent-badge {
|
|
2023
|
+
font: 600 9.5px var(--mono); letter-spacing: 0.05em; text-transform: uppercase;
|
|
2024
|
+
padding: 1px 6px; border-radius: 8px; border: 1px solid var(--border);
|
|
2025
|
+
color: var(--text-dim);
|
|
2026
|
+
}
|
|
2027
|
+
.agent-badge.tone-ok { color: var(--accent); border-color: var(--accent-soft); }
|
|
2028
|
+
.agent-badge.tone-warn { color: var(--warn); }
|
|
2029
|
+
.agent-badge.tone-bad { color: var(--danger); }
|
|
2030
|
+
.agent-desc-row { display: flex; align-items: center; gap: 8px; }
|
|
2031
|
+
.agent-desc-row h3 { margin: 0; font: 600 15px var(--mono); }
|
|
2032
|
+
.agent-desc { margin: 6px 0 14px; max-width: 68ch; font-size: 12.5px; color: var(--text-dim); }
|
|
2033
|
+
.agent-field { display: grid; grid-template-columns: 10rem 1fr; gap: 10px; align-items: start; margin-bottom: 8px; max-width: 44rem; }
|
|
2034
|
+
.agent-field label { font: 12.5px var(--mono); padding-top: 6px; }
|
|
2035
|
+
.agent-field label .req { margin-left: 6px; }
|
|
2036
|
+
.agent-input {
|
|
2037
|
+
padding: 5px 8px; font: 13px var(--mono);
|
|
2038
|
+
background: var(--surface); color: var(--text);
|
|
2039
|
+
border: 1px solid var(--border); border-radius: 6px; min-width: 0; width: 100%;
|
|
2040
|
+
}
|
|
2041
|
+
.agent-input:focus { outline: none; border-color: var(--accent); }
|
|
2042
|
+
.agent-run-row { margin: 10px 0 16px; }
|
|
2043
|
+
.agent-stats { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
|
|
2044
|
+
.agent-answer {
|
|
2045
|
+
margin: 0; padding: 14px 16px; overflow: auto; max-height: 46vh;
|
|
2046
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
2047
|
+
font: 12px/1.55 var(--mono); color: var(--text);
|
|
2048
|
+
}
|
|
2049
|
+
.agent-answer-error { color: var(--danger); border-color: var(--danger); }
|
|
2050
|
+
.agent-error { color: var(--danger); font: 12.5px var(--mono); }
|
|
2051
|
+
.agent-links { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
2052
|
+
.agent-transcript { margin-top: 22px; border-top: 1px solid var(--border); padding-top: 10px; }
|
|
2053
|
+
.agent-transcript h4 { margin: 0 0 6px; font: 600 11px var(--mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
|
|
2054
|
+
.agent-past {
|
|
2055
|
+
display: flex; gap: 12px; align-items: baseline; width: 100%;
|
|
2056
|
+
padding: 4px 8px; border: none; background: none; border-radius: 6px;
|
|
2057
|
+
font: 12px var(--mono); color: var(--text-dim); cursor: pointer; text-align: left;
|
|
2058
|
+
}
|
|
2059
|
+
.agent-past:hover { background: var(--surface-2); }
|
|
2060
|
+
.agent-past-tool { color: var(--text); font-weight: 600; flex: none; }
|
|
2061
|
+
.agent-past-args { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2062
|
+
.agent-past-cost { margin-left: auto; flex: none; color: var(--text-faint); }
|
|
2063
|
+
@media (max-width: 800px) {
|
|
2064
|
+
.agent-body { grid-template-columns: 1fr; }
|
|
2065
|
+
.agent-tools { flex-direction: row; flex-wrap: wrap; }
|
|
2066
|
+
.agent-field { grid-template-columns: 1fr; gap: 2px; }
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
/* ---------- connection manager ---------- */
|
|
2070
|
+
|
|
2071
|
+
/* A place, not a dropdown. Full-screen because managing connections is a task
|
|
2072
|
+
you finish and leave rather than something you do in passing. */
|
|
2073
|
+
.conn-manager {
|
|
2074
|
+
position: fixed; inset: 0; z-index: 80;
|
|
2075
|
+
background: var(--bg);
|
|
2076
|
+
overflow-y: auto;
|
|
2077
|
+
}
|
|
2078
|
+
.conn-inner { max-width: 1040px; margin: 0 auto; padding: 34px 26px 60px; }
|
|
2079
|
+
|
|
2080
|
+
.conn-head {
|
|
2081
|
+
display: flex; align-items: flex-start; gap: 20px;
|
|
2082
|
+
padding-bottom: 18px;
|
|
2083
|
+
border-bottom: 1px solid var(--border);
|
|
2084
|
+
}
|
|
2085
|
+
.diagram-scope { font-size: 11px; padding: 1px 8px; }
|
|
2086
|
+
|
|
2087
|
+
.conn-brand { display: flex; align-items: center; gap: 7px; margin-bottom: 14px; }
|
|
2088
|
+
.conn-brand-name { font: 600 13px var(--mono); letter-spacing: -0.01em; color: var(--text-dim); }
|
|
2089
|
+
.conn-head h2 { margin: 0 0 4px; font: 600 19px var(--mono); letter-spacing: -0.015em; }
|
|
2090
|
+
.conn-lede { margin: 0; font-size: 12.5px; color: var(--text-dim); max-width: 46ch; }
|
|
2091
|
+
.conn-head-actions { margin-left: auto; display: flex; gap: 8px; flex: none; }
|
|
2092
|
+
|
|
2093
|
+
.conn-manage {
|
|
2094
|
+
flex: none; display: inline-flex; align-items: center; justify-content: center;
|
|
2095
|
+
width: 28px; height: 28px; line-height: 1;
|
|
2096
|
+
background: none; border: 1px solid transparent; border-radius: 6px;
|
|
2097
|
+
color: var(--text-dim); cursor: pointer;
|
|
2098
|
+
}
|
|
2099
|
+
.conn-manage:hover { color: var(--text); border-color: var(--border); background: var(--surface-2); }
|
|
2100
|
+
|
|
2101
|
+
.conn-toolbar { display: flex; align-items: center; gap: 12px; padding: 16px 0 8px; }
|
|
2102
|
+
.conn-filter {
|
|
2103
|
+
flex: 1; max-width: 22rem; padding: 6px 10px;
|
|
2104
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
2105
|
+
background: var(--surface); color: var(--text);
|
|
2106
|
+
font: 12.5px var(--mono);
|
|
2107
|
+
}
|
|
2108
|
+
.conn-filter:focus { outline: none; border-color: var(--accent); }
|
|
2109
|
+
.conn-count { font: 11px var(--mono); color: var(--text-faint); }
|
|
2110
|
+
|
|
2111
|
+
/* One connection, one line.
|
|
2112
|
+
|
|
2113
|
+
Cards were legible and did not scale: at 170px each, fifty connections is
|
|
2114
|
+
eight thousand pixels of scrolling to find one name. Nothing was dropped to
|
|
2115
|
+
get to forty — what moved is the actions, which are the same four on every
|
|
2116
|
+
row and so do not need fifty permanent copies. */
|
|
2117
|
+
.conn-list { display: flex; flex-direction: column; }
|
|
2118
|
+
|
|
2119
|
+
.conn-row {
|
|
2120
|
+
display: grid;
|
|
2121
|
+
grid-template-columns: 8px minmax(8rem, auto) minmax(0, 1fr) auto 6rem auto;
|
|
2122
|
+
align-items: center; gap: 14px;
|
|
2123
|
+
padding: 9px 12px;
|
|
2124
|
+
border-bottom: 1px solid var(--border);
|
|
2125
|
+
cursor: pointer;
|
|
2126
|
+
}
|
|
2127
|
+
.conn-row:hover { background: var(--surface); }
|
|
2128
|
+
.conn-row.cursor, .conn-row:focus-visible {
|
|
2129
|
+
outline: none;
|
|
2130
|
+
background: var(--accent-soft);
|
|
2131
|
+
box-shadow: inset 3px 0 0 var(--accent);
|
|
2132
|
+
}
|
|
2133
|
+
.conn-row.active { background: var(--accent-soft); }
|
|
2134
|
+
.conn-row.failed { background: color-mix(in srgb, var(--danger) 5%, transparent); }
|
|
2135
|
+
|
|
2136
|
+
/* State as a dot: three values that need to be scannable down a column of
|
|
2137
|
+
fifty, where a word would be read fifty times. The word is still there at
|
|
2138
|
+
the end of the row for anyone who wants it. */
|
|
2139
|
+
.conn-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-faint); }
|
|
2140
|
+
.conn-dot.ok { background: var(--accent); }
|
|
2141
|
+
.conn-dot.bad { background: var(--danger); }
|
|
2142
|
+
.conn-dot.idle { background: var(--border-strong); }
|
|
2143
|
+
/* Reading a schema is a wait worth showing: a Postgres catalog query across
|
|
2144
|
+
sixty tables takes long enough to notice, and a remote database much
|
|
2145
|
+
longer. */
|
|
2146
|
+
.conn-dot.busy { background: var(--key); animation: conn-pulse 1s ease-in-out infinite; }
|
|
2147
|
+
@keyframes conn-pulse { 50% { opacity: 0.25; } }
|
|
2148
|
+
@media (prefers-reduced-motion: reduce) { .conn-dot.busy { animation: none; } }
|
|
2149
|
+
|
|
2150
|
+
.conn-name { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
|
|
2151
|
+
.conn-name-text {
|
|
2152
|
+
font: 13px var(--mono); color: var(--text);
|
|
2153
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
2154
|
+
}
|
|
2155
|
+
.conn-row.active .conn-name-text { color: var(--accent); font-weight: 600; }
|
|
2156
|
+
|
|
2157
|
+
.conn-detail {
|
|
2158
|
+
font: 11.5px var(--mono); color: var(--text-faint);
|
|
2159
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
2160
|
+
}
|
|
2161
|
+
.conn-meta { display: flex; gap: 5px; flex: none; }
|
|
2162
|
+
.conn-badge {
|
|
2163
|
+
padding: 1px 6px; border-radius: 999px;
|
|
2164
|
+
border: 1px solid var(--border); color: var(--text-faint);
|
|
2165
|
+
font: 9.5px var(--mono); text-transform: uppercase; letter-spacing: 0.05em;
|
|
2166
|
+
white-space: nowrap;
|
|
2167
|
+
}
|
|
2168
|
+
.conn-badge.current { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
|
|
2169
|
+
.conn-badge.source-session { border-color: var(--key); color: var(--key); }
|
|
2170
|
+
/* A boundary, not a status: coloured like the other warnings so it reads as
|
|
2171
|
+
a fact about what this connection will do rather than a label. */
|
|
2172
|
+
.conn-badge.shape-only { border-color: var(--warn, var(--key)); color: var(--warn, var(--key)); }
|
|
2173
|
+
|
|
2174
|
+
.conn-status { font: 11px var(--mono); text-align: right; }
|
|
2175
|
+
.conn-status.ok { color: var(--text-dim); }
|
|
2176
|
+
.conn-status.bad { color: var(--danger); }
|
|
2177
|
+
.conn-status.idle { color: var(--text-faint); }
|
|
2178
|
+
.conn-status.busy { color: var(--key); }
|
|
2179
|
+
|
|
2180
|
+
/* Revealed on approach. Kept in the layout at all times so the row does not
|
|
2181
|
+
reflow when they appear — only their visibility changes. */
|
|
2182
|
+
.conn-actions { display: flex; gap: 4px; flex: none; visibility: hidden; }
|
|
2183
|
+
.conn-row:hover .conn-actions,
|
|
2184
|
+
.conn-row:focus-within .conn-actions,
|
|
2185
|
+
.conn-row.cursor .conn-actions { visibility: visible; }
|
|
2186
|
+
.conn-action {
|
|
2187
|
+
padding: 3px 8px; border-radius: 5px;
|
|
2188
|
+
background: none; border: 1px solid var(--border);
|
|
2189
|
+
color: var(--text-dim); cursor: pointer;
|
|
2190
|
+
font: 11px var(--mono); white-space: nowrap;
|
|
2191
|
+
}
|
|
2192
|
+
.conn-action:hover { color: var(--accent); border-color: var(--accent); background: var(--surface); }
|
|
2193
|
+
/* The action becomes a label while it runs, so it cannot be pressed twice and
|
|
2194
|
+
the row does not change width when it does. */
|
|
2195
|
+
.conn-action.working { border-color: transparent; color: var(--key); cursor: default; }
|
|
2196
|
+
/* Kept visible while working even without a hover, or the row would go quiet
|
|
2197
|
+
the moment the pointer left it. */
|
|
2198
|
+
.conn-row.busy .conn-actions { visibility: visible; }
|
|
2199
|
+
.conn-remove { color: var(--danger); }
|
|
2200
|
+
.conn-remove:hover { color: var(--danger); border-color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); }
|
|
2201
|
+
|
|
2202
|
+
/* The reason a row is red, under it. A one-line list has nowhere else to put
|
|
2203
|
+
an error, and an error hidden in a tooltip is an error nobody reads. */
|
|
2204
|
+
.conn-error, .conn-warning {
|
|
2205
|
+
grid-column: 2 / -1;
|
|
2206
|
+
margin: 2px 0 4px; font: 11.5px var(--mono);
|
|
2207
|
+
}
|
|
2208
|
+
.conn-error { color: var(--danger); }
|
|
2209
|
+
.conn-warning { color: var(--key); }
|
|
2210
|
+
|
|
2211
|
+
.conn-note {
|
|
2212
|
+
margin-top: 26px; padding-top: 18px;
|
|
2213
|
+
border-top: 1px solid var(--border);
|
|
2214
|
+
font-size: 12px; color: var(--text-dim);
|
|
2215
|
+
}
|
|
2216
|
+
.conn-note p { margin: 0 0 8px; }
|
|
2217
|
+
.conn-note ul { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 5px; }
|
|
2218
|
+
.conn-note li { font-family: var(--mono); font-size: 11.5px; color: var(--text-faint); }
|
|
2219
|
+
|
|
2220
|
+
/* ---------- deleting a row ---------- */
|
|
2221
|
+
|
|
2222
|
+
.record-delete { color: var(--danger); font-size: 11px; padding: 3px 10px; }
|
|
2223
|
+
.record-delete:hover { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); }
|
|
2224
|
+
|
|
2225
|
+
/* The impact report. Each line leads with the consequence rather than the
|
|
2226
|
+
count, because "3 rows in invoice_line" is not an answer to "what happens if
|
|
2227
|
+
I delete this" and "3 rows will be deleted too" is. */
|
|
2228
|
+
.delete-impact { margin: 10px 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
|
|
2229
|
+
.delete-impact li {
|
|
2230
|
+
display: flex; align-items: baseline; gap: 10px;
|
|
2231
|
+
padding: 7px 10px; border-radius: 6px;
|
|
2232
|
+
border-left: 3px solid var(--border);
|
|
2233
|
+
background: var(--surface-2);
|
|
2234
|
+
font: 12px/1.5 var(--mono);
|
|
2235
|
+
}
|
|
2236
|
+
.impact-what { flex: 1; }
|
|
2237
|
+
.impact-rule { font-size: 10px; color: var(--text-faint); white-space: nowrap; }
|
|
2238
|
+
/* Three tones for three outcomes, so the shape of the answer reads before the
|
|
2239
|
+
words do. */
|
|
2240
|
+
.impact-bad { border-left-color: var(--danger); }
|
|
2241
|
+
.impact-bad .impact-what { color: var(--danger); }
|
|
2242
|
+
.impact-warn { border-left-color: var(--key); }
|
|
2243
|
+
.impact-stop { border-left-color: var(--text-dim); }
|
|
2244
|
+
.impact-unknown { border-left-color: var(--text-faint); }
|
|
2245
|
+
|
|
2246
|
+
.delete-note, .delete-blocked {
|
|
2247
|
+
margin: 10px 0 0; font-size: 11.5px; color: var(--text-dim); line-height: 1.5;
|
|
2248
|
+
}
|
|
2249
|
+
.delete-blocked { color: var(--key); }
|
|
2250
|
+
.delete-confirm { max-width: 34rem; }
|
|
2251
|
+
|
|
2252
|
+
/* A toast can carry one thing to do about it — see `toast` in core.js. */
|
|
2253
|
+
.toast { display: flex; align-items: center; gap: 12px; }
|
|
2254
|
+
/* Restores what `display: flex` took away.
|
|
2255
|
+
|
|
2256
|
+
`hidden` is how core.js dismisses a toast, and it works by way of the user
|
|
2257
|
+
agent's `[hidden] { display: none }` — which any `display` declaration in a
|
|
2258
|
+
stylesheet outranks. So every toast was hidden in the accessibility tree,
|
|
2259
|
+
still on screen, and stayed there until the next one replaced it. */
|
|
2260
|
+
.toast[hidden] { display: none; }
|
|
2261
|
+
.toast-action {
|
|
2262
|
+
flex: none; padding: 3px 10px; border-radius: 5px;
|
|
2263
|
+
background: none; border: 1px solid currentColor;
|
|
2264
|
+
color: inherit; cursor: pointer; font: 11.5px var(--mono);
|
|
2265
|
+
}
|
|
2266
|
+
.toast-action:hover { background: color-mix(in srgb, currentColor 15%, transparent); }
|
|
2267
|
+
|
|
2268
|
+
/* ---------- a row's children, opened in place ---------- */
|
|
2269
|
+
|
|
2270
|
+
.expander-cell { width: 1.9rem; padding-left: 10px; padding-right: 0; }
|
|
2271
|
+
.expander {
|
|
2272
|
+
padding: 0 4px; line-height: 1;
|
|
2273
|
+
background: none; border: none;
|
|
2274
|
+
color: var(--text-faint); cursor: pointer;
|
|
2275
|
+
font-size: 10px;
|
|
2276
|
+
}
|
|
2277
|
+
.expander:hover { color: var(--accent); }
|
|
2278
|
+
tr.expanded .expander { color: var(--accent); }
|
|
2279
|
+
/* The row stays visibly attached to what it opened. */
|
|
2280
|
+
tr.expanded > td { border-bottom-color: transparent; background: var(--accent-soft); }
|
|
2281
|
+
|
|
2282
|
+
.detail-row > td { padding: 0; background: var(--surface-2); }
|
|
2283
|
+
.detail-cell {
|
|
2284
|
+
padding: 12px 16px 16px 34px !important;
|
|
2285
|
+
border-bottom: 1px solid var(--border-strong);
|
|
2286
|
+
/* Its own scroll, so a wide child table does not widen the parent — every
|
|
2287
|
+
column of which would then be laid out around data it has nothing to do
|
|
2288
|
+
with. */
|
|
2289
|
+
max-width: 100%;
|
|
2290
|
+
}
|
|
2291
|
+
.detail-section + .detail-section { margin-top: 16px; }
|
|
2292
|
+
.detail-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
|
|
2293
|
+
.detail-head h4 {
|
|
2294
|
+
margin: 0; font: 600 11px var(--mono);
|
|
2295
|
+
text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim);
|
|
2296
|
+
}
|
|
2297
|
+
.detail-via { font: 10px var(--mono); color: var(--text-faint); }
|
|
2298
|
+
.detail-count {
|
|
2299
|
+
font: 11px var(--mono); color: var(--accent);
|
|
2300
|
+
font-variant-numeric: tabular-nums;
|
|
2301
|
+
}
|
|
2302
|
+
/* Beside the count, not banished to the far right.
|
|
2303
|
+
|
|
2304
|
+
`margin-left: auto` pushed it to the edge of a full-width head, which was
|
|
2305
|
+
fine when the child table underneath was also full width. Now that a child
|
|
2306
|
+
table is only as wide as it needs, a "See all" a thousand pixels to the
|
|
2307
|
+
right of the last column belongs to nothing the eye can see — it reads as
|
|
2308
|
+
page furniture rather than as this section's control. */
|
|
2309
|
+
.detail-all { margin-left: 4px; font-size: 11px; padding: 2px 8px; }
|
|
2310
|
+
/* The inner grid keeps its own inset rather than inheriting the page's. */
|
|
2311
|
+
.detail-cell .table-wrap { padding: 0; }
|
|
2312
|
+
/* The child's width must not reach the parent.
|
|
2313
|
+
|
|
2314
|
+
A table sized to its contents inside a cell with `colspan` hands its
|
|
2315
|
+
preferred width *up*: a thirteen-column child made the outer table 2834px
|
|
2316
|
+
wide and put a horizontal scrollbar on the whole page. `contain:
|
|
2317
|
+
inline-size` sizes this box from its parent as though it had no contents,
|
|
2318
|
+
so a wide child scrolls inside its own strip and the result above it stays
|
|
2319
|
+
where it was. */
|
|
2320
|
+
.detail-cell .table-wrap { contain: inline-size; }
|
|
2321
|
+
|
|
2322
|
+
/* A child table is as wide as its contents, not as wide as the window.
|
|
2323
|
+
|
|
2324
|
+
`width: 100%` is right for the result you asked for and wrong for a table
|
|
2325
|
+
nested inside it: six narrow columns stretched across seventeen hundred
|
|
2326
|
+
pixels put `ID` in the middle of the row with nothing either side of it,
|
|
2327
|
+
and four such tables under one row each spread their columns to different
|
|
2328
|
+
places. Sized to content they pack left, start at the same edge, and read
|
|
2329
|
+
as a group of small tables rather than as four sparse ones.
|
|
2330
|
+
|
|
2331
|
+
Wider than the container is still possible — a child with twenty columns —
|
|
2332
|
+
and `.table-wrap` already scrolls, which is what the outer table does too. */
|
|
2333
|
+
/* As wide as its contents, but never wider than the row it sits in.
|
|
2334
|
+
|
|
2335
|
+
`width: auto` alone let a thirteen-column child sprawl to nearly three
|
|
2336
|
+
thousand pixels, giving every expanded row its own horizontal scrollbar to
|
|
2337
|
+
fight. The cap brings the wide ones back to the container, where their
|
|
2338
|
+
columns compress exactly as a top-level result's do, and leaves the narrow
|
|
2339
|
+
ones — the common case, and the one that looked scattered — at their own
|
|
2340
|
+
width. */
|
|
2341
|
+
.detail-cell table { width: auto; max-width: 100%; }
|
|
2342
|
+
/* Tighter than the 30rem a top-level result allows. A description column that
|
|
2343
|
+
takes half the width of a nested table is the same spreading problem in one
|
|
2344
|
+
column instead of all of them. */
|
|
2345
|
+
.detail-cell th, .detail-cell td { max-width: 18rem; }
|
|
2346
|
+
|
|
2347
|
+
/* A child table's header is not a peer of the page's.
|
|
2348
|
+
|
|
2349
|
+
Both were `--surface` on `--border-strong`, so a nested header read as
|
|
2350
|
+
another top-level one and the two competed for the same rank — worst where
|
|
2351
|
+
an expanded row sits directly under the real header and the eye has to work
|
|
2352
|
+
out which columns belong to which table. The inner one takes the tinted
|
|
2353
|
+
ground it is sitting on and a hairline in the ordinary border colour: still
|
|
2354
|
+
legibly a header, plainly the quieter of the two. */
|
|
2355
|
+
.detail-cell thead th {
|
|
2356
|
+
/* A step away from the ground it sits on, not the same colour as it.
|
|
2357
|
+
|
|
2358
|
+
Plain `--surface-2` *is* the expanded row's background, so the header
|
|
2359
|
+
dissolved into its own container and the child table lost its top edge.
|
|
2360
|
+
Mixed toward the text colour instead, which darkens it in the light
|
|
2361
|
+
theme and lightens it in the dark one — a raised strip either way, and
|
|
2362
|
+
one step quieter than the page's own header rather than level with it. */
|
|
2363
|
+
background: color-mix(in srgb, var(--text) 7%, var(--surface-2));
|
|
2364
|
+
border-bottom: 1px solid var(--border-strong);
|
|
2365
|
+
color: var(--text-dim);
|
|
2366
|
+
font-size: 10.5px;
|
|
2367
|
+
}
|
|
2368
|
+
.detail-cell thead th.sorted { color: var(--accent); border-bottom-color: var(--accent); }
|
|
2369
|
+
/* Sticky belongs to the page's header, not to a table nested three rows down:
|
|
2370
|
+
an inner header pinning itself to the top of the scroller detaches from the
|
|
2371
|
+
rows it names. */
|
|
2372
|
+
.detail-cell thead th { position: static; }
|
|
2373
|
+
.detail-cell .pinned { background: color-mix(in srgb, var(--text) 7%, var(--surface-2)); }
|
|
2374
|
+
|
|
2375
|
+
/* ---------- what is in this database ---------- */
|
|
2376
|
+
|
|
2377
|
+
/* Barely rounded. A 10px radius on a card full of monospace tables reads as
|
|
2378
|
+
a decade-old dashboard; the tabs went square for the same reason, and a
|
|
2379
|
+
hairline of curvature is enough to say "card" without saying "widget". */
|
|
2380
|
+
/* Room to breathe. Twelve pixels of padding around eleven lines of 10px
|
|
2381
|
+
monospace reads as congested however carefully the lines are spaced. */
|
|
2382
|
+
.shape {
|
|
2383
|
+
max-width: 62rem;
|
|
2384
|
+
margin: 0 0 16px;
|
|
2385
|
+
padding: 16px 20px;
|
|
2386
|
+
background: var(--surface);
|
|
2387
|
+
border: 1px solid var(--border);
|
|
2388
|
+
border-radius: 3px;
|
|
2389
|
+
}
|
|
2390
|
+
.shape-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
|
|
2391
|
+
.shape-body { margin-top: 12px; }
|
|
2392
|
+
|
|
2393
|
+
/* The title is the control. A separate chevron would be one more thing in a
|
|
2394
|
+
row whose whole problem is how much it holds; the row already reads as the
|
|
2395
|
+
card's header, so clicking it folds the card. */
|
|
2396
|
+
.shape-toggle {
|
|
2397
|
+
margin: 0; padding: 0; border: none; background: none; cursor: pointer;
|
|
2398
|
+
font: 600 16px var(--mono); letter-spacing: -0.015em; color: var(--text);
|
|
2399
|
+
display: flex; align-items: baseline; gap: 7px;
|
|
2400
|
+
}
|
|
2401
|
+
.shape-toggle::before {
|
|
2402
|
+
content: "▾"; font-size: 10px; color: var(--text-faint);
|
|
2403
|
+
}
|
|
2404
|
+
/* The chevron still turns when motion is reduced — it is a state indicator,
|
|
2405
|
+
not decoration. Only the turning is animated, and only where animation is
|
|
2406
|
+
welcome. */
|
|
2407
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2408
|
+
.shape-toggle::before { transition: transform var(--t-hover) var(--ease-out); }
|
|
2409
|
+
}
|
|
2410
|
+
.shape-toggle:hover { color: var(--accent); }
|
|
2411
|
+
.shape-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
|
|
2412
|
+
.shape-counts { font: 11px var(--mono); color: var(--text-faint); font-variant-numeric: tabular-nums; }
|
|
2413
|
+
.shape-start { margin-left: auto; font-size: 11.5px; }
|
|
2414
|
+
|
|
2415
|
+
.shape-closed .shape-body { display: none; }
|
|
2416
|
+
.shape-closed .shape-toggle::before { transform: rotate(-90deg); }
|
|
2417
|
+
/* Collapsed, the card is one line, so the padding that framed five lines of
|
|
2418
|
+
prose reads as a box drawn around a sentence. */
|
|
2419
|
+
.shape-closed { padding: 9px 16px; }
|
|
2420
|
+
/* Only worth saying when the lines that said it in full are hidden. */
|
|
2421
|
+
.shape-gist { display: none; font: 11.5px var(--mono); color: var(--text-dim); }
|
|
2422
|
+
.shape-closed .shape-gist { display: inline; }
|
|
2423
|
+
|
|
2424
|
+
.shape-lines { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 9px; }
|
|
2425
|
+
/* A label column, so the four claims read as four answers to four questions
|
|
2426
|
+
rather than as a paragraph that happens to have been split. */
|
|
2427
|
+
.shape-line { display: grid; grid-template-columns: 9.5rem 1fr; gap: 14px; align-items: baseline; }
|
|
2428
|
+
@media (max-width: 640px) { .shape-line { grid-template-columns: 1fr; gap: 2px; } }
|
|
2429
|
+
.shape-label {
|
|
2430
|
+
font: 10px var(--mono); text-transform: uppercase; letter-spacing: 0.06em;
|
|
2431
|
+
color: var(--text-faint); text-align: right;
|
|
2432
|
+
}
|
|
2433
|
+
@media (max-width: 640px) { .shape-label { text-align: left; } }
|
|
2434
|
+
.shape-says { font: 12.5px/1.55 var(--mono); color: var(--text-dim); }
|
|
2435
|
+
|
|
2436
|
+
/* Every table named is a way in. A summary you cannot act on is a paragraph to
|
|
2437
|
+
read and then leave. */
|
|
2438
|
+
.shape-link {
|
|
2439
|
+
padding: 1px 5px; margin: 0 -1px; border-radius: 4px;
|
|
2440
|
+
background: none; border: none;
|
|
2441
|
+
color: var(--accent); cursor: pointer;
|
|
2442
|
+
font: inherit;
|
|
2443
|
+
}
|
|
2444
|
+
.shape-link:hover { background: var(--accent-soft); text-decoration: underline; }
|
|
2445
|
+
.shape-arrow { color: var(--text-faint); }
|
|
2446
|
+
|
|
2447
|
+
/* ---------- the keyboard cursor on a result ---------- */
|
|
2448
|
+
|
|
2449
|
+
/* One tab stop for the whole grid; the cursor moves inside it with the arrow
|
|
2450
|
+
keys. A hundred and sixty tab stops between the query bar and whatever is
|
|
2451
|
+
after it is not navigation. */
|
|
2452
|
+
.result table:focus { outline: none; }
|
|
2453
|
+
.result table:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
2454
|
+
|
|
2455
|
+
/* An inset ring rather than a background fill: the cell already carries
|
|
2456
|
+
meaning through colour — null is dim, numbers align right, references are
|
|
2457
|
+
chips — and painting over it to say "you are here" costs more than it
|
|
2458
|
+
says. */
|
|
2459
|
+
.cell-cursor {
|
|
2460
|
+
outline: 2px solid var(--accent);
|
|
2461
|
+
outline-offset: -2px;
|
|
2462
|
+
border-radius: 3px;
|
|
2463
|
+
}
|
|
2464
|
+
.cell-cursor:focus { outline-color: var(--accent); }
|
|
2465
|
+
|
|
2466
|
+
/* Copy needs an acknowledgement: the clipboard is somewhere else, so without
|
|
2467
|
+
one the key press has no visible effect at all. */
|
|
2468
|
+
.cell-copied { background: var(--accent-soft); }
|
|
2469
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2470
|
+
.cell-copied { transition: background var(--t-hover) var(--ease-out); }
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
|
|
2474
|
+
/* The expand-every-row control, in the header cell above the expanders. It is
|
|
2475
|
+
the same glyph as the row's own, one rank quieter, so it reads as "all of
|
|
2476
|
+
these" rather than as a different kind of control. */
|
|
2477
|
+
.expand-all { color: var(--text-faint); }
|
|
2478
|
+
.expand-all:hover { color: var(--accent); }
|
|
2479
|
+
.expand-all.working { opacity: 0.5; cursor: progress; }
|
|
2480
|
+
|
|
2481
|
+
/* The trail's root. Quiet like the crumbs beside it — it is a step in the
|
|
2482
|
+
trail, not a button competing with one — but always present, which is the
|
|
2483
|
+
whole point of it. */
|
|
2484
|
+
/* Full-strength, not dimmed like the crumbs beside it. It is the one thing in
|
|
2485
|
+
the trail that is a control rather than a step, and the whole reason it was
|
|
2486
|
+
added is that the way home was not noticeable. */
|
|
2487
|
+
.crumb-home {
|
|
2488
|
+
display: inline-flex; align-items: center;
|
|
2489
|
+
/* The row is baseline-aligned, which is right for the text crumbs and
|
|
2490
|
+
meaningless for an icon — an SVG has no baseline to sit on, so it hung
|
|
2491
|
+
above the line it was supposed to be part of. Centred against the line
|
|
2492
|
+
box instead. */
|
|
2493
|
+
align-self: center;
|
|
2494
|
+
color: var(--text);
|
|
2495
|
+
padding: 2px 4px;
|
|
2496
|
+
border-radius: var(--radius);
|
|
2497
|
+
}
|
|
2498
|
+
/* Inline SVG sits on the text baseline by default and leaves descender space
|
|
2499
|
+
under it, which is the other half of the same misalignment. */
|
|
2500
|
+
.crumb-home svg { display: block; }
|
|
2501
|
+
.crumb-home:hover { background: var(--surface-2); }
|
|
2502
|
+
.crumb-home:hover { color: var(--accent); }
|
|
2503
|
+
|
|
2504
|
+
/* Clauses that describe a query which did not run.
|
|
2505
|
+
|
|
2506
|
+
Not an error colour — the banner above already says what is wrong, and
|
|
2507
|
+
painting the chips red would claim each of them is the fault. They are
|
|
2508
|
+
simply not in effect, and look it. The × on each still works: removing the
|
|
2509
|
+
clause that broke the query is the fix. */
|
|
2510
|
+
.views.not-in-effect .clause { opacity: 0.55; }
|
|
2511
|
+
.views .view-save[disabled] { opacity: 0.45; cursor: default; }
|
|
2512
|
+
|
|
2513
|
+
/* ---------- the connection dialog ---------- */
|
|
2514
|
+
|
|
2515
|
+
.conn-dialog { min-width: min(560px, 92vw); }
|
|
2516
|
+
/* Two ways of saying the same thing, so they read as one control with two
|
|
2517
|
+
faces rather than as two separate forms. */
|
|
2518
|
+
.conn-tabs { display: flex; gap: 4px; margin: 4px 0 12px; }
|
|
2519
|
+
.conn-tab {
|
|
2520
|
+
background: none; border: 1px solid transparent; border-radius: var(--radius);
|
|
2521
|
+
padding: 4px 10px; font: inherit; font-size: 12px;
|
|
2522
|
+
color: var(--text-dim); cursor: pointer;
|
|
2523
|
+
}
|
|
2524
|
+
.conn-tab:hover { color: var(--text); }
|
|
2525
|
+
.conn-tab.active { color: var(--text); border-color: var(--border-strong); background: var(--surface-2); }
|
|
2526
|
+
/* Host and port sit together, as they are read; the rest take a line each. */
|
|
2527
|
+
/* Host and port share a row, because they are read as one thing. Everything
|
|
2528
|
+
else takes the width.
|
|
2529
|
+
|
|
2530
|
+
Keyed on which field it is rather than on its position: this was written as
|
|
2531
|
+
`nth-child` while the driver picker was the first child, and moving that
|
|
2532
|
+
picker out of the grid shifted every index — so the rule went on describing
|
|
2533
|
+
a layout that no longer existed and paired the wrong two boxes. A selector
|
|
2534
|
+
that survives reordering is worth the `:has`. */
|
|
2535
|
+
.conn-parts { display: grid; grid-template-columns: 2fr 1fr; gap: 10px 12px; }
|
|
2536
|
+
.conn-parts > .field-stack { grid-column: 1 / -1; }
|
|
2537
|
+
.conn-parts > .field-stack:has(input[aria-label='Host']) { grid-column: 1 / 2; }
|
|
2538
|
+
.conn-parts > .field-stack:has(input[aria-label='Port']) { grid-column: 2 / 3; }
|
|
2539
|
+
/* A password source is a choice, so it sits with the field it decides. */
|
|
2540
|
+
.conn-secret-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
|
|
2541
|
+
.conn-tabs-small { margin: 0; }
|
|
2542
|
+
.conn-tabs-small .conn-tab { padding: 2px 7px; font-size: 11px; }
|
|
2543
|
+
.conn-secret-note { margin: 4px 0 0; font-size: 11px; color: var(--text-faint); }
|
|
2544
|
+
|
|
2545
|
+
.conn-preview-row {
|
|
2546
|
+
display: flex; gap: 8px; align-items: baseline;
|
|
2547
|
+
margin-top: 12px; font-size: 12px; color: var(--text-dim);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
/* `hidden` is a UA rule and any explicit `display` beats it, so the row above
|
|
2551
|
+
has to say so itself. It is set now that the row only appears when it has
|
|
2552
|
+
something to add — a password the box above is holding back. */
|
|
2553
|
+
.conn-preview-row[hidden] { display: none; }
|
|
2554
|
+
/* Wraps rather than scrolls: the point of the preview is being able to read
|
|
2555
|
+
the whole thing before saving it. */
|
|
2556
|
+
.conn-preview {
|
|
2557
|
+
font-family: var(--mono); color: var(--text);
|
|
2558
|
+
word-break: break-all; background: var(--surface-2);
|
|
2559
|
+
border: 1px solid var(--rule, var(--border)); border-radius: 3px; padding: 1px 5px;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
/* ---------- the query modal (⌘/) ---------- */
|
|
2563
|
+
|
|
2564
|
+
/* Same z-index as the palette, for the same reason: it is bound at the
|
|
2565
|
+
document with capture, so it can be opened over anything and must never
|
|
2566
|
+
open underneath it. */
|
|
2567
|
+
.ask-backdrop {
|
|
2568
|
+
position: fixed; inset: 0; z-index: 100;
|
|
2569
|
+
background: rgb(0 0 0 / 0.28);
|
|
2570
|
+
display: flex; justify-content: center;
|
|
2571
|
+
padding-top: 18vh;
|
|
2572
|
+
}
|
|
2573
|
+
/* Wider than the palette. The palette shows a list of short names; this holds
|
|
2574
|
+
a sentence, and the whole reason it exists is that the header's bar is too
|
|
2575
|
+
narrow for one. */
|
|
2576
|
+
.ask {
|
|
2577
|
+
width: min(760px, 94vw); height: fit-content;
|
|
2578
|
+
background: var(--surface);
|
|
2579
|
+
border: 1px solid var(--border-strong);
|
|
2580
|
+
border-radius: 10px;
|
|
2581
|
+
box-shadow: 0 18px 60px rgb(0 0 0 / 0.28);
|
|
2582
|
+
overflow: hidden;
|
|
2583
|
+
}
|
|
2584
|
+
.ask-input {
|
|
2585
|
+
display: block; width: 100%;
|
|
2586
|
+
padding: 14px 16px;
|
|
2587
|
+
border: none; border-bottom: 1px solid var(--border);
|
|
2588
|
+
background: none; color: var(--text);
|
|
2589
|
+
font: 14px/1.5 var(--mono);
|
|
2590
|
+
resize: none;
|
|
2591
|
+
}
|
|
2592
|
+
.ask-input:focus { outline: none; border-bottom-color: var(--accent); }
|
|
2593
|
+
.ask-foot {
|
|
2594
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
2595
|
+
padding: 8px 16px;
|
|
2596
|
+
font-size: 12px; color: var(--text-dim);
|
|
2597
|
+
}
|
|
2598
|
+
.ask-hint code {
|
|
2599
|
+
font-family: var(--mono);
|
|
2600
|
+
color: var(--text);
|
|
2601
|
+
}
|
|
2602
|
+
.ask-keys { color: var(--text-faint); white-space: nowrap; }
|
|
2603
|
+
|
|
2604
|
+
/* "or fill in the parts" — a rule with the words sitting in the gap.
|
|
2605
|
+
|
|
2606
|
+
The two shapes are alternatives, and a plain gap between them reads as one
|
|
2607
|
+
long form where the second half continues the first. The line says choose,
|
|
2608
|
+
without a control to click. */
|
|
2609
|
+
.conn-or {
|
|
2610
|
+
display: flex;
|
|
2611
|
+
align-items: center;
|
|
2612
|
+
gap: 10px;
|
|
2613
|
+
margin: 14px 0 12px;
|
|
2614
|
+
color: var(--text-dim);
|
|
2615
|
+
font-size: 12px;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
.conn-or::before,
|
|
2619
|
+
.conn-or::after {
|
|
2620
|
+
content: "";
|
|
2621
|
+
flex: 1;
|
|
2622
|
+
border-top: 1px solid var(--border);
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
/* ---------- what the Test button answered ----------
|
|
2626
|
+
|
|
2627
|
+
This was a grey `<p class="note">` between two fields, in the same colour
|
|
2628
|
+
as the fine print under it — so the one line on the form that answers a
|
|
2629
|
+
question somebody just asked read as more static help. It is a block with
|
|
2630
|
+
a mark now, coloured by outcome, sitting against the buttons that produced
|
|
2631
|
+
it. */
|
|
2632
|
+
.conn-result {
|
|
2633
|
+
display: flex;
|
|
2634
|
+
gap: 9px;
|
|
2635
|
+
align-items: flex-start;
|
|
2636
|
+
margin: 14px 0 0;
|
|
2637
|
+
padding: 9px 11px;
|
|
2638
|
+
border: 1px solid var(--border);
|
|
2639
|
+
border-left-width: 3px;
|
|
2640
|
+
border-radius: var(--radius);
|
|
2641
|
+
background: var(--surface-2);
|
|
2642
|
+
font-size: 12.5px;
|
|
2643
|
+
line-height: 1.5;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
.conn-result[hidden] { display: none; }
|
|
2647
|
+
.conn-result.ok { border-left-color: var(--accent); }
|
|
2648
|
+
.conn-result.bad { border-left-color: var(--danger); }
|
|
2649
|
+
|
|
2650
|
+
.conn-result-mark { font-weight: 700; line-height: 1.4; }
|
|
2651
|
+
.conn-result.ok .conn-result-mark { color: var(--accent); }
|
|
2652
|
+
.conn-result.bad .conn-result-mark { color: var(--danger); }
|
|
2653
|
+
|
|
2654
|
+
.conn-result-text { margin: 0; color: var(--text); }
|
|
2655
|
+
|
|
2656
|
+
/* The warning the server works out and the form used to drop on the floor.
|
|
2657
|
+
"Password from inline" is a fact; this is why anyone should care. */
|
|
2658
|
+
.conn-result-detail {
|
|
2659
|
+
margin: 3px 0 0;
|
|
2660
|
+
color: var(--warn);
|
|
2661
|
+
font-size: 12px;
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
/* A line under a field saying what it is for, rather than a placeholder that
|
|
2665
|
+
vanishes the moment somebody types into it. */
|
|
2666
|
+
.field-hint {
|
|
2667
|
+
display: block;
|
|
2668
|
+
margin-top: 4px;
|
|
2669
|
+
font-size: 11.5px;
|
|
2670
|
+
line-height: 1.45;
|
|
2671
|
+
color: var(--text-dim);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
/* A paste with no `://` in it. The name tablewalk prints for a connection is
|
|
2675
|
+
its public URL form, so pasting one back is an ordinary thing to do — and
|
|
2676
|
+
it lands in the file-path branch. Which driver it wants is the one thing
|
|
2677
|
+
the string does not say, so it is asked rather than guessed. */
|
|
2678
|
+
.conn-scheme-hint {
|
|
2679
|
+
display: flex;
|
|
2680
|
+
flex-wrap: wrap;
|
|
2681
|
+
align-items: center;
|
|
2682
|
+
gap: 6px;
|
|
2683
|
+
margin: 5px 0 0;
|
|
2684
|
+
font-size: 11.5px;
|
|
2685
|
+
color: var(--warn);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
.conn-scheme-hint[hidden] { display: none; }
|
|
2689
|
+
|
|
2690
|
+
.conn-scheme-fix {
|
|
2691
|
+
font: 11.5px var(--mono);
|
|
2692
|
+
padding: 1px 7px;
|
|
2693
|
+
border: 1px solid var(--border-strong);
|
|
2694
|
+
border-radius: 4px;
|
|
2695
|
+
background: var(--surface);
|
|
2696
|
+
color: var(--text);
|
|
2697
|
+
cursor: pointer;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
.conn-scheme-fix:hover { border-color: var(--accent); color: var(--accent); }
|
|
2701
|
+
|
|
2702
|
+
/* The note under the form about what happens to this connection, and to the
|
|
2703
|
+
password in it. It was `--text-faint`, a mid-grey at half alpha meant for a
|
|
2704
|
+
rule between things — too quiet for a line that carries a guarantee about
|
|
2705
|
+
where a secret ends up. */
|
|
2706
|
+
.conn-fineprint {
|
|
2707
|
+
margin: 10px 0 0;
|
|
2708
|
+
font-size: 11.5px;
|
|
2709
|
+
line-height: 1.5;
|
|
2710
|
+
color: var(--text-dim);
|
|
2711
|
+
}
|