@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.
- package/app/dist/assets/{index-elinREb4.js → index-2hOoPFOR.js} +1 -1
- package/app/dist/assets/index-98ylvoBO.css +1 -0
- package/app/dist/assets/{index-CYeD2ReX.js → index-BEPqnnsd.js} +1 -1
- package/app/dist/assets/{index-D4gUWKEY.js → index-BehCztSl.js} +1 -1
- package/app/dist/assets/{index-CQTW3VDv.js → index-BfYWp0QC.js} +1 -1
- package/app/dist/assets/{index-DKEx6UO3.js → index-BobjskUl.js} +1 -1
- package/app/dist/assets/{index-wScN48Pp.js → index-BsSUa0GD.js} +1 -1
- package/app/dist/assets/{index-DYqwuP7-.js → index-CLZfwYyS.js} +1 -1
- package/app/dist/assets/index-CVzOx0nV.js +372 -0
- package/app/dist/assets/{index-BqSCUOm5.js → index-Ca-wW6uw.js} +1 -1
- package/app/dist/assets/{index-B8gM4RM5.js → index-CdpS6tGk.js} +1 -1
- package/app/dist/assets/{index-BtRwGJ7C.js → index-Cgaw2jCE.js} +1 -1
- package/app/dist/assets/{index-DuLf75hM.js → index-Cq0Maciq.js} +1 -1
- package/app/dist/assets/{index-YJiP4A__.js → index-D5pMhPrg.js} +1 -1
- package/app/dist/assets/{index-BOEUpgE6.js → index-D6vnTt4b.js} +2 -2
- package/app/dist/assets/{index-06CMoKux.js → index-DHALjxX5.js} +1 -1
- package/app/dist/assets/{index-DODIBnvQ.js → index-Dg4A5Pez.js} +1 -1
- package/app/dist/assets/{index-DRB8YvMM.js → index-IU6QYZAa.js} +1 -1
- package/app/dist/assets/{index-kyp-bYm5.js → index-RKEq45V5.js} +1 -1
- package/app/dist/assets/{index-BxYBD4wU.js → index-iGDqoXj_.js} +1 -1
- package/app/dist/index.html +2 -2
- package/app/src/lib/api/client.ts +9 -0
- package/app/src/lib/components/BlockCard.svelte +41 -2
- package/app/src/lib/components/BlockEditPanel.svelte +369 -53
- package/app/src/lib/components/BlockEditor.svelte +136 -357
- package/app/src/lib/components/ContentTree.svelte +181 -0
- package/app/src/lib/components/HeaderBar.svelte +38 -0
- package/app/src/lib/components/InlineEditor.svelte +4 -3
- package/app/src/lib/components/InsertBlockDialog.svelte +429 -0
- package/app/src/lib/components/MarkdownEditor.svelte +1 -1
- package/app/src/lib/components/PageCard.svelte +3 -4
- package/app/src/lib/components/PreviewPane.svelte +20 -2
- package/app/src/lib/components/RuneAttributes.svelte +249 -100
- package/app/src/lib/editor/block-parser.ts +190 -0
- package/app/src/lib/preview/block-renderer.ts +91 -12
- package/dist/community-tags-builder.d.ts +15 -0
- package/dist/community-tags-builder.d.ts.map +1 -0
- package/dist/community-tags-builder.js +70 -0
- package/dist/community-tags-builder.js.map +1 -0
- package/dist/layout-resolver.d.ts +11 -0
- package/dist/layout-resolver.d.ts.map +1 -1
- package/dist/layout-resolver.js +46 -3
- package/dist/layout-resolver.js.map +1 -1
- package/dist/preview-builder.d.ts.map +1 -1
- package/dist/preview-builder.js +11 -3
- package/dist/preview-builder.js.map +1 -1
- package/dist/preview.d.ts +12 -2
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +34 -4
- package/dist/preview.js.map +1 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +91 -13
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/preview-runtime/App.svelte +4 -2
- package/app/dist/assets/index-BYddt_8L.js +0 -324
- 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>
|