@domternal/theme 0.6.1 → 0.7.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domternal/theme",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Default themes and styles for Domternal editor",
5
5
  "author": "https://github.com/ThomasNowHere",
6
6
  "license": "MIT",
package/src/_base.scss CHANGED
@@ -48,10 +48,10 @@
48
48
  // Shared floating element fade-in animation
49
49
  // =============================================================================
50
50
  // Used by elements conditionally inserted into the DOM (emoji picker,
51
- // suggestion dropdown, toolbar dropdowns). Uses scale instead of translateY
51
+ // suggestion dropdown, toolbar dropdowns). Uses scale instead of translateY:
52
52
  // scale doesn't shift position so it won't conflict with floating-ui.
53
53
  // Elements using data-show (bubble menu, floating menu, popovers) use CSS
54
- // opacity transitions instead see their respective stylesheets.
54
+ // opacity transitions instead - see their respective stylesheets.
55
55
  // =============================================================================
56
56
 
57
57
  @keyframes dm-fade-in {
@@ -0,0 +1,178 @@
1
+ // =============================================================================
2
+ // Block Colors
3
+ // Notion-style block-level backgrounds and text colors. Applied when the
4
+ // BlockColor extension (@domternal/core) sets `data-bg-color` /
5
+ // `data-text-color` attributes on block elements. Token definitions live
6
+ // in `_variables.scss` (light) and `themes/_dark.scss` (dark).
7
+ // =============================================================================
8
+
9
+ // -----------------------------------------------------------------------------
10
+ // Background
11
+ // Tint the whole block. Scoped under `.dm-editor .ProseMirror` so it only
12
+ // applies inside a Domternal editor instance - won't leak to host content.
13
+ // Small inline padding so the tint visually "hugs" the text instead of
14
+ // bleeding edge-to-edge on narrow blocks.
15
+ // -----------------------------------------------------------------------------
16
+ .dm-editor .ProseMirror {
17
+ [data-bg-color="gray"] { background-color: var(--dm-block-bg-gray); }
18
+ [data-bg-color="brown"] { background-color: var(--dm-block-bg-brown); }
19
+ [data-bg-color="orange"] { background-color: var(--dm-block-bg-orange); }
20
+ [data-bg-color="yellow"] { background-color: var(--dm-block-bg-yellow); }
21
+ [data-bg-color="green"] { background-color: var(--dm-block-bg-green); }
22
+ [data-bg-color="blue"] { background-color: var(--dm-block-bg-blue); }
23
+ [data-bg-color="purple"] { background-color: var(--dm-block-bg-purple); }
24
+ [data-bg-color="pink"] { background-color: var(--dm-block-bg-pink); }
25
+ [data-bg-color="red"] { background-color: var(--dm-block-bg-red); }
26
+
27
+ // Inline padding is pre-applied to colorable blocks in `_content.scss`
28
+ // (and to the editor itself for lists/blockquotes) so toggling a color
29
+ // does NOT shift text - the tint paints into existing padding. This
30
+ // rule is therefore minimal: only the rounded corners + a hair of
31
+ // vertical padding so the tint reads as a "pill" around short text
32
+ // instead of an edge-to-edge band.
33
+ [data-bg-color] {
34
+ padding-block: 0.125rem;
35
+ border-radius: 0.25rem;
36
+ }
37
+
38
+ [data-text-color="gray"] { color: var(--dm-block-text-gray); }
39
+ [data-text-color="brown"] { color: var(--dm-block-text-brown); }
40
+ [data-text-color="orange"] { color: var(--dm-block-text-orange); }
41
+ [data-text-color="yellow"] { color: var(--dm-block-text-yellow); }
42
+ [data-text-color="green"] { color: var(--dm-block-text-green); }
43
+ [data-text-color="blue"] { color: var(--dm-block-text-blue); }
44
+ [data-text-color="purple"] { color: var(--dm-block-text-purple); }
45
+ [data-text-color="pink"] { color: var(--dm-block-text-pink); }
46
+ [data-text-color="red"] { color: var(--dm-block-text-red); }
47
+ }
48
+
49
+ // -----------------------------------------------------------------------------
50
+ // Swatch row - horizontal strip of color swatches, one per row in the
51
+ // BlockContextMenu Colors section (one for text, one for background).
52
+ // -----------------------------------------------------------------------------
53
+ .dm-block-color-row {
54
+ display: flex;
55
+ flex-wrap: wrap;
56
+ align-items: center;
57
+ gap: 0.25rem;
58
+ padding: 0.25rem 0.5rem;
59
+ }
60
+
61
+ .dm-block-color-row-label {
62
+ // Visually hidden but readable by screen readers. The visible row label
63
+ // comes from `.dm-block-context-menu-group-label` ("Colors") above.
64
+ position: absolute;
65
+ width: 1px;
66
+ height: 1px;
67
+ padding: 0;
68
+ overflow: hidden;
69
+ clip: rect(0, 0, 0, 0);
70
+ white-space: nowrap;
71
+ border: 0;
72
+ }
73
+
74
+ // -----------------------------------------------------------------------------
75
+ // Color swatches - used by BlockContextMenu's color picker and available
76
+ // for host apps that render their own pickers. One ring per palette entry
77
+ // plus a neutral "clear" swatch.
78
+ // -----------------------------------------------------------------------------
79
+ .dm-block-color-swatch {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ width: 1.5rem;
84
+ height: 1.5rem;
85
+ padding: 0;
86
+ border: 1px solid var(--dm-border-color, #e5e7eb);
87
+ border-radius: 50%;
88
+ background: transparent;
89
+ cursor: pointer;
90
+ transition: transform 0.08s ease, box-shadow 0.08s ease;
91
+
92
+ &:hover {
93
+ transform: scale(1.1);
94
+ box-shadow: 0 0 0 2px var(--dm-accent-surface, rgba(37, 99, 235, 0.15));
95
+ }
96
+
97
+ &:focus-visible,
98
+ // Currently-applied color for the targeted block. BlockContextMenu writes
99
+ // `aria-pressed="true"` on the swatch whose value matches the block's
100
+ // current text/bg color (`null` for "Default"); mirror the inline picker's
101
+ // `.dm-ncp-active` treatment so the active state reads the same in both UIs.
102
+ &[aria-pressed="true"] {
103
+ // OUTSET ring (offset > 0): swatch dot fills almost the entire button
104
+ // surface, so an inset outline would render UNDER the dot. Pulling the
105
+ // ring outward keeps it visible against any palette tint.
106
+ outline: 2px solid var(--dm-accent, #2563eb);
107
+ outline-offset: 1px;
108
+ }
109
+
110
+ // Inner tint dot - used for both bg and text variants so rows look uniform.
111
+ &::before {
112
+ content: '';
113
+ display: block;
114
+ width: 1rem;
115
+ height: 1rem;
116
+ border-radius: 50%;
117
+ }
118
+
119
+ // "No color" swatch - strike-through slash rendered via gradient.
120
+ &[data-color="null"]::before {
121
+ background:
122
+ linear-gradient(
123
+ to top right,
124
+ transparent calc(50% - 1px),
125
+ var(--dm-muted, #999) calc(50% - 1px),
126
+ var(--dm-muted, #999) calc(50% + 1px),
127
+ transparent calc(50% + 1px)
128
+ );
129
+ border: 1px solid var(--dm-border-color, #e5e7eb);
130
+ }
131
+ }
132
+
133
+ // Background swatches render the tint as the dot interior.
134
+ .dm-block-color-swatch--bg {
135
+ &[data-color="gray"]::before { background: var(--dm-block-bg-gray); }
136
+ &[data-color="brown"]::before { background: var(--dm-block-bg-brown); }
137
+ &[data-color="orange"]::before { background: var(--dm-block-bg-orange); }
138
+ &[data-color="yellow"]::before { background: var(--dm-block-bg-yellow); }
139
+ &[data-color="green"]::before { background: var(--dm-block-bg-green); }
140
+ &[data-color="blue"]::before { background: var(--dm-block-bg-blue); }
141
+ &[data-color="purple"]::before { background: var(--dm-block-bg-purple); }
142
+ &[data-color="pink"]::before { background: var(--dm-block-bg-pink); }
143
+ &[data-color="red"]::before { background: var(--dm-block-bg-red); }
144
+ }
145
+
146
+ // Text swatches render a capital "A" in the named color on a neutral dot
147
+ // background, mirroring Notion's convention so users can tell at a glance
148
+ // which row sets text vs background. The "A" comes from the `content`
149
+ // pseudo-element; the dot background stays neutral (var(--dm-surface)) so
150
+ // the colored glyph is readable regardless of palette tint.
151
+ .dm-block-color-swatch--text {
152
+ &::before {
153
+ content: 'A';
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ font-family: var(--dm-editor-font-family, system-ui, sans-serif);
158
+ font-size: 0.75rem;
159
+ font-weight: 600;
160
+ background: var(--dm-surface, #f8f9fa);
161
+ color: var(--dm-editor-text, #333);
162
+ }
163
+
164
+ // "Default" text swatch keeps its "A" and gets the diagonal slash from the
165
+ // base `[data-color="null"]::before` rule painted as the dot's background.
166
+ // The "A" glyph sits on top of the slash so users see "letterform crossed
167
+ // out" rather than an empty stroke (mirrors the inline-picker treatment).
168
+
169
+ &[data-color="gray"]::before { color: var(--dm-block-text-gray); }
170
+ &[data-color="brown"]::before { color: var(--dm-block-text-brown); }
171
+ &[data-color="orange"]::before { color: var(--dm-block-text-orange); }
172
+ &[data-color="yellow"]::before { color: var(--dm-block-text-yellow); }
173
+ &[data-color="green"]::before { color: var(--dm-block-text-green); }
174
+ &[data-color="blue"]::before { color: var(--dm-block-text-blue); }
175
+ &[data-color="purple"]::before { color: var(--dm-block-text-purple); }
176
+ &[data-color="pink"]::before { color: var(--dm-block-text-pink); }
177
+ &[data-color="red"]::before { color: var(--dm-block-text-red); }
178
+ }
@@ -0,0 +1,189 @@
1
+ // =============================================================================
2
+ // Block Handle
3
+ // Left-gutter handle (⋮⋮ drag + + insert) rendered on hover over each
4
+ // top-level block when BlockHandle extension is enabled.
5
+ // =============================================================================
6
+
7
+ // When the extension mounts it adds `dm-editor--has-block-handle` to the
8
+ // editor wrapper. Two things happen:
9
+ //
10
+ // 1) ProseMirror reserves gutter space with `padding-left` so text does
11
+ // not flow under the handle.
12
+ // 2) The editor opens its horizontal overflow so the handle - positioned
13
+ // with a negative `left` - can render in the page margin rather than
14
+ // eating into the content column.
15
+ //
16
+ // Gutter default is 4rem (64px). Handle is 8px OUTSIDE the editor's
17
+ // left edge (see `.dm-block-handle { left: -0.5rem }`) and ~46px wide,
18
+ // so it ends ~38px inside the editor; the remaining ~26px of gutter is
19
+ // breathing room before text, matching Notion's feel.
20
+ .dm-editor--has-block-handle {
21
+ overflow: visible;
22
+
23
+ .ProseMirror {
24
+ padding-left: var(--dm-block-handle-gutter, 4rem);
25
+ }
26
+ }
27
+
28
+ // Selected-node visual indicator: the base theme's `outline: 2px solid`
29
+ // (see `_prosemirror.scss`) paints OUTSIDE the block's border-box and
30
+ // bleeds over anything adjacent - including the `.notion-page` right
31
+ // border in the Notion demo, making it look like the border is missing.
32
+ // When BlockHandle is active, suppress the outline and replace it with a
33
+ // subtle `::before` halo that sits BEHIND the block content (`z-index: -1`)
34
+ // and therefore never overlaps surrounding chrome.
35
+ .dm-editor--has-block-handle .ProseMirror-selectednode,
36
+ .dm-editor--has-block-handle .ProseMirror-selectednoderange {
37
+ position: relative;
38
+ outline: none;
39
+
40
+ &::before {
41
+ content: '';
42
+ position: absolute;
43
+ inset: -0.25rem;
44
+ background-color: var(--dm-block-selected-halo, rgba(112, 207, 248, 0.25));
45
+ border-radius: 0.25rem;
46
+ pointer-events: none;
47
+ z-index: -1;
48
+ }
49
+ }
50
+
51
+ .dm-block-handle {
52
+ position: absolute;
53
+ // Tokenised so demos with a centered narrower content column can move
54
+ // the handle further out (e.g., `--dm-block-handle-left: -3.5rem`) to
55
+ // place it fully in the surrounding whitespace. Default `-0.5rem` keeps
56
+ // the handle just outside the editor's left border for full-width
57
+ // editors with the standard `--dm-block-handle-gutter` reservation on
58
+ // ProseMirror. Requires `overflow: visible` (set above on
59
+ // `.dm-editor--has-block-handle`) so it isn't clipped.
60
+ left: var(--dm-block-handle-left, -0.5rem);
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 2px;
64
+ padding: 2px;
65
+ opacity: 0;
66
+ visibility: hidden;
67
+ pointer-events: none;
68
+ transition: opacity 0.12s ease, visibility 0.12s;
69
+ z-index: var(--dm-z-handle, 25);
70
+
71
+ &[data-show] {
72
+ opacity: 1;
73
+ visibility: visible;
74
+ pointer-events: auto;
75
+ }
76
+
77
+ // Individual buttons (⋮⋮ drag, + insert)
78
+ &-btn {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ width: 1.25rem;
83
+ height: 1.5rem;
84
+ padding: 0;
85
+ border: none;
86
+ background: transparent;
87
+ color: var(--dm-muted, #999);
88
+ cursor: pointer;
89
+ border-radius: 3px;
90
+ transition: background-color 0.1s, color 0.1s;
91
+ // Defensive: stop text selection / touch pan from hijacking the
92
+ // pointer during mousedown-to-drag. Not strictly required on
93
+ // Chrome/Firefox but avoids edge cases on Safari + touch devices.
94
+ user-select: none;
95
+ -webkit-user-select: none;
96
+ touch-action: none;
97
+
98
+ &:hover {
99
+ background: var(--dm-button-hover-bg, rgba(0, 0, 0, 0.04));
100
+ color: var(--dm-editor-text, inherit);
101
+ }
102
+
103
+ &:focus-visible {
104
+ // INSET ring (-1px): block handle buttons sit in the gutter; an outset
105
+ // ring would extend into adjacent content / next sibling block.
106
+ outline: 2px solid var(--dm-accent, #2563eb);
107
+ outline-offset: -1px;
108
+ }
109
+
110
+ svg {
111
+ width: 1rem;
112
+ height: 1rem;
113
+ pointer-events: none;
114
+ }
115
+ }
116
+
117
+ // Drag handle - grab cursor + slight nudge active state
118
+ &-drag {
119
+ cursor: grab;
120
+
121
+ &:active {
122
+ cursor: grabbing;
123
+ }
124
+ }
125
+
126
+ // Insert (+) button - subtle accent on hover
127
+ &-plus {
128
+ &:hover {
129
+ color: var(--dm-accent, #2563eb);
130
+ }
131
+ }
132
+ }
133
+
134
+ // =============================================================================
135
+ // Drop indicator
136
+ // Custom horizontal drop line drawn during drag-from-handle. Positioned
137
+ // absolutely inside `.dm-editor` (which is `position: relative`) by JS,
138
+ // only made visible via the `data-show` attribute. Spans the resolved
139
+ // drop-target block's width so the user sees exactly which block (and
140
+ // which side) the drop will land at - including the difference between
141
+ // "into the list" and "as a sibling block" outcomes when dragging in
142
+ // the gap between a list and the next block.
143
+ // =============================================================================
144
+ .dm-block-drop-indicator {
145
+ position: absolute;
146
+ height: 2px;
147
+ background: var(--dm-accent, #2563eb);
148
+ border-radius: 1px;
149
+ pointer-events: none;
150
+ z-index: 5;
151
+ // Hidden by default; JS adds `data-show` to reveal.
152
+ display: none;
153
+ // Slight glow so the line reads on top of any background colour.
154
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.15);
155
+ // Nudge the line a few pixels DOWN from the upper block's bottom edge so
156
+ // it visually sits in the gap between blocks rather than glued to the
157
+ // block above. Visual-only (transform doesn't shift layout) so the
158
+ // computed `top` from JS still reflects the true anchor - the indicator
159
+ // and the actual drop position stay in sync.
160
+ transform: translateY(3px);
161
+ // Smooth glide when the indicator moves between targets or flips
162
+ // between sibling and nested mode mid-drag. Only position properties
163
+ // animate; the categorical style swap (filled bar ↔ dashed border)
164
+ // intentionally stays instant to keep the mode boundary readable.
165
+ // Short duration so the line never feels laggy on fast drags.
166
+ transition: left 80ms ease-out, width 80ms ease-out, top 80ms ease-out, transform 80ms ease-out;
167
+
168
+ &[data-show] {
169
+ display: block;
170
+ }
171
+
172
+ // Nested-mode visual: dashed line indented to the children-zone start
173
+ // of the target list item. Communicates "this drop becomes a nested
174
+ // child appended at the end" rather than "a sibling between blocks".
175
+ // JS positions `top` at the rect's BOTTOM edge and `left` indented by
176
+ // 24px (mirrors `--dm-block-children-indent`), so the dashed line
177
+ // visually sits where a child block would render inside the item.
178
+ &[data-mode='nested'] {
179
+ height: 0;
180
+ background: transparent;
181
+ border-radius: 0;
182
+ border-top: 2px dashed var(--dm-accent, #2563eb);
183
+ box-shadow: none;
184
+ // Lift slightly so the line sits INSIDE the listItem's bottom edge
185
+ // rather than in the gap below - cancels the global +3px nudge and
186
+ // pulls the line 2px above rect.bottom.
187
+ transform: translateY(-2px);
188
+ }
189
+ }
@@ -14,8 +14,11 @@
14
14
  --dm-button-active-color: var(--dm-accent, #2563eb);
15
15
  --dm-button-disabled-opacity: 0.4;
16
16
 
17
- // Smaller icons to match compact buttons
18
- .dm-toolbar-button svg {
17
+ // Smaller icons to match compact buttons. `:not(.dm-dropdown-caret)`
18
+ // preserves the dropdown caret's own 0.625rem sizing from `_toolbar.scss`
19
+ // - without this exclusion the bubble menu's selector wins on specificity
20
+ // and the caret renders the same size as a normal toolbar icon.
21
+ .dm-toolbar-button svg:not(.dm-dropdown-caret) {
19
22
  width: 1rem;
20
23
  height: 1rem;
21
24
  }
@@ -11,7 +11,7 @@
11
11
  width: max-content;
12
12
  }
13
13
 
14
- // "Default" reset button spans the full width of the grid
14
+ // "Default" reset button - spans the full width of the grid
15
15
  .dm-color-palette-reset {
16
16
  grid-column: 1 / -1;
17
17
  display: flex;
@@ -70,7 +70,7 @@
70
70
  0 0 0 3px var(--dm-accent, #2563eb);
71
71
  }
72
72
 
73
- // Active state checkmark indicator
73
+ // Active state - checkmark indicator
74
74
  &--active {
75
75
  box-shadow: 0 0 0 2px var(--dm-toolbar-bg, #f8f9fa),
76
76
  0 0 0 3px var(--dm-accent, #2563eb);
@@ -86,7 +86,7 @@
86
86
  }
87
87
  }
88
88
 
89
- // Dark theme adjust swatch border for dark backgrounds
89
+ // Dark theme - adjust swatch border for dark backgrounds
90
90
  .dm-theme-dark .dm-color-swatch,
91
91
  .dm-theme-auto .dm-color-swatch {
92
92
  border-color: rgba(255, 255, 255, 0.15);
package/src/_content.scss CHANGED
@@ -6,6 +6,19 @@
6
6
  // =============================================================================
7
7
 
8
8
  .dm-editor .ProseMirror {
9
+ // ---------------------------------------------------------------------------
10
+ // Block inline padding
11
+ // ---------------------------------------------------------------------------
12
+ // Pre-applied so that toggling `data-bg-color` on a block does NOT shift
13
+ // text - the colored background paints into existing padding instead of
14
+ // adding new padding (`_block-colors.scss` only sets `background-color`
15
+ // + `border-radius`, no padding). Mirrors Notion's layout. Wrapped in
16
+ // `:where()` so specificity stays at 0 and host apps can override per
17
+ // tag without selector wars.
18
+ :where(p, h1, h2, h3, h4, h5, h6) {
19
+ padding-inline: var(--dm-block-inline-padding);
20
+ }
21
+
9
22
  // ---------------------------------------------------------------------------
10
23
  // Headings
11
24
  // ---------------------------------------------------------------------------
@@ -78,7 +91,11 @@
78
91
  border-left: var(--dm-blockquote-border);
79
92
  color: var(--dm-blockquote-color);
80
93
  margin: 0.5em 0;
81
- padding: 0.1em 0 0.1em 0.8em;
94
+ // Right padding picks up the same `--dm-block-inline-padding` token
95
+ // so colored blockquotes get breathing room from the right edge.
96
+ // Left stays at 0.8em - that gap is for the border-left bar, not
97
+ // text inset.
98
+ padding: 0.1em var(--dm-block-inline-padding) 0.1em 0.8em;
82
99
  }
83
100
 
84
101
  // ---------------------------------------------------------------------------
@@ -89,12 +106,45 @@
89
106
  ol {
90
107
  margin: 0.75em 0;
91
108
  padding-left: 1.5em;
109
+ // Right padding so colored lists get breathing room on the right edge,
110
+ // matching the inline padding paragraphs and headings get above.
111
+ padding-right: var(--dm-block-inline-padding);
112
+ }
92
113
 
114
+ // Notion-style "children zone" indent for plain bullet/ordered lists ONLY.
115
+ // Scoped to `ul:not([data-type="taskList"]), ol` so it does NOT match the
116
+ // taskList wrapper - taskItems render via `<li><label/><div/></li>` and the
117
+ // generic rule would mistakenly apply margin-inline-start to that wrapper
118
+ // div, pushing the entire taskItem content (and the absolutely-positioned
119
+ // checkbox's containing block) one indent step to the right. Task list
120
+ // children-zone styling lives in `_task-list.scss` and is rooted on the
121
+ // inner `<div>` instead.
122
+ ul:not([data-type="taskList"]),
123
+ ol {
93
124
  li {
94
125
  margin: 0.25em 0;
95
126
 
96
127
  > p {
128
+ // List item paragraphs reset inline-end padding (bullet indentation
129
+ // is owned by the list container; doubling up shifts text under the
130
+ // bullet's hanging indent), and add a small inline-start padding
131
+ // (~5px at the editor's default font size) so the text breathes
132
+ // away from the bullet glyph instead of butting against it.
97
133
  margin: 0.1em 0;
134
+ padding-inline: 0.3em 0;
135
+ }
136
+
137
+ // Post-label blocks (heading, codeBlock, blockquote, etc.) sit
138
+ // indented one level below the label paragraph aligned with the
139
+ // bullet. ALL nested `<ul>` and `<ol>` (plain and taskList) are
140
+ // excluded because each already gains the same visual indent
141
+ // through its own `padding-left: 1.5em`, so adding this margin
142
+ // would push nested taskLists further right than nested plain
143
+ // bullet lists. `margin-inline-start` flips the indent to the
144
+ // right edge in RTL.
145
+ > :not(p:first-child):not(ul):not(ol) {
146
+ margin-inline-start: var(--dm-block-children-indent, 1.5rem);
147
+ margin-top: 0.25rem;
98
148
  }
99
149
  }
100
150
  }
@@ -0,0 +1,141 @@
1
+ // =============================================================================
2
+ // Block Context Menu
3
+ // Compact popup shown when the user clicks the ⋮⋮ BlockHandle drag button
4
+ // without dragging. Offers Delete / Duplicate / Turn into options for the
5
+ // targeted top-level block.
6
+ // =============================================================================
7
+
8
+ .dm-block-context-menu {
9
+ position: absolute;
10
+ display: none;
11
+ flex-direction: column;
12
+ min-width: 12rem;
13
+ max-height: 20rem;
14
+ overflow-y: auto;
15
+ // Stop wheel-delta chaining to the page once the menu hits its
16
+ // own scroll edges. The plugin's JS gate handles the no-overflow
17
+ // case where this property doesn't apply.
18
+ overscroll-behavior: contain;
19
+ padding: 0.25rem;
20
+ background: var(--dm-toolbar-bg, var(--dm-bg, #ffffff));
21
+ border: 1px solid var(--dm-border-color, #e5e7eb);
22
+ border-radius: var(--dm-toolbar-border-radius, 0.5rem);
23
+ box-shadow: var(--dm-popover-shadow,
24
+ 0 10px 25px rgba(0, 0, 0, 0.08),
25
+ 0 4px 10px rgba(0, 0, 0, 0.04));
26
+ z-index: var(--dm-z-popover, 50);
27
+
28
+ &[data-show] {
29
+ display: flex;
30
+ }
31
+
32
+ // Slim scrollbar (matches slash-command + floating-menu).
33
+ scrollbar-width: thin;
34
+ scrollbar-color: var(--dm-scrollbar-thumb, rgba(0, 0, 0, 0.18)) transparent;
35
+
36
+ &::-webkit-scrollbar {
37
+ width: 6px;
38
+ }
39
+ &::-webkit-scrollbar-track {
40
+ background: transparent;
41
+ }
42
+ &::-webkit-scrollbar-thumb {
43
+ background: var(--dm-scrollbar-thumb, rgba(0, 0, 0, 0.18));
44
+ border-radius: 3px;
45
+ }
46
+ &::-webkit-scrollbar-thumb:hover {
47
+ background: var(--dm-scrollbar-thumb-hover, rgba(0, 0, 0, 0.28));
48
+ }
49
+
50
+ &-group {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 1px;
54
+ }
55
+
56
+ // Label doubles as the visual separator above its group.
57
+ // `.dm-block-context-menu-group-label` is only emitted by the plugin for
58
+ // the "Turn into" section, so it naturally appears above the second
59
+ // group. Using the label as the divider (instead of styling the group
60
+ // itself) avoids the "double border" case where a labeled group follows
61
+ // another labeled group.
62
+ &-group-label {
63
+ padding: 0.5rem 0.5rem 0.125rem;
64
+ margin-top: 0.25rem;
65
+ border-top: 1px solid var(--dm-border-color, #e5e7eb);
66
+ font-size: 0.6875rem;
67
+ font-weight: 600;
68
+ letter-spacing: 0.04em;
69
+ text-transform: uppercase;
70
+ color: var(--dm-muted, #999);
71
+ user-select: none;
72
+ }
73
+
74
+ &-item {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 0.5rem;
78
+ width: 100%;
79
+ padding: 0.3125rem 0.5rem;
80
+ border: none;
81
+ border-radius: var(--dm-button-border-radius, 0.375rem);
82
+ background: transparent;
83
+ color: var(--dm-editor-text, inherit);
84
+ text-align: left;
85
+ font: inherit;
86
+ font-size: 0.875rem;
87
+ cursor: pointer;
88
+ transition: background-color 0.1s;
89
+
90
+ &:hover,
91
+ &:focus-visible {
92
+ background: var(--dm-button-hover-bg, rgba(0, 0, 0, 0.04));
93
+ }
94
+
95
+ &:focus-visible {
96
+ // INSET ring (-2px): menu items are tightly packed; an outset ring
97
+ // would overlap the neighbour. -2px keeps the ring fully inside the
98
+ // hover background fill.
99
+ outline: 2px solid var(--dm-accent, #2563eb);
100
+ outline-offset: -2px;
101
+ }
102
+ }
103
+
104
+ &-item-icon {
105
+ display: inline-flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ width: 1.125rem;
109
+ height: 1.125rem;
110
+ flex-shrink: 0;
111
+ color: var(--dm-muted, #999);
112
+
113
+ svg {
114
+ width: 1rem;
115
+ height: 1rem;
116
+ }
117
+ }
118
+
119
+ &-item-label {
120
+ flex: 1;
121
+ min-width: 0;
122
+ white-space: nowrap;
123
+ overflow: hidden;
124
+ text-overflow: ellipsis;
125
+ }
126
+ }
127
+
128
+ // =============================================================================
129
+ // Active block highlight
130
+ // Class applied via PM Decoration on the block whose BlockContextMenu is open.
131
+ // =============================================================================
132
+
133
+ .ProseMirror .dm-block-context-active {
134
+ background-color: var(--dm-block-context-active-bg, rgba(55, 53, 47, 0.06));
135
+ border-radius: 0.25rem;
136
+ // box-shadow expands the tint past the block's content box without
137
+ // affecting layout (negative margins would shift neighbours).
138
+ box-shadow: 0 0 0 4px var(--dm-block-context-active-bg, rgba(55, 53, 47, 0.06));
139
+ transition: background-color 0.12s ease, box-shadow 0.12s ease;
140
+ }
141
+