sqlite-hub 0.9.1 → 0.9.4

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 (56) hide show
  1. package/.github/workflows/ci.yml +36 -0
  2. package/README.md +2 -2
  3. package/bin/sqlite-hub.js +1 -1
  4. package/frontend/index.html +3 -158
  5. package/frontend/js/app.js +231 -5
  6. package/frontend/js/components/connectionCard.js +62 -87
  7. package/frontend/js/components/emptyState.js +20 -23
  8. package/frontend/js/components/modal.js +158 -199
  9. package/frontend/js/components/pageHeader.js +1 -1
  10. package/frontend/js/components/queryEditor.js +16 -30
  11. package/frontend/js/components/queryHistoryDetail.js +93 -164
  12. package/frontend/js/components/queryHistoryPanel.js +90 -100
  13. package/frontend/js/components/queryResults.js +3 -1
  14. package/frontend/js/components/rowEditorPanel.js +76 -56
  15. package/frontend/js/components/tableDesignerEditor.js +91 -116
  16. package/frontend/js/lib/queryChartOptions.js +5 -1
  17. package/frontend/js/lib/queryCharts.js +50 -2
  18. package/frontend/js/store.js +161 -23
  19. package/frontend/js/utils/tableDesigner.js +8 -2
  20. package/frontend/js/views/charts.js +126 -73
  21. package/frontend/js/views/data.js +147 -133
  22. package/frontend/js/views/editor.js +1 -0
  23. package/frontend/js/views/mediaTagging.js +131 -164
  24. package/frontend/js/views/structure.js +52 -48
  25. package/frontend/styles/base.css +57 -13
  26. package/frontend/styles/components.css +166 -96
  27. package/frontend/styles/layout.css +1 -1
  28. package/frontend/styles/structure-graph.css +11 -11
  29. package/frontend/styles/tailwind.css +81 -0
  30. package/frontend/styles/tailwind.generated.css +1 -0
  31. package/frontend/styles/tokens.css +50 -7
  32. package/frontend/styles/utilities.css +21 -0
  33. package/frontend/styles/views.css +94 -86
  34. package/package.json +16 -3
  35. package/server/routes/mediaTagging.js +2 -10
  36. package/server/routes/sql.js +35 -10
  37. package/server/server.js +24 -0
  38. package/server/services/sqlite/dataBrowserService.js +25 -5
  39. package/server/services/sqlite/exportService.js +4 -2
  40. package/server/services/sqlite/introspection.js +2 -2
  41. package/server/services/sqlite/mediaTaggingService.js +166 -53
  42. package/server/services/sqlite/structureService.js +2 -2
  43. package/server/services/sqlite/tableDesigner/sql.js +19 -3
  44. package/server/services/storage/appStateStore.js +227 -87
  45. package/server/services/storage/queryHistoryChartUtils.js +19 -2
  46. package/server/utils/appPaths.js +55 -19
  47. package/server/utils/fileValidation.js +94 -8
  48. package/tailwind.config.cjs +73 -0
  49. package/tests/security-paths.test.js +84 -0
  50. package/tests/sql-identifier-safety.test.js +66 -0
  51. package/.npmingnore +0 -4
  52. package/changelog.md +0 -77
  53. package/docs/DESIGN_GUIDELINES.md +0 -36
  54. package/scripts/publish_brew.sh +0 -466
  55. package/scripts/publish_npm.sh +0 -241
  56. package/shortkeys.md +0 -5
@@ -180,52 +180,39 @@ function renderParentTagFields({
180
180
 
181
181
  const isCreatingParentTag = Boolean(parentToggleColumn ? tagFormValues[parentToggleColumn.name] : false);
182
182
  const selectedParentTagId = parentSelectColumn ? String(tagFormValues[parentSelectColumn.name] ?? '') : '';
183
-
184
- return `
185
- ${
186
- parentSelectColumn
187
- ? `
188
- <label class="media-tagging-field">
189
- <span class="media-tagging-field__label">Parent Tag</span>
190
- <select
191
- class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
192
- data-bind="media-tagging-tag-form-field"
193
- data-field="${escapeHtml(parentSelectColumn.name)}"
194
- ${isCreatingParentTag ? 'disabled' : ''}
195
- >
196
- ${renderOptionList(
197
- parentTagOptions.map(tag => ({
198
- value: String(tag.identityValue ?? ''),
199
- label: tag.label,
200
- })),
201
- selectedParentTagId,
202
- parentTagOptions.length ? 'Select a parent tag' : 'No parent tags available',
203
- )}
204
- </select>
205
- </label>
206
- `
207
- : ''
208
- }
209
- ${
210
- parentToggleColumn
211
- ? `
212
- <div class="media-tagging-field">
213
- <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
214
- <input
215
- data-bind="media-tagging-tag-form-field"
216
- data-field="${escapeHtml(parentToggleColumn.name)}"
217
- type="checkbox"
218
- ${isCreatingParentTag ? 'checked' : ''}
219
- />
220
- <span class="media-tagging-field__meta">
221
- <span class="media-tagging-field__label">Create Parent Tag</span>
222
- </span>
223
- </label>
224
- </div>
225
- `
226
- : ''
227
- }
228
- `;
183
+ const parentSelectMarkup = parentSelectColumn
184
+ ? [
185
+ '<label class="media-tagging-field"><span class="media-tagging-field__label">Parent Tag</span>',
186
+ '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-tag-form-field" data-field="',
187
+ escapeHtml(parentSelectColumn.name),
188
+ '" ',
189
+ isCreatingParentTag ? 'disabled' : '',
190
+ '>',
191
+ renderOptionList(
192
+ parentTagOptions.map(tag => ({
193
+ value: String(tag.identityValue ?? ''),
194
+ label: tag.label,
195
+ })),
196
+ selectedParentTagId,
197
+ parentTagOptions.length ? 'Select a parent tag' : 'No parent tags available',
198
+ ),
199
+ '</select></label>',
200
+ ].join('')
201
+ : '';
202
+ const parentToggleMarkup = parentToggleColumn
203
+ ? [
204
+ '<div class="media-tagging-field"><label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
205
+ '<input data-bind="media-tagging-tag-form-field" data-field="',
206
+ escapeHtml(parentToggleColumn.name),
207
+ '" type="checkbox" ',
208
+ isCreatingParentTag ? 'checked' : '',
209
+ '/>',
210
+ '<span class="media-tagging-field__meta"><span class="media-tagging-field__label">Create Parent Tag</span></span>',
211
+ '</label></div>',
212
+ ].join('')
213
+ : '';
214
+
215
+ return [parentSelectMarkup, parentToggleMarkup].join('');
229
216
  }
230
217
 
231
218
  function renderTagList(tags = [], selectedTagKeys = []) {
@@ -285,42 +272,40 @@ function renderCreatedTagList(tags = [], { canRemove = false, removingTagKey = n
285
272
  return `
286
273
  <div class="media-tagging-created-tags custom-scrollbar">
287
274
  ${tags
288
- .map(
289
- tag => `
290
- <article class="media-tagging-created-tag">
291
- <div class="media-tagging-created-tag__content">
292
- <div class="media-tagging-created-tag__label">${escapeHtml(tag.label)}</div>
293
- ${
294
- tag.isParentTag || tag.parentTagLabel
295
- ? `
296
- <div class="media-tagging-created-tag__meta">
297
- ${tag.isParentTag ? '<span class="media-tagging-created-tag__badge">Parent</span>' : ''}
298
- ${
299
- tag.parentTagLabel
300
- ? `<span class="media-tagging-created-tag__badge">${escapeHtml(tag.parentTagLabel)}</span>`
301
- : ''
302
- }
303
- </div>
304
- `
305
- : ''
306
- }
307
- </div>
308
- <button
309
- aria-label="Remove tag"
310
- class="delete-button media-tagging-created-tag__remove"
311
- data-action="remove-media-tag"
312
- data-tag-key="${escapeHtml(tag.key)}"
313
- type="button"
314
- title="Remove tag"
315
- ${canRemove && removingTagKey !== tag.key ? '' : 'disabled'}
316
- >
317
- <span class="material-symbols-outlined text-[18px]">
318
- ${removingTagKey === tag.key ? 'hourglass_top' : 'delete'}
319
- </span>
320
- </button>
321
- </article>
322
- `,
323
- )
275
+ .map(tag => {
276
+ const parentMetaMarkup =
277
+ tag.isParentTag || tag.parentTagLabel
278
+ ? [
279
+ '<div class="media-tagging-created-tag__meta">',
280
+ tag.isParentTag ? '<span class="media-tagging-created-tag__badge">Parent</span>' : '',
281
+ tag.parentTagLabel
282
+ ? [
283
+ '<span class="media-tagging-created-tag__badge">',
284
+ escapeHtml(tag.parentTagLabel),
285
+ '</span>',
286
+ ].join('')
287
+ : '',
288
+ '</div>',
289
+ ].join('')
290
+ : '';
291
+
292
+ return [
293
+ '<article class="media-tagging-created-tag">',
294
+ '<div class="media-tagging-created-tag__content">',
295
+ '<div class="media-tagging-created-tag__label">',
296
+ escapeHtml(tag.label),
297
+ '</div>',
298
+ parentMetaMarkup,
299
+ '</div>',
300
+ '<button aria-label="Remove tag" class="delete-button media-tagging-created-tag__remove" data-action="remove-media-tag" data-tag-key="',
301
+ escapeHtml(tag.key),
302
+ '" type="button" title="Remove tag" ',
303
+ canRemove && removingTagKey !== tag.key ? '' : 'disabled',
304
+ '><span class="material-symbols-outlined text-[18px]">',
305
+ removingTagKey === tag.key ? 'hourglass_top' : 'delete',
306
+ '</span></button></article>',
307
+ ].join('');
308
+ })
324
309
  .join('')}
325
310
  </div>
326
311
  `;
@@ -633,89 +618,71 @@ function renderTaggingSection(state) {
633
618
  const mediaTableColumns = state.mediaTagging.mediaTableColumns ?? [];
634
619
  const pathCandidates = state.mediaTagging.pathCandidates ?? [];
635
620
  const booleanCandidates = state.mediaTagging.booleanCandidates ?? [];
621
+ const tableOptions = renderOptionList(
622
+ tables
623
+ .map(table => ({ value: table.name, label: table.name }))
624
+ .sort((a, b) => a.label.localeCompare(b.label)),
625
+ draft.mediaTable,
626
+ 'Select a media table',
627
+ );
628
+ const pathOptions = renderOptionList(
629
+ mediaTableColumns
630
+ .map(column => ({
631
+ value: column.name,
632
+ label: pathCandidates.includes(column.name)
633
+ ? [column.name, ' // suggested'].join('')
634
+ : column.name,
635
+ }))
636
+ .sort((a, b) => a.label.localeCompare(b.label)),
637
+ draft.pathColumn,
638
+ 'Select the media path column',
639
+ );
640
+ const taggedOptions = renderOptionList(
641
+ mediaTableColumns
642
+ .map(column => ({
643
+ value: column.name,
644
+ label: booleanCandidates.includes(column.name)
645
+ ? [column.name, ' // suggested'].join('')
646
+ : column.name,
647
+ }))
648
+ .sort((a, b) => a.label.localeCompare(b.label)),
649
+ draft.taggedColumn,
650
+ mediaTableColumns.length ? 'Select the tagged flag column' : 'No columns available',
651
+ );
636
652
 
637
- return `
638
- <section class="media-tagging-card media-tagging-card--tagging shell-section">
639
- <div class="media-tagging-card__header">
640
- <div>
641
- <div class="media-tagging-card__eyebrow">2. Tagging</div>
642
- <h2 class="media-tagging-card__title">Media Source</h2>
643
- </div>
644
- </div>
645
- <div class="media-tagging-card__body">
646
- <div class="media-tagging-form-grid">
647
- <label class="media-tagging-field">
648
- <span class="media-tagging-field__label">Media Table</span>
649
- <select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-field" data-field="mediaTable">
650
- ${renderOptionList(
651
- tables
652
- .map(table => ({ value: table.name, label: table.name }))
653
- .sort((a, b) => a.label.localeCompare(b.label)),
654
- draft.mediaTable,
655
- 'Select a media table',
656
- )}
657
- </select>
658
- </label>
659
- <div></div>
660
- <label class="media-tagging-field">
661
- <span class="media-tagging-field__label">Path Column</span>
662
- <select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-field" data-field="pathColumn">
663
- ${renderOptionList(
664
- mediaTableColumns
665
- .map(column => ({
666
- value: column.name,
667
- label: pathCandidates.includes(column.name) ? `${column.name} // suggested` : column.name,
668
- }))
669
- .sort((a, b) => a.label.localeCompare(b.label)),
670
- draft.pathColumn,
671
- 'Select the media path column',
672
- )}
673
- </select>
674
- </label>
675
- <label class="media-tagging-field">
676
- <span class="media-tagging-field__label">Tagged Boolean Column</span>
677
- <select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-field" data-field="taggedColumn">
678
- ${renderOptionList(
679
- mediaTableColumns
680
- .map(column => ({
681
- value: column.name,
682
- label: booleanCandidates.includes(column.name) ? `${column.name} // suggested` : column.name,
683
- }))
684
- .sort((a, b) => a.label.localeCompare(b.label)),
685
- draft.taggedColumn,
686
- mediaTableColumns.length ? 'Select the tagged flag column' : 'No columns available',
687
- )}
688
- </select>
689
- </label>
690
- </div>
691
-
692
- <div class="media-tagging-query-grid">
693
- ${renderSqlTextarea({
694
- label: 'Untagged Query',
695
- value: draft.untaggedQuery,
696
- dataBind: 'media-tagging-field',
697
- dataField: 'untaggedQuery',
698
- })}
699
- ${renderSqlTextarea({
700
- label: 'Tagged Query',
701
- value: draft.taggedQuery,
702
- dataBind: 'media-tagging-field',
703
- dataField: 'taggedQuery',
704
- })}
705
- </div>
706
-
707
- <div class="flex flex-wrap items-center gap-3">
708
- <button
709
- class="standard-button"
710
- data-action="reset-media-tagging-queries"
711
- type="button"
712
- >
713
- Reset Queries to Defaults
714
- </button>
715
- </div>
716
- </div>
717
- </section>
718
- `;
653
+ return [
654
+ '<section class="media-tagging-card media-tagging-card--tagging shell-section">',
655
+ '<div class="media-tagging-card__header"><div><div class="media-tagging-card__eyebrow">2. Tagging</div><h2 class="media-tagging-card__title">Media Source</h2></div></div>',
656
+ '<div class="media-tagging-card__body"><div class="media-tagging-form-grid">',
657
+ '<label class="media-tagging-field"><span class="media-tagging-field__label">Media Table</span>',
658
+ '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-field" data-field="mediaTable">',
659
+ tableOptions,
660
+ '</select></label><div></div>',
661
+ '<label class="media-tagging-field"><span class="media-tagging-field__label">Path Column</span>',
662
+ '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-field" data-field="pathColumn">',
663
+ pathOptions,
664
+ '</select></label>',
665
+ '<label class="media-tagging-field"><span class="media-tagging-field__label">Tagged Boolean Column</span>',
666
+ '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-field" data-field="taggedColumn">',
667
+ taggedOptions,
668
+ '</select></label></div>',
669
+ '<div class="media-tagging-query-grid">',
670
+ renderSqlTextarea({
671
+ label: 'Untagged Query',
672
+ value: draft.untaggedQuery,
673
+ dataBind: 'media-tagging-field',
674
+ dataField: 'untaggedQuery',
675
+ }),
676
+ renderSqlTextarea({
677
+ label: 'Tagged Query',
678
+ value: draft.taggedQuery,
679
+ dataBind: 'media-tagging-field',
680
+ dataField: 'taggedQuery',
681
+ }),
682
+ '</div>',
683
+ '<div class="flex flex-wrap items-center gap-3"><button class="standard-button" data-action="reset-media-tagging-queries" type="button">Reset Queries to Defaults</button></div>',
684
+ '</div></section>',
685
+ ].join('');
719
686
  }
720
687
 
721
688
  function renderMappingSection(state) {
@@ -3,60 +3,64 @@ import { escapeHtml, formatNumber } from "../utils/format.js";
3
3
 
4
4
  function renderEntryGroup(title, entries, activeName, options = {}) {
5
5
  const { compact = false, showMeta = true } = options;
6
+ const entriesMarkup = entries.length
7
+ ? [
8
+ '<div class="space-y-2">',
9
+ entries
10
+ .map((entry) => {
11
+ const isActive = entry.name === activeName;
12
+ const entryAttributes =
13
+ entry.type === "table"
14
+ ? [
15
+ 'data-structure-entry-name="',
16
+ escapeHtml(entry.name),
17
+ '"',
18
+ ].join("")
19
+ : "";
20
+ const metaMarkup = showMeta
21
+ ? [
22
+ '<div class="mt-1 text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">',
23
+ escapeHtml(entry.tableName || entry.type),
24
+ "</div>",
25
+ ].join("")
26
+ : "";
27
+
28
+ return [
29
+ '<button class="w-full border px-3 ',
30
+ compact ? "py-2.5" : "py-3",
31
+ " text-left transition-colors ",
32
+ isActive
33
+ ? "border-primary-container/30 bg-surface-container-high"
34
+ : "border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high",
35
+ '" data-action="select-structure-entry" data-entry-name="',
36
+ escapeHtml(entry.name),
37
+ '" ',
38
+ entryAttributes,
39
+ ' type="button">',
40
+ '<div class="font-mono text-xs ',
41
+ isActive ? "text-primary-container" : "text-on-surface",
42
+ '">',
43
+ escapeHtml(entry.name),
44
+ "</div>",
45
+ metaMarkup,
46
+ "</button>",
47
+ ].join("");
48
+ })
49
+ .join(""),
50
+ "</div>",
51
+ ].join("")
52
+ : [
53
+ '<div class="text-sm text-on-surface-variant/45">No ',
54
+ escapeHtml(title.toLowerCase()),
55
+ " found.</div>",
56
+ ].join("");
6
57
 
7
58
  return `
8
59
  <section class="structure-sidebar__section">
9
60
  <div class="mb-4 shrink-0 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
10
61
  ${escapeHtml(title)}
11
62
  </div>
12
- ${
13
- entries.length
14
- ? `
15
- <div class="space-y-2">
16
- ${entries
17
- .map(
18
- (entry) => `
19
- <button
20
- class="w-full border px-3 ${compact ? "py-2.5" : "py-3"} text-left transition-colors ${
21
- entry.name === activeName
22
- ? "border-primary-container/30 bg-surface-container-high"
23
- : "border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high"
24
- }"
25
- data-action="select-structure-entry"
26
- data-entry-name="${escapeHtml(entry.name)}"
27
- ${
28
- entry.type === "table"
29
- ? `data-structure-entry-name="${escapeHtml(entry.name)}"`
30
- : ""
31
- }
32
- type="button"
33
- >
34
- <div class="font-mono text-xs ${
35
- entry.name === activeName
36
- ? "text-primary-container"
37
- : "text-on-surface"
38
- }">
39
- ${escapeHtml(entry.name)}
40
- </div>
41
- ${
42
- showMeta
43
- ? `
44
- <div class="mt-1 text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
45
- ${escapeHtml(entry.tableName || entry.type)}
46
- </div>
47
- `
48
- : ""
49
- }
50
- </button>
51
- `
52
- )
53
- .join("")}
54
- </div>
55
- `
56
- : `<div class="text-sm text-on-surface-variant/45">No ${escapeHtml(
57
- title.toLowerCase()
58
- )} found.</div>`
59
- }
63
+ ${entriesMarkup}
60
64
  </section>
61
65
  `;
62
66
  }
@@ -28,6 +28,26 @@ button {
28
28
  cursor: pointer;
29
29
  }
30
30
 
31
+ button:disabled,
32
+ button[aria-disabled='true'],
33
+ input:disabled,
34
+ select:disabled,
35
+ textarea:disabled {
36
+ cursor: not-allowed;
37
+ }
38
+
39
+ :where(a, button, input, select, textarea, [tabindex]:not([tabindex='-1'])):focus-visible {
40
+ outline: 2px solid var(--color-primary-container);
41
+ outline-offset: 2px;
42
+ }
43
+
44
+ .control-shell:focus-within,
45
+ .standard-checkbox:focus-within,
46
+ .sql-highlight-shell:focus-within {
47
+ border-color: var(--primary-alpha-35);
48
+ box-shadow: var(--focus-ring-inset);
49
+ }
50
+
31
51
  .control-button {
32
52
  align-items: center;
33
53
  box-sizing: border-box;
@@ -52,22 +72,22 @@ button {
52
72
 
53
73
  input.control-input--ghost {
54
74
  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;
75
+ background: transparent;
76
+ background-color: transparent;
77
+ background-image: none;
78
+ border: 0;
79
+ border-color: transparent;
80
+ box-shadow: none;
61
81
  -webkit-appearance: none;
62
82
  padding: 0;
63
83
  }
64
84
 
65
85
  input.control-input--ghost:focus,
66
86
  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;
87
+ background: transparent;
88
+ background-color: transparent;
89
+ border-color: transparent;
90
+ box-shadow: none;
71
91
  outline: none;
72
92
  }
73
93
 
@@ -105,6 +125,7 @@ img {
105
125
  }
106
126
 
107
127
  [hidden] {
128
+ /* Hidden must beat utility display classes generated by Tailwind CDN. */
108
129
  display: none !important;
109
130
  }
110
131
 
@@ -118,9 +139,6 @@ img {
118
139
 
119
140
  .font-mono {
120
141
  font-family: var(--font-family-mono);
121
- overflow: hidden;
122
- white-space: nowrap;
123
- text-overflow: ellipsis;
124
142
  }
125
143
 
126
144
  .material-symbols-outlined {
@@ -218,12 +236,14 @@ img {
218
236
  width: 100%;
219
237
  word-break: normal;
220
238
  overflow-wrap: anywhere;
239
+ font-variant-ligatures: none;
221
240
  }
222
241
 
223
242
  .query-editor-highlight {
224
243
  color: var(--color-on-surface);
225
244
  overflow: visible;
226
245
  pointer-events: none;
246
+ user-select: none;
227
247
  }
228
248
 
229
249
  .query-editor-input {
@@ -236,6 +256,14 @@ img {
236
256
  overflow: auto;
237
257
  }
238
258
 
259
+ .query-editor-input:focus-visible {
260
+ outline: none;
261
+ }
262
+
263
+ .query-editor-layer:focus-within {
264
+ box-shadow: inset 0 0 0 1px var(--primary-alpha-30);
265
+ }
266
+
239
267
  .query-editor-gutter-track {
240
268
  min-height: 100%;
241
269
  will-change: transform;
@@ -260,3 +288,19 @@ img {
260
288
  border-color: var(--color-primary-container);
261
289
  }
262
290
  }
291
+
292
+ @media (prefers-reduced-motion: reduce) {
293
+ *,
294
+ *::before,
295
+ *::after {
296
+ animation-duration: 0.001ms;
297
+ animation-iteration-count: 1;
298
+ scroll-behavior: auto;
299
+ transition-duration: 0.001ms;
300
+ }
301
+
302
+ .cursor-blink {
303
+ animation: none;
304
+ border-right-color: var(--color-primary-container);
305
+ }
306
+ }