@domternal/theme 0.6.2 → 0.7.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/src/_toc.scss ADDED
@@ -0,0 +1,450 @@
1
+ // =============================================================================
2
+ // Table of Contents - Floating Outline
3
+ // Right-rail outline rendered by `@domternal/extension-toc`. Lives OUTSIDE
4
+ // `.dm-editor` (which has `overflow: hidden`), so tokens pull from `:root`.
5
+ // =============================================================================
6
+
7
+ .dm-toc-outline {
8
+ // Stack ticks vertically with even gaps. flex-end keeps the right
9
+ // edge of every tick aligned even though widths differ per level.
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: flex-end;
13
+ gap: var(--dm-toc-tick-gap, 10px);
14
+ padding: var(--dm-toc-padding-block, 12px) var(--dm-toc-padding-inline, 8px);
15
+ z-index: var(--dm-toc-z-index, 10);
16
+
17
+ // No background / no border in collapsed state - just a column of ticks.
18
+ // The expanded card adds its own chrome.
19
+ background: transparent;
20
+ border: 0;
21
+ margin: 0;
22
+ pointer-events: auto;
23
+
24
+ // Positioning by `data-anchor`:
25
+ //
26
+ // - editor: nav is `position: sticky`. Plugin toggles
27
+ // `data-bottom-visible` via an IntersectionObserver on a sentinel
28
+ // at the shell's bottom edge.
29
+ // - bottom-visible='false' (editor extends below the viewport):
30
+ // nav sticks at `--dm-toc-mid-top` (= `calc(50vh - height/2)`,
31
+ // set from the measured nav height). The BOX is centered around
32
+ // 50vh - no transform - so the visual is fully constrained by
33
+ // the containing block and never escapes the editor's bounds.
34
+ // - bottom-visible='true' (editor's bottom is on screen): sticky
35
+ // offset becomes `1rem`. The nav's natural position is set by
36
+ // the shell (flex-center) or by inline margin-top (frozen mode)
37
+ // so it stays put until the viewport top scrolls past it.
38
+ // - viewport: `position: fixed` to the right edge of the viewport,
39
+ // vertically centered.
40
+ &[data-anchor='editor'] {
41
+ position: sticky;
42
+ top: var(--dm-toc-mid-top, 50vh);
43
+ }
44
+ &[data-anchor='editor'][data-bottom-visible='true'] {
45
+ top: var(--dm-toc-editor-top, 1rem);
46
+ }
47
+ &[data-anchor='viewport'] {
48
+ position: fixed;
49
+ top: 50%;
50
+ right: var(--dm-toc-right-offset, 24px);
51
+ transform: translateY(-50%);
52
+ }
53
+
54
+ // Visibility guards. The plugin sets data-state and data-viewport
55
+ // based on heading count and matchMedia. Hiding via CSS (vs JS
56
+ // `display: none` directly) lets consumers override without
57
+ // monkey-patching the plugin.
58
+ //
59
+ // State machine: hidden | collapsed | expanded
60
+ // hidden - 0 headings / mobile / < minHeadings
61
+ // collapsed - default; ticks visible, card hidden
62
+ // expanded - hover or focus-within; card visible, ticks hidden
63
+ &[data-state='hidden'],
64
+ &[data-viewport='mobile'] {
65
+ display: none;
66
+ }
67
+ }
68
+
69
+ // Editor-mode shell: out-of-flow column running the host's full
70
+ // vertical extent at its right inside edge. Width shrinks to the nav's
71
+ // intrinsic width; the shell itself is invisible. The nav inside
72
+ // uses the shell's height as its sticky range.
73
+ .dm-toc-outline-shell[data-anchor='editor'] {
74
+ position: absolute;
75
+ top: 25px;
76
+ bottom: 25px;
77
+ right: var(--dm-toc-page-offset, 8px);
78
+ z-index: var(--dm-toc-z-index, 10);
79
+ pointer-events: none;
80
+
81
+ &:has(.dm-toc-outline[data-state='hidden']),
82
+ &:has(.dm-toc-outline[data-viewport='mobile']) {
83
+ display: none;
84
+ }
85
+ }
86
+
87
+ // mode='center': flex-center sets the nav's natural position to host
88
+ // middle (used on mount when the editor fits the viewport). mode='frozen'
89
+ // uses inline margin-top instead (set by the plugin at the B→A
90
+ // transition). mode='middle' uses default block layout.
91
+ .dm-toc-outline-shell[data-anchor='editor'][data-mode='center'] {
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: flex-end;
95
+ }
96
+
97
+ .dm-toc-outline-tick {
98
+ // Reset button defaults - we draw a solid filled rectangle.
99
+ appearance: none;
100
+ border: 0;
101
+ margin: 0;
102
+ padding: 0;
103
+ font: inherit;
104
+ cursor: pointer;
105
+ background: var(--dm-toc-tick-color, rgba(55, 53, 47, 0.4));
106
+ height: var(--dm-toc-tick-height, 2px);
107
+ border-radius: var(--dm-toc-tick-radius, 1px);
108
+ // Width per heading level - h1 widest, deeper levels narrower.
109
+ // Default for unmatched levels falls through to h3-width.
110
+ width: var(--dm-toc-tick-h3-width, 8px);
111
+ transition:
112
+ background-color 120ms ease-out,
113
+ width 120ms ease-out;
114
+
115
+ &:hover {
116
+ background: var(--dm-toc-tick-hover-color, rgba(55, 53, 47, 0.7));
117
+ }
118
+
119
+ // Keyboard focus needs a real ring. The hover-only alpha bump
120
+ // (0.4 -> 0.7) is too subtle to act as a focus indicator for users
121
+ // who can't see hover state. Outline-offset 3px keeps the ring
122
+ // clear of the tick body without colliding with the next tick at
123
+ // the default `--dm-toc-tick-gap: 10px`.
124
+ &:focus-visible {
125
+ background: var(--dm-toc-tick-hover-color, rgba(55, 53, 47, 0.7));
126
+ outline: 2px solid var(--dm-accent, #2563eb);
127
+ outline-offset: 3px;
128
+ }
129
+
130
+ // Active state: the tick for the heading the user is reading. Two
131
+ // selectors so consumers can target either the JS class (`--active`)
132
+ // or the a11y attribute (`aria-current="location"`).
133
+ &.dm-toc--active,
134
+ &[aria-current='location'] {
135
+ background: var(--dm-toc-tick-active-color, rgba(55, 53, 47, 0.95));
136
+ }
137
+
138
+ // Per-level width selectors. h1 / h2 / h3 are the canonical Notion
139
+ // levels; we also handle h4-h6 for consumers configuring custom
140
+ // levels via `levels: [1,2,3,4,5,6]`.
141
+ &[data-level='1'] { width: var(--dm-toc-tick-h1-width, 18px); }
142
+ &[data-level='2'] { width: var(--dm-toc-tick-h2-width, 12px); }
143
+ &[data-level='3'] { width: var(--dm-toc-tick-h3-width, 8px); }
144
+ &[data-level='4'] { width: var(--dm-toc-tick-h4-width, 6px); }
145
+ &[data-level='5'] { width: var(--dm-toc-tick-h5-width, 5px); }
146
+ &[data-level='6'] { width: var(--dm-toc-tick-h6-width, 4px); }
147
+ }
148
+
149
+ // Ticks fade out when the card takes over.
150
+ .dm-toc-outline[data-state='expanded'] .dm-toc-outline-tick {
151
+ opacity: 0;
152
+ pointer-events: none;
153
+ }
154
+
155
+ // =============================================================================
156
+ // Expanded card
157
+ // =============================================================================
158
+ // The card is absolutely positioned within the nav (which is fixed at
159
+ // the right gutter). Anchored to the nav's right edge and vertically
160
+ // centered against the same midline as the tick column - it visually
161
+ // REPLACES the ticks rather than appearing alongside them. CSS opacity
162
+ // + transform handle the slide-in transition; the plugin only flips
163
+ // the parent's data-state attribute.
164
+
165
+ .dm-toc-outline-card {
166
+ position: absolute;
167
+ right: 0;
168
+ top: 50%;
169
+ // `--dm-toc-card-shift-y` is set by the plugin to nudge the card back
170
+ // into the viewport when the nav sits near the top or bottom edge.
171
+ transform:
172
+ translateY(calc(-50% + var(--dm-toc-card-shift-y, 0px)))
173
+ translateX(var(--dm-toc-card-offset, 8px));
174
+ min-width: var(--dm-toc-card-min-width, 180px);
175
+ max-width: var(--dm-toc-card-max-width, 260px);
176
+ // Long heading lists scroll internally rather than overflowing the
177
+ // viewport; the margin keeps the card off the chrome edges.
178
+ max-height: calc(100vh - 2 * var(--dm-toc-card-viewport-margin, 16px));
179
+ overflow-y: auto;
180
+ padding: var(--dm-toc-card-padding-block, 6px) 0;
181
+ background: var(--dm-toc-card-bg, rgba(255, 255, 255, 0.98));
182
+ border: var(--dm-toc-card-border, 1px solid rgba(0, 0, 0, 0.06));
183
+ border-radius: var(--dm-toc-card-radius, 6px);
184
+ box-shadow: var(--dm-toc-card-shadow, 0 8px 24px rgba(0, 0, 0, 0.08));
185
+
186
+ display: flex;
187
+ flex-direction: column;
188
+ align-items: stretch;
189
+
190
+ // Hidden by default in collapsed state. opacity + transform together
191
+ // let the card softly slide in from the right when expanding. The
192
+ // pointer-events flip prevents interactions with the invisible card.
193
+ opacity: 0;
194
+ pointer-events: none;
195
+ transition:
196
+ opacity var(--dm-toc-card-transition, 180ms) ease-out,
197
+ transform var(--dm-toc-card-transition, 180ms) ease-out;
198
+ }
199
+
200
+ .dm-toc-outline[data-state='expanded'] .dm-toc-outline-card {
201
+ opacity: 1;
202
+ pointer-events: auto;
203
+ transform:
204
+ translateY(calc(-50% + var(--dm-toc-card-shift-y, 0px)))
205
+ translateX(0);
206
+ }
207
+
208
+ .dm-toc-outline-row {
209
+ appearance: none;
210
+ border: 0;
211
+ margin: 0;
212
+ background: transparent;
213
+ cursor: pointer;
214
+ text-align: start;
215
+ font: inherit;
216
+ font-size: var(--dm-toc-row-font-size, 13px);
217
+ line-height: var(--dm-toc-row-line-height, 1.4);
218
+ color: var(--dm-toc-row-color, rgba(55, 53, 47, 0.7));
219
+ padding:
220
+ var(--dm-toc-row-padding-block, 4px)
221
+ var(--dm-toc-row-padding-inline-end, 16px)
222
+ var(--dm-toc-row-padding-block, 4px)
223
+ var(--dm-toc-row-padding-inline-start, 12px);
224
+ // Truncate long heading text - it's a navigation aid, not the
225
+ // headings themselves.
226
+ white-space: nowrap;
227
+ overflow: hidden;
228
+ text-overflow: ellipsis;
229
+ border-radius: 3px;
230
+ transition: background-color 120ms ease-out, color 120ms ease-out;
231
+
232
+ &:hover {
233
+ background: var(--dm-toc-row-hover-bg, rgba(0, 0, 0, 0.04));
234
+ }
235
+
236
+ &:focus-visible {
237
+ outline: 2px solid var(--dm-accent, #2563eb);
238
+ outline-offset: -2px;
239
+ }
240
+
241
+ // Active row mirrors the active tick. Selector matches both the
242
+ // JS-applied class (`--active`, shared with ticks) and the a11y
243
+ // attribute the plugin sets simultaneously - same convention as ticks.
244
+ &.dm-toc--active,
245
+ &[aria-current='location'] {
246
+ color: var(--dm-toc-row-active-color, rgba(55, 53, 47, 0.95));
247
+ font-weight: var(--dm-toc-row-active-weight, 600);
248
+ }
249
+
250
+ // Per-level indent - h1 sits flush at the start padding, h2 nudges
251
+ // in by one row-indent, h3 by two, etc. Keeps the visual hierarchy
252
+ // legible at a glance even when text is truncated.
253
+ &[data-level='1'] {
254
+ padding-inline-start: var(--dm-toc-row-padding-inline-start, 12px);
255
+ }
256
+ &[data-level='2'] {
257
+ padding-inline-start:
258
+ calc(var(--dm-toc-row-padding-inline-start, 12px) + var(--dm-toc-row-indent, 14px));
259
+ }
260
+ &[data-level='3'] {
261
+ padding-inline-start:
262
+ calc(var(--dm-toc-row-padding-inline-start, 12px) + var(--dm-toc-row-indent, 14px) * 2);
263
+ }
264
+ &[data-level='4'] {
265
+ padding-inline-start:
266
+ calc(var(--dm-toc-row-padding-inline-start, 12px) + var(--dm-toc-row-indent, 14px) * 3);
267
+ }
268
+ &[data-level='5'] {
269
+ padding-inline-start:
270
+ calc(var(--dm-toc-row-padding-inline-start, 12px) + var(--dm-toc-row-indent, 14px) * 4);
271
+ }
272
+ &[data-level='6'] {
273
+ padding-inline-start:
274
+ calc(var(--dm-toc-row-padding-inline-start, 12px) + var(--dm-toc-row-indent, 14px) * 5);
275
+ }
276
+ }
277
+
278
+ // =============================================================================
279
+ // Inline /toc block
280
+ // Lives INSIDE the editor as a regular content block. NodeView renders
281
+ // the heading list; click on a row routes to scrollToHeading.
282
+ // =============================================================================
283
+
284
+ .dm-toc-block {
285
+ display: block;
286
+ margin-block: var(--dm-toc-block-margin-block, 1rem);
287
+ padding:
288
+ var(--dm-toc-block-padding-block, 12px)
289
+ var(--dm-toc-block-padding-inline, 16px);
290
+ background: var(--dm-toc-block-bg, rgba(0, 0, 0, 0.02));
291
+ border: var(--dm-toc-block-border, 1px solid rgba(0, 0, 0, 0.08));
292
+ border-radius: var(--dm-toc-block-radius, 6px);
293
+
294
+ // Block is contenteditable=false (atom node, NodeView-rendered).
295
+ // Surface a subtle outline ring on PM selection so the user knows
296
+ // the block is selected (e.g. for delete with Backspace on the
297
+ // gap cursor immediately after).
298
+ &.ProseMirror-selectednode {
299
+ outline: 2px solid var(--dm-accent, #2563eb);
300
+ outline-offset: 2px;
301
+ }
302
+ }
303
+
304
+ .dm-toc-block-empty {
305
+ margin: 0;
306
+ color: var(--dm-toc-block-empty-color, rgba(55, 53, 47, 0.5));
307
+ font-style: var(--dm-toc-block-empty-font-style, italic);
308
+ }
309
+
310
+ .dm-toc-block-list {
311
+ list-style: none;
312
+ padding: 0;
313
+ margin: 0;
314
+ }
315
+
316
+ .dm-toc-block-item {
317
+ margin: 0;
318
+ }
319
+
320
+ .dm-toc-block-link {
321
+ // Reset button defaults for an in-flow link-style affordance.
322
+ appearance: none;
323
+ border: 0;
324
+ background: transparent;
325
+ cursor: pointer;
326
+ font: inherit;
327
+ font-size: var(--dm-toc-block-link-font-size, 0.9375rem);
328
+ line-height: var(--dm-toc-block-link-line-height, 1.5);
329
+ color: var(--dm-toc-block-link-color, rgba(55, 53, 47, 0.8));
330
+ text-align: start;
331
+ width: 100%;
332
+ display: block;
333
+ padding:
334
+ var(--dm-toc-block-link-padding-block, 4px)
335
+ var(--dm-toc-block-link-padding-inline, 8px);
336
+ border-radius: var(--dm-toc-block-link-radius, 3px);
337
+ transition: background-color 120ms ease-out, color 120ms ease-out;
338
+
339
+ &:hover {
340
+ color: var(--dm-toc-block-link-hover-color, rgba(55, 53, 47, 1));
341
+ background: var(--dm-toc-block-link-hover-bg, rgba(0, 0, 0, 0.04));
342
+ }
343
+
344
+ &:focus-visible {
345
+ outline: 2px solid var(--dm-accent, #2563eb);
346
+ outline-offset: -2px;
347
+ }
348
+
349
+ // Active row mirrors the outline's active tick/row contract.
350
+ &.dm-toc-block-link--active,
351
+ &[aria-current='location'] {
352
+ color: var(--dm-toc-block-link-active-color, rgba(55, 53, 47, 0.95));
353
+ font-weight: var(--dm-toc-block-link-active-weight, 600);
354
+ }
355
+
356
+ // Per-level indent. Same shape as outline rows; padding on the
357
+ // link itself so the hover background covers the full visual row
358
+ // including the indent.
359
+ &[data-level='1'] {
360
+ padding-inline-start: var(--dm-toc-block-link-padding-inline, 8px);
361
+ }
362
+ &[data-level='2'] {
363
+ padding-inline-start:
364
+ calc(var(--dm-toc-block-link-padding-inline, 8px) + var(--dm-toc-block-link-indent, 16px));
365
+ }
366
+ &[data-level='3'] {
367
+ padding-inline-start:
368
+ calc(var(--dm-toc-block-link-padding-inline, 8px) + var(--dm-toc-block-link-indent, 16px) * 2);
369
+ }
370
+ &[data-level='4'] {
371
+ padding-inline-start:
372
+ calc(var(--dm-toc-block-link-padding-inline, 8px) + var(--dm-toc-block-link-indent, 16px) * 3);
373
+ }
374
+ &[data-level='5'] {
375
+ padding-inline-start:
376
+ calc(var(--dm-toc-block-link-padding-inline, 8px) + var(--dm-toc-block-link-indent, 16px) * 4);
377
+ }
378
+ &[data-level='6'] {
379
+ padding-inline-start:
380
+ calc(var(--dm-toc-block-link-padding-inline, 8px) + var(--dm-toc-block-link-indent, 16px) * 5);
381
+ }
382
+ }
383
+
384
+ // Reduced-motion guard. Repeated here defensively even though `index.scss`
385
+ // has a global block, in case that block is ever scoped to `.dm-editor`-only.
386
+ @media (prefers-reduced-motion: reduce) {
387
+ .dm-toc-outline-tick,
388
+ .dm-toc-outline-card,
389
+ .dm-toc-outline-row,
390
+ .dm-toc-block-link {
391
+ transition: none;
392
+ }
393
+
394
+ // Card start-state offset becomes 0 too: with no transition, the
395
+ // X translate would jump from `var(--dm-toc-card-offset)` to 0
396
+ // instantly anyway, but resetting the start position to 0 means
397
+ // the card never enters its mid-animation visual state on a fast
398
+ // expand/collapse cycle (which can briefly leak a 1-frame
399
+ // off-position card).
400
+ .dm-toc-outline-card {
401
+ transform:
402
+ translateY(calc(-50% + var(--dm-toc-card-shift-y, 0px)))
403
+ translateX(0);
404
+ }
405
+ }
406
+
407
+ // Print: hide the floating outline (navigation chrome, not content).
408
+ // Inline `/toc` blocks stay since they belong to the document.
409
+ @media print {
410
+ .dm-toc-outline {
411
+ display: none;
412
+ }
413
+ .dm-toc-block {
414
+ background: transparent;
415
+ border-color: rgba(0, 0, 0, 0.2);
416
+ }
417
+ }
418
+
419
+ // Forced-colors / Windows High Contrast: backgrounds are stripped, so
420
+ // ticks need explicit borders to stay visible. We use `Highlight` /
421
+ // `ButtonText` (OS palette) rather than `forced-color-adjust: none`.
422
+ @media (forced-colors: active) {
423
+ .dm-toc-outline-tick {
424
+ background: ButtonText;
425
+ border: 1px solid ButtonText;
426
+
427
+ &.dm-toc--active,
428
+ &[aria-current='location'] {
429
+ background: Highlight;
430
+ border-color: Highlight;
431
+ }
432
+ }
433
+
434
+ .dm-toc-outline-card {
435
+ border: 1px solid ButtonText;
436
+ }
437
+
438
+ .dm-toc-outline-row,
439
+ .dm-toc-block-link {
440
+ &.dm-toc--active,
441
+ &.dm-toc-block-link--active,
442
+ &[aria-current='location'] {
443
+ color: Highlight;
444
+ }
445
+ }
446
+
447
+ .dm-toc-block {
448
+ border: 1px solid ButtonText;
449
+ }
450
+ }
package/src/_toolbar.scss CHANGED
@@ -50,7 +50,7 @@
50
50
  color: var(--dm-button-active-color);
51
51
  }
52
52
 
53
- // Expanded state panel/popover is open (same visual as dropdown triggers)
53
+ // Expanded state - panel/popover is open (same visual as dropdown triggers)
54
54
  &[aria-expanded="true"] {
55
55
  background: var(--dm-button-hover-bg);
56
56
  }
@@ -75,7 +75,7 @@
75
75
  }
76
76
  }
77
77
 
78
- // Dropdown trigger wider to fit caret
78
+ // Dropdown trigger - wider to fit caret
79
79
  .dm-toolbar-dropdown-trigger {
80
80
  position: relative;
81
81
  width: auto;
@@ -133,7 +133,7 @@
133
133
  position: relative;
134
134
  }
135
135
 
136
- // Dropdown panel positioned by floating-ui (strategy: absolute)
136
+ // Dropdown panel - positioned by floating-ui (strategy: absolute)
137
137
  .dm-toolbar-dropdown-panel {
138
138
  position: absolute;
139
139
  z-index: 50;
@@ -155,7 +155,7 @@
155
155
  }
156
156
  }
157
157
 
158
- // Display-mode: icon-only compact grid-like panel
158
+ // Display-mode: icon-only - compact grid-like panel
159
159
  .dm-toolbar-dropdown-panel[data-display-mode="icon"] {
160
160
  min-width: 0;
161
161
 
@@ -166,7 +166,7 @@
166
166
  }
167
167
  }
168
168
 
169
- // Display-mode: text-only shrink to text width, no icon gap
169
+ // Display-mode: text-only - shrink to text width, no icon gap
170
170
  .dm-toolbar-dropdown-panel[data-display-mode="text"] {
171
171
  min-width: 0;
172
172