sqlite-hub 0.7.0 → 0.8.7

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.
Files changed (31) hide show
  1. package/changelog.md +14 -0
  2. package/docs/DESIGN_GUIDELINES.md +36 -0
  3. package/frontend/index.html +79 -0
  4. package/frontend/js/app.js +89 -8
  5. package/frontend/js/components/connectionCard.js +3 -4
  6. package/frontend/js/components/emptyState.js +4 -4
  7. package/frontend/js/components/modal.js +32 -28
  8. package/frontend/js/components/queryEditor.js +2 -4
  9. package/frontend/js/components/queryHistoryDetail.js +16 -7
  10. package/frontend/js/components/queryHistoryPanel.js +2 -2
  11. package/frontend/js/components/rowEditorPanel.js +59 -54
  12. package/frontend/js/components/sidebar.js +32 -33
  13. package/frontend/js/components/structureGraph.js +54 -122
  14. package/frontend/js/components/tableDesignerEditor.js +9 -8
  15. package/frontend/js/components/tableDesignerSidebar.js +11 -10
  16. package/frontend/js/components/tableDesignerSqlPreview.js +60 -28
  17. package/frontend/js/store.js +23 -0
  18. package/frontend/js/views/charts.js +66 -38
  19. package/frontend/js/views/connections.js +5 -17
  20. package/frontend/js/views/data.js +12 -13
  21. package/frontend/js/views/overview.js +149 -10
  22. package/frontend/js/views/settings.js +2 -2
  23. package/frontend/js/views/structure.js +74 -70
  24. package/frontend/js/views/tableDesigner.js +7 -2
  25. package/frontend/styles/base.css +62 -0
  26. package/frontend/styles/components.css +63 -105
  27. package/frontend/styles/structure-graph.css +19 -82
  28. package/frontend/styles/tokens.css +2 -0
  29. package/frontend/styles/views.css +33 -36
  30. package/package.json +1 -1
  31. package/server/services/sqlite/overviewService.js +68 -0
@@ -58,7 +58,7 @@ function renderTopTables(overview) {
58
58
  );
59
59
 
60
60
  return `
61
- <section class="shell-section xl:col-span-2">
61
+ <section class="shell-section">
62
62
  <div class="flex items-center justify-between bg-surface-container-highest px-4 py-2">
63
63
  <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Top Tables</span>
64
64
  <span class="material-symbols-outlined text-xs text-on-surface-variant">table_rows</span>
@@ -131,7 +131,7 @@ function renderOperationalSurface(overview) {
131
131
  ? `
132
132
  <div class="border-t border-outline-variant/10 px-4 py-3">
133
133
  <button
134
- class="toolbar-button border border-outline-variant/20 bg-surface-container px-3 py-1.5 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
134
+ class="standard-button"
135
135
  data-action="open-overview-in-finder"
136
136
  type="button"
137
137
  >
@@ -145,17 +145,154 @@ function renderOperationalSurface(overview) {
145
145
  `;
146
146
  }
147
147
 
148
+ function pluralize(count, singular, plural = `${singular}s`) {
149
+ return Number(count) === 1 ? singular : plural;
150
+ }
151
+
152
+ function getSchemaMapNarrative(preview) {
153
+ const relationshipCount = Number(preview.relationshipCount ?? 0);
154
+ const fkClusters = Number(preview.fkClusters ?? 0);
155
+ const isolatedTables = Number(preview.isolatedTables ?? 0);
156
+ const connectedTables = Math.max(0, Number(preview.tableCount ?? 0) - isolatedTables);
157
+
158
+ if (relationshipCount === 0 || fkClusters === 0) {
159
+ return {
160
+ title: "Mostly standalone schema",
161
+ body:
162
+ "There are no meaningful foreign-key groups yet. The Structure view will read more like a list of isolated tables than a connected graph.",
163
+ };
164
+ }
165
+
166
+ if (isolatedTables === 0) {
167
+ return {
168
+ title: "Connected schema",
169
+ body: `Foreign keys tie the whole schema together in ${formatNumber(
170
+ fkClusters
171
+ )} ${pluralize(fkClusters, "cluster")}. Structure is useful here because dependencies and joins are visible at a glance.`,
172
+ };
173
+ }
174
+
175
+ return {
176
+ title: "One connected core, several islands",
177
+ body: `${formatNumber(connectedTables)} ${pluralize(
178
+ connectedTables,
179
+ "table"
180
+ )} are connected through foreign keys, while ${formatNumber(
181
+ isolatedTables
182
+ )} ${pluralize(
183
+ isolatedTables,
184
+ "table"
185
+ )} stand alone. Structure helps most when you want to inspect the connected core and ignore the isolated tables.`,
186
+ };
187
+ }
188
+
189
+ function renderSchemaMapPreview(overview) {
190
+ const preview = overview.schemaMap ?? {
191
+ tableCount: overview.counts?.tables ?? 0,
192
+ indexCount: overview.counts?.indexes ?? 0,
193
+ relationshipCount: 0,
194
+ fkClusters: 0,
195
+ isolatedTables: 0,
196
+ };
197
+ const narrative = getSchemaMapNarrative(preview);
198
+ const stats = [
199
+ { label: "FK Links", value: formatNumber(preview.relationshipCount) },
200
+ { label: "FK Clusters", value: formatNumber(preview.fkClusters) },
201
+ { label: "Isolated Tables", value: formatNumber(preview.isolatedTables) },
202
+ ];
203
+
204
+ return `
205
+ <section class="shell-section overflow-hidden bg-[radial-gradient(circle_at_top_left,rgba(45,250,255,0.12),transparent_34%),radial-gradient(circle_at_bottom_right,rgba(252,227,0,0.12),transparent_42%),linear-gradient(135deg,#1b1b19,#101111)]">
206
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest/60 px-4 py-2">
207
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Schema Map</span>
208
+ <span class="material-symbols-outlined text-xs text-on-surface-variant">account_tree</span>
209
+ </div>
210
+ <div class="space-y-5 p-4">
211
+ <div class="flex flex-col gap-5 xl:flex-row xl:items-end xl:justify-between">
212
+ <div>
213
+ <div class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
214
+ ${escapeHtml(narrative.title)}
215
+ </div>
216
+ <p class="mt-2 max-w-3xl text-sm leading-6 text-on-surface-variant/70">
217
+ ${escapeHtml(narrative.body)}
218
+ </p>
219
+ </div>
220
+ <div class="flex justify-start xl:justify-end">
221
+ <button
222
+ class="standard-button"
223
+ data-action="navigate"
224
+ data-to="/structure"
225
+ type="button"
226
+ >
227
+ <span class="material-symbols-outlined text-sm">account_tree</span>
228
+ Open Structure
229
+ </button>
230
+ </div>
231
+ </div>
232
+ <div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
233
+ ${stats
234
+ .map(
235
+ (stat) => `
236
+ <div class="border border-outline-variant/10 bg-surface-container-lowest/70 px-3 py-3">
237
+ <div class="text-[9px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
238
+ ${escapeHtml(stat.label)}
239
+ </div>
240
+ <div class="mt-2 font-headline text-2xl font-black uppercase tracking-tight text-on-surface">
241
+ ${escapeHtml(stat.value)}
242
+ </div>
243
+ </div>
244
+ `
245
+ )
246
+ .join("")}
247
+ </div>
248
+ <div class="border border-outline-variant/10 bg-surface-container-lowest/70 px-4 py-4">
249
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
250
+ Why It Helps
251
+ </div>
252
+ <div class="mt-3 grid gap-3 md:grid-cols-3">
253
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
254
+ <div class="text-[9px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
255
+ FK Links
256
+ </div>
257
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant/70">
258
+ Shows how many actual relationships exist between tables.
259
+ </div>
260
+ </div>
261
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
262
+ <div class="text-[9px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
263
+ FK Clusters
264
+ </div>
265
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant/70">
266
+ Tells you whether the schema is one connected area or split into separate groups.
267
+ </div>
268
+ </div>
269
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
270
+ <div class="text-[9px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
271
+ Isolated Tables
272
+ </div>
273
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant/70">
274
+ Highlights how many tables have no FK links and can usually be ignored during relationship analysis.
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </section>
281
+ `;
282
+ }
283
+
148
284
  function renderIntegrityCard(overview, readOnly) {
149
285
  return `
150
- <section class="relative overflow-hidden border border-outline-variant/10 bg-[radial-gradient(circle_at_top_right,rgba(252,227,0,0.14),transparent_30%),linear-gradient(135deg,#1c1b1b,#0e0e0e)]">
151
- <div class="pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,rgba(32,31,31,0.95),rgba(32,31,31,0.2))]"></div>
286
+ <section class="shell-section relative overflow-hidden bg-[radial-gradient(circle_at_top_right,rgba(252,227,0,0.14),transparent_30%),linear-gradient(135deg,#1c1b1b,#0e0e0e)]">
287
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest/60 px-4 py-2">
288
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Integrity Status</span>
289
+ <span class="material-symbols-outlined text-xs text-on-surface-variant">security</span>
290
+ </div>
291
+ <div class="pointer-events-none absolute inset-x-0 bottom-0 top-10 bg-[linear-gradient(to_top,rgba(32,31,31,0.95),rgba(32,31,31,0.2))]"></div>
152
292
  <div class="relative flex min-h-[220px] flex-col justify-end p-6">
153
293
  <div class="mb-6 flex h-16 w-16 items-center justify-center border border-primary-container/20 bg-primary-container/10">
154
294
  <span class="material-symbols-outlined text-4xl text-primary-container">security</span>
155
295
  </div>
156
- <div class="text-[10px] font-black uppercase tracking-[0.25em] text-primary-container">
157
- INTEGRITY_STATUS
158
- </div>
159
296
  <div class="mt-2 flex flex-wrap items-center gap-2">
160
297
  ${renderStatusBadge(overview.sqlite?.integrityCheck ?? "unknown", "success")}
161
298
  ${renderStatusBadge(
@@ -190,10 +327,9 @@ export function renderOverviewView(state) {
190
327
  : "System Registry: NO_ACTIVE_DATABASE",
191
328
  actions: `
192
329
  <button
193
- class="toolbar-button toolbar-button--primary bg-primary-container px-4 py-2 font-headline text-xs font-bold uppercase tracking-widest text-on-primary clipped-corner"
330
+ class="signature-button"
194
331
  data-action="navigate"
195
332
  data-to="/editor"
196
- style="--clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);"
197
333
  type="button"
198
334
  >
199
335
  <span class="material-symbols-outlined text-sm">terminal</span>
@@ -218,7 +354,10 @@ export function renderOverviewView(state) {
218
354
  ? `
219
355
  ${renderOverviewMetrics(overview)}
220
356
  <div class="grid grid-cols-1 gap-6 xl:grid-cols-3">
221
- ${renderTopTables(overview)}
357
+ <div class="space-y-6 xl:col-span-2">
358
+ ${renderTopTables(overview)}
359
+ ${renderSchemaMapPreview(overview)}
360
+ </div>
222
361
  <div class="space-y-6">
223
362
  ${renderOperationalSurface(overview)}
224
363
  ${renderIntegrityCard(overview, readOnly)}
@@ -56,7 +56,7 @@ function renderSettingsContent(state) {
56
56
  Copyright Oliver Jessner
57
57
  </div>
58
58
  <a
59
- class="inline-flex items-center gap-2 border border-outline-variant/20 bg-surface-container-low px-4 py-3 text-xs font-bold uppercase tracking-[0.2em] text-primary-container transition-colors hover:bg-surface-container-highest"
59
+ class="standard-button"
60
60
  href="https://oliverjessner.at"
61
61
  rel="noreferrer"
62
62
  target="_blank"
@@ -77,7 +77,7 @@ function renderSettingsContent(state) {
77
77
  Open Source Code
78
78
  </div>
79
79
  <a
80
- class="inline-flex items-center gap-2 border border-outline-variant/20 bg-surface-container-low px-4 py-3 text-xs font-bold uppercase tracking-[0.2em] text-primary-container transition-colors hover:bg-surface-container-highest"
80
+ class="standard-button"
81
81
  href="https://github.com/oliverjessner/sqlite-hub"
82
82
  rel="noreferrer"
83
83
  target="_blank"
@@ -1,4 +1,4 @@
1
- import { clearInspector, renderInspector } from "../components/structureGraph.js";
1
+ import { clearInspector, renderDdlSection, renderInspector } from "../components/structureGraph.js";
2
2
  import { escapeHtml, formatNumber } from "../utils/format.js";
3
3
 
4
4
  function renderEntryGroup(title, entries, activeName, options = {}) {
@@ -172,20 +172,16 @@ function renderObjectInspector(detail) {
172
172
  : ""
173
173
  }
174
174
 
175
- <section class="structure-graph__section">
176
- <div class="structure-graph__section-title">DDL</div>
177
- <pre class="structure-graph__ddl custom-scrollbar">${escapeHtml(
178
- detail.ddl || "No DDL available."
179
- )}</pre>
180
- </section>
175
+ ${renderDdlSection(detail.ddl)}
181
176
  </div>
182
177
  `;
183
178
  }
184
179
 
185
- function renderGraphSurface(structure, selectedName, detail, detailLoading) {
180
+ function renderGraphSurface(structure, selectedName, detail, detailLoading, tablesVisible = true) {
186
181
  const graph = structure?.graph ?? { tables: [], relationshipCount: 0 };
187
182
  const selectedGraphTable =
188
183
  graph.tables?.find((table) => table.name === selectedName && table.type === "table") ?? null;
184
+ const toolbarButtonClass = "standard-button";
189
185
  const inspectorMarkup = selectedGraphTable
190
186
  ? renderInspector(selectedGraphTable)
191
187
  : detailLoading
@@ -198,20 +194,20 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading) {
198
194
  <section class="structure-graph" data-structure-graph-root>
199
195
  <div class="structure-graph__toolbar">
200
196
  <div class="structure-graph__toolbar-main">
201
- <label class="structure-graph__search" data-structure-graph-search>
202
- <span class="material-symbols-outlined structure-graph__search-icon">search</span>
203
- <input
204
- class="structure-graph__search-input"
205
- data-structure-graph-search-input
206
- placeholder="Find table"
207
- spellcheck="false"
208
- type="search"
209
- />
210
- </label>
197
+ <button
198
+ class="${toolbarButtonClass}"
199
+ data-action="toggle-structure-tables"
200
+ type="button"
201
+ >
202
+ <span class="material-symbols-outlined text-sm">${
203
+ tablesVisible ? "visibility_off" : "visibility"
204
+ }</span>
205
+ ${tablesVisible ? "Hide Tables" : "Show Tables"}
206
+ </button>
211
207
  </div>
212
208
  <div class="structure-graph__toolbar-actions">
213
209
  <button
214
- class="structure-graph__button"
210
+ class="${toolbarButtonClass}"
215
211
  data-structure-graph-action="fit"
216
212
  type="button"
217
213
  >
@@ -219,7 +215,7 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading) {
219
215
  Fit Graph
220
216
  </button>
221
217
  <button
222
- class="structure-graph__button"
218
+ class="${toolbarButtonClass}"
223
219
  data-structure-graph-action="relayout"
224
220
  type="button"
225
221
  >
@@ -227,7 +223,7 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading) {
227
223
  Recalculate Layout
228
224
  </button>
229
225
  <button
230
- class="structure-graph__button"
226
+ class="${toolbarButtonClass}"
231
227
  data-structure-graph-action="clear"
232
228
  type="button"
233
229
  >
@@ -235,7 +231,7 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading) {
235
231
  Clear Selection
236
232
  </button>
237
233
  <button
238
- class="structure-graph__button is-disabled"
234
+ class="${toolbarButtonClass}"
239
235
  data-structure-graph-action="open-data"
240
236
  disabled
241
237
  type="button"
@@ -244,7 +240,7 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading) {
244
240
  Open Data
245
241
  </button>
246
242
  <button
247
- class="structure-graph__button"
243
+ class="${toolbarButtonClass}"
248
244
  data-structure-graph-action="toggle-inspector"
249
245
  type="button"
250
246
  >
@@ -310,57 +306,64 @@ export function renderStructureView(state) {
310
306
  const structure = state.structure.data;
311
307
  const detail =
312
308
  state.structure.detail?.name === state.structure.selectedName ? state.structure.detail : null;
309
+ const tablesVisible = state.structure.tablesVisible !== false;
313
310
 
314
311
  return {
315
312
  main: `
316
313
  <section class="view-surface structure-view">
317
- <aside class="structure-view__sidebar">
318
- <div class="structure-view__sidebar-header">
319
- <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
320
- Objects
321
- </div>
322
- <div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
323
- total ${escapeHtml(
324
- formatNumber(
325
- (structure?.grouped?.tables?.length ?? 0) +
326
- (structure?.grouped?.views?.length ?? 0) +
327
- (structure?.grouped?.indexes?.length ?? 0) +
328
- (structure?.grouped?.triggers?.length ?? 0)
329
- )
330
- )}
331
- </div>
332
- </div>
333
- <div class="structure-view__sidebar-body custom-scrollbar">
334
- ${
335
- structure
336
- ? `
337
- ${renderEntryGroup(
338
- "Tables",
339
- structure.grouped.tables,
340
- state.structure.selectedName,
341
- { compact: true, showMeta: false }
342
- )}
343
- ${renderEntryGroup(
344
- "Views",
345
- structure.grouped.views,
346
- state.structure.selectedName
347
- )}
348
- ${renderEntryGroup(
349
- "Indexes",
350
- structure.grouped.indexes,
351
- state.structure.selectedName,
352
- { compact: true, showMeta: false }
353
- )}
354
- ${renderEntryGroup(
355
- "Triggers",
356
- structure.grouped.triggers,
357
- state.structure.selectedName
314
+ ${
315
+ tablesVisible
316
+ ? `
317
+ <aside class="structure-view__sidebar">
318
+ <div class="structure-view__sidebar-header">
319
+ <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
320
+ Objects
321
+ </div>
322
+ <div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
323
+ total ${escapeHtml(
324
+ formatNumber(
325
+ (structure?.grouped?.tables?.length ?? 0) +
326
+ (structure?.grouped?.views?.length ?? 0) +
327
+ (structure?.grouped?.indexes?.length ?? 0) +
328
+ (structure?.grouped?.triggers?.length ?? 0)
329
+ )
358
330
  )}
359
- `
360
- : ""
361
- }
362
- </div>
363
- </aside>
331
+ </div>
332
+ </div>
333
+ <div class="structure-view__sidebar-body custom-scrollbar">
334
+ ${
335
+ structure
336
+ ? `
337
+ ${renderEntryGroup(
338
+ "Tables",
339
+ structure.grouped.tables,
340
+ state.structure.selectedName,
341
+ { compact: true, showMeta: false }
342
+ )}
343
+ ${renderEntryGroup(
344
+ "Views",
345
+ structure.grouped.views,
346
+ state.structure.selectedName
347
+ )}
348
+ ${renderEntryGroup(
349
+ "Indexes",
350
+ structure.grouped.indexes,
351
+ state.structure.selectedName,
352
+ { compact: true, showMeta: false }
353
+ )}
354
+ ${renderEntryGroup(
355
+ "Triggers",
356
+ structure.grouped.triggers,
357
+ state.structure.selectedName
358
+ )}
359
+ `
360
+ : ""
361
+ }
362
+ </div>
363
+ </aside>
364
+ `
365
+ : ""
366
+ }
364
367
 
365
368
  <section class="structure-view__detail">
366
369
  ${renderStructureWorkspaceHeader(structure, state.structure.selectedName)}
@@ -390,7 +393,8 @@ export function renderStructureView(state) {
390
393
  structure,
391
394
  state.structure.selectedName,
392
395
  detail,
393
- state.structure.detailLoading
396
+ state.structure.detailLoading,
397
+ tablesVisible
394
398
  )}
395
399
  </div>
396
400
  `
@@ -26,8 +26,13 @@ export function renderTableDesignerView(state) {
26
26
  <div class="table-designer-workspace__top">
27
27
  ${renderTableDesignerEditor(state)}
28
28
  </div>
29
- <div class="table-designer-workspace__bottom">
30
- ${renderTableDesignerSqlPreview(state.tableDesigner.draft)}
29
+ <div class="table-designer-workspace__bottom${
30
+ state.tableDesigner.sqlPreviewVisible ? "" : " is-collapsed"
31
+ }">
32
+ ${renderTableDesignerSqlPreview(
33
+ state.tableDesigner.draft,
34
+ state.tableDesigner.sqlPreviewVisible
35
+ )}
31
36
  </div>
32
37
  </div>
33
38
  </section>
@@ -28,6 +28,68 @@ button {
28
28
  cursor: pointer;
29
29
  }
30
30
 
31
+ .control-button {
32
+ align-items: center;
33
+ box-sizing: border-box;
34
+ display: inline-flex;
35
+ gap: var(--spacing-2);
36
+ height: var(--control-height);
37
+ line-height: 1;
38
+ min-height: var(--control-height);
39
+ padding: 0 var(--control-padding-inline);
40
+ }
41
+
42
+ .control-input,
43
+ .control-select {
44
+ box-sizing: border-box;
45
+ height: var(--control-height);
46
+ min-height: var(--control-height);
47
+ }
48
+
49
+ .control-input {
50
+ padding: 0 var(--control-padding-inline);
51
+ }
52
+
53
+ input.control-input--ghost {
54
+ appearance: none;
55
+ background: transparent !important;
56
+ background-color: transparent !important;
57
+ background-image: none !important;
58
+ border: 0 !important;
59
+ border-color: transparent !important;
60
+ box-shadow: none !important;
61
+ -webkit-appearance: none;
62
+ padding: 0;
63
+ }
64
+
65
+ input.control-input--ghost:focus,
66
+ input.control-input--ghost:not(:focus) {
67
+ background: transparent !important;
68
+ background-color: transparent !important;
69
+ border-color: transparent !important;
70
+ box-shadow: none !important;
71
+ outline: none;
72
+ }
73
+
74
+ .control-select {
75
+ padding: 0 var(--control-padding-inline);
76
+ }
77
+
78
+ .control-shell {
79
+ box-sizing: border-box;
80
+ min-height: var(--control-height);
81
+ }
82
+
83
+ .control-icon-button {
84
+ align-items: center;
85
+ box-sizing: border-box;
86
+ display: inline-flex;
87
+ height: var(--control-height);
88
+ justify-content: center;
89
+ min-height: var(--control-height);
90
+ width: var(--control-height);
91
+ }
92
+
31
93
  a {
32
94
  color: inherit;
33
95
  text-decoration: none;