@qalma/kit 0.1.0-beta.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.
@@ -0,0 +1,2467 @@
1
+ import { clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import * as i0 from '@angular/core';
4
+ import { inject, ElementRef, input, computed, Directive, ChangeDetectionStrategy, Component, signal, DestroyRef, afterNextRender, output, effect } from '@angular/core';
5
+ import { cva } from 'class-variance-authority';
6
+ import { NgIcon, provideIcons } from '@ng-icons/core';
7
+ import { QalmaCommand, QalmaToolbar } from '@qalma/editor';
8
+ import { NgTemplateOutlet } from '@angular/common';
9
+ import { lucideUnlink, lucideUndo2, lucideUnderline, lucideTrash2, lucideTextQuote, lucideTable, lucideSuperscript, lucideSubscript, lucideStrikethrough, lucideSquareCode, lucideRows3, lucideRedo2, lucidePilcrow, lucideOutdent, lucideListTodo, lucideListOrdered, lucideList, lucideLetterText, lucideItalic, lucideIndent, lucideHeading3, lucideHeading2, lucideHeading1, lucideHeading, lucideEraser, lucideColumns3, lucideCode, lucideBold, lucideBetweenVerticalEnd, lucideBetweenHorizontalEnd, lucideAlignRight, lucideAlignLeft, lucideAlignJustify, lucideAlignCenter, lucideMousePointer2, lucideGripVertical, lucideCopy, lucideArrowUp, lucideArrowDown, lucideMinus, lucideX, lucidePencil, lucideLink, lucideExternalLink, lucideCheck } from '@ng-icons/lucide';
10
+
11
+ function cn(...inputs) {
12
+ return twMerge(clsx(inputs));
13
+ }
14
+
15
+ const buttonVariants = cva('inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_ng-icon]:shrink-0', {
16
+ variants: {
17
+ variant: {
18
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
19
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20
+ outline: 'border border-border bg-card text-foreground hover:bg-secondary hover:text-secondary-foreground',
21
+ ghost: 'text-foreground hover:bg-secondary hover:text-secondary-foreground',
22
+ accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
23
+ link: 'text-accent underline-offset-4 hover:underline',
24
+ },
25
+ size: {
26
+ default: 'h-9 px-4 py-2',
27
+ sm: 'h-8 rounded-md px-3 text-xs',
28
+ lg: 'h-11 rounded-md px-6 text-base',
29
+ icon: 'h-9 w-9',
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: 'default',
34
+ size: 'default',
35
+ },
36
+ });
37
+ /**
38
+ * Headless-styled button directive so it composes onto native `<button>` and
39
+ * `<a>` elements while reading from the host's Tailwind design tokens.
40
+ */
41
+ class QalmaButton {
42
+ host = inject(ElementRef);
43
+ initialClass = this.host.nativeElement.getAttribute('class') ?? '';
44
+ variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
45
+ size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
46
+ computedClass = computed(() => cn(buttonVariants({ variant: this.variant(), size: this.size() }), this.initialClass), ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
47
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaButton, deps: [], target: i0.ɵɵFactoryTarget.Directive });
48
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: QalmaButton, isStandalone: true, selector: "button[qalmaBtn], a[qalmaBtn]", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "computedClass()" } }, ngImport: i0 });
49
+ }
50
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaButton, decorators: [{
51
+ type: Directive,
52
+ args: [{
53
+ selector: 'button[qalmaBtn], a[qalmaBtn]',
54
+ host: {
55
+ '[class]': 'computedClass()',
56
+ },
57
+ }]
58
+ }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
59
+
60
+ /**
61
+ * Default styling for a toolbar command button. Icon-only, square, and driven
62
+ * entirely by the host's shadcn-style design tokens (`text-muted-foreground`,
63
+ * `bg-secondary`, `bg-accent-subtle`…) so it themes with the surrounding app.
64
+ * The `qalma-command-active` class is toggled by the `QalmaCommand` directive
65
+ * when the underlying command is active.
66
+ */
67
+ const TOOLBAR_BUTTON_CLASS = 'inline-flex h-[1.85rem] w-[1.85rem] cursor-pointer items-center justify-center rounded-[0.4rem] border border-transparent text-muted-foreground transition hover:bg-secondary hover:text-foreground disabled:cursor-not-allowed disabled:opacity-45 [&.qalma-command-active]:border-accent/40 [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent';
68
+ /**
69
+ * Generic, declarative toolbar button for a single editor command.
70
+ *
71
+ * Renders a native `<button>` wired to the `QalmaCommand` directive (execute /
72
+ * active / disabled / aria) with an `<ng-icon>`, so a whole toolbar becomes a
73
+ * list of `<qalma-toolbar-button command icon label />` instead of the
74
+ * six-line `<button qalmaCommand><ng-icon></button>` block repeated per
75
+ * command. The host uses `display: contents` so the inner button participates
76
+ * directly in the surrounding toolbar's flex layout.
77
+ *
78
+ * Icons resolve from the `NgIcon` provider scope of the surrounding component
79
+ * (register them with `provideIcons({ … })` as usual).
80
+ */
81
+ class QalmaToolbarButton {
82
+ /** Command name to execute, e.g. `toggleBold`. */
83
+ command = input.required(...(ngDevMode ? [{ debugName: "command" }] : /* istanbul ignore next */ []));
84
+ /** Optional command payload (forwarded to `qalmaCommandValue`). */
85
+ value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
86
+ /** Lucide (or any registered) icon name, e.g. `lucideBold`. */
87
+ icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
88
+ /** Accessible name — used for both `title` and `aria-label`. */
89
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
90
+ /** Optional icon-size class override (default: `text-[0.9rem]`). */
91
+ iconClass = input('text-[0.9rem]', ...(ngDevMode ? [{ debugName: "iconClass" }] : /* istanbul ignore next */ []));
92
+ buttonClass = TOOLBAR_BUTTON_CLASS;
93
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaToolbarButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
94
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.17", type: QalmaToolbarButton, isStandalone: true, selector: "qalma-toolbar-button", inputs: { command: { classPropertyName: "command", publicName: "command", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, iconClass: { classPropertyName: "iconClass", publicName: "iconClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
95
+ <button
96
+ type="button"
97
+ [qalmaCommand]="command()"
98
+ [qalmaCommandValue]="value()"
99
+ [class]="buttonClass"
100
+ [title]="label()"
101
+ [attr.aria-label]="label()"
102
+ >
103
+ <ng-icon [class]="iconClass()" [name]="icon()" aria-hidden="true" />
104
+ </button>
105
+ `, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: QalmaCommand, selector: "button[qalmaCommand]", inputs: ["qalmaCommand", "qalmaCommandValue"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
106
+ }
107
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaToolbarButton, decorators: [{
108
+ type: Component,
109
+ args: [{
110
+ selector: 'qalma-toolbar-button',
111
+ changeDetection: ChangeDetectionStrategy.OnPush,
112
+ imports: [NgIcon, QalmaCommand],
113
+ host: {
114
+ class: 'contents',
115
+ },
116
+ template: `
117
+ <button
118
+ type="button"
119
+ [qalmaCommand]="command()"
120
+ [qalmaCommandValue]="value()"
121
+ [class]="buttonClass"
122
+ [title]="label()"
123
+ [attr.aria-label]="label()"
124
+ >
125
+ <ng-icon [class]="iconClass()" [name]="icon()" aria-hidden="true" />
126
+ </button>
127
+ `,
128
+ }]
129
+ }], propDecorators: { command: [{ type: i0.Input, args: [{ isSignal: true, alias: "command", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], iconClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconClass", required: false }] }] } });
130
+
131
+ function isToolbarCommandItem(item) {
132
+ return 'command' in item;
133
+ }
134
+ // ---------------------------------------------------------------------------
135
+ // Default command fragments — the declarative source of truth for the standard
136
+ // editor toolbar, reusable across consumers (playground, sandbox, downstream
137
+ // apps). Compose them into `ToolbarGroup[]` and hand them to
138
+ // `<qalma-toolbar-registry>`; interleave custom controls with `ToolbarTemplateItem`.
139
+ // ---------------------------------------------------------------------------
140
+ const QALMA_TOOLBAR_HEADINGS = [
141
+ { command: 'setParagraph', icon: 'lucidePilcrow', label: 'Paragraph' },
142
+ { command: 'toggleHeading1', icon: 'lucideHeading1', label: 'Heading 1' },
143
+ { command: 'toggleHeading2', icon: 'lucideHeading2', label: 'Heading 2' },
144
+ { command: 'toggleHeading3', icon: 'lucideHeading3', label: 'Heading 3' },
145
+ ];
146
+ const QALMA_TOOLBAR_INLINE_MARKS = [
147
+ { command: 'toggleBold', icon: 'lucideBold', label: 'Bold' },
148
+ { command: 'toggleItalic', icon: 'lucideItalic', label: 'Italic' },
149
+ { command: 'toggleUnderline', icon: 'lucideUnderline', label: 'Underline' },
150
+ {
151
+ command: 'toggleStrike',
152
+ icon: 'lucideStrikethrough',
153
+ label: 'Strikethrough',
154
+ },
155
+ { command: 'toggleInlineCode', icon: 'lucideCode', label: 'Inline code' },
156
+ { command: 'toggleMonospace', icon: 'lucideLetterText', label: 'Monospace' },
157
+ { command: 'toggleSubscript', icon: 'lucideSubscript', label: 'Subscript' },
158
+ {
159
+ command: 'toggleSuperscript',
160
+ icon: 'lucideSuperscript',
161
+ label: 'Superscript',
162
+ },
163
+ ];
164
+ const QALMA_TOOLBAR_ALIGN = [
165
+ { command: 'setTextAlignLeft', icon: 'lucideAlignLeft', label: 'Align left' },
166
+ {
167
+ command: 'setTextAlignCenter',
168
+ icon: 'lucideAlignCenter',
169
+ label: 'Align center',
170
+ },
171
+ {
172
+ command: 'setTextAlignRight',
173
+ icon: 'lucideAlignRight',
174
+ label: 'Align right',
175
+ },
176
+ {
177
+ command: 'setTextAlignJustify',
178
+ icon: 'lucideAlignJustify',
179
+ label: 'Justify',
180
+ },
181
+ ];
182
+ const QALMA_TOOLBAR_LISTS = [
183
+ { command: 'toggleBulletList', icon: 'lucideList', label: 'Bullet list' },
184
+ {
185
+ command: 'toggleOrderedList',
186
+ icon: 'lucideListOrdered',
187
+ label: 'Ordered list',
188
+ },
189
+ { command: 'toggleTaskList', icon: 'lucideListTodo', label: 'Task list' },
190
+ { command: 'liftListItem', icon: 'lucideOutdent', label: 'Outdent' },
191
+ { command: 'sinkListItem', icon: 'lucideIndent', label: 'Indent' },
192
+ { command: 'toggleBlockquote', icon: 'lucideTextQuote', label: 'Blockquote' },
193
+ { command: 'toggleCodeBlock', icon: 'lucideSquareCode', label: 'Code block' },
194
+ ];
195
+ /** Always-available table entry point (inserting a new table). */
196
+ const QALMA_TOOLBAR_TABLE_INSERT = {
197
+ command: 'insertTable',
198
+ icon: 'lucideTable',
199
+ label: 'Insert table',
200
+ };
201
+ /** Table editing commands — only meaningful while the selection is in a table. */
202
+ const QALMA_TOOLBAR_TABLE_OPS = [
203
+ {
204
+ command: 'addRowAfter',
205
+ icon: 'lucideBetweenHorizontalEnd',
206
+ label: 'Add row',
207
+ },
208
+ {
209
+ command: 'addColumnAfter',
210
+ icon: 'lucideBetweenVerticalEnd',
211
+ label: 'Add column',
212
+ },
213
+ { command: 'deleteRow', icon: 'lucideRows3', label: 'Delete row' },
214
+ { command: 'deleteColumn', icon: 'lucideColumns3', label: 'Delete column' },
215
+ {
216
+ command: 'toggleHeaderRow',
217
+ icon: 'lucideHeading',
218
+ label: 'Toggle header row',
219
+ },
220
+ { command: 'deleteTable', icon: 'lucideTrash2', label: 'Delete table' },
221
+ ];
222
+ const QALMA_TOOLBAR_HISTORY = [
223
+ { command: 'undo', icon: 'lucideUndo2', label: 'Undo' },
224
+ { command: 'redo', icon: 'lucideRedo2', label: 'Redo' },
225
+ ];
226
+ /** Standalone command that usually sits alongside the color controls. */
227
+ const QALMA_TOOLBAR_CLEAR_FORMATTING = {
228
+ command: 'clearFormatting',
229
+ icon: 'lucideEraser',
230
+ label: 'Clear formatting',
231
+ };
232
+ /** Standalone command that usually sits alongside the link/insert controls. */
233
+ const QALMA_TOOLBAR_UNSET_LINK = {
234
+ command: 'unsetLink',
235
+ icon: 'lucideUnlink',
236
+ label: 'Unlink',
237
+ };
238
+
239
+ /** Vertical divider rendered between non-empty toolbar groups. */
240
+ const TOOLBAR_SEPARATOR_CLASS = 'mx-0.5 h-5 w-px shrink-0 self-center bg-border';
241
+ /**
242
+ * Renders a declarative toolbar registry: an ordered list of groups, each a
243
+ * list of command buttons and/or inline custom-control templates, with a
244
+ * separator drawn between non-empty groups.
245
+ *
246
+ * Meant to sit inside the editor's `<qalma-toolbar>` (which owns the
247
+ * `role="toolbar"` semantics); the host uses `display: contents` so the
248
+ * rendered buttons participate directly in that container's flex layout.
249
+ *
250
+ * ```html
251
+ * <qalma-toolbar class="flex …">
252
+ * <ng-template #colors> … custom controls … </ng-template>
253
+ * <qalma-toolbar-registry [groups]="[headings, marks, [{ template: colors }]]" />
254
+ * </qalma-toolbar>
255
+ * ```
256
+ */
257
+ class QalmaToolbarRegistry {
258
+ groups = input.required(...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
259
+ separatorClass = input(TOOLBAR_SEPARATOR_CLASS, ...(ngDevMode ? [{ debugName: "separatorClass" }] : /* istanbul ignore next */ []));
260
+ /** Drop empty groups so they neither render content nor a dangling separator. */
261
+ visibleGroups = computed(() => this.groups().filter((group) => group.length > 0), ...(ngDevMode ? [{ debugName: "visibleGroups" }] : /* istanbul ignore next */ []));
262
+ asCommand(item) {
263
+ return isToolbarCommandItem(item) ? item : null;
264
+ }
265
+ asTemplate(item) {
266
+ return item.template;
267
+ }
268
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaToolbarRegistry, deps: [], target: i0.ɵɵFactoryTarget.Component });
269
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaToolbarRegistry, isStandalone: true, selector: "qalma-toolbar-registry", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: true, transformFunction: null }, separatorClass: { classPropertyName: "separatorClass", publicName: "separatorClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
270
+ @for (group of visibleGroups(); track $index; let first = $first) {
271
+ @if (!first) {
272
+ <span [class]="separatorClass()" aria-hidden="true"></span>
273
+ }
274
+ @for (item of group; track $index) {
275
+ @if (asCommand(item); as command) {
276
+ <qalma-toolbar-button
277
+ [command]="command.command"
278
+ [value]="command.value"
279
+ [icon]="command.icon"
280
+ [label]="command.label"
281
+ />
282
+ } @else {
283
+ <ng-container [ngTemplateOutlet]="asTemplate(item)" />
284
+ }
285
+ }
286
+ }
287
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: QalmaToolbarButton, selector: "qalma-toolbar-button", inputs: ["command", "value", "icon", "label", "iconClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
288
+ }
289
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaToolbarRegistry, decorators: [{
290
+ type: Component,
291
+ args: [{
292
+ selector: 'qalma-toolbar-registry',
293
+ changeDetection: ChangeDetectionStrategy.OnPush,
294
+ imports: [NgTemplateOutlet, QalmaToolbarButton],
295
+ host: {
296
+ class: 'contents',
297
+ },
298
+ template: `
299
+ @for (group of visibleGroups(); track $index; let first = $first) {
300
+ @if (!first) {
301
+ <span [class]="separatorClass()" aria-hidden="true"></span>
302
+ }
303
+ @for (item of group; track $index) {
304
+ @if (asCommand(item); as command) {
305
+ <qalma-toolbar-button
306
+ [command]="command.command"
307
+ [value]="command.value"
308
+ [icon]="command.icon"
309
+ [label]="command.label"
310
+ />
311
+ } @else {
312
+ <ng-container [ngTemplateOutlet]="asTemplate(item)" />
313
+ }
314
+ }
315
+ }
316
+ `,
317
+ }]
318
+ }], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: true }] }], separatorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorClass", required: false }] }] } });
319
+
320
+ /**
321
+ * Registers the Lucide icons referenced by the default toolbar command
322
+ * fragments (`QALMA_TOOLBAR_*`), so consumers can wire the standard toolbar
323
+ * without re-declaring the icon set:
324
+ *
325
+ * ```ts
326
+ * providers: [provideQalmaToolbarIcons()]
327
+ * ```
328
+ *
329
+ * Add your own `provideIcons({ … })` alongside it for any custom controls
330
+ * (color pickers, upload buttons…) that live outside the command registry.
331
+ */
332
+ function provideQalmaToolbarIcons() {
333
+ return provideIcons({
334
+ lucideAlignCenter,
335
+ lucideAlignJustify,
336
+ lucideAlignLeft,
337
+ lucideAlignRight,
338
+ lucideBetweenHorizontalEnd,
339
+ lucideBetweenVerticalEnd,
340
+ lucideBold,
341
+ lucideCode,
342
+ lucideColumns3,
343
+ lucideEraser,
344
+ lucideHeading,
345
+ lucideHeading1,
346
+ lucideHeading2,
347
+ lucideHeading3,
348
+ lucideIndent,
349
+ lucideItalic,
350
+ lucideLetterText,
351
+ lucideList,
352
+ lucideListOrdered,
353
+ lucideListTodo,
354
+ lucideOutdent,
355
+ lucidePilcrow,
356
+ lucideRedo2,
357
+ lucideRows3,
358
+ lucideSquareCode,
359
+ lucideStrikethrough,
360
+ lucideSubscript,
361
+ lucideSuperscript,
362
+ lucideTable,
363
+ lucideTextQuote,
364
+ lucideTrash2,
365
+ lucideUnderline,
366
+ lucideUndo2,
367
+ lucideUnlink,
368
+ });
369
+ }
370
+
371
+ const DEFAULT_GAP$1 = 8;
372
+ const DEFAULT_EDGE_MARGIN = 8;
373
+ function anchorToRect(anchor, options) {
374
+ const gap = options.gap ?? DEFAULT_GAP$1;
375
+ const edgeMargin = options.edgeMargin ?? DEFAULT_EDGE_MARGIN;
376
+ const { boundary, size } = options;
377
+ if (options.placement === 'left' || options.placement === 'right') {
378
+ const mainAxis = options.placement === 'left'
379
+ ? anchor.left - gap - size.width
380
+ : anchor.right + gap;
381
+ const crossAxis = resolveCrossAxis(options.align, anchor.top, anchor.bottom, size.height);
382
+ return {
383
+ left: clamp$1(mainAxis, boundary.left + edgeMargin, boundary.right - edgeMargin - size.width),
384
+ top: clamp$1(crossAxis, boundary.top + edgeMargin, boundary.bottom - edgeMargin - size.height),
385
+ };
386
+ }
387
+ const mainAxis = options.placement === 'bottom'
388
+ ? anchor.bottom + gap
389
+ : anchor.top - gap - size.height;
390
+ const crossAxis = resolveCrossAxis(options.align, anchor.left, anchor.right, size.width);
391
+ return {
392
+ top: clamp$1(mainAxis, boundary.top + edgeMargin, boundary.bottom - edgeMargin - size.height),
393
+ left: clamp$1(crossAxis, boundary.left + edgeMargin, boundary.right - edgeMargin - size.width),
394
+ };
395
+ }
396
+ function resolveCrossAxis(align, start, end, size) {
397
+ const span = end - start;
398
+ if (size >= span) {
399
+ // The floating element doesn't fit within the anchor (e.g. a
400
+ // single-line block shorter than the button anchored to it). Center it
401
+ // instead of clamping to an arbitrary edge.
402
+ return start + span / 2 - size / 2;
403
+ }
404
+ if (typeof align === 'number') {
405
+ return clamp$1(align - size / 2, start, end - size);
406
+ }
407
+ switch (align) {
408
+ case 'end':
409
+ return end - size;
410
+ case 'center':
411
+ return start + (end - start) / 2 - size / 2;
412
+ case 'start':
413
+ default:
414
+ return start;
415
+ }
416
+ }
417
+ function clamp$1(value, min, max) {
418
+ const lo = Math.min(min, max);
419
+ const hi = Math.max(min, max);
420
+ return Math.min(Math.max(value, lo), hi);
421
+ }
422
+
423
+ /**
424
+ * Shared outside-click / Escape dismiss behavior for floating overlays
425
+ * (popovers, contextual menus, autocomplete lists). Generalizes the
426
+ * ad-hoc click-outside/Escape handling that used to be reimplemented
427
+ * separately per feature.
428
+ */
429
+ class DismissibleOverlay {
430
+ options;
431
+ pointerDown = (event) => {
432
+ if (!this.options.isInside(event.target)) {
433
+ this.options.onDismiss();
434
+ }
435
+ };
436
+ keydown = (event) => {
437
+ if (event.key === 'Escape') {
438
+ this.options.onDismiss();
439
+ }
440
+ };
441
+ constructor(options) {
442
+ this.options = options;
443
+ }
444
+ connect(destroyRef) {
445
+ document.addEventListener('pointerdown', this.pointerDown, true);
446
+ document.addEventListener('keydown', this.keydown);
447
+ destroyRef.onDestroy(() => {
448
+ document.removeEventListener('pointerdown', this.pointerDown, true);
449
+ document.removeEventListener('keydown', this.keydown);
450
+ });
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Arrow-key navigation + Enter-to-select for autocomplete-style popovers
456
+ * (mention menu, slash command menu). Shared so both features stop
457
+ * reimplementing the same wrap-around index math.
458
+ *
459
+ * Takes a plain key string rather than a `KeyboardEvent` — some callers
460
+ * relay keys through a `CustomEvent` (no real `KeyboardEvent` to read),
461
+ * so preventing the default action is left to the caller.
462
+ */
463
+ class KeyboardNavigableList {
464
+ options;
465
+ constructor(options) {
466
+ this.options = options;
467
+ }
468
+ /** Returns true when the key was handled (caller should stop further propagation / preventDefault). */
469
+ handleKey(key) {
470
+ const items = this.options.items();
471
+ if (items.length === 0) {
472
+ return false;
473
+ }
474
+ switch (key) {
475
+ case 'ArrowDown':
476
+ this.options.setActiveIndex((this.options.activeIndex() + 1) % items.length);
477
+ return true;
478
+ case 'ArrowUp':
479
+ this.options.setActiveIndex((this.options.activeIndex() - 1 + items.length) % items.length);
480
+ return true;
481
+ case 'Enter': {
482
+ const index = this.options.activeIndex();
483
+ const item = items[index];
484
+ if (item === undefined) {
485
+ return false;
486
+ }
487
+ this.options.onSelect(item, index);
488
+ return true;
489
+ }
490
+ default:
491
+ return false;
492
+ }
493
+ }
494
+ }
495
+
496
+ const DRAG_START_DISTANCE = 8;
497
+ const HANDLE_EDGE_MARGIN = 8;
498
+ const HANDLE_GAP = 8;
499
+ const HANDLE_BUTTON_SIZE = 30;
500
+ const DROP_LINE_EDGE_MARGIN = 8;
501
+ class QalmaDragHandleController {
502
+ editor;
503
+ handleState = signal(null, ...(ngDevMode ? [{ debugName: "handleState" }] : /* istanbul ignore next */ []));
504
+ dropIndicatorState = signal(null, ...(ngDevMode ? [{ debugName: "dropIndicatorState" }] : /* istanbul ignore next */ []));
505
+ draggedBlockHighlightState = signal(null, ...(ngDevMode ? [{ debugName: "draggedBlockHighlightState" }] : /* istanbul ignore next */ []));
506
+ handle = this.handleState.asReadonly();
507
+ dropIndicator = this.dropIndicatorState.asReadonly();
508
+ draggedBlockHighlight = this.draggedBlockHighlightState.asReadonly();
509
+ surface = null;
510
+ currentBlock = null;
511
+ pendingBlock = null;
512
+ lastPointerClientY = null;
513
+ refreshFrame = null;
514
+ handleKey = null;
515
+ dragSession = null;
516
+ constructor(editor) {
517
+ this.editor = editor;
518
+ }
519
+ connect(surface, destroyRef) {
520
+ this.surface = surface;
521
+ const pointerMove = (event) => this.handlePointerMove(event);
522
+ const pointerLeave = (event) => this.handlePointerLeave(event);
523
+ const scheduleRefresh = () => this.scheduleRefresh();
524
+ surface.addEventListener('pointermove', pointerMove, {
525
+ passive: true,
526
+ });
527
+ surface.addEventListener('mouseleave', pointerLeave);
528
+ surface.addEventListener('scroll', scheduleRefresh, {
529
+ passive: true,
530
+ });
531
+ window.addEventListener('scroll', scheduleRefresh, {
532
+ passive: true,
533
+ });
534
+ window.addEventListener('resize', scheduleRefresh, {
535
+ passive: true,
536
+ });
537
+ destroyRef.onDestroy(() => {
538
+ this.finishDrag(false);
539
+ this.cancelScheduledRefresh();
540
+ this.surface = null;
541
+ this.currentBlock = null;
542
+ this.pendingBlock = null;
543
+ surface.removeEventListener('pointermove', pointerMove);
544
+ surface.removeEventListener('mouseleave', pointerLeave);
545
+ surface.removeEventListener('scroll', scheduleRefresh);
546
+ window.removeEventListener('scroll', scheduleRefresh);
547
+ window.removeEventListener('resize', scheduleRefresh);
548
+ });
549
+ }
550
+ hide() {
551
+ this.currentBlock = null;
552
+ this.pendingBlock = null;
553
+ this.lastPointerClientY = null;
554
+ this.cancelScheduledRefresh();
555
+ this.setHandle(null);
556
+ }
557
+ startDrag(event, handle) {
558
+ if (event.button !== 0 || this.dragSession) {
559
+ return;
560
+ }
561
+ const surface = this.surface;
562
+ const sourceBlock = surface && findDragHandleBlockByPos(surface, handle.target.pos);
563
+ if (!surface || !sourceBlock) {
564
+ return;
565
+ }
566
+ event.preventDefault();
567
+ const session = {
568
+ handle,
569
+ sourceBlock,
570
+ originX: event.clientX,
571
+ originY: event.clientY,
572
+ dragging: false,
573
+ previousBodyCursor: document.body.style.cursor,
574
+ previousBodyUserSelect: document.body.style.userSelect,
575
+ pointerMove: (moveEvent) => this.handleDragPointerMove(moveEvent),
576
+ pointerUp: (upEvent) => this.handleDragPointerUp(upEvent),
577
+ pointerCancel: () => this.finishDrag(false),
578
+ };
579
+ this.dragSession = session;
580
+ window.addEventListener('pointermove', session.pointerMove, {
581
+ passive: false,
582
+ });
583
+ window.addEventListener('pointerup', session.pointerUp, {
584
+ passive: false,
585
+ });
586
+ window.addEventListener('pointercancel', session.pointerCancel);
587
+ }
588
+ handlePointerMove(event) {
589
+ if (this.dragSession?.dragging) {
590
+ return;
591
+ }
592
+ const surface = this.surface;
593
+ const target = event.target;
594
+ if (!surface || !(target instanceof Element)) {
595
+ return;
596
+ }
597
+ const block = findDragHandleBlock(target, surface);
598
+ if (!block) {
599
+ return;
600
+ }
601
+ if (event instanceof MouseEvent) {
602
+ this.lastPointerClientY = event.clientY;
603
+ }
604
+ this.pendingBlock = block;
605
+ this.scheduleRefresh();
606
+ }
607
+ handlePointerLeave(event) {
608
+ if (this.dragSession?.dragging) {
609
+ return;
610
+ }
611
+ const relatedTarget = event instanceof MouseEvent ? event.relatedTarget : null;
612
+ if (relatedTarget instanceof Element &&
613
+ relatedTarget.closest('[data-qalma-drag-handle]')) {
614
+ return;
615
+ }
616
+ this.hide();
617
+ }
618
+ handleDragPointerMove(event) {
619
+ const session = this.dragSession;
620
+ if (!session || !(event instanceof MouseEvent)) {
621
+ return;
622
+ }
623
+ const distance = Math.hypot(event.clientX - session.originX, event.clientY - session.originY);
624
+ if (!session.dragging && distance < DRAG_START_DISTANCE) {
625
+ return;
626
+ }
627
+ event.preventDefault();
628
+ if (!session.dragging) {
629
+ this.activateDragSession(session);
630
+ }
631
+ this.updateDropIndicator(event.clientY);
632
+ }
633
+ handleDragPointerUp(event) {
634
+ if (this.dragSession?.dragging && event instanceof MouseEvent) {
635
+ event.preventDefault();
636
+ }
637
+ this.finishDrag(true);
638
+ }
639
+ activateDragSession(session) {
640
+ session.dragging = true;
641
+ this.surface?.setAttribute('data-qalma-drag-active', 'true');
642
+ document.body.style.cursor = 'grabbing';
643
+ document.body.style.userSelect = 'none';
644
+ this.updateDraggedBlockHighlight(session.sourceBlock);
645
+ }
646
+ updateDropIndicator(clientY) {
647
+ const surface = this.surface;
648
+ const session = this.dragSession;
649
+ const candidate = surface && session
650
+ ? findDragDropCandidate(surface, session.handle.target.pos, clientY)
651
+ : null;
652
+ if (!candidate ||
653
+ !this.editor().canExecute('moveBlockTo', candidate.target)) {
654
+ this.setDropIndicator(null);
655
+ return;
656
+ }
657
+ this.setDropIndicator({
658
+ target: candidate.target,
659
+ transform: `translate3d(${Math.round(candidate.lineX)}px, ${Math.round(candidate.lineY)}px, 0)`,
660
+ width: Math.round(candidate.lineWidth),
661
+ });
662
+ }
663
+ updateDraggedBlockHighlight(block) {
664
+ const rect = block.getBoundingClientRect();
665
+ this.draggedBlockHighlightState.set({
666
+ transform: `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top)}px, 0)`,
667
+ width: Math.round(rect.width),
668
+ height: Math.round(rect.height),
669
+ });
670
+ }
671
+ finishDrag(shouldDrop) {
672
+ const session = this.dragSession;
673
+ if (!session) {
674
+ return;
675
+ }
676
+ const dropTarget = this.dropIndicatorState()?.target ?? null;
677
+ window.removeEventListener('pointermove', session.pointerMove);
678
+ window.removeEventListener('pointerup', session.pointerUp);
679
+ window.removeEventListener('pointercancel', session.pointerCancel);
680
+ this.surface?.removeAttribute('data-qalma-drag-active');
681
+ document.body.style.cursor = session.previousBodyCursor;
682
+ document.body.style.userSelect = session.previousBodyUserSelect;
683
+ this.dragSession = null;
684
+ this.setDropIndicator(null);
685
+ this.draggedBlockHighlightState.set(null);
686
+ if (shouldDrop && session.dragging && dropTarget) {
687
+ this.editor().execute('moveBlockTo', dropTarget);
688
+ this.hide();
689
+ }
690
+ }
691
+ scheduleRefresh() {
692
+ if (this.refreshFrame !== null) {
693
+ return;
694
+ }
695
+ this.refreshFrame = requestAnimationFrame(() => {
696
+ this.refreshFrame = null;
697
+ this.refresh();
698
+ });
699
+ }
700
+ refresh() {
701
+ const block = this.pendingBlock ?? this.currentBlock;
702
+ this.pendingBlock = null;
703
+ if (!block) {
704
+ this.hide();
705
+ return;
706
+ }
707
+ this.updateFromBlock(block);
708
+ }
709
+ updateFromBlock(block) {
710
+ const surface = this.surface;
711
+ const target = parseDragHandleTarget(block);
712
+ if (!surface || !target) {
713
+ this.hide();
714
+ return;
715
+ }
716
+ const rect = block.getBoundingClientRect();
717
+ if (!isRectVisibleInsideSurface$1(rect, surface)) {
718
+ this.hide();
719
+ return;
720
+ }
721
+ const surfaceRect = surface.getBoundingClientRect();
722
+ const position = anchorToRect(rect, {
723
+ placement: 'left',
724
+ boundary: surfaceRect,
725
+ size: { width: HANDLE_BUTTON_SIZE, height: HANDLE_BUTTON_SIZE },
726
+ gap: HANDLE_GAP,
727
+ edgeMargin: HANDLE_EDGE_MARGIN,
728
+ align: this.lastPointerClientY ?? rect.top + rect.height / 2,
729
+ });
730
+ const canMoveUp = this.editor().canExecute('moveBlockUp', target);
731
+ const canMoveDown = this.editor().canExecute('moveBlockDown', target);
732
+ this.currentBlock = block;
733
+ this.setHandle({
734
+ target,
735
+ transform: `translate3d(${Math.round(position.left)}px, ${Math.round(position.top)}px, 0)`,
736
+ blockType: block.dataset['qalmaDragHandleType'] ?? 'block',
737
+ canMoveUp,
738
+ canMoveDown,
739
+ });
740
+ }
741
+ cancelScheduledRefresh() {
742
+ if (this.refreshFrame === null) {
743
+ return;
744
+ }
745
+ cancelAnimationFrame(this.refreshFrame);
746
+ this.refreshFrame = null;
747
+ }
748
+ setHandle(handle) {
749
+ const nextKey = handle
750
+ ? [
751
+ handle.target.pos,
752
+ handle.transform,
753
+ handle.blockType,
754
+ handle.canMoveUp,
755
+ handle.canMoveDown,
756
+ ].join(':')
757
+ : null;
758
+ if (nextKey === this.handleKey) {
759
+ return;
760
+ }
761
+ this.handleKey = nextKey;
762
+ this.handleState.set(handle);
763
+ }
764
+ setDropIndicator(indicator) {
765
+ this.dropIndicatorState.set(indicator);
766
+ }
767
+ }
768
+ function findDragDropCandidate(surface, sourcePos, clientY) {
769
+ const blocks = Array.from(surface.querySelectorAll('[data-qalma-drag-handle-block]'));
770
+ const lineRect = getDropLineRect(surface);
771
+ let lastCandidate = null;
772
+ for (const block of blocks) {
773
+ const range = parseDragHandleBlockRange(block);
774
+ if (!range) {
775
+ continue;
776
+ }
777
+ const rect = block.getBoundingClientRect();
778
+ const midpoint = rect.top + rect.height / 2;
779
+ if (clientY < midpoint) {
780
+ return {
781
+ target: {
782
+ pos: sourcePos,
783
+ targetPos: range.pos,
784
+ },
785
+ lineX: lineRect.left,
786
+ lineY: rect.top,
787
+ lineWidth: lineRect.width,
788
+ };
789
+ }
790
+ lastCandidate = {
791
+ target: {
792
+ pos: sourcePos,
793
+ targetPos: range.to,
794
+ },
795
+ lineX: lineRect.left,
796
+ lineY: rect.bottom,
797
+ lineWidth: lineRect.width,
798
+ };
799
+ }
800
+ return lastCandidate;
801
+ }
802
+ function getDropLineRect(surface) {
803
+ const content = surface.querySelector('.ProseMirror') ?? surface;
804
+ const rect = content.getBoundingClientRect();
805
+ const width = Math.max(0, rect.width - DROP_LINE_EDGE_MARGIN * 2);
806
+ return {
807
+ left: rect.left + DROP_LINE_EDGE_MARGIN,
808
+ width,
809
+ };
810
+ }
811
+ function findDragHandleBlock(target, surface) {
812
+ const block = target.closest('[data-qalma-drag-handle-block]');
813
+ return block && surface.contains(block) ? block : null;
814
+ }
815
+ function findDragHandleBlockByPos(surface, pos) {
816
+ const blocks = Array.from(surface.querySelectorAll('[data-qalma-drag-handle-block]'));
817
+ return (blocks.find((block) => parseDragHandleTarget(block)?.pos === pos) ?? null);
818
+ }
819
+ function parseDragHandleTarget(block) {
820
+ const pos = Number(block.dataset['qalmaDragHandlePos']);
821
+ return Number.isInteger(pos) ? { pos } : null;
822
+ }
823
+ function parseDragHandleBlockRange(block) {
824
+ const target = parseDragHandleTarget(block);
825
+ const to = Number(block.dataset['qalmaDragHandleTo']);
826
+ return target && Number.isInteger(to) ? { ...target, to } : null;
827
+ }
828
+ function isRectVisibleInsideSurface$1(rect, surface) {
829
+ const surfaceRect = surface.getBoundingClientRect();
830
+ return (rect.bottom > surfaceRect.top &&
831
+ rect.top < surfaceRect.bottom &&
832
+ rect.right > surfaceRect.left &&
833
+ rect.left < surfaceRect.right);
834
+ }
835
+
836
+ class QalmaDragHandleDirective {
837
+ editor = input.required({ ...(ngDevMode ? { debugName: "editor" } : /* istanbul ignore next */ {}), alias: 'qalmaDragHandle' });
838
+ destroyRef = inject(DestroyRef);
839
+ host = inject(ElementRef);
840
+ controller = new QalmaDragHandleController(() => this.editor());
841
+ handle = this.controller.handle;
842
+ dropIndicator = this.controller.dropIndicator;
843
+ draggedBlockHighlight = this.controller.draggedBlockHighlight;
844
+ constructor() {
845
+ afterNextRender(() => {
846
+ this.controller.connect(this.host.nativeElement, this.destroyRef);
847
+ });
848
+ }
849
+ hide() {
850
+ this.controller.hide();
851
+ }
852
+ startDrag(event, handle) {
853
+ this.controller.startDrag(event, handle);
854
+ }
855
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
856
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: QalmaDragHandleDirective, isStandalone: true, selector: "[qalmaDragHandle]", inputs: { editor: { classPropertyName: "editor", publicName: "qalmaDragHandle", isSignal: true, isRequired: true, transformFunction: null } }, exportAs: ["qalmaDragHandle"], ngImport: i0 });
857
+ }
858
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandleDirective, decorators: [{
859
+ type: Directive,
860
+ args: [{
861
+ exportAs: 'qalmaDragHandle',
862
+ selector: '[qalmaDragHandle]',
863
+ }]
864
+ }], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "qalmaDragHandle", required: true }] }] } });
865
+
866
+ class QalmaDragHandle {
867
+ editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
868
+ handle = input(null, ...(ngDevMode ? [{ debugName: "handle" }] : /* istanbul ignore next */ []));
869
+ dropIndicator = input(null, ...(ngDevMode ? [{ debugName: "dropIndicator" }] : /* istanbul ignore next */ []));
870
+ draggedBlockHighlight = input(null, ...(ngDevMode ? [{ debugName: "draggedBlockHighlight" }] : /* istanbul ignore next */ []));
871
+ dismiss = output();
872
+ dragStart = output();
873
+ menuOpen = signal(false, ...(ngDevMode ? [{ debugName: "menuOpen" }] : /* istanbul ignore next */ []));
874
+ constructor() {
875
+ // Close the block-actions menu on an outside pointerdown or Escape. A click
876
+ // on the handle button or the menu itself (both under `[data-qalma-drag-handle]`)
877
+ // is treated as inside, so it doesn't self-dismiss. Connected for the
878
+ // component's lifetime; the callback is a no-op while the menu is closed.
879
+ new DismissibleOverlay({
880
+ isInside: (target) => target instanceof Element &&
881
+ !!target.closest('[data-qalma-drag-handle]'),
882
+ onDismiss: () => this.menuOpen.set(false),
883
+ }).connect(inject(DestroyRef));
884
+ }
885
+ toggleMenu() {
886
+ this.menuOpen.update((open) => !open);
887
+ }
888
+ startDrag(event, handle) {
889
+ this.menuOpen.set(false);
890
+ this.dragStart.emit({ event, handle });
891
+ }
892
+ canExecute(command, handle) {
893
+ return this.editor().canExecute(command, handle.target);
894
+ }
895
+ execute(command, handle) {
896
+ if (this.editor().execute(command, handle.target)) {
897
+ this.dismissHandle();
898
+ }
899
+ }
900
+ preserveSelection(event) {
901
+ event.preventDefault();
902
+ }
903
+ dismissFromKeyboard(event) {
904
+ event.preventDefault();
905
+ this.dismissHandle();
906
+ }
907
+ dismissHandle() {
908
+ this.menuOpen.set(false);
909
+ this.dismiss.emit();
910
+ }
911
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandle, deps: [], target: i0.ɵɵFactoryTarget.Component });
912
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaDragHandle, isStandalone: true, selector: "qalma-drag-handle", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, handle: { classPropertyName: "handle", publicName: "handle", isSignal: true, isRequired: false, transformFunction: null }, dropIndicator: { classPropertyName: "dropIndicator", publicName: "dropIndicator", isSignal: true, isRequired: false, transformFunction: null }, draggedBlockHighlight: { classPropertyName: "draggedBlockHighlight", publicName: "draggedBlockHighlight", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss", dragStart: "dragStart" }, providers: [
913
+ provideIcons({
914
+ lucideArrowDown,
915
+ lucideArrowUp,
916
+ lucideCopy,
917
+ lucideGripVertical,
918
+ lucideMousePointer2,
919
+ lucideTrash2,
920
+ }),
921
+ ], ngImport: i0, template: `
922
+ @if (draggedBlockHighlight(); as highlight) {
923
+ <div
924
+ data-qalma-dragging-block-highlight
925
+ class="pointer-events-none fixed left-0 top-0 z-20 rounded-md border border-accent bg-accent-subtle/80 opacity-70 shadow-md will-change-transform"
926
+ [style.transform]="highlight.transform"
927
+ [style.width.px]="highlight.width"
928
+ [style.height.px]="highlight.height"
929
+ ></div>
930
+ }
931
+
932
+ @if (dropIndicator(); as indicator) {
933
+ <div
934
+ data-qalma-drag-drop-line
935
+ class="pointer-events-none fixed left-0 top-0 z-40 h-0.5 rounded-full bg-accent shadow-md will-change-transform"
936
+ [style.transform]="indicator.transform"
937
+ [style.width.px]="indicator.width"
938
+ ></div>
939
+ }
940
+
941
+ @if (handle(); as handle) {
942
+ <div
943
+ data-qalma-drag-handle
944
+ class="fixed left-0 top-0 z-30 flex items-start gap-1 will-change-transform"
945
+ tabindex="-1"
946
+ [style.transform]="handle.transform"
947
+ (mousedown)="preserveSelection($event)"
948
+ (keydown.escape)="dismissFromKeyboard($event)"
949
+ >
950
+ <button
951
+ qalmaBtn
952
+ variant="ghost"
953
+ size="icon"
954
+ type="button"
955
+ class="!h-[1.85rem] !w-[1.85rem] cursor-grab rounded-[0.4rem] border border-transparent bg-card/95 text-muted-foreground shadow-sm active:cursor-grabbing hover:border-border hover:bg-secondary hover:text-foreground"
956
+ [class.border-accent]="menuOpen()"
957
+ [class.text-accent]="menuOpen()"
958
+ (pointerdown)="startDrag($event, handle)"
959
+ (click)="toggleMenu()"
960
+ title="Block actions"
961
+ aria-label="Block actions"
962
+ [attr.aria-expanded]="menuOpen()"
963
+ >
964
+ <ng-icon
965
+ class="text-[0.9rem]"
966
+ name="lucideGripVertical"
967
+ aria-hidden="true"
968
+ />
969
+ </button>
970
+
971
+ @if (menuOpen()) {
972
+ <div
973
+ role="menu"
974
+ aria-label="Block actions"
975
+ class="w-44 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg"
976
+ >
977
+ <button
978
+ type="button"
979
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
980
+ [disabled]="!canExecute('selectBlock', handle)"
981
+ (mousedown)="preserveSelection($event)"
982
+ (click)="execute('selectBlock', handle)"
983
+ role="menuitem"
984
+ >
985
+ <ng-icon name="lucideMousePointer2" aria-hidden="true" />
986
+ Select block
987
+ </button>
988
+ <button
989
+ type="button"
990
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
991
+ [disabled]="!handle.canMoveUp"
992
+ (mousedown)="preserveSelection($event)"
993
+ (click)="execute('moveBlockUp', handle)"
994
+ role="menuitem"
995
+ >
996
+ <ng-icon name="lucideArrowUp" aria-hidden="true" />
997
+ Move up
998
+ </button>
999
+ <button
1000
+ type="button"
1001
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
1002
+ [disabled]="!handle.canMoveDown"
1003
+ (mousedown)="preserveSelection($event)"
1004
+ (click)="execute('moveBlockDown', handle)"
1005
+ role="menuitem"
1006
+ >
1007
+ <ng-icon name="lucideArrowDown" aria-hidden="true" />
1008
+ Move down
1009
+ </button>
1010
+ <button
1011
+ type="button"
1012
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
1013
+ [disabled]="!canExecute('duplicateBlock', handle)"
1014
+ (mousedown)="preserveSelection($event)"
1015
+ (click)="execute('duplicateBlock', handle)"
1016
+ role="menuitem"
1017
+ >
1018
+ <ng-icon name="lucideCopy" aria-hidden="true" />
1019
+ Duplicate
1020
+ </button>
1021
+ <button
1022
+ type="button"
1023
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-destructive transition hover:bg-destructive/10 disabled:cursor-not-allowed disabled:opacity-45"
1024
+ [disabled]="!canExecute('deleteBlock', handle)"
1025
+ (mousedown)="preserveSelection($event)"
1026
+ (click)="execute('deleteBlock', handle)"
1027
+ role="menuitem"
1028
+ >
1029
+ <ng-icon name="lucideTrash2" aria-hidden="true" />
1030
+ Delete
1031
+ </button>
1032
+ </div>
1033
+ }
1034
+ </div>
1035
+ }
1036
+ `, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: QalmaButton, selector: "button[qalmaBtn], a[qalmaBtn]", inputs: ["variant", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1037
+ }
1038
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandle, decorators: [{
1039
+ type: Component,
1040
+ args: [{
1041
+ changeDetection: ChangeDetectionStrategy.OnPush,
1042
+ imports: [NgIcon, QalmaButton],
1043
+ providers: [
1044
+ provideIcons({
1045
+ lucideArrowDown,
1046
+ lucideArrowUp,
1047
+ lucideCopy,
1048
+ lucideGripVertical,
1049
+ lucideMousePointer2,
1050
+ lucideTrash2,
1051
+ }),
1052
+ ],
1053
+ selector: 'qalma-drag-handle',
1054
+ template: `
1055
+ @if (draggedBlockHighlight(); as highlight) {
1056
+ <div
1057
+ data-qalma-dragging-block-highlight
1058
+ class="pointer-events-none fixed left-0 top-0 z-20 rounded-md border border-accent bg-accent-subtle/80 opacity-70 shadow-md will-change-transform"
1059
+ [style.transform]="highlight.transform"
1060
+ [style.width.px]="highlight.width"
1061
+ [style.height.px]="highlight.height"
1062
+ ></div>
1063
+ }
1064
+
1065
+ @if (dropIndicator(); as indicator) {
1066
+ <div
1067
+ data-qalma-drag-drop-line
1068
+ class="pointer-events-none fixed left-0 top-0 z-40 h-0.5 rounded-full bg-accent shadow-md will-change-transform"
1069
+ [style.transform]="indicator.transform"
1070
+ [style.width.px]="indicator.width"
1071
+ ></div>
1072
+ }
1073
+
1074
+ @if (handle(); as handle) {
1075
+ <div
1076
+ data-qalma-drag-handle
1077
+ class="fixed left-0 top-0 z-30 flex items-start gap-1 will-change-transform"
1078
+ tabindex="-1"
1079
+ [style.transform]="handle.transform"
1080
+ (mousedown)="preserveSelection($event)"
1081
+ (keydown.escape)="dismissFromKeyboard($event)"
1082
+ >
1083
+ <button
1084
+ qalmaBtn
1085
+ variant="ghost"
1086
+ size="icon"
1087
+ type="button"
1088
+ class="!h-[1.85rem] !w-[1.85rem] cursor-grab rounded-[0.4rem] border border-transparent bg-card/95 text-muted-foreground shadow-sm active:cursor-grabbing hover:border-border hover:bg-secondary hover:text-foreground"
1089
+ [class.border-accent]="menuOpen()"
1090
+ [class.text-accent]="menuOpen()"
1091
+ (pointerdown)="startDrag($event, handle)"
1092
+ (click)="toggleMenu()"
1093
+ title="Block actions"
1094
+ aria-label="Block actions"
1095
+ [attr.aria-expanded]="menuOpen()"
1096
+ >
1097
+ <ng-icon
1098
+ class="text-[0.9rem]"
1099
+ name="lucideGripVertical"
1100
+ aria-hidden="true"
1101
+ />
1102
+ </button>
1103
+
1104
+ @if (menuOpen()) {
1105
+ <div
1106
+ role="menu"
1107
+ aria-label="Block actions"
1108
+ class="w-44 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg"
1109
+ >
1110
+ <button
1111
+ type="button"
1112
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
1113
+ [disabled]="!canExecute('selectBlock', handle)"
1114
+ (mousedown)="preserveSelection($event)"
1115
+ (click)="execute('selectBlock', handle)"
1116
+ role="menuitem"
1117
+ >
1118
+ <ng-icon name="lucideMousePointer2" aria-hidden="true" />
1119
+ Select block
1120
+ </button>
1121
+ <button
1122
+ type="button"
1123
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
1124
+ [disabled]="!handle.canMoveUp"
1125
+ (mousedown)="preserveSelection($event)"
1126
+ (click)="execute('moveBlockUp', handle)"
1127
+ role="menuitem"
1128
+ >
1129
+ <ng-icon name="lucideArrowUp" aria-hidden="true" />
1130
+ Move up
1131
+ </button>
1132
+ <button
1133
+ type="button"
1134
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
1135
+ [disabled]="!handle.canMoveDown"
1136
+ (mousedown)="preserveSelection($event)"
1137
+ (click)="execute('moveBlockDown', handle)"
1138
+ role="menuitem"
1139
+ >
1140
+ <ng-icon name="lucideArrowDown" aria-hidden="true" />
1141
+ Move down
1142
+ </button>
1143
+ <button
1144
+ type="button"
1145
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
1146
+ [disabled]="!canExecute('duplicateBlock', handle)"
1147
+ (mousedown)="preserveSelection($event)"
1148
+ (click)="execute('duplicateBlock', handle)"
1149
+ role="menuitem"
1150
+ >
1151
+ <ng-icon name="lucideCopy" aria-hidden="true" />
1152
+ Duplicate
1153
+ </button>
1154
+ <button
1155
+ type="button"
1156
+ class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-destructive transition hover:bg-destructive/10 disabled:cursor-not-allowed disabled:opacity-45"
1157
+ [disabled]="!canExecute('deleteBlock', handle)"
1158
+ (mousedown)="preserveSelection($event)"
1159
+ (click)="execute('deleteBlock', handle)"
1160
+ role="menuitem"
1161
+ >
1162
+ <ng-icon name="lucideTrash2" aria-hidden="true" />
1163
+ Delete
1164
+ </button>
1165
+ </div>
1166
+ }
1167
+ </div>
1168
+ }
1169
+ `,
1170
+ }]
1171
+ }], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], handle: [{ type: i0.Input, args: [{ isSignal: true, alias: "handle", required: false }] }], dropIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropIndicator", required: false }] }], draggedBlockHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "draggedBlockHighlight", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], dragStart: [{ type: i0.Output, args: ["dragStart"] }] } });
1172
+
1173
+ const DEFAULT_MARGIN = 12;
1174
+ const DEFAULT_GAP = 8;
1175
+ /**
1176
+ * Positions a floating list under an anchor rect, flipping it above when there
1177
+ * is not enough room below, and clamping its height and horizontal position to
1178
+ * the viewport. Shared by the mention and slash-command menus, whose flip
1179
+ * behavior `anchorToRect` does not cover (it has no flip / dynamic max-height).
1180
+ */
1181
+ function flipAbovePlacement(rect, options) {
1182
+ const { width, desiredHeight, minHeight, margin = DEFAULT_MARGIN, gap = DEFAULT_GAP, } = options;
1183
+ const availableBelow = window.innerHeight - rect.bottom - margin - gap;
1184
+ const availableAbove = rect.top - margin - gap;
1185
+ const openAbove = availableBelow < desiredHeight && availableAbove > availableBelow;
1186
+ const availableHeight = Math.max(minHeight, openAbove ? availableAbove : availableBelow);
1187
+ const maxHeight = Math.min(desiredHeight, availableHeight);
1188
+ const leftBoundary = Math.max(margin, window.innerWidth - width - margin);
1189
+ return {
1190
+ left: Math.min(Math.max(rect.left, margin), leftBoundary),
1191
+ top: openAbove ? null : rect.bottom + gap,
1192
+ bottom: openAbove ? window.innerHeight - rect.top + gap : null,
1193
+ maxHeight,
1194
+ };
1195
+ }
1196
+
1197
+ /** Option buttons carry this attribute so the base can focus/scroll to one by index. */
1198
+ const SUGGESTION_OPTION_INDEX_ATTR = 'data-suggestion-index';
1199
+ /** The scrollable options container carries this attribute (optional). */
1200
+ const SUGGESTION_OPTIONS_SCROLLER_ATTR = 'data-suggestion-options';
1201
+ /**
1202
+ * Shared behavior for caret-anchored suggestion menus (mention, slash command):
1203
+ * flip-above placement input, active-option highlighting, and in-menu keyboard
1204
+ * handling (Escape dismisses, Arrow moves + focuses, Enter/Space picks) with
1205
+ * wrap-around and optional scroll-into-view. Concrete menus only supply their
1206
+ * own item markup and expose their option list via `optionList`.
1207
+ */
1208
+ class QalmaSuggestionMenu {
1209
+ elementRef = inject(ElementRef);
1210
+ placement = input(null, ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
1211
+ activeIndex = input(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1212
+ activate = output();
1213
+ pick = output();
1214
+ dismiss = output();
1215
+ preserveSelection(event) {
1216
+ event.preventDefault();
1217
+ }
1218
+ handleOptionKeydown(event, option, index) {
1219
+ if (event.key === 'Escape') {
1220
+ event.preventDefault();
1221
+ this.dismiss.emit();
1222
+ return;
1223
+ }
1224
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
1225
+ event.preventDefault();
1226
+ const nextIndex = this.getNextIndex(index, event.key === 'ArrowDown' ? 1 : -1);
1227
+ this.activate.emit(nextIndex);
1228
+ queueMicrotask(() => this.focusOption(nextIndex));
1229
+ return;
1230
+ }
1231
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
1232
+ event.preventDefault();
1233
+ this.pick.emit(option);
1234
+ }
1235
+ }
1236
+ focusOption(index) {
1237
+ this.optionElement(index)?.focus();
1238
+ // No-op for menus without a marked scroller (native focus handles scroll).
1239
+ this.scrollOptionIntoView(index);
1240
+ }
1241
+ scrollOptionIntoView(index) {
1242
+ const scroller = this.optionsScroller();
1243
+ const option = this.optionElement(index);
1244
+ if (!scroller || !option) {
1245
+ return;
1246
+ }
1247
+ const optionTop = option.offsetTop - scroller.offsetTop;
1248
+ const optionBottom = optionTop + option.offsetHeight;
1249
+ const visibleTop = scroller.scrollTop;
1250
+ const visibleBottom = visibleTop + scroller.clientHeight;
1251
+ if (optionTop < visibleTop) {
1252
+ scroller.scrollTop = optionTop;
1253
+ }
1254
+ else if (optionBottom > visibleBottom) {
1255
+ scroller.scrollTop = optionBottom - scroller.clientHeight;
1256
+ }
1257
+ }
1258
+ getNextIndex(index, delta) {
1259
+ const length = this.optionList().length;
1260
+ return length > 0 ? (index + delta + length) % length : index;
1261
+ }
1262
+ optionElement(index) {
1263
+ return this.elementRef.nativeElement.querySelector(`[${SUGGESTION_OPTION_INDEX_ATTR}="${index}"]`);
1264
+ }
1265
+ optionsScroller() {
1266
+ return this.elementRef.nativeElement.querySelector(`[${SUGGESTION_OPTIONS_SCROLLER_ATTR}]`);
1267
+ }
1268
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSuggestionMenu, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1269
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: QalmaSuggestionMenu, isStandalone: true, inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, activeIndex: { classPropertyName: "activeIndex", publicName: "activeIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activate: "activate", pick: "pick", dismiss: "dismiss" }, ngImport: i0 });
1270
+ }
1271
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSuggestionMenu, decorators: [{
1272
+ type: Directive
1273
+ }], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], activeIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndex", required: false }] }], activate: [{ type: i0.Output, args: ["activate"] }], pick: [{ type: i0.Output, args: ["pick"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
1274
+
1275
+ /**
1276
+ * Caret-anchored mention suggestion menu: an avatar-initial list with a loading
1277
+ * state. Positioning, keyboard handling and active highlighting come from
1278
+ * {@link QalmaSuggestionMenu}; feed it a `placement` (see `flipAbovePlacement`)
1279
+ * and the filtered `suggestions`.
1280
+ */
1281
+ class QalmaMentionMenu extends QalmaSuggestionMenu {
1282
+ suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
1283
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1284
+ optionList = this.suggestions;
1285
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaMentionMenu, deps: null, target: i0.ɵɵFactoryTarget.Component });
1286
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaMentionMenu, isStandalone: true, selector: "qalma-mention-menu", inputs: { suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: `
1287
+ @if (placement(); as placement) {
1288
+ <div
1289
+ role="listbox"
1290
+ aria-label="Mention suggestions"
1291
+ class="fixed z-20 w-[min(280px,calc(100vw-24px))] overflow-hidden rounded-md border border-border bg-popover p-1 text-sm text-popover-foreground shadow-lg outline-none"
1292
+ [style.left.px]="placement.left"
1293
+ [style.top.px]="placement.top"
1294
+ [style.bottom.px]="placement.bottom"
1295
+ [style.max-height.px]="placement.maxHeight"
1296
+ >
1297
+ @if (loading()) {
1298
+ <div
1299
+ class="flex items-center gap-2 px-2 py-2 text-sm text-muted-foreground"
1300
+ >
1301
+ <span
1302
+ class="size-7 animate-pulse rounded-sm border border-border bg-secondary"
1303
+ aria-hidden="true"
1304
+ ></span>
1305
+ <span>Loading…</span>
1306
+ </div>
1307
+ } @else {
1308
+ <div class="max-h-full overflow-y-auto">
1309
+ @for (
1310
+ option of suggestions();
1311
+ track option.id;
1312
+ let index = $index
1313
+ ) {
1314
+ <button
1315
+ type="button"
1316
+ role="option"
1317
+ [attr.data-suggestion-index]="index"
1318
+ class="group grid h-12 w-full min-w-0 cursor-pointer grid-cols-[1.75rem_minmax(0,1fr)] items-center gap-2 rounded-sm px-2 text-left transition-colors hover:bg-accent-subtle hover:text-foreground focus:bg-accent-subtle focus:text-foreground focus:outline-none"
1319
+ [class.bg-accent-subtle]="index === activeIndex()"
1320
+ [class.text-foreground]="index === activeIndex()"
1321
+ [attr.aria-selected]="index === activeIndex()"
1322
+ [attr.tabindex]="index === activeIndex() ? 0 : -1"
1323
+ (mouseenter)="activate.emit(index)"
1324
+ (mousedown)="preserveSelection($event)"
1325
+ (keydown)="handleOptionKeydown($event, option, index)"
1326
+ (click)="pick.emit(option)"
1327
+ >
1328
+ <span
1329
+ class="flex size-7 items-center justify-center rounded-sm border border-border bg-card text-xs font-semibold text-foreground"
1330
+ [class.border-accent]="index === activeIndex()"
1331
+ [class.bg-background]="index === activeIndex()"
1332
+ [class.text-accent]="index === activeIndex()"
1333
+ aria-hidden="true"
1334
+ >
1335
+ {{ option.label.slice(0, 1) }}
1336
+ </span>
1337
+ <span class="min-w-0">
1338
+ <span class="block truncate font-medium leading-5">{{
1339
+ option.label
1340
+ }}</span>
1341
+ @if (index === activeIndex()) {
1342
+ <span
1343
+ class="block truncate text-xs leading-4 text-foreground"
1344
+ >
1345
+ {{ option.description }}
1346
+ </span>
1347
+ } @else {
1348
+ <span
1349
+ class="block truncate text-xs leading-4 text-muted-foreground group-hover:text-foreground group-focus:text-foreground"
1350
+ >
1351
+ {{ option.description }}
1352
+ </span>
1353
+ }
1354
+ </span>
1355
+ </button>
1356
+ }
1357
+ </div>
1358
+ }
1359
+ </div>
1360
+ }
1361
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1362
+ }
1363
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaMentionMenu, decorators: [{
1364
+ type: Component,
1365
+ args: [{
1366
+ changeDetection: ChangeDetectionStrategy.OnPush,
1367
+ selector: 'qalma-mention-menu',
1368
+ template: `
1369
+ @if (placement(); as placement) {
1370
+ <div
1371
+ role="listbox"
1372
+ aria-label="Mention suggestions"
1373
+ class="fixed z-20 w-[min(280px,calc(100vw-24px))] overflow-hidden rounded-md border border-border bg-popover p-1 text-sm text-popover-foreground shadow-lg outline-none"
1374
+ [style.left.px]="placement.left"
1375
+ [style.top.px]="placement.top"
1376
+ [style.bottom.px]="placement.bottom"
1377
+ [style.max-height.px]="placement.maxHeight"
1378
+ >
1379
+ @if (loading()) {
1380
+ <div
1381
+ class="flex items-center gap-2 px-2 py-2 text-sm text-muted-foreground"
1382
+ >
1383
+ <span
1384
+ class="size-7 animate-pulse rounded-sm border border-border bg-secondary"
1385
+ aria-hidden="true"
1386
+ ></span>
1387
+ <span>Loading…</span>
1388
+ </div>
1389
+ } @else {
1390
+ <div class="max-h-full overflow-y-auto">
1391
+ @for (
1392
+ option of suggestions();
1393
+ track option.id;
1394
+ let index = $index
1395
+ ) {
1396
+ <button
1397
+ type="button"
1398
+ role="option"
1399
+ [attr.data-suggestion-index]="index"
1400
+ class="group grid h-12 w-full min-w-0 cursor-pointer grid-cols-[1.75rem_minmax(0,1fr)] items-center gap-2 rounded-sm px-2 text-left transition-colors hover:bg-accent-subtle hover:text-foreground focus:bg-accent-subtle focus:text-foreground focus:outline-none"
1401
+ [class.bg-accent-subtle]="index === activeIndex()"
1402
+ [class.text-foreground]="index === activeIndex()"
1403
+ [attr.aria-selected]="index === activeIndex()"
1404
+ [attr.tabindex]="index === activeIndex() ? 0 : -1"
1405
+ (mouseenter)="activate.emit(index)"
1406
+ (mousedown)="preserveSelection($event)"
1407
+ (keydown)="handleOptionKeydown($event, option, index)"
1408
+ (click)="pick.emit(option)"
1409
+ >
1410
+ <span
1411
+ class="flex size-7 items-center justify-center rounded-sm border border-border bg-card text-xs font-semibold text-foreground"
1412
+ [class.border-accent]="index === activeIndex()"
1413
+ [class.bg-background]="index === activeIndex()"
1414
+ [class.text-accent]="index === activeIndex()"
1415
+ aria-hidden="true"
1416
+ >
1417
+ {{ option.label.slice(0, 1) }}
1418
+ </span>
1419
+ <span class="min-w-0">
1420
+ <span class="block truncate font-medium leading-5">{{
1421
+ option.label
1422
+ }}</span>
1423
+ @if (index === activeIndex()) {
1424
+ <span
1425
+ class="block truncate text-xs leading-4 text-foreground"
1426
+ >
1427
+ {{ option.description }}
1428
+ </span>
1429
+ } @else {
1430
+ <span
1431
+ class="block truncate text-xs leading-4 text-muted-foreground group-hover:text-foreground group-focus:text-foreground"
1432
+ >
1433
+ {{ option.description }}
1434
+ </span>
1435
+ }
1436
+ </span>
1437
+ </button>
1438
+ }
1439
+ </div>
1440
+ }
1441
+ </div>
1442
+ }
1443
+ `,
1444
+ }]
1445
+ }], propDecorators: { suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }] } });
1446
+
1447
+ /**
1448
+ * Caret-anchored slash-command menu: an icon + shortcut list with a "Commands"
1449
+ * header and an Escape hint footer, scrolling the active option into view.
1450
+ * Positioning, keyboard handling and active highlighting come from
1451
+ * {@link QalmaSuggestionMenu}; feed it a `placement` (see `flipAbovePlacement`)
1452
+ * and the filtered `options`. Ships the default block-command icon set.
1453
+ */
1454
+ class QalmaSlashCommandMenu extends QalmaSuggestionMenu {
1455
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
1456
+ optionList = this.options;
1457
+ constructor() {
1458
+ super();
1459
+ effect(() => {
1460
+ const activeIndex = this.activeIndex();
1461
+ this.options();
1462
+ queueMicrotask(() => this.scrollOptionIntoView(activeIndex));
1463
+ });
1464
+ }
1465
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSlashCommandMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
1466
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaSlashCommandMenu, isStandalone: true, selector: "qalma-slash-command-menu", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
1467
+ provideIcons({
1468
+ lucideCode,
1469
+ lucideHeading1,
1470
+ lucideHeading2,
1471
+ lucideHeading3,
1472
+ lucideList,
1473
+ lucideListOrdered,
1474
+ lucideListTodo,
1475
+ lucideMinus,
1476
+ lucidePilcrow,
1477
+ lucideSquareCode,
1478
+ lucideTable,
1479
+ lucideTextQuote,
1480
+ }),
1481
+ ], usesInheritance: true, ngImport: i0, template: `
1482
+ @if (placement(); as placement) {
1483
+ <div
1484
+ role="listbox"
1485
+ aria-label="Slash command suggestions"
1486
+ class="fixed z-20 flex w-[min(320px,calc(100vw-24px))] flex-col overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none"
1487
+ [style.left.px]="placement.left"
1488
+ [style.top.px]="placement.top"
1489
+ [style.bottom.px]="placement.bottom"
1490
+ [style.max-height.px]="placement.maxHeight"
1491
+ >
1492
+ <div
1493
+ class="shrink-0 px-2 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
1494
+ >
1495
+ Commands
1496
+ </div>
1497
+ <div data-suggestion-options class="min-h-0 flex-1 overflow-y-auto">
1498
+ @for (option of options(); track option.id; let index = $index) {
1499
+ <button
1500
+ type="button"
1501
+ role="option"
1502
+ [attr.data-suggestion-index]="index"
1503
+ class="group grid h-11 w-full min-w-0 cursor-pointer grid-cols-[1.75rem_minmax(0,1fr)_auto] items-center gap-2 rounded-sm px-2 text-left text-sm transition-colors hover:bg-accent-subtle hover:text-foreground focus:bg-accent-subtle focus:text-foreground focus:outline-none"
1504
+ [class.bg-accent-subtle]="index === activeIndex()"
1505
+ [class.text-foreground]="index === activeIndex()"
1506
+ [attr.aria-selected]="index === activeIndex()"
1507
+ [attr.tabindex]="index === activeIndex() ? 0 : -1"
1508
+ (mouseenter)="activate.emit(index)"
1509
+ (mousedown)="preserveSelection($event)"
1510
+ (keydown)="handleOptionKeydown($event, option, index)"
1511
+ (click)="pick.emit(option)"
1512
+ >
1513
+ <span
1514
+ class="flex size-7 items-center justify-center rounded-sm border border-border bg-card text-[14px] text-foreground"
1515
+ [class.border-accent]="index === activeIndex()"
1516
+ [class.bg-background]="index === activeIndex()"
1517
+ [class.text-accent]="index === activeIndex()"
1518
+ >
1519
+ <ng-icon [name]="option.icon" aria-hidden="true" />
1520
+ </span>
1521
+ <span class="min-w-0">
1522
+ <span class="block truncate font-medium leading-5">{{
1523
+ option.label
1524
+ }}</span>
1525
+ @if (index === activeIndex()) {
1526
+ <span
1527
+ class="block truncate text-xs leading-4 text-foreground"
1528
+ >
1529
+ {{ option.description }}
1530
+ </span>
1531
+ } @else {
1532
+ <span
1533
+ class="block truncate text-xs leading-4 text-muted-foreground group-hover:text-foreground group-focus:text-foreground"
1534
+ >
1535
+ {{ option.description }}
1536
+ </span>
1537
+ }
1538
+ </span>
1539
+ <kbd
1540
+ class="rounded-sm border border-border bg-card px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground"
1541
+ [class.border-accent]="index === activeIndex()"
1542
+ [class.bg-background]="index === activeIndex()"
1543
+ [class.text-accent]="index === activeIndex()"
1544
+ >
1545
+ {{ option.shortcut }}
1546
+ </kbd>
1547
+ </button>
1548
+ }
1549
+ </div>
1550
+ <div
1551
+ class="mt-1 flex h-8 shrink-0 items-center justify-between border-t border-border px-2 text-[11px] text-muted-foreground"
1552
+ >
1553
+ <span>Close menu</span>
1554
+ <kbd
1555
+ class="rounded-sm border border-border bg-card px-1.5 py-0.5 font-mono"
1556
+ >esc</kbd
1557
+ >
1558
+ </div>
1559
+ </div>
1560
+ }
1561
+ `, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1562
+ }
1563
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSlashCommandMenu, decorators: [{
1564
+ type: Component,
1565
+ args: [{
1566
+ changeDetection: ChangeDetectionStrategy.OnPush,
1567
+ imports: [NgIcon],
1568
+ providers: [
1569
+ provideIcons({
1570
+ lucideCode,
1571
+ lucideHeading1,
1572
+ lucideHeading2,
1573
+ lucideHeading3,
1574
+ lucideList,
1575
+ lucideListOrdered,
1576
+ lucideListTodo,
1577
+ lucideMinus,
1578
+ lucidePilcrow,
1579
+ lucideSquareCode,
1580
+ lucideTable,
1581
+ lucideTextQuote,
1582
+ }),
1583
+ ],
1584
+ selector: 'qalma-slash-command-menu',
1585
+ template: `
1586
+ @if (placement(); as placement) {
1587
+ <div
1588
+ role="listbox"
1589
+ aria-label="Slash command suggestions"
1590
+ class="fixed z-20 flex w-[min(320px,calc(100vw-24px))] flex-col overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none"
1591
+ [style.left.px]="placement.left"
1592
+ [style.top.px]="placement.top"
1593
+ [style.bottom.px]="placement.bottom"
1594
+ [style.max-height.px]="placement.maxHeight"
1595
+ >
1596
+ <div
1597
+ class="shrink-0 px-2 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
1598
+ >
1599
+ Commands
1600
+ </div>
1601
+ <div data-suggestion-options class="min-h-0 flex-1 overflow-y-auto">
1602
+ @for (option of options(); track option.id; let index = $index) {
1603
+ <button
1604
+ type="button"
1605
+ role="option"
1606
+ [attr.data-suggestion-index]="index"
1607
+ class="group grid h-11 w-full min-w-0 cursor-pointer grid-cols-[1.75rem_minmax(0,1fr)_auto] items-center gap-2 rounded-sm px-2 text-left text-sm transition-colors hover:bg-accent-subtle hover:text-foreground focus:bg-accent-subtle focus:text-foreground focus:outline-none"
1608
+ [class.bg-accent-subtle]="index === activeIndex()"
1609
+ [class.text-foreground]="index === activeIndex()"
1610
+ [attr.aria-selected]="index === activeIndex()"
1611
+ [attr.tabindex]="index === activeIndex() ? 0 : -1"
1612
+ (mouseenter)="activate.emit(index)"
1613
+ (mousedown)="preserveSelection($event)"
1614
+ (keydown)="handleOptionKeydown($event, option, index)"
1615
+ (click)="pick.emit(option)"
1616
+ >
1617
+ <span
1618
+ class="flex size-7 items-center justify-center rounded-sm border border-border bg-card text-[14px] text-foreground"
1619
+ [class.border-accent]="index === activeIndex()"
1620
+ [class.bg-background]="index === activeIndex()"
1621
+ [class.text-accent]="index === activeIndex()"
1622
+ >
1623
+ <ng-icon [name]="option.icon" aria-hidden="true" />
1624
+ </span>
1625
+ <span class="min-w-0">
1626
+ <span class="block truncate font-medium leading-5">{{
1627
+ option.label
1628
+ }}</span>
1629
+ @if (index === activeIndex()) {
1630
+ <span
1631
+ class="block truncate text-xs leading-4 text-foreground"
1632
+ >
1633
+ {{ option.description }}
1634
+ </span>
1635
+ } @else {
1636
+ <span
1637
+ class="block truncate text-xs leading-4 text-muted-foreground group-hover:text-foreground group-focus:text-foreground"
1638
+ >
1639
+ {{ option.description }}
1640
+ </span>
1641
+ }
1642
+ </span>
1643
+ <kbd
1644
+ class="rounded-sm border border-border bg-card px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground"
1645
+ [class.border-accent]="index === activeIndex()"
1646
+ [class.bg-background]="index === activeIndex()"
1647
+ [class.text-accent]="index === activeIndex()"
1648
+ >
1649
+ {{ option.shortcut }}
1650
+ </kbd>
1651
+ </button>
1652
+ }
1653
+ </div>
1654
+ <div
1655
+ class="mt-1 flex h-8 shrink-0 items-center justify-between border-t border-border px-2 text-[11px] text-muted-foreground"
1656
+ >
1657
+ <span>Close menu</span>
1658
+ <kbd
1659
+ class="rounded-sm border border-border bg-card px-1.5 py-0.5 font-mono"
1660
+ >esc</kbd
1661
+ >
1662
+ </div>
1663
+ </div>
1664
+ }
1665
+ `,
1666
+ }]
1667
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
1668
+
1669
+ const LINK_POPOVER_WIDTH = 360;
1670
+ // Both the editing and preview rows are a single h-9 (36px) control plus
1671
+ // padding/border; this is an estimate since the actual element isn't
1672
+ // rendered yet when the placement is computed.
1673
+ const LINK_POPOVER_HEIGHT_ESTIMATE = 56;
1674
+ function createLinkPopoverPlacement(element) {
1675
+ const rect = element.getBoundingClientRect();
1676
+ return anchorToRect(rect, {
1677
+ placement: 'bottom',
1678
+ boundary: {
1679
+ top: 0,
1680
+ bottom: window.innerHeight,
1681
+ left: 0,
1682
+ right: window.innerWidth,
1683
+ },
1684
+ size: { width: LINK_POPOVER_WIDTH, height: LINK_POPOVER_HEIGHT_ESTIMATE },
1685
+ gap: 8,
1686
+ edgeMargin: 16,
1687
+ align: 'start',
1688
+ });
1689
+ }
1690
+ function findEditorLinkElement(target) {
1691
+ if (!(target instanceof Element)) {
1692
+ return null;
1693
+ }
1694
+ const element = target.closest('.ProseMirror a[href]');
1695
+ return element instanceof HTMLAnchorElement ? element : null;
1696
+ }
1697
+
1698
+ class LinkPopoverController {
1699
+ editor;
1700
+ popover = signal(null, ...(ngDevMode ? [{ debugName: "popover" }] : /* istanbul ignore next */ []));
1701
+ href = signal('', ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
1702
+ hideTimeout;
1703
+ constructor(editor) {
1704
+ this.editor = editor;
1705
+ }
1706
+ showToolbarEditor(event) {
1707
+ const linkState = this.editor.query('link');
1708
+ const target = event.currentTarget;
1709
+ if (!(target instanceof HTMLElement)) {
1710
+ return;
1711
+ }
1712
+ this.keepOpen();
1713
+ this.href.set(linkState?.href ?? 'https://');
1714
+ this.popover.set({
1715
+ ...createLinkPopoverPlacement(target),
1716
+ editing: true,
1717
+ element: null,
1718
+ href: linkState?.href ?? '',
1719
+ rel: linkState?.rel ?? null,
1720
+ target: linkState?.target ?? null,
1721
+ text: linkState?.text ?? '',
1722
+ });
1723
+ }
1724
+ showPreview(event) {
1725
+ const element = findEditorLinkElement(event.target);
1726
+ if (!element) {
1727
+ return;
1728
+ }
1729
+ this.keepOpen();
1730
+ this.popover.set({
1731
+ ...createLinkPopoverPlacement(element),
1732
+ editing: false,
1733
+ element,
1734
+ href: element.href,
1735
+ rel: element.rel || null,
1736
+ target: element.target === '_blank' ? '_blank' : null,
1737
+ text: element.textContent?.trim() ?? '',
1738
+ });
1739
+ }
1740
+ scheduleHideFromEvent(event) {
1741
+ const nextTarget = event.relatedTarget;
1742
+ if (nextTarget instanceof Element &&
1743
+ (findEditorLinkElement(nextTarget) ||
1744
+ nextTarget.closest('[data-qalma-link-popover]'))) {
1745
+ return;
1746
+ }
1747
+ if (!this.popover()?.editing) {
1748
+ this.scheduleHide();
1749
+ }
1750
+ }
1751
+ edit(popover) {
1752
+ this.keepOpen();
1753
+ this.selectLink(popover);
1754
+ this.href.set(popover.href);
1755
+ this.popover.set({
1756
+ ...popover,
1757
+ editing: true,
1758
+ });
1759
+ }
1760
+ save(popover) {
1761
+ const href = this.href().trim();
1762
+ if (!href) {
1763
+ return;
1764
+ }
1765
+ this.selectLink(popover);
1766
+ this.editor.execute('setLink', href);
1767
+ this.hide();
1768
+ }
1769
+ remove(popover) {
1770
+ this.selectLink(popover);
1771
+ this.editor.execute('unsetLink');
1772
+ this.hide();
1773
+ }
1774
+ hide() {
1775
+ this.clearHideTimeout();
1776
+ this.popover.set(null);
1777
+ }
1778
+ keepOpen() {
1779
+ this.clearHideTimeout();
1780
+ }
1781
+ scheduleHide() {
1782
+ this.clearHideTimeout();
1783
+ if (this.popover()?.editing) {
1784
+ return;
1785
+ }
1786
+ this.hideTimeout = setTimeout(() => {
1787
+ if (this.popover()?.editing) {
1788
+ this.hideTimeout = undefined;
1789
+ return;
1790
+ }
1791
+ this.popover.set(null);
1792
+ this.hideTimeout = undefined;
1793
+ }, 160);
1794
+ }
1795
+ selectLink(popover) {
1796
+ if (popover.element) {
1797
+ this.editor.execute('selectLink', {
1798
+ element: popover.element,
1799
+ });
1800
+ }
1801
+ }
1802
+ clearHideTimeout() {
1803
+ if (!this.hideTimeout) {
1804
+ return;
1805
+ }
1806
+ clearTimeout(this.hideTimeout);
1807
+ this.hideTimeout = undefined;
1808
+ }
1809
+ }
1810
+
1811
+ class QalmaLinkPopover {
1812
+ popover = input(null, ...(ngDevMode ? [{ debugName: "popover" }] : /* istanbul ignore next */ []));
1813
+ href = input.required(...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
1814
+ hrefChange = output();
1815
+ edit = output();
1816
+ save = output();
1817
+ remove = output();
1818
+ dismiss = output();
1819
+ keepOpen = output();
1820
+ scheduleHide = output();
1821
+ updateHref(event) {
1822
+ const target = event.target;
1823
+ if (target instanceof HTMLInputElement) {
1824
+ this.hrefChange.emit(target.value);
1825
+ }
1826
+ }
1827
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaLinkPopover, deps: [], target: i0.ɵɵFactoryTarget.Component });
1828
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaLinkPopover, isStandalone: true, selector: "qalma-link-popover", inputs: { popover: { classPropertyName: "popover", publicName: "popover", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { hrefChange: "hrefChange", edit: "edit", save: "save", remove: "remove", dismiss: "dismiss", keepOpen: "keepOpen", scheduleHide: "scheduleHide" }, providers: [
1829
+ provideIcons({
1830
+ lucideCheck,
1831
+ lucideExternalLink,
1832
+ lucideLink,
1833
+ lucidePencil,
1834
+ lucideUnlink,
1835
+ lucideX,
1836
+ }),
1837
+ ], ngImport: i0, template: `
1838
+ @if (popover(); as popover) {
1839
+ <div
1840
+ data-qalma-link-popover
1841
+ role="dialog"
1842
+ aria-label="Link preview"
1843
+ class="fixed z-20 w-[min(360px,calc(100vw-32px))] rounded-md border border-border bg-popover p-1.5 text-sm text-popover-foreground shadow-lg outline-none"
1844
+ [style.left.px]="popover.left"
1845
+ [style.top.px]="popover.top"
1846
+ (mouseenter)="keepOpen.emit()"
1847
+ (mouseleave)="scheduleHide.emit()"
1848
+ (focusin)="keepOpen.emit()"
1849
+ (focusout)="scheduleHide.emit()"
1850
+ >
1851
+ @if (popover.editing) {
1852
+ <div class="flex items-center gap-1.5">
1853
+ <label
1854
+ class="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-md border border-input bg-background px-2 text-sm transition focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30"
1855
+ >
1856
+ <ng-icon
1857
+ class="shrink-0 text-muted-foreground"
1858
+ name="lucideLink"
1859
+ aria-hidden="true"
1860
+ />
1861
+ <input
1862
+ class="min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground"
1863
+ type="url"
1864
+ aria-label="Edit link URL"
1865
+ [value]="href()"
1866
+ (input)="updateHref($event)"
1867
+ />
1868
+ </label>
1869
+ <button
1870
+ qalmaBtn
1871
+ size="icon"
1872
+ type="button"
1873
+ [disabled]="!href().trim()"
1874
+ (click)="save.emit(popover)"
1875
+ title="Save link"
1876
+ aria-label="Save link"
1877
+ >
1878
+ <ng-icon name="lucideCheck" aria-hidden="true" />
1879
+ </button>
1880
+ <button
1881
+ qalmaBtn
1882
+ variant="outline"
1883
+ size="icon"
1884
+ type="button"
1885
+ (click)="dismiss.emit()"
1886
+ title="Cancel"
1887
+ aria-label="Cancel"
1888
+ >
1889
+ <ng-icon name="lucideX" aria-hidden="true" />
1890
+ </button>
1891
+ </div>
1892
+ } @else {
1893
+ <div class="flex items-center gap-1.5">
1894
+ <a
1895
+ class="inline-flex h-9 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-border bg-card px-2.5 text-accent transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
1896
+ [href]="popover.href"
1897
+ [target]="popover.target ?? '_blank'"
1898
+ [rel]="popover.rel ?? 'noopener noreferrer'"
1899
+ title="Open link"
1900
+ >
1901
+ <span class="min-w-0 truncate font-medium">{{
1902
+ popover.href
1903
+ }}</span>
1904
+ <ng-icon
1905
+ class="shrink-0 text-base"
1906
+ name="lucideExternalLink"
1907
+ aria-hidden="true"
1908
+ />
1909
+ </a>
1910
+ <button
1911
+ qalmaBtn
1912
+ variant="outline"
1913
+ size="icon"
1914
+ type="button"
1915
+ (click)="edit.emit(popover)"
1916
+ title="Edit link"
1917
+ aria-label="Edit link"
1918
+ >
1919
+ <ng-icon name="lucidePencil" aria-hidden="true" />
1920
+ </button>
1921
+ <button
1922
+ qalmaBtn
1923
+ variant="ghost"
1924
+ size="icon"
1925
+ type="button"
1926
+ class="text-destructive hover:bg-destructive/10 hover:text-destructive"
1927
+ (click)="remove.emit(popover)"
1928
+ title="Unlink"
1929
+ aria-label="Unlink"
1930
+ >
1931
+ <ng-icon name="lucideUnlink" aria-hidden="true" />
1932
+ </button>
1933
+ </div>
1934
+ }
1935
+ </div>
1936
+ }
1937
+ `, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: QalmaButton, selector: "button[qalmaBtn], a[qalmaBtn]", inputs: ["variant", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1938
+ }
1939
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaLinkPopover, decorators: [{
1940
+ type: Component,
1941
+ args: [{
1942
+ changeDetection: ChangeDetectionStrategy.OnPush,
1943
+ imports: [NgIcon, QalmaButton],
1944
+ providers: [
1945
+ provideIcons({
1946
+ lucideCheck,
1947
+ lucideExternalLink,
1948
+ lucideLink,
1949
+ lucidePencil,
1950
+ lucideUnlink,
1951
+ lucideX,
1952
+ }),
1953
+ ],
1954
+ selector: 'qalma-link-popover',
1955
+ template: `
1956
+ @if (popover(); as popover) {
1957
+ <div
1958
+ data-qalma-link-popover
1959
+ role="dialog"
1960
+ aria-label="Link preview"
1961
+ class="fixed z-20 w-[min(360px,calc(100vw-32px))] rounded-md border border-border bg-popover p-1.5 text-sm text-popover-foreground shadow-lg outline-none"
1962
+ [style.left.px]="popover.left"
1963
+ [style.top.px]="popover.top"
1964
+ (mouseenter)="keepOpen.emit()"
1965
+ (mouseleave)="scheduleHide.emit()"
1966
+ (focusin)="keepOpen.emit()"
1967
+ (focusout)="scheduleHide.emit()"
1968
+ >
1969
+ @if (popover.editing) {
1970
+ <div class="flex items-center gap-1.5">
1971
+ <label
1972
+ class="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-md border border-input bg-background px-2 text-sm transition focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30"
1973
+ >
1974
+ <ng-icon
1975
+ class="shrink-0 text-muted-foreground"
1976
+ name="lucideLink"
1977
+ aria-hidden="true"
1978
+ />
1979
+ <input
1980
+ class="min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground"
1981
+ type="url"
1982
+ aria-label="Edit link URL"
1983
+ [value]="href()"
1984
+ (input)="updateHref($event)"
1985
+ />
1986
+ </label>
1987
+ <button
1988
+ qalmaBtn
1989
+ size="icon"
1990
+ type="button"
1991
+ [disabled]="!href().trim()"
1992
+ (click)="save.emit(popover)"
1993
+ title="Save link"
1994
+ aria-label="Save link"
1995
+ >
1996
+ <ng-icon name="lucideCheck" aria-hidden="true" />
1997
+ </button>
1998
+ <button
1999
+ qalmaBtn
2000
+ variant="outline"
2001
+ size="icon"
2002
+ type="button"
2003
+ (click)="dismiss.emit()"
2004
+ title="Cancel"
2005
+ aria-label="Cancel"
2006
+ >
2007
+ <ng-icon name="lucideX" aria-hidden="true" />
2008
+ </button>
2009
+ </div>
2010
+ } @else {
2011
+ <div class="flex items-center gap-1.5">
2012
+ <a
2013
+ class="inline-flex h-9 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-border bg-card px-2.5 text-accent transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
2014
+ [href]="popover.href"
2015
+ [target]="popover.target ?? '_blank'"
2016
+ [rel]="popover.rel ?? 'noopener noreferrer'"
2017
+ title="Open link"
2018
+ >
2019
+ <span class="min-w-0 truncate font-medium">{{
2020
+ popover.href
2021
+ }}</span>
2022
+ <ng-icon
2023
+ class="shrink-0 text-base"
2024
+ name="lucideExternalLink"
2025
+ aria-hidden="true"
2026
+ />
2027
+ </a>
2028
+ <button
2029
+ qalmaBtn
2030
+ variant="outline"
2031
+ size="icon"
2032
+ type="button"
2033
+ (click)="edit.emit(popover)"
2034
+ title="Edit link"
2035
+ aria-label="Edit link"
2036
+ >
2037
+ <ng-icon name="lucidePencil" aria-hidden="true" />
2038
+ </button>
2039
+ <button
2040
+ qalmaBtn
2041
+ variant="ghost"
2042
+ size="icon"
2043
+ type="button"
2044
+ class="text-destructive hover:bg-destructive/10 hover:text-destructive"
2045
+ (click)="remove.emit(popover)"
2046
+ title="Unlink"
2047
+ aria-label="Unlink"
2048
+ >
2049
+ <ng-icon name="lucideUnlink" aria-hidden="true" />
2050
+ </button>
2051
+ </div>
2052
+ }
2053
+ </div>
2054
+ }
2055
+ `,
2056
+ }]
2057
+ }], propDecorators: { popover: [{ type: i0.Input, args: [{ isSignal: true, alias: "popover", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], hrefChange: [{ type: i0.Output, args: ["hrefChange"] }], edit: [{ type: i0.Output, args: ["edit"] }], save: [{ type: i0.Output, args: ["save"] }], remove: [{ type: i0.Output, args: ["remove"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], keepOpen: [{ type: i0.Output, args: ["keepOpen"] }], scheduleHide: [{ type: i0.Output, args: ["scheduleHide"] }] } });
2058
+
2059
+ class QalmaContextualToolbar {
2060
+ editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
2061
+ placement = input(null, ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
2062
+ requestLink = output();
2063
+ dismiss = output();
2064
+ canSetLink = computed(() => this.editor().canExecute('setLink', 'https://angular.dev'), ...(ngDevMode ? [{ debugName: "canSetLink" }] : /* istanbul ignore next */ []));
2065
+ linkActive = computed(() => this.editor().isCommandActive('setLink'), ...(ngDevMode ? [{ debugName: "linkActive" }] : /* istanbul ignore next */ []));
2066
+ preserveSelection(event) {
2067
+ event.preventDefault();
2068
+ }
2069
+ dismissFromKeyboard(event) {
2070
+ event.preventDefault();
2071
+ this.dismiss.emit();
2072
+ }
2073
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaContextualToolbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2074
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaContextualToolbar, isStandalone: true, selector: "qalma-contextual-toolbar", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { requestLink: "requestLink", dismiss: "dismiss" }, providers: [
2075
+ provideIcons({
2076
+ lucideBold,
2077
+ lucideCode,
2078
+ lucideItalic,
2079
+ lucideLetterText,
2080
+ lucideLink,
2081
+ }),
2082
+ ], ngImport: i0, template: `
2083
+ @if (placement(); as placement) {
2084
+ <qalma-toolbar
2085
+ label="Selection formatting"
2086
+ class="fixed left-0 top-0 z-30 flex items-center gap-0.5 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg will-change-transform"
2087
+ [style.transform]="placement.transform"
2088
+ (mousedown)="preserveSelection($event)"
2089
+ (keydown.escape)="dismissFromKeyboard($event)"
2090
+ >
2091
+ <button
2092
+ qalmaBtn
2093
+ variant="ghost"
2094
+ size="icon"
2095
+ type="button"
2096
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2097
+ qalmaCommand="toggleBold"
2098
+ title="Bold"
2099
+ aria-label="Bold"
2100
+ >
2101
+ <ng-icon
2102
+ class="text-[0.9rem]"
2103
+ name="lucideBold"
2104
+ aria-hidden="true"
2105
+ />
2106
+ </button>
2107
+ <button
2108
+ qalmaBtn
2109
+ variant="ghost"
2110
+ size="icon"
2111
+ type="button"
2112
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2113
+ qalmaCommand="toggleItalic"
2114
+ title="Italic"
2115
+ aria-label="Italic"
2116
+ >
2117
+ <ng-icon
2118
+ class="text-[0.9rem]"
2119
+ name="lucideItalic"
2120
+ aria-hidden="true"
2121
+ />
2122
+ </button>
2123
+ <button
2124
+ qalmaBtn
2125
+ variant="ghost"
2126
+ size="icon"
2127
+ type="button"
2128
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2129
+ qalmaCommand="toggleInlineCode"
2130
+ title="Inline code"
2131
+ aria-label="Inline code"
2132
+ >
2133
+ <ng-icon
2134
+ class="text-[0.9rem]"
2135
+ name="lucideCode"
2136
+ aria-hidden="true"
2137
+ />
2138
+ </button>
2139
+ <button
2140
+ qalmaBtn
2141
+ variant="ghost"
2142
+ size="icon"
2143
+ type="button"
2144
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2145
+ qalmaCommand="toggleMonospace"
2146
+ title="Monospace"
2147
+ aria-label="Monospace"
2148
+ >
2149
+ <ng-icon
2150
+ class="text-[0.9rem]"
2151
+ name="lucideLetterText"
2152
+ aria-hidden="true"
2153
+ />
2154
+ </button>
2155
+ <span class="mx-0.5 h-5 w-px bg-border" aria-hidden="true"></span>
2156
+ <button
2157
+ qalmaBtn
2158
+ variant="ghost"
2159
+ size="icon"
2160
+ type="button"
2161
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2162
+ [class.qalma-command-active]="linkActive()"
2163
+ [attr.aria-pressed]="linkActive()"
2164
+ [disabled]="!canSetLink()"
2165
+ (mousedown)="preserveSelection($event)"
2166
+ (click)="requestLink.emit($event)"
2167
+ title="Link"
2168
+ aria-label="Link"
2169
+ >
2170
+ <ng-icon
2171
+ class="text-[0.9rem]"
2172
+ name="lucideLink"
2173
+ aria-hidden="true"
2174
+ />
2175
+ </button>
2176
+ </qalma-toolbar>
2177
+ }
2178
+ `, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: QalmaButton, selector: "button[qalmaBtn], a[qalmaBtn]", inputs: ["variant", "size"] }, { kind: "directive", type: QalmaCommand, selector: "button[qalmaCommand]", inputs: ["qalmaCommand", "qalmaCommandValue"] }, { kind: "component", type: QalmaToolbar, selector: "qalma-toolbar", inputs: ["label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2179
+ }
2180
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaContextualToolbar, decorators: [{
2181
+ type: Component,
2182
+ args: [{
2183
+ changeDetection: ChangeDetectionStrategy.OnPush,
2184
+ imports: [NgIcon, QalmaButton, QalmaCommand, QalmaToolbar],
2185
+ providers: [
2186
+ provideIcons({
2187
+ lucideBold,
2188
+ lucideCode,
2189
+ lucideItalic,
2190
+ lucideLetterText,
2191
+ lucideLink,
2192
+ }),
2193
+ ],
2194
+ selector: 'qalma-contextual-toolbar',
2195
+ template: `
2196
+ @if (placement(); as placement) {
2197
+ <qalma-toolbar
2198
+ label="Selection formatting"
2199
+ class="fixed left-0 top-0 z-30 flex items-center gap-0.5 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg will-change-transform"
2200
+ [style.transform]="placement.transform"
2201
+ (mousedown)="preserveSelection($event)"
2202
+ (keydown.escape)="dismissFromKeyboard($event)"
2203
+ >
2204
+ <button
2205
+ qalmaBtn
2206
+ variant="ghost"
2207
+ size="icon"
2208
+ type="button"
2209
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2210
+ qalmaCommand="toggleBold"
2211
+ title="Bold"
2212
+ aria-label="Bold"
2213
+ >
2214
+ <ng-icon
2215
+ class="text-[0.9rem]"
2216
+ name="lucideBold"
2217
+ aria-hidden="true"
2218
+ />
2219
+ </button>
2220
+ <button
2221
+ qalmaBtn
2222
+ variant="ghost"
2223
+ size="icon"
2224
+ type="button"
2225
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2226
+ qalmaCommand="toggleItalic"
2227
+ title="Italic"
2228
+ aria-label="Italic"
2229
+ >
2230
+ <ng-icon
2231
+ class="text-[0.9rem]"
2232
+ name="lucideItalic"
2233
+ aria-hidden="true"
2234
+ />
2235
+ </button>
2236
+ <button
2237
+ qalmaBtn
2238
+ variant="ghost"
2239
+ size="icon"
2240
+ type="button"
2241
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2242
+ qalmaCommand="toggleInlineCode"
2243
+ title="Inline code"
2244
+ aria-label="Inline code"
2245
+ >
2246
+ <ng-icon
2247
+ class="text-[0.9rem]"
2248
+ name="lucideCode"
2249
+ aria-hidden="true"
2250
+ />
2251
+ </button>
2252
+ <button
2253
+ qalmaBtn
2254
+ variant="ghost"
2255
+ size="icon"
2256
+ type="button"
2257
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2258
+ qalmaCommand="toggleMonospace"
2259
+ title="Monospace"
2260
+ aria-label="Monospace"
2261
+ >
2262
+ <ng-icon
2263
+ class="text-[0.9rem]"
2264
+ name="lucideLetterText"
2265
+ aria-hidden="true"
2266
+ />
2267
+ </button>
2268
+ <span class="mx-0.5 h-5 w-px bg-border" aria-hidden="true"></span>
2269
+ <button
2270
+ qalmaBtn
2271
+ variant="ghost"
2272
+ size="icon"
2273
+ type="button"
2274
+ class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
2275
+ [class.qalma-command-active]="linkActive()"
2276
+ [attr.aria-pressed]="linkActive()"
2277
+ [disabled]="!canSetLink()"
2278
+ (mousedown)="preserveSelection($event)"
2279
+ (click)="requestLink.emit($event)"
2280
+ title="Link"
2281
+ aria-label="Link"
2282
+ >
2283
+ <ng-icon
2284
+ class="text-[0.9rem]"
2285
+ name="lucideLink"
2286
+ aria-hidden="true"
2287
+ />
2288
+ </button>
2289
+ </qalma-toolbar>
2290
+ }
2291
+ `,
2292
+ }]
2293
+ }], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], requestLink: [{ type: i0.Output, args: ["requestLink"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
2294
+
2295
+ const TOOLBAR_EDGE_MARGIN = 16;
2296
+ const TOOLBAR_GAP = 8;
2297
+ class QalmaSelectionToolbarController {
2298
+ editor;
2299
+ placementState = signal(null, ...(ngDevMode ? [{ debugName: "placementState" }] : /* istanbul ignore next */ []));
2300
+ placement = this.placementState.asReadonly();
2301
+ surface = null;
2302
+ refreshFrame = null;
2303
+ placementKey = null;
2304
+ constructor(editor) {
2305
+ this.editor = editor;
2306
+ }
2307
+ connect(surface, destroyRef) {
2308
+ this.surface = surface;
2309
+ const refresh = () => this.refresh();
2310
+ const scheduleRefresh = () => this.scheduleRefresh();
2311
+ const handleKeydown = (event) => this.handleKeydown(event);
2312
+ surface.addEventListener('qalma-selection-update', refresh);
2313
+ surface.addEventListener('keydown', handleKeydown);
2314
+ surface.addEventListener('keyup', refresh);
2315
+ surface.addEventListener('mouseup', refresh);
2316
+ surface.addEventListener('scroll', scheduleRefresh, {
2317
+ passive: true,
2318
+ });
2319
+ window.addEventListener('scroll', scheduleRefresh, {
2320
+ passive: true,
2321
+ });
2322
+ window.addEventListener('resize', scheduleRefresh, {
2323
+ passive: true,
2324
+ });
2325
+ destroyRef.onDestroy(() => {
2326
+ this.cancelScheduledRefresh();
2327
+ this.surface = null;
2328
+ surface.removeEventListener('qalma-selection-update', refresh);
2329
+ surface.removeEventListener('keydown', handleKeydown);
2330
+ surface.removeEventListener('keyup', refresh);
2331
+ surface.removeEventListener('mouseup', refresh);
2332
+ surface.removeEventListener('scroll', scheduleRefresh);
2333
+ window.removeEventListener('scroll', scheduleRefresh);
2334
+ window.removeEventListener('resize', scheduleRefresh);
2335
+ });
2336
+ }
2337
+ refresh() {
2338
+ const surface = this.surface;
2339
+ const selection = this.editor().query('selection');
2340
+ if (!surface ||
2341
+ !selection ||
2342
+ selection.empty ||
2343
+ selection.text.trim().length === 0) {
2344
+ this.hide();
2345
+ return;
2346
+ }
2347
+ const domSelection = window.getSelection();
2348
+ if (!domSelection ||
2349
+ domSelection.rangeCount === 0 ||
2350
+ !isSelectionInsideSurface(domSelection, surface)) {
2351
+ this.hide();
2352
+ return;
2353
+ }
2354
+ const rect = getVisibleSelectionRect(domSelection.getRangeAt(0));
2355
+ if (!rect || !isRectVisibleInsideSurface(rect, surface)) {
2356
+ this.hide();
2357
+ return;
2358
+ }
2359
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
2360
+ const x = Math.round(clamp(rect.left + rect.width / 2, TOOLBAR_EDGE_MARGIN, viewportWidth - TOOLBAR_EDGE_MARGIN));
2361
+ const y = Math.round(Math.max(TOOLBAR_EDGE_MARGIN, rect.top - TOOLBAR_GAP));
2362
+ this.setPlacement({
2363
+ transform: `translate3d(${x}px, ${y}px, 0) translate(-50%, -100%)`,
2364
+ });
2365
+ }
2366
+ hide() {
2367
+ this.setPlacement(null);
2368
+ }
2369
+ handleKeydown(event) {
2370
+ if (!(event instanceof KeyboardEvent) ||
2371
+ event.key !== 'Escape' ||
2372
+ !this.placement()) {
2373
+ return;
2374
+ }
2375
+ event.preventDefault();
2376
+ this.hide();
2377
+ }
2378
+ scheduleRefresh() {
2379
+ if (!this.placement() || this.refreshFrame !== null) {
2380
+ return;
2381
+ }
2382
+ this.refreshFrame = requestAnimationFrame(() => {
2383
+ this.refreshFrame = null;
2384
+ this.refresh();
2385
+ });
2386
+ }
2387
+ cancelScheduledRefresh() {
2388
+ if (this.refreshFrame === null) {
2389
+ return;
2390
+ }
2391
+ cancelAnimationFrame(this.refreshFrame);
2392
+ this.refreshFrame = null;
2393
+ }
2394
+ setPlacement(placement) {
2395
+ const nextKey = placement?.transform ?? null;
2396
+ if (nextKey === this.placementKey) {
2397
+ return;
2398
+ }
2399
+ this.placementKey = nextKey;
2400
+ this.placementState.set(placement);
2401
+ }
2402
+ }
2403
+ function getVisibleSelectionRect(range) {
2404
+ const rect = range.getBoundingClientRect();
2405
+ if (rect.width > 0 || rect.height > 0) {
2406
+ return rect;
2407
+ }
2408
+ for (const clientRect of Array.from(range.getClientRects())) {
2409
+ if (clientRect.width > 0 || clientRect.height > 0) {
2410
+ return clientRect;
2411
+ }
2412
+ }
2413
+ return null;
2414
+ }
2415
+ function isSelectionInsideSurface(selection, surface) {
2416
+ return (isNodeInsideSurface(selection.anchorNode, surface) &&
2417
+ isNodeInsideSurface(selection.focusNode, surface));
2418
+ }
2419
+ function isNodeInsideSurface(node, surface) {
2420
+ const element = node instanceof Element ? node : node?.parentElement;
2421
+ return Boolean(element && surface.contains(element));
2422
+ }
2423
+ function isRectVisibleInsideSurface(rect, surface) {
2424
+ const surfaceRect = surface.getBoundingClientRect();
2425
+ return (rect.bottom > surfaceRect.top &&
2426
+ rect.top < surfaceRect.bottom &&
2427
+ rect.right > surfaceRect.left &&
2428
+ rect.left < surfaceRect.right);
2429
+ }
2430
+ function clamp(value, min, max) {
2431
+ return Math.min(Math.max(value, min), max);
2432
+ }
2433
+
2434
+ class QalmaSelectionToolbarDirective {
2435
+ editor = input.required({ ...(ngDevMode ? { debugName: "editor" } : /* istanbul ignore next */ {}), alias: 'qalmaSelectionToolbar' });
2436
+ destroyRef = inject(DestroyRef);
2437
+ host = inject(ElementRef);
2438
+ controller = new QalmaSelectionToolbarController(() => this.editor());
2439
+ placement = this.controller.placement;
2440
+ constructor() {
2441
+ afterNextRender(() => {
2442
+ this.controller.connect(this.host.nativeElement, this.destroyRef);
2443
+ });
2444
+ }
2445
+ refresh() {
2446
+ this.controller.refresh();
2447
+ }
2448
+ hide() {
2449
+ this.controller.hide();
2450
+ }
2451
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSelectionToolbarDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2452
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: QalmaSelectionToolbarDirective, isStandalone: true, selector: "[qalmaSelectionToolbar]", inputs: { editor: { classPropertyName: "editor", publicName: "qalmaSelectionToolbar", isSignal: true, isRequired: true, transformFunction: null } }, exportAs: ["qalmaSelectionToolbar"], ngImport: i0 });
2453
+ }
2454
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSelectionToolbarDirective, decorators: [{
2455
+ type: Directive,
2456
+ args: [{
2457
+ exportAs: 'qalmaSelectionToolbar',
2458
+ selector: '[qalmaSelectionToolbar]',
2459
+ }]
2460
+ }], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "qalmaSelectionToolbar", required: true }] }] } });
2461
+
2462
+ /**
2463
+ * Generated bundle index. Do not edit.
2464
+ */
2465
+
2466
+ export { DismissibleOverlay, KeyboardNavigableList, LinkPopoverController, QALMA_TOOLBAR_ALIGN, QALMA_TOOLBAR_CLEAR_FORMATTING, QALMA_TOOLBAR_HEADINGS, QALMA_TOOLBAR_HISTORY, QALMA_TOOLBAR_INLINE_MARKS, QALMA_TOOLBAR_LISTS, QALMA_TOOLBAR_TABLE_INSERT, QALMA_TOOLBAR_TABLE_OPS, QALMA_TOOLBAR_UNSET_LINK, QalmaButton, QalmaContextualToolbar, QalmaDragHandle, QalmaDragHandleController, QalmaDragHandleDirective, QalmaLinkPopover, QalmaMentionMenu, QalmaSelectionToolbarController, QalmaSelectionToolbarDirective, QalmaSlashCommandMenu, QalmaSuggestionMenu, QalmaToolbarButton, QalmaToolbarRegistry, SUGGESTION_OPTIONS_SCROLLER_ATTR, SUGGESTION_OPTION_INDEX_ATTR, TOOLBAR_BUTTON_CLASS, TOOLBAR_SEPARATOR_CLASS, anchorToRect, buttonVariants, cn, createLinkPopoverPlacement, findEditorLinkElement, flipAbovePlacement, isToolbarCommandItem, provideQalmaToolbarIcons };
2467
+ //# sourceMappingURL=qalma-kit.mjs.map