@h-k-dev/angular-email-editor 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +127 -2
  2. package/fesm2022/h-k-dev-angular-email-editor-actions.mjs +179 -0
  3. package/fesm2022/h-k-dev-angular-email-editor-actions.mjs.map +1 -0
  4. package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs +383 -0
  5. package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs.map +1 -0
  6. package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs +1106 -0
  7. package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs.map +1 -0
  8. package/fesm2022/h-k-dev-angular-email-editor-address.mjs +489 -0
  9. package/fesm2022/h-k-dev-angular-email-editor-address.mjs.map +1 -0
  10. package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs +64 -0
  11. package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs.map +1 -0
  12. package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs +387 -0
  13. package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs.map +1 -0
  14. package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs +80 -0
  15. package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs.map +1 -0
  16. package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs +346 -0
  17. package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs.map +1 -0
  18. package/fesm2022/h-k-dev-angular-email-editor-focus.mjs +78 -0
  19. package/fesm2022/h-k-dev-angular-email-editor-focus.mjs.map +1 -0
  20. package/fesm2022/h-k-dev-angular-email-editor-image.mjs +132 -0
  21. package/fesm2022/h-k-dev-angular-email-editor-image.mjs.map +1 -0
  22. package/fesm2022/h-k-dev-angular-email-editor-material.mjs +124 -0
  23. package/fesm2022/h-k-dev-angular-email-editor-material.mjs.map +1 -0
  24. package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs +243 -0
  25. package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs.map +1 -0
  26. package/fesm2022/h-k-dev-angular-email-editor-palette.mjs +87 -0
  27. package/fesm2022/h-k-dev-angular-email-editor-palette.mjs.map +1 -0
  28. package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs +307 -0
  29. package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs.map +1 -0
  30. package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs +392 -0
  31. package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs.map +1 -0
  32. package/fesm2022/h-k-dev-angular-email-editor.mjs +18820 -0
  33. package/fesm2022/h-k-dev-angular-email-editor.mjs.map +1 -0
  34. package/package.json +133 -3
  35. package/styles/_scroll.scss +101 -0
  36. package/types/h-k-dev-angular-email-editor-actions.d.ts +147 -0
  37. package/types/h-k-dev-angular-email-editor-address-chip.d.ts +234 -0
  38. package/types/h-k-dev-angular-email-editor-address-input.d.ts +449 -0
  39. package/types/h-k-dev-angular-email-editor-address.d.ts +125 -0
  40. package/types/h-k-dev-angular-email-editor-anchor.d.ts +44 -0
  41. package/types/h-k-dev-angular-email-editor-attachment-chip.d.ts +271 -0
  42. package/types/h-k-dev-angular-email-editor-attachment-chips.d.ts +48 -0
  43. package/types/h-k-dev-angular-email-editor-chat-input.d.ts +101 -0
  44. package/types/h-k-dev-angular-email-editor-focus.d.ts +46 -0
  45. package/types/h-k-dev-angular-email-editor-image.d.ts +66 -0
  46. package/types/h-k-dev-angular-email-editor-material.d.ts +65 -0
  47. package/types/h-k-dev-angular-email-editor-metrics.d.ts +110 -0
  48. package/types/h-k-dev-angular-email-editor-palette.d.ts +51 -0
  49. package/types/h-k-dev-angular-email-editor-proposal.d.ts +170 -0
  50. package/types/h-k-dev-angular-email-editor-suggestion-menu.d.ts +177 -0
  51. package/types/h-k-dev-angular-email-editor.d.ts +3959 -0
@@ -0,0 +1,110 @@
1
+ import { Node } from 'prosemirror-model';
2
+ import { PreparedText } from '@chenglou/pretext';
3
+ import { PreparedRichInline } from '@chenglou/pretext/rich-inline';
4
+ import { FunctionalExtension } from '@h-k-dev/angular-email-editor';
5
+
6
+ /**
7
+ * Math-based text measurement for the editor document, built on
8
+ * `@chenglou/pretext`. Block heights and line counts are computed from cached
9
+ * canvas glyph widths instead of DOM reads (`offsetHeight`,
10
+ * `getBoundingClientRect`), so measuring never triggers a layout reflow.
11
+ */
12
+ /** Typography of the live editor surface, read from computed style once. */
13
+ interface EditorTypography {
14
+ fontFamily: string;
15
+ /** Body text size in px. */
16
+ fontSize: number;
17
+ fontWeight: number;
18
+ fontStyle: string;
19
+ /** Body line height in px. */
20
+ lineHeight: number;
21
+ /** Vertical gap between sibling top-level blocks (`--prose-gap`), px. */
22
+ blockGap: number;
23
+ /** Heading font-size factors for levels 1-6, relative to the body size. */
24
+ headingScale: readonly number[];
25
+ /** Horizontal space consumed by wrapper blocks, px. */
26
+ indents: {
27
+ blockquote: number;
28
+ list: number;
29
+ /** Vertical gap between adjacent list items, px. */
30
+ listItemGap: number;
31
+ };
32
+ }
33
+ /**
34
+ * Reads the typography pretext needs from the editor's computed style. One
35
+ * DOM read at startup (and again once webfonts settle) — never per keystroke.
36
+ */
37
+ declare function readTypography(dom: HTMLElement): EditorTypography;
38
+ interface TextMetrics {
39
+ words: number;
40
+ characters: number;
41
+ /** Wrapped line count across all text blocks at the current width. */
42
+ lines: number;
43
+ /**
44
+ * Estimated rendered height of the document in px, block gaps included.
45
+ * Text-only: atom blocks (images) contribute no height since their
46
+ * rendered size isn't knowable without loading them.
47
+ */
48
+ estimatedHeight: number;
49
+ }
50
+ /**
51
+ * Prepared measurements for one textblock. Single-font blocks (the common
52
+ * case in email) go through `prepare()` with `pre-wrap`, matching the editor
53
+ * CSS exactly — hard breaks become `\n` in one prepared handle. Blocks with
54
+ * mark-dependent fonts (bold/italic) use the rich-inline API, one prepared
55
+ * handle per hard-break segment — `pre-wrap` too, so a bold word's line
56
+ * breaks and spaces count exactly as the plain line's.
57
+ */
58
+ type PreparedBlock = {
59
+ kind: 'plain';
60
+ prepared: PreparedText;
61
+ } | {
62
+ kind: 'rich';
63
+ segments: PreparedRichInline[];
64
+ };
65
+ interface CachedBlock {
66
+ block: PreparedBlock;
67
+ lineHeight: number;
68
+ words: number;
69
+ characters: number;
70
+ /** Line count memoised for the last width measured at. */
71
+ lastWidth: number;
72
+ lastLines: number;
73
+ }
74
+ /**
75
+ * Measurement cache keyed on ProseMirror's immutable nodes: blocks untouched
76
+ * by a transaction keep their node identity, so across keystrokes only the
77
+ * edited block is ever re-prepared. Create one per editor view; replace it
78
+ * wholesale to invalidate (e.g. after webfonts load).
79
+ */
80
+ type BlockMeasureCache = WeakMap<Node, CachedBlock>;
81
+ declare const createBlockMeasureCache: () => BlockMeasureCache;
82
+ /**
83
+ * Measures the whole document at the given content width. Pure arithmetic
84
+ * over cached glyph widths — no DOM access.
85
+ */
86
+ declare function measureDoc(doc: Node, contentWidth: number, typography: EditorTypography, cache: BlockMeasureCache): TextMetrics;
87
+
88
+ interface TextMetricsOptions {
89
+ /** Called with fresh metrics after every doc change and width resize. */
90
+ onMetrics: (metrics: TextMetrics) => void;
91
+ /** Called when a measurement fails — no canvas to measure with (a server
92
+ render, jsdom), or a font the browser cannot use. The editor goes on
93
+ unharmed; that measurement reports nothing, and the next change, or
94
+ the web fonts arriving, tries again. */
95
+ onError?: (error: unknown) => void;
96
+ }
97
+ /**
98
+ * Live document metrics (words, lines, estimated height) computed with
99
+ * `@chenglou/pretext` — pure arithmetic over cached glyph widths, no DOM
100
+ * measurement on the keystroke path. Unchanged blocks are never re-measured:
101
+ * the cache keys on ProseMirror's immutable nodes.
102
+ *
103
+ * Metrics are an extra, never a dependency of the editor: a measurement
104
+ * that throws is reported through `onError` and skipped, and without a
105
+ * `ResizeObserver` the width read at mount stands.
106
+ */
107
+ declare const createTextMetrics: (options: TextMetricsOptions) => FunctionalExtension;
108
+
109
+ export { createBlockMeasureCache, createTextMetrics, measureDoc, readTypography };
110
+ export type { BlockMeasureCache, EditorTypography, TextMetrics, TextMetricsOptions };
@@ -0,0 +1,51 @@
1
+ import { InjectionToken, Provider } from '@angular/core';
2
+ import { PaletteColor, ExtensionContext, SuggestionCommandItem } from '@h-k-dev/angular-email-editor';
3
+
4
+ /** The colours a host offers: for text, and for a background. */
5
+ interface EmailPalette {
6
+ readonly text: readonly PaletteColor[];
7
+ readonly background: readonly PaletteColor[];
8
+ }
9
+ /** The library's own palette — the swatches that pass on both a light and a
10
+ dark client (`emailTextPalette`, `emailBackgroundPalette`). */
11
+ declare const emailPalette: EmailPalette;
12
+ /** A host's palette: either list, the other staying the library's — or a
13
+ function of the library's palette, to mix: keep it and add a brand
14
+ colour, take a few out, replace one side. */
15
+ type EmailPaletteConfig = Partial<EmailPalette> | ((defaults: EmailPalette) => Partial<EmailPalette>);
16
+ /**
17
+ * The palette in use — the library's unless a host provides its own
18
+ * ({@link providePalette}). What the colour pickers offer and what the `/`
19
+ * menu's colour rows are made of ({@link colorSuggestions}).
20
+ */
21
+ declare const EMAIL_PALETTE: InjectionToken<EmailPalette>;
22
+ /** The palette a config comes to, over the library's. */
23
+ declare function resolvePalette(config: EmailPaletteConfig): EmailPalette;
24
+ /**
25
+ * Makes a palette the app's — in `appConfig.providers`, or a component's:
26
+ *
27
+ * providePalette({ text: BRAND_TEXT_COLORS })
28
+ * providePalette((defaults) => ({
29
+ * text: [...defaults.text, { name: 'Brand', value: '#0a5c36' }],
30
+ * }))
31
+ *
32
+ * Every colour goes into an email as written: keep to what a client shows
33
+ * on both a light and a dark page (`passesDualContrast`).
34
+ */
35
+ declare function providePalette(config: EmailPaletteConfig): Provider;
36
+ /** The palette in use, in an injection context. */
37
+ declare function injectPalette(): EmailPalette;
38
+ /**
39
+ * The palette as rows of a `/` menu, the way a chat's slash menu lists
40
+ * colours: "Red text", "Red background" — one row per colour of either
41
+ * side, in the section `color`, each carrying its `swatch`. Picked, a row
42
+ * colours the selection, or what is typed next at a bare caret (the
43
+ * `TextStyle` mark's `setColor` / `setBackgroundColor`); nothing, when the
44
+ * kit has no `TextStyle`.
45
+ *
46
+ * items: (ctx) => [...extensionSuggestions(ctx), ...colorSuggestions(ctx, palette)]
47
+ */
48
+ declare function colorSuggestions(ctx: ExtensionContext, palette: EmailPalette): SuggestionCommandItem[];
49
+
50
+ export { EMAIL_PALETTE, colorSuggestions, emailPalette, injectPalette, providePalette, resolvePalette };
51
+ export type { EmailPalette, EmailPaletteConfig };
@@ -0,0 +1,170 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Signal, Injector } from '@angular/core';
3
+ import { ContentStreamRange, ContentStreamWriter, StreamContentOptions, ContentStreamRun, Editor } from '@h-k-dev/angular-email-editor';
4
+ import { AnchorRect } from '@h-k-dev/angular-email-editor/anchor';
5
+
6
+ /**
7
+ * A proposal in an editor, as a host's panel sees it: what is proposed and
8
+ * whether it is still coming, where it stands, and the three things to do
9
+ * about it. The reactive face of `proposeContent` / `acceptProposal` /
10
+ * `discardProposal` (the main entry, which needs `createContentProposal()`
11
+ * and `createContentStream()` in the kit).
12
+ */
13
+ interface EditorProposal {
14
+ /** Something is proposed — written into the text, not the writer's yet. */
15
+ readonly active: Signal<boolean>;
16
+ /** The proposal is still being written. */
17
+ readonly streaming: Signal<boolean>;
18
+ /** Asked, and nothing has come yet: the model is thinking — for a host
19
+ to show it, since there is nothing in the text to see. */
20
+ readonly thinking: Signal<boolean>;
21
+ /** Where it stands, in the document. */
22
+ readonly range: Signal<ContentStreamRange | null>;
23
+ /** Its box in viewport coordinates — the last line's, for a panel to
24
+ stand under; a caret's when nothing is proposed yet. Measured anew on
25
+ every change of the editor, so it follows the writing. */
26
+ readonly box: Signal<AnchorRect | null>;
27
+ /** Proposes content at the selection (or at `target`), written by
28
+ `callback` — see `streamContent` for the writer and the options. A
29
+ proposal already there is discarded first. `null` without an editor. */
30
+ propose(callback: (writer: ContentStreamWriter) => Promise<void> | void, options?: Omit<StreamContentOptions, 'history'> & {
31
+ target?: number | ContentStreamRange;
32
+ }): ContentStreamRun | null;
33
+ /** Writes a part of the proposal again — `range`, inside it, or the
34
+ editor's selection when it lies inside — the rest standing. `null`
35
+ when there is no proposal holding it. */
36
+ revise(callback: (writer: ContentStreamWriter) => Promise<void> | void, options?: Omit<StreamContentOptions, 'history'> & {
37
+ range?: ContentStreamRange;
38
+ }): ContentStreamRun | null;
39
+ /** The editor's selection, when it is a range inside the proposal —
40
+ what {@link revise} would write again; null otherwise. */
41
+ readonly selectedPart: Signal<ContentStreamRange | null>;
42
+ /** Stops the writing where it is; what is proposed stays proposed. */
43
+ stop(): void;
44
+ /** Takes the proposal into the document: one change, one undo. */
45
+ accept(): boolean;
46
+ /** Takes it out, as if never written. */
47
+ discard(): boolean;
48
+ }
49
+ /**
50
+ * The proposal of an editor, bound and live — for a host's own panel: an
51
+ * input of its choosing to ask with, and buttons to accept or discard:
52
+ *
53
+ * readonly proposal = injectProposal(() => this.editor());
54
+ *
55
+ * this.proposal.propose(async ({ write, signal }) => {
56
+ * for await (const piece of model.write(prompt, { signal })) write(piece);
57
+ * }, { format: 'html' });
58
+ *
59
+ * <button [emailProposalAccept]="proposal">Apply</button>
60
+ * <button [emailProposalDiscard]="proposal">Discard</button>
61
+ *
62
+ * `editor` is read reactively: the proposal follows the editor it is given.
63
+ * Call in an injection context, or hand it an injector.
64
+ */
65
+ declare function injectProposal(editor: () => Editor | undefined, options?: {
66
+ injector?: Injector;
67
+ }): EditorProposal;
68
+ /**
69
+ * The proposal of an editor, as a directive: put it on the element that
70
+ * holds the host's panel — the chat, the buttons — and everything inside
71
+ * has it, by injection, with nothing to bind:
72
+ *
73
+ * <div [emailProposal]="editor" #p="emailProposal">
74
+ * <button emailProposalAccept [disabled]="!p.active()">Apply</button>
75
+ * <button emailProposalDiscard>Discard</button>
76
+ * </div>
77
+ *
78
+ * The directive *is* the {@link EditorProposal}: `p.active()`,
79
+ * `p.propose(…)`, `p.accept()` — the same face `injectProposal` gives a
80
+ * class, for a host that lives in its template. The two are one thing
81
+ * looked at from two sides; a trigger takes either, bound or injected.
82
+ */
83
+ declare class Proposal implements EditorProposal {
84
+ #private;
85
+ /** The editor the proposal is in — read reactively, so it may come
86
+ later, or change. */
87
+ readonly editor: i0.InputSignal<Editor | undefined>;
88
+ readonly active: Signal<boolean>;
89
+ readonly streaming: Signal<boolean>;
90
+ readonly thinking: Signal<boolean>;
91
+ readonly range: Signal<ContentStreamRange | null>;
92
+ readonly box: Signal<AnchorRect | null>;
93
+ readonly selectedPart: Signal<ContentStreamRange | null>;
94
+ propose(...args: Parameters<EditorProposal['propose']>): ContentStreamRun | null;
95
+ revise(...args: Parameters<EditorProposal['revise']>): ContentStreamRun | null;
96
+ stop(): void;
97
+ accept(): boolean;
98
+ discard(): boolean;
99
+ static ɵfac: i0.ɵɵFactoryDeclaration<Proposal, never>;
100
+ static ɵdir: i0.ɵɵDirectiveDeclaration<Proposal, "[emailProposal]", ["emailProposal"], { "editor": { "alias": "emailProposal"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
101
+ }
102
+ /**
103
+ * Makes the host's own button the one that accepts the proposal: a click
104
+ * takes it into the document and hands the caret back; disabled — by the
105
+ * host's own means, bound from `disabled()` — while nothing is proposed or
106
+ * it is still being written. The proposal is bound, or, bare, the
107
+ * `[emailProposal]` ancestor's.
108
+ *
109
+ * <button mat-button [emailProposalAccept]="proposal" #apply="emailProposalAccept"
110
+ * [disabled]="apply.disabled()">Apply</button>
111
+ */
112
+ declare class ProposalAccept {
113
+ #private;
114
+ readonly proposal: i0.InputSignalWithTransform<EditorProposal | undefined, "" | EditorProposal | undefined>;
115
+ readonly disabled: Signal<boolean>;
116
+ run(): boolean;
117
+ static ɵfac: i0.ɵɵFactoryDeclaration<ProposalAccept, never>;
118
+ static ɵdir: i0.ɵɵDirectiveDeclaration<ProposalAccept, "[emailProposalAccept]", ["emailProposalAccept"], { "proposal": { "alias": "emailProposalAccept"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
119
+ }
120
+ /**
121
+ * Makes the host's own button the one that discards the proposal: a click
122
+ * takes it out, wherever the writing has got to. The proposal is bound,
123
+ * or, bare, the `[emailProposal]` ancestor's.
124
+ *
125
+ * <button mat-button [emailProposalDiscard]="proposal">Discard</button>
126
+ */
127
+ declare class ProposalDiscard {
128
+ #private;
129
+ readonly proposal: i0.InputSignalWithTransform<EditorProposal | undefined, "" | EditorProposal | undefined>;
130
+ readonly disabled: Signal<boolean>;
131
+ run(): boolean;
132
+ static ɵfac: i0.ɵɵFactoryDeclaration<ProposalDiscard, never>;
133
+ static ɵdir: i0.ɵɵDirectiveDeclaration<ProposalDiscard, "[emailProposalDiscard]", ["emailProposalDiscard"], { "proposal": { "alias": "emailProposalDiscard"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
134
+ }
135
+ /**
136
+ * The keys of a proposal, heard from anywhere on the page — the text, the
137
+ * host's input, a button — while one stands, before anything else hears
138
+ * them (the document, capture phase):
139
+ *
140
+ * - **Ctrl-Enter** (⌘-Enter on a Mac) accepts: a proposal on the table is
141
+ * what the key commits, and whatever the editor binds to it (a send,
142
+ * say) never sees it. While the proposal is still being written the key
143
+ * is swallowed — nothing is sent, nothing is applied; one thing at a
144
+ * time. `accepted` says it happened.
145
+ * - **Escape** is the host's: `escape` carries the event, and the host
146
+ * decides — close a menu over the text first, or let the proposal go —
147
+ * because what else is open is the host's to know. Nothing is prevented
148
+ * or stopped for it; the host does that on the event if it acts.
149
+ *
150
+ * Nothing is heard while nothing is proposed, and an IME's own keys stay
151
+ * the IME's. The proposal is bound, or, bare, the `[emailProposal]`
152
+ * ancestor's.
153
+ *
154
+ * <div [emailProposal]="editor" emailProposalKeys
155
+ * (accepted)="done()" (escape)="$event.preventDefault(); leave()">
156
+ */
157
+ declare class ProposalKeys {
158
+ #private;
159
+ readonly proposal: i0.InputSignalWithTransform<EditorProposal | undefined, "" | EditorProposal | undefined>;
160
+ /** Ctrl-Enter (⌘-Enter) took the proposal into the document. */
161
+ readonly accepted: i0.OutputEmitterRef<void>;
162
+ /** Escape, with a proposal standing — for the host to act on. */
163
+ readonly escape: i0.OutputEmitterRef<KeyboardEvent>;
164
+ constructor();
165
+ static ɵfac: i0.ɵɵFactoryDeclaration<ProposalKeys, never>;
166
+ static ɵdir: i0.ɵɵDirectiveDeclaration<ProposalKeys, "[emailProposalKeys]", ["emailProposalKeys"], { "proposal": { "alias": "emailProposalKeys"; "required": false; "isSignal": true; }; }, { "accepted": "accepted"; "escape": "escape"; }, never, never, true, never>;
167
+ }
168
+
169
+ export { Proposal, ProposalAccept, ProposalDiscard, ProposalKeys, injectProposal };
170
+ export type { EditorProposal };
@@ -0,0 +1,177 @@
1
+ import * as _h_k_dev_angular_email_editor from '@h-k-dev/angular-email-editor';
2
+ import { SuggestionGroup, SuggestionMenuState, SuggestionItem } from '@h-k-dev/angular-email-editor';
3
+ import * as _angular_core from '@angular/core';
4
+ import { TemplateRef, ElementRef } from '@angular/core';
5
+
6
+ /**
7
+ * A suggestion menu's header slot, shown on level 2 above the group's
8
+ * children. The default is a back button and the group's title; a host
9
+ * that wants its own — a Material icon button, the group's icon, a
10
+ * breadcrumb — hands the menu a template:
11
+ *
12
+ * <div email-suggestion-menu [state]="state()">
13
+ * <ng-template emailSuggestionMenuHeader let-group let-back="back" let-label="backLabel">
14
+ * <button mat-icon-button tabindex="-1" [attr.aria-label]="label" (click)="back()">
15
+ * <mat-icon>arrow_back</mat-icon>
16
+ * </button>
17
+ * {{ group.title }}
18
+ * </ng-template>
19
+ * …
20
+ * </div>
21
+ *
22
+ * The template renders inside the menu's own `header` box.
23
+ */
24
+ /** What the header template is given. */
25
+ interface SuggestionMenuHeaderContext {
26
+ /** The group whose children are listed — its title already translated. */
27
+ readonly $implicit: SuggestionGroup;
28
+ /** Back to level 1 with an empty query. */
29
+ readonly back: () => void;
30
+ /** The back control's accessible name, as the menu was given it. */
31
+ readonly backLabel: string;
32
+ }
33
+ /** Marks the `ng-template` the menu renders as its level-2 header. */
34
+ declare class SuggestionMenuHeader {
35
+ readonly template: TemplateRef<SuggestionMenuHeaderContext>;
36
+ static ngTemplateContextGuard(_directive: SuggestionMenuHeader, context: unknown): context is SuggestionMenuHeaderContext;
37
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SuggestionMenuHeader, never>;
38
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SuggestionMenuHeader, "ng-template[emailSuggestionMenuHeader]", never, {}, {}, never, never, true, never>;
39
+ }
40
+
41
+ /**
42
+ * A suggestion menu's floating box — the element `createSuggestionMenu` is
43
+ * given, rendered from the state it reports. The extension does the work
44
+ * (finding the trigger, filtering, paging, the keys, placing the box,
45
+ * applying); this gives the state its roles and ids, so the editor's
46
+ * `aria-controls` and `aria-activedescendant` land on real elements, keeps
47
+ * the next page coming as the list scrolls, and says what is going on.
48
+ *
49
+ * **The pair.** The menu is the container; the host renders one
50
+ * `[email-suggestion-menu-item]` per item into it and decides what a row
51
+ * shows (an icon, the title, a detail). Rows are options of the menu's
52
+ * listbox and never take focus: the caret stays in the editor, which is the
53
+ * combobox. One pair serves every trigger — `/`, `{{`, `||` — at once: they
54
+ * never open together, so a menu of several triggers is still one element
55
+ * and one state, which says whose list it is (`trigger`, `label`).
56
+ *
57
+ * <div email-suggestion-menu #menu [state]="state()">
58
+ * @for (item of state()?.items; track item.id) {
59
+ * <div email-suggestion-menu-item [item]="item">{{ item.title }}</div>
60
+ * }
61
+ * </div>
62
+ *
63
+ * createSuggestionMenu({ element: menu, onChange: (s) => state.set(s), triggers: [
64
+ * { trigger: '/', label: 'Insert block', items: extensionSuggestions },
65
+ * { trigger: '{{', label: 'Tokens', source: fetchMergeTags },
66
+ * ] })
67
+ *
68
+ * **Two levels.** Inside a group the menu shows a header above the list — a
69
+ * back button and the group's title by default, or the host's
70
+ * `ng-template[emailSuggestionMenuHeader]` — names the listbox after the
71
+ * group, and says "No results" rather than closing when nothing matches.
72
+ *
73
+ * **Said twice, differently.** What is going on shows as a line under the
74
+ * list, at once. A screen reader hears it from a separate live region that
75
+ * waits for the typing to settle (`announceDelay`) and also counts the
76
+ * results — the highlighted option's id does not change when the list is
77
+ * filtered under it, so nothing else would say the list is a new one.
78
+ *
79
+ * Every word it says is an input, English by default.
80
+ */
81
+ declare class SuggestionMenu {
82
+ #private;
83
+ /** The extension's latest state — undefined before the editor mounts. */
84
+ readonly state: _angular_core.InputSignal<SuggestionMenuState | undefined>;
85
+ /** Names the listbox on level 1 when the open trigger has no `label` of
86
+ its own — a menu of several triggers names each list there ("Insert
87
+ block", "Personalization tokens"); level 2 is named by its group. */
88
+ readonly label: _angular_core.InputSignal<string>;
89
+ /** The level-2 back button's accessible name. */
90
+ readonly backLabel: _angular_core.InputSignal<string>;
91
+ /** Said while a source is still searching. */
92
+ readonly loadingLabel: _angular_core.InputSignal<string>;
93
+ /** Said on level 2 when nothing matches. */
94
+ readonly emptyLabel: _angular_core.InputSignal<string>;
95
+ /** Said while a further page is on its way. */
96
+ readonly loadingMoreLabel: _angular_core.InputSignal<string>;
97
+ /** Said when the source's answer failed. */
98
+ readonly errorLabel: _angular_core.InputSignal<string>;
99
+ /** Announced once a list is in: how many rows it has. */
100
+ readonly resultsLabel: _angular_core.InputSignal<(count: number) => string>;
101
+ /** Milliseconds the announcement waits for the typing to settle, so a
102
+ screen reader hears the list that stayed, not one per letter. Going
103
+ quiet (the menu closes) never waits. 0 announces at once. */
104
+ readonly announceDelay: _angular_core.InputSignal<number>;
105
+ /** Gap between the trigger's line and the menu, in px. */
106
+ readonly offset: _angular_core.InputSignal<number>;
107
+ /** The keyboard-highlighted row's index; -1 before there is a state. */
108
+ readonly activeIndex: _angular_core.Signal<number>;
109
+ /** The extension's id for the option at an index — the same function for
110
+ the menu's whole life. */
111
+ readonly optionId: _angular_core.Signal<((index: number) => string) | undefined>;
112
+ /** Where `item` sits in the current list; -1 when it is not in it. */
113
+ indexOf(item: SuggestionItem): number;
114
+ protected readonly header: _angular_core.Signal<SuggestionMenuHeader | undefined>;
115
+ protected readonly listbox: _angular_core.Signal<ElementRef<HTMLElement>>;
116
+ protected readonly end: _angular_core.Signal<ElementRef<HTMLElement>>;
117
+ /** The group being searched, on level 2. */
118
+ protected readonly parent: _angular_core.Signal<_h_k_dev_angular_email_editor.SuggestionGroup | null>;
119
+ protected readonly listLabel: _angular_core.Signal<string>;
120
+ /** The header template's context, while there is a group to head. */
121
+ protected readonly headerContext: _angular_core.Signal<SuggestionMenuHeaderContext | null>;
122
+ /** The line under the list: failed, searching, loading more, nothing
123
+ found, or nothing. Not "searching" over stale rows — dimmed, they say
124
+ it themselves, and a line that came and went with every keystroke
125
+ would resize the box (and have it placed again) twice per letter. */
126
+ protected readonly status: _angular_core.Signal<string>;
127
+ /** The announcement, once the typing has settled. */
128
+ protected readonly announced: _angular_core.Resource<string>;
129
+ constructor();
130
+ protected readonly back: () => void;
131
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SuggestionMenu, never>;
132
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SuggestionMenu, "[email-suggestion-menu]", never, { "state": { "alias": "state"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "backLabel": { "alias": "backLabel"; "required": false; "isSignal": true; }; "loadingLabel": { "alias": "loadingLabel"; "required": false; "isSignal": true; }; "emptyLabel": { "alias": "emptyLabel"; "required": false; "isSignal": true; }; "loadingMoreLabel": { "alias": "loadingMoreLabel"; "required": false; "isSignal": true; }; "errorLabel": { "alias": "errorLabel"; "required": false; "isSignal": true; }; "resultsLabel": { "alias": "resultsLabel"; "required": false; "isSignal": true; }; "announceDelay": { "alias": "announceDelay"; "required": false; "isSignal": true; }; "offset": { "alias": "offset"; "required": false; "isSignal": true; }; }, {}, ["header"], ["*"], true, never>;
133
+ }
134
+
135
+ /**
136
+ * One row of a suggestion menu: an option of its listbox, standing for one of
137
+ * the state's items. The host fills the row; the row takes its id (the one
138
+ * the editor's `aria-activedescendant` names), reflects the highlight as
139
+ * `aria-selected` and `data-active` — which is what the menu keeps scrolled
140
+ * into view — and applies its item on click: a group opens, a command runs.
141
+ *
142
+ * Never focusable, never a button: the caret stays in the editor, and the
143
+ * extension keeps a press on the menu from taking it away.
144
+ */
145
+ declare class SuggestionMenuItem {
146
+ #private;
147
+ /** The item this row stands for — one of the menu state's `items`. */
148
+ readonly item: _angular_core.InputSignal<SuggestionItem>;
149
+ /** The keyboard highlight is on this row. */
150
+ readonly active: _angular_core.Signal<boolean>;
151
+ protected readonly id: _angular_core.Signal<string | null>;
152
+ protected select(): void;
153
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SuggestionMenuItem, never>;
154
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SuggestionMenuItem, "[email-suggestion-menu-item]", never, { "item": { "alias": "item"; "required": true; "isSignal": true; }; }, {}, never, ["*"], true, never>;
155
+ }
156
+
157
+ /**
158
+ * A heading in a suggestion menu's list: the words of a section
159
+ * (`SuggestionItem.section`, worded by the state's `sectionTitle`), shown
160
+ * where the section changes from one row to the next. Never an option: it
161
+ * takes no highlight, no click, and no place in the count — presentation,
162
+ * the way a chat's slash menu heads its groups.
163
+ *
164
+ * @for (item of state.items; track item.id) {
165
+ * @if (item.section && item.section !== state.items[$index - 1]?.section) {
166
+ * <div email-suggestion-menu-section>{{ state.sectionTitle(item.section) }}</div>
167
+ * }
168
+ * <div email-suggestion-menu-item [item]="item">…</div>
169
+ * }
170
+ */
171
+ declare class SuggestionMenuSection {
172
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SuggestionMenuSection, never>;
173
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SuggestionMenuSection, "[email-suggestion-menu-section]", never, {}, {}, never, ["*"], true, never>;
174
+ }
175
+
176
+ export { SuggestionMenu, SuggestionMenuHeader, SuggestionMenuItem, SuggestionMenuSection };
177
+ export type { SuggestionMenuHeaderContext };