@refrakt-md/editor 0.7.1 → 0.8.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.
Files changed (58) hide show
  1. package/app/dist/assets/{index-elinREb4.js → index-2hOoPFOR.js} +1 -1
  2. package/app/dist/assets/index-98ylvoBO.css +1 -0
  3. package/app/dist/assets/{index-CYeD2ReX.js → index-BEPqnnsd.js} +1 -1
  4. package/app/dist/assets/{index-D4gUWKEY.js → index-BehCztSl.js} +1 -1
  5. package/app/dist/assets/{index-CQTW3VDv.js → index-BfYWp0QC.js} +1 -1
  6. package/app/dist/assets/{index-DKEx6UO3.js → index-BobjskUl.js} +1 -1
  7. package/app/dist/assets/{index-wScN48Pp.js → index-BsSUa0GD.js} +1 -1
  8. package/app/dist/assets/{index-DYqwuP7-.js → index-CLZfwYyS.js} +1 -1
  9. package/app/dist/assets/index-CVzOx0nV.js +372 -0
  10. package/app/dist/assets/{index-BqSCUOm5.js → index-Ca-wW6uw.js} +1 -1
  11. package/app/dist/assets/{index-B8gM4RM5.js → index-CdpS6tGk.js} +1 -1
  12. package/app/dist/assets/{index-BtRwGJ7C.js → index-Cgaw2jCE.js} +1 -1
  13. package/app/dist/assets/{index-DuLf75hM.js → index-Cq0Maciq.js} +1 -1
  14. package/app/dist/assets/{index-YJiP4A__.js → index-D5pMhPrg.js} +1 -1
  15. package/app/dist/assets/{index-BOEUpgE6.js → index-D6vnTt4b.js} +2 -2
  16. package/app/dist/assets/{index-06CMoKux.js → index-DHALjxX5.js} +1 -1
  17. package/app/dist/assets/{index-DODIBnvQ.js → index-Dg4A5Pez.js} +1 -1
  18. package/app/dist/assets/{index-DRB8YvMM.js → index-IU6QYZAa.js} +1 -1
  19. package/app/dist/assets/{index-kyp-bYm5.js → index-RKEq45V5.js} +1 -1
  20. package/app/dist/assets/{index-BxYBD4wU.js → index-iGDqoXj_.js} +1 -1
  21. package/app/dist/index.html +2 -2
  22. package/app/src/lib/api/client.ts +9 -0
  23. package/app/src/lib/components/BlockCard.svelte +41 -2
  24. package/app/src/lib/components/BlockEditPanel.svelte +369 -53
  25. package/app/src/lib/components/BlockEditor.svelte +136 -357
  26. package/app/src/lib/components/ContentTree.svelte +181 -0
  27. package/app/src/lib/components/HeaderBar.svelte +38 -0
  28. package/app/src/lib/components/InlineEditor.svelte +4 -3
  29. package/app/src/lib/components/InsertBlockDialog.svelte +429 -0
  30. package/app/src/lib/components/MarkdownEditor.svelte +1 -1
  31. package/app/src/lib/components/PageCard.svelte +3 -4
  32. package/app/src/lib/components/PreviewPane.svelte +20 -2
  33. package/app/src/lib/components/RuneAttributes.svelte +249 -100
  34. package/app/src/lib/editor/block-parser.ts +190 -0
  35. package/app/src/lib/preview/block-renderer.ts +91 -12
  36. package/dist/community-tags-builder.d.ts +15 -0
  37. package/dist/community-tags-builder.d.ts.map +1 -0
  38. package/dist/community-tags-builder.js +70 -0
  39. package/dist/community-tags-builder.js.map +1 -0
  40. package/dist/layout-resolver.d.ts +11 -0
  41. package/dist/layout-resolver.d.ts.map +1 -1
  42. package/dist/layout-resolver.js +46 -3
  43. package/dist/layout-resolver.js.map +1 -1
  44. package/dist/preview-builder.d.ts.map +1 -1
  45. package/dist/preview-builder.js +11 -3
  46. package/dist/preview-builder.js.map +1 -1
  47. package/dist/preview.d.ts +12 -2
  48. package/dist/preview.d.ts.map +1 -1
  49. package/dist/preview.js +34 -4
  50. package/dist/preview.js.map +1 -1
  51. package/dist/server.d.ts +5 -1
  52. package/dist/server.d.ts.map +1 -1
  53. package/dist/server.js +91 -13
  54. package/dist/server.js.map +1 -1
  55. package/package.json +6 -6
  56. package/preview-runtime/App.svelte +4 -2
  57. package/app/dist/assets/index-BYddt_8L.js +0 -324
  58. package/app/dist/assets/index-DlrXwdpb.css +0 -1
@@ -0,0 +1,181 @@
1
+ <script lang="ts">
2
+ import type { ContentNode } from '../editor/block-parser.js';
3
+
4
+ interface Props {
5
+ nodes: ContentNode[];
6
+ activePath: number[];
7
+ onselect: (path: number[]) => void;
8
+ // Root node (only at top level)
9
+ rootLabel?: string;
10
+ onrootclick?: () => void;
11
+ isRootActive?: boolean;
12
+ // Internal (for recursion)
13
+ depth?: number;
14
+ pathPrefix?: number[];
15
+ }
16
+
17
+ let {
18
+ nodes, activePath, onselect,
19
+ rootLabel, onrootclick, isRootActive = false,
20
+ depth = 0, pathPrefix = [],
21
+ }: Props = $props();
22
+
23
+ function isActivePath(path: number[]): boolean {
24
+ if (path.length !== activePath.length) return false;
25
+ return path.every((v, i) => v === activePath[i]);
26
+ }
27
+
28
+ function isInActivePath(path: number[]): boolean {
29
+ if (path.length > activePath.length) return false;
30
+ return path.every((v, i) => v === activePath[i]);
31
+ }
32
+
33
+ const TYPE_ICONS: Record<string, string> = {
34
+ heading: 'M3 3v10M13 3v10M3 8h10',
35
+ paragraph: 'M2 4h12M2 8h12M2 12h8',
36
+ fence: 'M5 4L2 8l3 4M11 4l3 4-3 4',
37
+ list: 'M4 4h10M4 8h10M4 12h10M2 4h0M2 8h0M2 12h0',
38
+ quote: 'M4 4h8M4 7h6M1 3v8',
39
+ hr: 'M2 8h12',
40
+ image: 'M2 2h12v12H2zM5 5l-3 7h12l-4-5-2 2.5',
41
+ };
42
+ </script>
43
+
44
+ <!-- Root node (only rendered at top level) -->
45
+ {#if rootLabel && onrootclick}
46
+ <div class="tree-node">
47
+ <button
48
+ type="button"
49
+ class="tree-node__btn tree-node__btn--rune"
50
+ class:active={isRootActive}
51
+ onclick={onrootclick}
52
+ >
53
+ <span class="tree-node__indicator">▾</span>
54
+ <span class="tree-node__rune-dot"></span>
55
+ <span class="tree-node__label tree-node__label--rune">{rootLabel}</span>
56
+ </button>
57
+ </div>
58
+ {/if}
59
+
60
+ {#each nodes as node, i}
61
+ {@const nodePath = [...pathPrefix, i]}
62
+ {@const isRune = node.type === 'rune'}
63
+ {@const isActive = isActivePath(nodePath)}
64
+ {@const isAncestor = isInActivePath(nodePath) && !isActive}
65
+ {@const hasChildren = isRune && !node.selfClosing && node.children && node.children.length > 0}
66
+
67
+ <div class="tree-node" style="padding-left: {(depth + (rootLabel ? 1 : 0)) * 16}px">
68
+ {#if isRune}
69
+ <button
70
+ type="button"
71
+ class="tree-node__btn tree-node__btn--rune"
72
+ class:active={isActive}
73
+ class:ancestor={isAncestor}
74
+ onclick={() => onselect(nodePath)}
75
+ >
76
+ <span class="tree-node__indicator">{hasChildren ? '▾' : '▸'}</span>
77
+ <span class="tree-node__rune-dot"></span>
78
+ <span class="tree-node__label tree-node__label--rune">{node.label}</span>
79
+ </button>
80
+ {:else}
81
+ <button
82
+ type="button"
83
+ class="tree-node__btn tree-node__btn--content"
84
+ class:active={isActive}
85
+ onclick={() => onselect(nodePath)}
86
+ >
87
+ <svg class="tree-node__icon" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
88
+ <path d={TYPE_ICONS[node.type] ?? 'M2 4h12M2 8h12M2 12h8'} />
89
+ </svg>
90
+ <span class="tree-node__label tree-node__label--content">{node.label}</span>
91
+ </button>
92
+ {/if}
93
+ </div>
94
+
95
+ {#if hasChildren && node.children}
96
+ <svelte:self
97
+ nodes={node.children}
98
+ {activePath}
99
+ {onselect}
100
+ depth={depth + 1}
101
+ pathPrefix={nodePath}
102
+ />
103
+ {/if}
104
+ {/each}
105
+
106
+ <style>
107
+ .tree-node {
108
+ display: flex;
109
+ }
110
+
111
+ .tree-node__btn {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 0.35rem;
115
+ width: 100%;
116
+ padding: 0.2rem 0.4rem;
117
+ border: none;
118
+ border-radius: calc(var(--ed-radius-sm, 4px) - 1px);
119
+ background: transparent;
120
+ color: var(--ed-text-secondary);
121
+ font-size: var(--ed-text-base);
122
+ cursor: pointer;
123
+ text-align: left;
124
+ transition: background var(--ed-transition-fast);
125
+ }
126
+
127
+ .tree-node__btn:hover {
128
+ background: var(--ed-surface-2);
129
+ }
130
+
131
+ .tree-node__btn.active {
132
+ background: var(--ed-accent-muted);
133
+ color: var(--ed-accent);
134
+ }
135
+
136
+ .tree-node__btn.ancestor {
137
+ color: var(--ed-text-primary);
138
+ }
139
+
140
+ .tree-node__btn--content {
141
+ color: var(--ed-text-muted);
142
+ font-size: var(--ed-text-sm);
143
+ }
144
+
145
+ .tree-node__btn--content.active {
146
+ background: var(--ed-surface-2);
147
+ color: var(--ed-text-secondary);
148
+ }
149
+
150
+ .tree-node__indicator {
151
+ width: 12px;
152
+ font-size: 10px;
153
+ color: var(--ed-text-muted);
154
+ flex-shrink: 0;
155
+ text-align: center;
156
+ }
157
+
158
+ .tree-node__rune-dot {
159
+ width: 5px;
160
+ height: 5px;
161
+ border-radius: 50%;
162
+ background: var(--ed-warning);
163
+ flex-shrink: 0;
164
+ }
165
+
166
+ .tree-node__icon {
167
+ flex-shrink: 0;
168
+ opacity: 0.5;
169
+ }
170
+
171
+ .tree-node__label--rune {
172
+ font-weight: 500;
173
+ }
174
+
175
+ .tree-node__label--content {
176
+ overflow: hidden;
177
+ text-overflow: ellipsis;
178
+ white-space: nowrap;
179
+ min-width: 0;
180
+ }
181
+ </style>
@@ -113,6 +113,37 @@
113
113
  </svg>
114
114
  </button>
115
115
  </div>
116
+ <span class="header__divider"></span>
117
+ <div class="header__group">
118
+ <button
119
+ class="header__btn header__btn--icon"
120
+ class:header__btn--active={editorState.previewTheme === 'light'}
121
+ onclick={() => editorState.previewTheme = 'light'}
122
+ title="Light mode"
123
+ >
124
+ <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
125
+ <circle cx="8" cy="8" r="3" />
126
+ <line x1="8" y1="1.5" x2="8" y2="3" />
127
+ <line x1="8" y1="13" x2="8" y2="14.5" />
128
+ <line x1="2.4" y1="2.4" x2="3.5" y2="3.5" />
129
+ <line x1="12.5" y1="12.5" x2="13.6" y2="13.6" />
130
+ <line x1="1.5" y1="8" x2="3" y2="8" />
131
+ <line x1="13" y1="8" x2="14.5" y2="8" />
132
+ <line x1="2.4" y1="13.6" x2="3.5" y2="12.5" />
133
+ <line x1="12.5" y1="3.5" x2="13.6" y2="2.4" />
134
+ </svg>
135
+ </button>
136
+ <button
137
+ class="header__btn header__btn--icon"
138
+ class:header__btn--active={editorState.previewTheme === 'dark'}
139
+ onclick={() => editorState.previewTheme = 'dark'}
140
+ title="Dark mode"
141
+ >
142
+ <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
143
+ <path d="M13.5 8.5a5.5 5.5 0 0 1-7-7 5.5 5.5 0 1 0 7 7Z" />
144
+ </svg>
145
+ </button>
146
+ </div>
116
147
  {:else if isEdit}
117
148
  <div class="header__group">
118
149
  <button
@@ -253,6 +284,13 @@
253
284
  gap: var(--ed-space-2);
254
285
  }
255
286
 
287
+ .header__divider {
288
+ width: 1px;
289
+ height: 16px;
290
+ background: var(--ed-border-strong);
291
+ opacity: 0.5;
292
+ }
293
+
256
294
  /* ── Unified button style ─────────────────────────── */
257
295
 
258
296
  .header__btn {
@@ -18,10 +18,11 @@
18
18
  content: string;
19
19
  onchange: (content: string) => void;
20
20
  runes: () => RuneInfo[];
21
+ aggregated?: () => Record<string, unknown>;
21
22
  language?: string;
22
23
  }
23
24
 
24
- let { content, onchange, runes, language }: Props = $props();
25
+ let { content, onchange, runes, aggregated, language }: Props = $props();
25
26
 
26
27
  let container: HTMLElement;
27
28
  let view = $state<EditorView | undefined>(undefined);
@@ -127,7 +128,7 @@
127
128
  completionCompartment.of(codeMode ? [] : autocompletion({
128
129
  override: [
129
130
  runeCompletionSource(runes),
130
- attributeCompletionSource(runes),
131
+ attributeCompletionSource(runes, aggregated),
131
132
  ],
132
133
  icons: false,
133
134
  })),
@@ -175,7 +176,7 @@
175
176
  completionCompartment.reconfigure(autocompletion({
176
177
  override: [
177
178
  runeCompletionSource(runes),
178
- attributeCompletionSource(runes),
179
+ attributeCompletionSource(runes, aggregated),
179
180
  ],
180
181
  icons: false,
181
182
  })),
@@ -0,0 +1,429 @@
1
+ <script lang="ts">
2
+ import type { RuneInfo } from '../api/client.js';
3
+
4
+ interface Props {
5
+ runes: RuneInfo[];
6
+ runesByCategory: Map<string, RuneInfo[]>;
7
+ oninsert: (type: 'heading' | 'paragraph' | 'fence' | 'hr' | 'rune', runeName?: string) => void;
8
+ onclose: () => void;
9
+ }
10
+
11
+ let { runes, runesByCategory, oninsert, onclose }: Props = $props();
12
+
13
+ // Preferred tab order
14
+ const TAB_ORDER = ['Content', 'Section', 'Layout', 'Code & Data', 'Semantic', 'Design', 'Site'];
15
+
16
+ let activeTab = $state('Content');
17
+ let search = $state('');
18
+ let dialogEl: HTMLDialogElement;
19
+ let searchEl: HTMLInputElement;
20
+
21
+ // Sorted category names following TAB_ORDER
22
+ let categories = $derived.by(() => {
23
+ const present = new Set(runesByCategory.keys());
24
+ const ordered = TAB_ORDER.filter(c => present.has(c));
25
+ // Append any categories not in TAB_ORDER
26
+ for (const c of present) {
27
+ if (!ordered.includes(c)) ordered.push(c);
28
+ }
29
+ return ordered;
30
+ });
31
+
32
+ // Filtered runes when searching
33
+ let searchResults = $derived.by(() => {
34
+ if (!search.trim()) return null;
35
+ const q = search.toLowerCase();
36
+ return runes.filter(r =>
37
+ r.name.toLowerCase().includes(q) ||
38
+ (r.description && r.description.toLowerCase().includes(q))
39
+ );
40
+ });
41
+
42
+ // Standard content blocks (shown in Content tab)
43
+ const standardBlocks = [
44
+ { type: 'heading' as const, name: 'Heading', description: 'Section heading', icon: 'M3 3v10M13 3v10M3 8h10' },
45
+ { type: 'paragraph' as const, name: 'Paragraph', description: 'Body text', icon: 'M2 4h12M2 8h12M2 12h8' },
46
+ { type: 'fence' as const, name: 'Code Block', description: 'Fenced code', icon: 'M5 4L2 8l3 4M11 4l3 4-3 4' },
47
+ { type: 'hr' as const, name: 'Divider', description: 'Horizontal rule', icon: 'M2 8h12' },
48
+ ];
49
+
50
+ $effect(() => {
51
+ dialogEl?.showModal();
52
+ // Focus search on open
53
+ searchEl?.focus();
54
+ });
55
+
56
+ function handleBackdropClick(e: MouseEvent) {
57
+ if (e.target === dialogEl) onclose();
58
+ }
59
+
60
+ function handleInsertStandard(type: 'heading' | 'paragraph' | 'fence' | 'hr') {
61
+ oninsert(type);
62
+ }
63
+
64
+ function handleInsertRune(name: string) {
65
+ oninsert('rune', name);
66
+ }
67
+ </script>
68
+
69
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
70
+ <dialog
71
+ bind:this={dialogEl}
72
+ class="insert-dialog"
73
+ onclose={onclose}
74
+ onclick={handleBackdropClick}
75
+ onkeydown={(e) => { if (e.key === 'Escape') { e.preventDefault(); onclose(); } }}
76
+ >
77
+ <div class="insert-dialog__inner">
78
+ <!-- Header -->
79
+ <div class="insert-dialog__header">
80
+ <h2 class="insert-dialog__title">Insert Block</h2>
81
+ <div class="insert-dialog__search-wrap">
82
+ <svg class="insert-dialog__search-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
83
+ <circle cx="7" cy="7" r="4.5" />
84
+ <path d="M10.5 10.5L14 14" />
85
+ </svg>
86
+ <input
87
+ bind:this={searchEl}
88
+ class="insert-dialog__search"
89
+ type="text"
90
+ placeholder="Search blocks..."
91
+ bind:value={search}
92
+ />
93
+ </div>
94
+ <button class="insert-dialog__close" onclick={onclose}>
95
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
96
+ <path d="M4 4l8 8M12 4l-8 8" />
97
+ </svg>
98
+ </button>
99
+ </div>
100
+
101
+ <!-- Tabs -->
102
+ {#if !searchResults}
103
+ <div class="insert-dialog__tabs">
104
+ {#each categories as category}
105
+ <button
106
+ type="button"
107
+ class="insert-dialog__tab"
108
+ class:active={activeTab === category}
109
+ onclick={() => activeTab = category}
110
+ >
111
+ {category}
112
+ </button>
113
+ {/each}
114
+ </div>
115
+ {/if}
116
+
117
+ <!-- Body -->
118
+ <div class="insert-dialog__body">
119
+ {#if searchResults}
120
+ <!-- Search results -->
121
+ {#if searchResults.length === 0}
122
+ <div class="insert-dialog__empty">No blocks match "{search}"</div>
123
+ {:else}
124
+ <div class="insert-dialog__grid">
125
+ {#each searchResults as rune}
126
+ <button
127
+ class="insert-dialog__btn insert-dialog__btn--rune"
128
+ onclick={() => handleInsertRune(rune.name)}
129
+ >
130
+ <span class="insert-dialog__rune-dot"></span>
131
+ <span class="insert-dialog__rune-info">
132
+ <span class="insert-dialog__rune-name">{rune.name}</span>
133
+ {#if rune.description}
134
+ <span class="insert-dialog__rune-desc">{rune.description}</span>
135
+ {/if}
136
+ </span>
137
+ </button>
138
+ {/each}
139
+ </div>
140
+ {/if}
141
+ {:else}
142
+ <!-- Tab content -->
143
+ <div class="insert-dialog__grid">
144
+ {#if activeTab === 'Content'}
145
+ <!-- Standard blocks first -->
146
+ {#each standardBlocks as block}
147
+ <button
148
+ class="insert-dialog__btn"
149
+ onclick={() => handleInsertStandard(block.type)}
150
+ >
151
+ <svg class="insert-dialog__btn-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
152
+ <path d={block.icon} />
153
+ </svg>
154
+ <span class="insert-dialog__btn-info">
155
+ <span class="insert-dialog__btn-name">{block.name}</span>
156
+ <span class="insert-dialog__btn-desc">{block.description}</span>
157
+ </span>
158
+ </button>
159
+ {/each}
160
+ {/if}
161
+ <!-- Runes for active category -->
162
+ {#each runesByCategory.get(activeTab) ?? [] as rune}
163
+ <button
164
+ class="insert-dialog__btn insert-dialog__btn--rune"
165
+ onclick={() => handleInsertRune(rune.name)}
166
+ >
167
+ <span class="insert-dialog__rune-dot"></span>
168
+ <span class="insert-dialog__rune-info">
169
+ <span class="insert-dialog__rune-name">{rune.name}</span>
170
+ {#if rune.description}
171
+ <span class="insert-dialog__rune-desc">{rune.description}</span>
172
+ {/if}
173
+ </span>
174
+ </button>
175
+ {/each}
176
+ </div>
177
+ {/if}
178
+ </div>
179
+ </div>
180
+ </dialog>
181
+
182
+ <style>
183
+ .insert-dialog {
184
+ border: none;
185
+ border-radius: var(--ed-radius-lg, 12px);
186
+ background: var(--ed-surface-0);
187
+ box-shadow: var(--ed-shadow-lg);
188
+ padding: 0;
189
+ margin: auto;
190
+ width: 960px;
191
+ max-width: calc(100vw - 2rem);
192
+ height: min(600px, calc(100vh - 4rem));
193
+ display: flex;
194
+ flex-direction: column;
195
+ overflow: hidden;
196
+ animation: dialog-enter 0.15s ease-out;
197
+ }
198
+
199
+ .insert-dialog::backdrop {
200
+ background: rgba(0, 0, 0, 0.3);
201
+ backdrop-filter: blur(2px);
202
+ animation: backdrop-fade 0.15s ease-out;
203
+ }
204
+
205
+ @keyframes dialog-enter {
206
+ from { opacity: 0; transform: translateY(8px) scale(0.98); }
207
+ to { opacity: 1; transform: translateY(0) scale(1); }
208
+ }
209
+
210
+ @keyframes backdrop-fade {
211
+ from { opacity: 0; }
212
+ to { opacity: 1; }
213
+ }
214
+
215
+ .insert-dialog__inner {
216
+ display: flex;
217
+ flex-direction: column;
218
+ min-height: 0;
219
+ height: 100%;
220
+ }
221
+
222
+ /* Header */
223
+ .insert-dialog__header {
224
+ display: flex;
225
+ align-items: center;
226
+ gap: var(--ed-space-3);
227
+ padding: var(--ed-space-3) var(--ed-space-4);
228
+ border-bottom: 1px solid var(--ed-border-subtle);
229
+ flex-shrink: 0;
230
+ }
231
+
232
+ .insert-dialog__title {
233
+ font-size: var(--ed-text-md);
234
+ font-weight: 600;
235
+ color: var(--ed-text-primary);
236
+ margin: 0;
237
+ white-space: nowrap;
238
+ }
239
+
240
+ .insert-dialog__search-wrap {
241
+ flex: 1;
242
+ position: relative;
243
+ max-width: 240px;
244
+ margin-left: auto;
245
+ }
246
+
247
+ .insert-dialog__search-icon {
248
+ position: absolute;
249
+ left: 0.5rem;
250
+ top: 50%;
251
+ transform: translateY(-50%);
252
+ color: var(--ed-text-muted);
253
+ pointer-events: none;
254
+ }
255
+
256
+ .insert-dialog__search {
257
+ width: 100%;
258
+ padding: var(--ed-space-1) var(--ed-space-2) var(--ed-space-1) 1.8rem;
259
+ border: 1px solid var(--ed-border-default);
260
+ border-radius: var(--ed-radius-sm);
261
+ font-size: var(--ed-text-sm);
262
+ color: var(--ed-text-primary);
263
+ background: var(--ed-surface-1);
264
+ outline: none;
265
+ }
266
+
267
+ .insert-dialog__search:focus {
268
+ border-color: var(--ed-accent);
269
+ box-shadow: 0 0 0 2px var(--ed-accent-ring);
270
+ }
271
+
272
+ .insert-dialog__close {
273
+ flex-shrink: 0;
274
+ display: flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ width: 28px;
278
+ height: 28px;
279
+ border: none;
280
+ border-radius: var(--ed-radius-sm);
281
+ background: transparent;
282
+ color: var(--ed-text-muted);
283
+ cursor: pointer;
284
+ transition: background var(--ed-transition-fast), color var(--ed-transition-fast);
285
+ }
286
+
287
+ .insert-dialog__close:hover {
288
+ background: var(--ed-surface-2);
289
+ color: var(--ed-text-secondary);
290
+ }
291
+
292
+ /* Tabs */
293
+ .insert-dialog__tabs {
294
+ display: flex;
295
+ gap: 2px;
296
+ padding: var(--ed-space-2) var(--ed-space-4);
297
+ border-bottom: 1px solid var(--ed-border-subtle);
298
+ background: var(--ed-surface-1);
299
+ flex-shrink: 0;
300
+ overflow-x: auto;
301
+ }
302
+
303
+ .insert-dialog__tab {
304
+ padding: 0.3rem 0.7rem;
305
+ border: none;
306
+ background: transparent;
307
+ color: var(--ed-text-muted);
308
+ font-size: var(--ed-text-sm);
309
+ font-weight: 500;
310
+ cursor: pointer;
311
+ border-radius: var(--ed-radius-sm);
312
+ white-space: nowrap;
313
+ transition: background var(--ed-transition-fast), color var(--ed-transition-fast);
314
+ }
315
+
316
+ .insert-dialog__tab:hover {
317
+ color: var(--ed-text-secondary);
318
+ background: var(--ed-surface-2);
319
+ }
320
+
321
+ .insert-dialog__tab.active {
322
+ background: var(--ed-surface-0);
323
+ color: var(--ed-text-primary);
324
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
325
+ }
326
+
327
+ /* Body */
328
+ .insert-dialog__body {
329
+ flex: 1;
330
+ min-height: 0;
331
+ overflow-y: auto;
332
+ padding: var(--ed-space-4);
333
+ }
334
+
335
+ .insert-dialog__empty {
336
+ text-align: center;
337
+ padding: 2rem;
338
+ color: var(--ed-text-muted);
339
+ font-size: var(--ed-text-sm);
340
+ }
341
+
342
+ /* Grid */
343
+ .insert-dialog__grid {
344
+ display: grid;
345
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
346
+ gap: 0.4rem;
347
+ }
348
+
349
+ /* Block buttons */
350
+ .insert-dialog__btn {
351
+ display: flex;
352
+ align-items: flex-start;
353
+ gap: 0.5rem;
354
+ padding: var(--ed-space-2) var(--ed-space-3);
355
+ border: 1px solid var(--ed-border-default);
356
+ border-radius: var(--ed-radius-sm);
357
+ background: var(--ed-surface-0);
358
+ color: var(--ed-text-secondary);
359
+ font-size: var(--ed-text-sm);
360
+ cursor: pointer;
361
+ text-align: left;
362
+ transition: background var(--ed-transition-fast), border-color var(--ed-transition-fast);
363
+ }
364
+
365
+ .insert-dialog__btn:hover {
366
+ background: var(--ed-accent-muted);
367
+ border-color: var(--ed-accent);
368
+ color: var(--ed-heading);
369
+ }
370
+
371
+ .insert-dialog__btn-icon {
372
+ flex-shrink: 0;
373
+ opacity: 0.6;
374
+ margin-top: 0.15rem;
375
+ }
376
+
377
+ .insert-dialog__btn-info {
378
+ display: flex;
379
+ flex-direction: column;
380
+ gap: 0.1rem;
381
+ min-width: 0;
382
+ }
383
+
384
+ .insert-dialog__btn-name {
385
+ font-weight: 500;
386
+ }
387
+
388
+ .insert-dialog__btn-desc {
389
+ font-size: 10px;
390
+ color: var(--ed-text-muted);
391
+ overflow: hidden;
392
+ text-overflow: ellipsis;
393
+ white-space: nowrap;
394
+ }
395
+
396
+ /* Rune buttons */
397
+ .insert-dialog__rune-dot {
398
+ width: 6px;
399
+ height: 6px;
400
+ border-radius: 50%;
401
+ background: var(--ed-warning);
402
+ flex-shrink: 0;
403
+ margin-top: 0.35rem;
404
+ }
405
+
406
+ .insert-dialog__rune-info {
407
+ display: flex;
408
+ flex-direction: column;
409
+ gap: 0.1rem;
410
+ min-width: 0;
411
+ }
412
+
413
+ .insert-dialog__rune-name {
414
+ font-weight: 500;
415
+ }
416
+
417
+ .insert-dialog__rune-desc {
418
+ font-size: 10px;
419
+ color: var(--ed-text-muted);
420
+ overflow: hidden;
421
+ text-overflow: ellipsis;
422
+ white-space: nowrap;
423
+ }
424
+
425
+ .insert-dialog__btn--rune:hover {
426
+ background: var(--ed-warning-subtle);
427
+ border-color: var(--ed-warning);
428
+ }
429
+ </style>
@@ -54,7 +54,7 @@
54
54
  autocompletion({
55
55
  override: [
56
56
  runeCompletionSource(() => editorState.runes),
57
- attributeCompletionSource(() => editorState.runes),
57
+ attributeCompletionSource(() => editorState.runes, () => editorState.aggregated),
58
58
  ],
59
59
  icons: false,
60
60
  }),