@lifecorner/angular-ui-kit 3.11.4 → 3.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/lifecorner-angular-ui-kit-accordion.mjs +6 -3
- package/fesm2022/lifecorner-angular-ui-kit-accordion.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-calendar.mjs +11 -6
- package/fesm2022/lifecorner-angular-ui-kit-calendar.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs +7 -2
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs +7 -2
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-header.mjs +4 -2
- package/fesm2022/lifecorner-angular-ui-kit-header.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-internal-a11y.mjs +20 -0
- package/fesm2022/lifecorner-angular-ui-kit-internal-a11y.mjs.map +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-pagination.mjs +5 -2
- package/fesm2022/lifecorner-angular-ui-kit-pagination.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs +6 -2
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-stepper.mjs +11 -6
- package/fesm2022/lifecorner-angular-ui-kit-stepper.mjs.map +1 -1
- package/package.json +5 -1
- package/types/lifecorner-angular-ui-kit-accordion.d.ts +31 -9
- package/types/lifecorner-angular-ui-kit-calendar.d.ts +1 -0
- package/types/lifecorner-angular-ui-kit-file-upload.d.ts +18 -3
- package/types/lifecorner-angular-ui-kit-gallery.d.ts +17 -2
- package/types/lifecorner-angular-ui-kit-header.d.ts +12 -1
- package/types/lifecorner-angular-ui-kit-internal-a11y.d.ts +27 -0
- package/types/lifecorner-angular-ui-kit-pagination.d.ts +15 -2
- package/types/lifecorner-angular-ui-kit-rich-text-editor.d.ts +13 -1
- package/types/lifecorner-angular-ui-kit-stepper.d.ts +16 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-rich-text-editor.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/rich-text-editor/rich-text-editor.component.ts","../../../../libs/angular-ui-kit/src/lib/rich-text-editor/rich-text-editor.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n ElementRef,\n SecurityContext,\n computed,\n effect,\n forwardRef,\n inject,\n input,\n linkedSignal,\n output,\n signal,\n untracked,\n viewChild,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { DomSanitizer } from '@angular/platform-browser';\nimport { isSafeImageUrl, isSafeLinkUrl } from '@lifecorner/angular-ui-kit/internal/url';\n\nexport type RichTextEditorMode = 'rich' | 'markdown' | 'split';\nexport type ToolbarAction =\n | 'bold'\n | 'italic'\n | 'underline'\n | 'strikethrough'\n | 'h1'\n | 'h2'\n | 'h3'\n | 'ul'\n | 'ol'\n | 'blockquote'\n | 'code'\n | 'codeblock'\n | 'link'\n | 'image'\n | 'hr';\n\nexport interface ToolbarConfig {\n actions: ToolbarAction[];\n}\n\nconst HTML_ESCAPES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n};\n\n/** Escapes text for HTML — every character that could open a tag or an attribute. */\nfunction escapeHtml(text: string): string {\n return text.replace(/[&<>\"']/g, (c) => HTML_ESCAPES[c]);\n}\n\n/**\n * The URL as it may go into `href`/`src`, or null if it must not: only relative\n * URLs and the allow-listed schemes pass (see `safe-url.ts`), so a link in\n * user-supplied markdown can never run script when clicked.\n */\nfunction safeUrl(url: string, image = false): string | null {\n const value = url.trim();\n if (!value) return null;\n return (image ? isSafeImageUrl(value) : isSafeLinkUrl(value)) ? value : null;\n}\n\nconst DEFAULT_TOOLBAR: ToolbarConfig = {\n actions: [\n 'bold',\n 'italic',\n 'underline',\n 'strikethrough',\n 'h1',\n 'h2',\n 'h3',\n 'ul',\n 'ol',\n 'blockquote',\n 'code',\n 'codeblock',\n 'link',\n 'hr',\n ],\n};\n\n/**\n * Rich Text Editor with Markdown support.\n *\n * Supports three modes:\n * - `rich`: WYSIWYG editing with toolbar\n * - `markdown`: Raw markdown editing with syntax highlighting\n * - `split`: Side-by-side markdown and preview\n *\n * Implements ControlValueAccessor for reactive form integration.\n * The value is always stored as Markdown.\n *\n * The built-in markdown → HTML conversion escapes the source before it applies\n * any markup and only lets http(s)/mailto/tel and relative URLs through as\n * links, so markdown that arrives from a server or another user cannot inject\n * markup or script. The result is additionally run through Angular's HTML\n * sanitizer before it reaches the DOM.\n *\n * @example\n * ```html\n * <lc-rich-text-editor\n * [(ngModel)]=\"content\"\n * mode=\"split\"\n * placeholder=\"Write something...\" />\n * ```\n */\n@Component({\n selector: 'lc-rich-text-editor',\n standalone: true,\n imports: [CommonModule],\n templateUrl: './rich-text-editor.component.html',\n styleUrl: './rich-text-editor.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => RichTextEditorComponent),\n multi: true,\n },\n ],\n})\nexport class RichTextEditorComponent implements ControlValueAccessor {\n /** Editor mode */\n mode = input<RichTextEditorMode>('rich');\n\n /** Placeholder text */\n placeholder = input<string>('Start typing...');\n\n /** Minimum height in px */\n minHeight = input<number>(200);\n\n /** Maximum height in px (0 = unlimited) */\n maxHeight = input<number>(0);\n\n /** Whether the editor is disabled */\n disabled = input<boolean>(false);\n\n /** Whether the editor is readonly */\n readonly = input<boolean>(false);\n\n /** Toolbar configuration */\n toolbar = input<ToolbarConfig>(DEFAULT_TOOLBAR);\n\n /** Whether to show word count */\n showWordCount = input<boolean>(true);\n\n /** Tooltips and accessible names of the toolbar actions, by action (`{ bold: 'Fett' }`); missing ones keep the English default. */\n actionTitles = input<Partial<Record<ToolbarAction, string>>>({});\n\n /** Label of the rich-text mode button. */\n richModeLabel = input('Rich');\n\n /** Tooltip of the rich-text mode button. */\n richModeTitle = input('Rich Text');\n\n /** Label of the Markdown mode button. */\n markdownModeLabel = input('MD');\n\n /** Tooltip of the Markdown mode button. */\n markdownModeTitle = input('Markdown');\n\n /** Label of the split-view mode button. */\n splitModeLabel = input('Split');\n\n /** Tooltip of the split-view mode button. */\n splitModeTitle = input('Split View');\n\n /** Accessible name of the Markdown text area. */\n markdownEditorAriaLabel = input('Markdown editor');\n\n /** Unit after the word count (`showWordCount`). */\n wordsLabel = input('words');\n\n /** Unit after the character count (`showWordCount`). */\n charactersLabel = input('characters');\n\n /** Emitted on every content change */\n readonly contentChange = output<string>();\n\n private readonly editorArea = viewChild<ElementRef<HTMLTextAreaElement>>('editorArea');\n private readonly richArea = viewChild<ElementRef<HTMLDivElement>>('richArea');\n private readonly sanitizer = inject(DomSanitizer);\n\n /** Internal markdown value */\n protected markdown = signal('');\n\n /** Current active mode: follows the `mode` input until the user toggles it. */\n protected activeMode = linkedSignal<RichTextEditorMode>(() => this.mode());\n\n /** Disabled through the form control (`setDisabledState`), as opposed to the input. */\n private readonly formDisabled = signal(false);\n\n /** Disabled by either route — the one flag the template consults. */\n protected readonly isDisabled = computed(() => this.disabled() || this.formDisabled());\n\n /** Whether the content can currently be edited at all. */\n protected readonly editable = computed(() => !this.isDisabled() && !this.readonly());\n\n /** Word count */\n protected wordCount = computed(() => {\n const text = this.markdown().trim();\n if (!text) return 0;\n return text.split(/\\s+/).length;\n });\n\n /** Character count */\n protected charCount = computed(() => this.markdown().length);\n\n /**\n * Rendered HTML from markdown — escaped by the converter and, on top of that,\n * passed through Angular's HTML sanitizer, so it is safe to assign to\n * `innerHTML` directly.\n */\n protected renderedHtml = computed(() => this.sanitizeHtml(this.markdownToHtml(this.markdown())));\n\n // CVA\n private onChange: (value: string) => void = () => { /* set by registerOnChange */ };\n private onTouched: () => void = () => { /* set by registerOnTouched */ };\n\n constructor() {\n // The rich area lives inside an `@switch`, so it comes and goes with the\n // mode. Whenever it (re)appears, it starts empty and has to be filled from\n // the markdown — including the very first time, which is after any\n // `writeValue` a form control performs before the view exists. Only the\n // element's presence is tracked: re-rendering on every markdown change would\n // wipe the caret while the user types in it.\n effect(() => {\n const el = this.richArea()?.nativeElement;\n if (el) untracked(() => this.renderRich(el));\n });\n }\n\n // -- ControlValueAccessor --\n writeValue(value: string | null | undefined): void {\n this.markdown.set(value || '');\n const el = this.richArea()?.nativeElement;\n if (el) this.renderRich(el);\n }\n\n registerOnChange(fn: (value: string) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.formDisabled.set(isDisabled);\n }\n\n // -- Mode switching --\n protected switchMode(mode: RichTextEditorMode): void {\n if (this.activeMode() === 'rich' && mode !== 'rich') {\n // Sync rich content to markdown before the rich area is torn down.\n const el = this.richArea()?.nativeElement;\n if (el) this.markdown.set(this.htmlToMarkdown(el.innerHTML));\n }\n // Entering rich mode: the effect in the constructor renders the markdown\n // into the freshly created rich area.\n this.activeMode.set(mode);\n }\n\n // -- Toolbar actions --\n protected onToolbarAction(action: ToolbarAction): void {\n if (!this.editable()) return;\n\n if (this.activeMode() === 'rich') {\n this.execRichCommand(action);\n } else {\n this.insertMarkdownSyntax(action);\n }\n }\n\n private execRichCommand(action: ToolbarAction): void {\n switch (action) {\n case 'bold':\n document.execCommand('bold');\n break;\n case 'italic':\n document.execCommand('italic');\n break;\n case 'underline':\n document.execCommand('underline');\n break;\n case 'strikethrough':\n document.execCommand('strikeThrough');\n break;\n case 'h1':\n document.execCommand('formatBlock', false, 'h1');\n break;\n case 'h2':\n document.execCommand('formatBlock', false, 'h2');\n break;\n case 'h3':\n document.execCommand('formatBlock', false, 'h3');\n break;\n case 'ul':\n document.execCommand('insertUnorderedList');\n break;\n case 'ol':\n document.execCommand('insertOrderedList');\n break;\n case 'blockquote':\n document.execCommand('formatBlock', false, 'blockquote');\n break;\n case 'code':\n document.execCommand('insertHTML', false, '<code>' + this.getSelection() + '</code>');\n break;\n case 'codeblock':\n document.execCommand('insertHTML', false, '<pre><code>' + this.getSelection() + '</code></pre>');\n break;\n case 'link': {\n const url = prompt('Enter URL:');\n if (url) document.execCommand('createLink', false, url);\n break;\n }\n case 'hr':\n document.execCommand('insertHorizontalRule');\n break;\n }\n this.syncFromRich();\n }\n\n private insertMarkdownSyntax(action: ToolbarAction): void {\n const textarea = this.editorArea()?.nativeElement;\n if (!textarea) return;\n\n const start = textarea.selectionStart;\n const end = textarea.selectionEnd;\n const text = this.markdown();\n const selected = text.substring(start, end);\n\n let insertion = '';\n let cursorOffset = 0;\n\n switch (action) {\n case 'bold':\n insertion = `**${selected || 'bold text'}**`;\n cursorOffset = selected ? insertion.length : 2;\n break;\n case 'italic':\n insertion = `*${selected || 'italic text'}*`;\n cursorOffset = selected ? insertion.length : 1;\n break;\n case 'underline':\n insertion = `<u>${selected || 'underlined text'}</u>`;\n cursorOffset = selected ? insertion.length : 3;\n break;\n case 'strikethrough':\n insertion = `~~${selected || 'strikethrough'}~~`;\n cursorOffset = selected ? insertion.length : 2;\n break;\n case 'h1':\n insertion = `# ${selected || 'Heading 1'}`;\n cursorOffset = insertion.length;\n break;\n case 'h2':\n insertion = `## ${selected || 'Heading 2'}`;\n cursorOffset = insertion.length;\n break;\n case 'h3':\n insertion = `### ${selected || 'Heading 3'}`;\n cursorOffset = insertion.length;\n break;\n case 'ul':\n insertion = `- ${selected || 'List item'}`;\n cursorOffset = insertion.length;\n break;\n case 'ol':\n insertion = `1. ${selected || 'List item'}`;\n cursorOffset = insertion.length;\n break;\n case 'blockquote':\n insertion = `> ${selected || 'Quote'}`;\n cursorOffset = insertion.length;\n break;\n case 'code':\n insertion = `\\`${selected || 'code'}\\``;\n cursorOffset = selected ? insertion.length : 1;\n break;\n case 'codeblock':\n insertion = `\\`\\`\\`\\n${selected || 'code'}\\n\\`\\`\\``;\n cursorOffset = 4;\n break;\n case 'link':\n insertion = `[${selected || 'link text'}](url)`;\n cursorOffset = selected ? insertion.length - 5 : 1;\n break;\n case 'image':\n insertion = ``;\n cursorOffset = selected ? insertion.length - 5 : 2;\n break;\n case 'hr':\n insertion = `\\n---\\n`;\n cursorOffset = insertion.length;\n break;\n }\n\n const newText = text.substring(0, start) + insertion + text.substring(end);\n this.markdown.set(newText);\n this.emitChange(newText);\n\n setTimeout(() => {\n textarea.focus();\n textarea.setSelectionRange(start + cursorOffset, start + cursorOffset);\n });\n }\n\n // -- Content input handlers --\n protected onMarkdownInput(event: Event): void {\n const value = (event.target as HTMLTextAreaElement).value;\n this.markdown.set(value);\n this.emitChange(value);\n }\n\n protected onRichInput(): void {\n this.syncFromRich();\n }\n\n protected onBlur(): void {\n this.onTouched();\n }\n\n private syncFromRich(): void {\n const el = this.richArea()?.nativeElement;\n if (!el) return;\n const md = this.htmlToMarkdown(el.innerHTML);\n this.markdown.set(md);\n this.emitChange(md);\n }\n\n /** Puts the current markdown into the rich area, as sanitised HTML. */\n private renderRich(el: HTMLDivElement): void {\n el.innerHTML = this.renderedHtml();\n }\n\n /**\n * Angular's HTML sanitizer, as the last line of defence behind the escaping\n * `markdownToHtml` does itself: whatever it might miss, this strips.\n */\n private sanitizeHtml(html: string): string {\n return this.sanitizer.sanitize(SecurityContext.HTML, html) ?? '';\n }\n\n private emitChange(value: string): void {\n this.onChange(value);\n this.contentChange.emit(value);\n }\n\n private getSelection(): string {\n return window.getSelection()?.toString() || '';\n }\n\n // -- Toolbar label helpers --\n protected getActionLabel(action: ToolbarAction): string {\n const labels: Record<ToolbarAction, string> = {\n bold: 'B',\n italic: 'I',\n underline: 'U',\n strikethrough: 'S',\n h1: 'H1',\n h2: 'H2',\n h3: 'H3',\n ul: '•',\n ol: '1.',\n blockquote: '\"',\n code: '<>',\n codeblock: '{ }',\n link: '🔗',\n image: '🖼',\n hr: '—',\n };\n return labels[action] || action;\n }\n\n protected getActionTitle(action: ToolbarAction): string {\n const titles: Record<ToolbarAction, string> = {\n bold: 'Bold',\n italic: 'Italic',\n underline: 'Underline',\n strikethrough: 'Strikethrough',\n h1: 'Heading 1',\n h2: 'Heading 2',\n h3: 'Heading 3',\n ul: 'Unordered List',\n ol: 'Ordered List',\n blockquote: 'Blockquote',\n code: 'Inline Code',\n codeblock: 'Code Block',\n link: 'Insert Link',\n image: 'Insert Image',\n hr: 'Horizontal Rule',\n };\n return this.actionTitles()[action] || titles[action] || action;\n }\n\n // -- Markdown <-> HTML conversion (simple built-in) --\n /**\n * Converts the editor's markdown subset to HTML. The source is HTML-escaped\n * *first*, so nothing in it can open a tag or an attribute; the conversions\n * below then only ever add markup of their own. Raw HTML in the markdown is\n * therefore shown as text — with one exception, `<u>`, which the toolbar's\n * underline action emits (markdown has no underline) and which is let back\n * through in escaped form only.\n */\n protected markdownToHtml(md: string): string {\n if (!md) return '';\n\n let html = escapeHtml(md);\n\n // Code blocks (must be first)\n html = html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g, '<pre><code class=\"language-$1\">$2</code></pre>');\n\n // Headings\n html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');\n html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');\n html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');\n\n // Horizontal rule\n html = html.replace(/^---$/gm, '<hr>');\n\n // Bold/Italic\n html = html.replace(/\\*\\*\\*(.+?)\\*\\*\\*/g, '<strong><em>$1</em></strong>');\n html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');\n html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');\n\n // Underline: the one raw tag the editor itself writes into the markdown.\n html = html.replace(/<u>(.+?)<\\/u>/g, '<u>$1</u>');\n\n // Inline code\n html = html.replace(/`(.+?)`/g, '<code>$1</code>');\n\n // Links and images. Only safe URLs become attributes; anything else is\n // shown as its text — a `javascript:` link that just isn't a link is far\n // better than one that runs. Quotes are already escaped (see above), so\n // the URL cannot break out of the attribute either.\n html = html.replace(/!\\[(.+?)\\]\\((.+?)\\)/g, (_m, alt: string, src: string) => {\n const url = safeUrl(src, true);\n return url ? `<img alt=\"${alt}\" src=\"${url}\" />` : alt;\n });\n html = html.replace(/\\[(.+?)\\]\\((.+?)\\)/g, (_m, text: string, href: string) => {\n const url = safeUrl(href);\n return url ? `<a href=\"${url}\">${text}</a>` : text;\n });\n\n // Blockquotes — the marker is `>` by now, the source having been escaped.\n html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');\n\n // Unordered lists\n html = html.replace(/^- (.+)$/gm, '<li>$1</li>');\n html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');\n\n // Ordered lists\n html = html.replace(/^\\d+\\. (.+)$/gm, '<li>$1</li>');\n\n // Paragraphs (lines that aren't already wrapped)\n html = html.replace(/^(?!<[a-z])((?!^\\s*$).+)$/gm, '<p>$1</p>');\n\n // Clean up empty paragraphs\n html = html.replace(/<p>\\s*<\\/p>/g, '');\n\n return html.trim();\n }\n\n private htmlToMarkdown(html: string): string {\n if (!html) return '';\n\n let md = html;\n\n // Block elements\n md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, '# $1\\n');\n md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, '## $1\\n');\n md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, '### $1\\n');\n md = md.replace(/<hr\\s*\\/?>/gi, '---\\n');\n md = md.replace(/<blockquote[^>]*>(.*?)<\\/blockquote>/gi, '> $1\\n');\n md = md.replace(/<pre><code[^>]*>([\\s\\S]*?)<\\/code><\\/pre>/gi, '```\\n$1\\n```\\n');\n\n // Lists\n md = md.replace(/<ul[^>]*>([\\s\\S]*?)<\\/ul>/gi, '$1');\n md = md.replace(/<ol[^>]*>([\\s\\S]*?)<\\/ol>/gi, '$1');\n md = md.replace(/<li[^>]*>(.*?)<\\/li>/gi, '- $1\\n');\n\n // Inline\n md = md.replace(/<strong[^>]*>(.*?)<\\/strong>/gi, '**$1**');\n md = md.replace(/<b[^>]*>(.*?)<\\/b>/gi, '**$1**');\n md = md.replace(/<em[^>]*>(.*?)<\\/em>/gi, '*$1*');\n md = md.replace(/<i[^>]*>(.*?)<\\/i>/gi, '*$1*');\n md = md.replace(/<del[^>]*>(.*?)<\\/del>/gi, '~~$1~~');\n md = md.replace(/<u[^>]*>(.*?)<\\/u>/gi, '<u>$1</u>');\n md = md.replace(/<code[^>]*>(.*?)<\\/code>/gi, '`$1`');\n md = md.replace(/<a[^>]*href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/gi, '[$2]($1)');\n md = md.replace(/<img[^>]*alt=\"([^\"]*)\"[^>]*src=\"([^\"]*)\"[^>]*\\/?>/gi, '');\n\n // Paragraphs and line breaks\n md = md.replace(/<p[^>]*>(.*?)<\\/p>/gi, '$1\\n\\n');\n md = md.replace(/<br\\s*\\/?>/gi, '\\n');\n md = md.replace(/<div[^>]*>(.*?)<\\/div>/gi, '$1\\n');\n\n // Strip remaining HTML tags\n md = md.replace(/<[^>]+>/g, '');\n\n // Decode HTML entities\n md = md.replace(/&/g, '&');\n md = md.replace(/</g, '<');\n md = md.replace(/>/g, '>');\n md = md.replace(/"/g, '\"');\n md = md.replace(/ /g, ' ');\n\n // Normalize whitespace\n md = md.replace(/\\n{3,}/g, '\\n\\n');\n\n return md.trim();\n }\n}\n","<div class=\"lc-rich-text-editor\" [class.lc-rich-text-editor--disabled]=\"isDisabled()\">\n <!-- Toolbar -->\n <div class=\"lc-rich-text-editor__toolbar\">\n <div class=\"lc-rich-text-editor__toolbar-actions\">\n @for (action of toolbar().actions; track action) {\n <button\n type=\"button\"\n class=\"lc-rich-text-editor__toolbar-btn\"\n [title]=\"getActionTitle(action)\"\n [attr.aria-label]=\"getActionTitle(action)\"\n [disabled]=\"!editable()\"\n (click)=\"onToolbarAction(action)\"\n >\n {{ getActionLabel(action) }}\n </button>\n }\n </div>\n <div class=\"lc-rich-text-editor__toolbar-modes\">\n <button\n type=\"button\"\n class=\"lc-rich-text-editor__mode-btn\"\n [class.active]=\"activeMode() === 'rich'\"\n (click)=\"switchMode('rich')\"\n [attr.title]=\"richModeTitle()\"\n >{{ richModeLabel() }}</button>\n <button\n type=\"button\"\n class=\"lc-rich-text-editor__mode-btn\"\n [class.active]=\"activeMode() === 'markdown'\"\n (click)=\"switchMode('markdown')\"\n [attr.title]=\"markdownModeTitle()\"\n >{{ markdownModeLabel() }}</button>\n <button\n type=\"button\"\n class=\"lc-rich-text-editor__mode-btn\"\n [class.active]=\"activeMode() === 'split'\"\n (click)=\"switchMode('split')\"\n [attr.title]=\"splitModeTitle()\"\n >{{ splitModeLabel() }}</button>\n </div>\n </div>\n\n <!-- Editor area -->\n <div\n class=\"lc-rich-text-editor__content\"\n [style.min-height.px]=\"minHeight()\"\n [style.max-height.px]=\"maxHeight() || null\"\n >\n @switch (activeMode()) {\n @case ('rich') {\n <!-- The rich area is filled from the markdown by the component (see the\n constructor effect); it must never receive markup Angular hasn't\n sanitised. `contenteditable` follows the disabled/readonly state —\n a greyed-out toolbar alone would still let the user type. -->\n <div\n #richArea\n class=\"lc-rich-text-editor__rich\"\n [attr.contenteditable]=\"editable() ? 'true' : 'false'\"\n [attr.aria-disabled]=\"isDisabled() ? 'true' : null\"\n [attr.aria-readonly]=\"readonly() && !isDisabled() ? 'true' : null\"\n [attr.data-placeholder]=\"placeholder()\"\n [class.lc-rich-text-editor__rich--empty]=\"!markdown()\"\n (input)=\"onRichInput()\"\n (blur)=\"onBlur()\"\n role=\"textbox\"\n aria-multiline=\"true\"\n [attr.aria-label]=\"placeholder()\"\n ></div>\n }\n @case ('markdown') {\n <textarea\n #editorArea\n class=\"lc-rich-text-editor__textarea\"\n [placeholder]=\"placeholder()\"\n [value]=\"markdown()\"\n [disabled]=\"isDisabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"onMarkdownInput($event)\"\n (blur)=\"onBlur()\"\n spellcheck=\"false\"\n [attr.aria-label]=\"markdownEditorAriaLabel()\"\n ></textarea>\n }\n @case ('split') {\n <div class=\"lc-rich-text-editor__split\">\n <textarea\n #editorArea\n class=\"lc-rich-text-editor__textarea lc-rich-text-editor__split-editor\"\n [placeholder]=\"placeholder()\"\n [value]=\"markdown()\"\n [disabled]=\"isDisabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"onMarkdownInput($event)\"\n (blur)=\"onBlur()\"\n spellcheck=\"false\"\n [attr.aria-label]=\"markdownEditorAriaLabel()\"\n ></textarea>\n <div\n class=\"lc-rich-text-editor__split-preview\"\n [innerHTML]=\"renderedHtml()\"\n ></div>\n </div>\n }\n }\n </div>\n\n <!-- Footer -->\n @if (showWordCount()) {\n <div class=\"lc-rich-text-editor__footer\">\n <span>{{ wordCount() }} {{ wordsLabel() }}</span>\n <span>{{ charCount() }} {{ charactersLabel() }}</span>\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-rich-text-editor.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/rich-text-editor/rich-text-editor.component.ts","../../../../libs/angular-ui-kit/src/lib/rich-text-editor/rich-text-editor.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n ElementRef,\n SecurityContext,\n computed,\n effect,\n forwardRef,\n inject,\n input,\n linkedSignal,\n output,\n signal,\n untracked,\n viewChild,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { DomSanitizer } from '@angular/platform-browser';\nimport { isSafeImageUrl, isSafeLinkUrl } from '@lifecorner/angular-ui-kit/internal/url';\nimport { nameWithText } from '@lifecorner/angular-ui-kit/internal/a11y';\n\nexport type RichTextEditorMode = 'rich' | 'markdown' | 'split';\nexport type ToolbarAction =\n | 'bold'\n | 'italic'\n | 'underline'\n | 'strikethrough'\n | 'h1'\n | 'h2'\n | 'h3'\n | 'ul'\n | 'ol'\n | 'blockquote'\n | 'code'\n | 'codeblock'\n | 'link'\n | 'image'\n | 'hr';\n\nexport interface ToolbarConfig {\n actions: ToolbarAction[];\n}\n\nconst HTML_ESCAPES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n};\n\n/** Escapes text for HTML — every character that could open a tag or an attribute. */\nfunction escapeHtml(text: string): string {\n return text.replace(/[&<>\"']/g, (c) => HTML_ESCAPES[c]);\n}\n\n/**\n * The URL as it may go into `href`/`src`, or null if it must not: only relative\n * URLs and the allow-listed schemes pass (see `safe-url.ts`), so a link in\n * user-supplied markdown can never run script when clicked.\n */\nfunction safeUrl(url: string, image = false): string | null {\n const value = url.trim();\n if (!value) return null;\n return (image ? isSafeImageUrl(value) : isSafeLinkUrl(value)) ? value : null;\n}\n\nconst DEFAULT_TOOLBAR: ToolbarConfig = {\n actions: [\n 'bold',\n 'italic',\n 'underline',\n 'strikethrough',\n 'h1',\n 'h2',\n 'h3',\n 'ul',\n 'ol',\n 'blockquote',\n 'code',\n 'codeblock',\n 'link',\n 'hr',\n ],\n};\n\n/**\n * Rich Text Editor with Markdown support.\n *\n * Supports three modes:\n * - `rich`: WYSIWYG editing with toolbar\n * - `markdown`: Raw markdown editing with syntax highlighting\n * - `split`: Side-by-side markdown and preview\n *\n * Implements ControlValueAccessor for reactive form integration.\n * The value is always stored as Markdown.\n *\n * The built-in markdown → HTML conversion escapes the source before it applies\n * any markup and only lets http(s)/mailto/tel and relative URLs through as\n * links, so markdown that arrives from a server or another user cannot inject\n * markup or script. The result is additionally run through Angular's HTML\n * sanitizer before it reaches the DOM.\n *\n * @example\n * ```html\n * <lc-rich-text-editor\n * [(ngModel)]=\"content\"\n * mode=\"split\"\n * placeholder=\"Write something...\" />\n * ```\n */\n@Component({\n selector: 'lc-rich-text-editor',\n standalone: true,\n imports: [CommonModule],\n templateUrl: './rich-text-editor.component.html',\n styleUrl: './rich-text-editor.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => RichTextEditorComponent),\n multi: true,\n },\n ],\n})\nexport class RichTextEditorComponent implements ControlValueAccessor {\n /** Editor mode */\n mode = input<RichTextEditorMode>('rich');\n\n /** Placeholder text */\n placeholder = input<string>('Start typing...');\n\n /** Minimum height in px */\n minHeight = input<number>(200);\n\n /** Maximum height in px (0 = unlimited) */\n maxHeight = input<number>(0);\n\n /** Whether the editor is disabled */\n disabled = input<boolean>(false);\n\n /** Whether the editor is readonly */\n readonly = input<boolean>(false);\n\n /** Toolbar configuration */\n toolbar = input<ToolbarConfig>(DEFAULT_TOOLBAR);\n\n /** Whether to show word count */\n showWordCount = input<boolean>(true);\n\n /**\n * Tooltips and accessible names of the toolbar actions, by action\n * (`{ bold: 'Fett' }`); missing ones keep the English default. A visible\n * button label with letters or digits that the title doesn't contain is\n * appended (`Heading 1, H1`, WCAG 2.5.3 Label in Name).\n * @default {}\n */\n actionTitles = input<Partial<Record<ToolbarAction, string>>>({});\n\n /** Label of the rich-text mode button. */\n richModeLabel = input('Rich');\n\n /** Tooltip of the rich-text mode button. */\n richModeTitle = input('Rich Text');\n\n /** Label of the Markdown mode button. */\n markdownModeLabel = input('MD');\n\n /** Tooltip of the Markdown mode button. */\n markdownModeTitle = input('Markdown');\n\n /** Label of the split-view mode button. */\n splitModeLabel = input('Split');\n\n /** Tooltip of the split-view mode button. */\n splitModeTitle = input('Split View');\n\n /** Accessible name of the Markdown text area. */\n markdownEditorAriaLabel = input('Markdown editor');\n\n /** Unit after the word count (`showWordCount`). */\n wordsLabel = input('words');\n\n /** Unit after the character count (`showWordCount`). */\n charactersLabel = input('characters');\n\n /** Emitted on every content change */\n readonly contentChange = output<string>();\n\n private readonly editorArea = viewChild<ElementRef<HTMLTextAreaElement>>('editorArea');\n private readonly richArea = viewChild<ElementRef<HTMLDivElement>>('richArea');\n private readonly sanitizer = inject(DomSanitizer);\n\n /** Internal markdown value */\n protected markdown = signal('');\n\n /** Current active mode: follows the `mode` input until the user toggles it. */\n protected activeMode = linkedSignal<RichTextEditorMode>(() => this.mode());\n\n /** Disabled through the form control (`setDisabledState`), as opposed to the input. */\n private readonly formDisabled = signal(false);\n\n /** Disabled by either route — the one flag the template consults. */\n protected readonly isDisabled = computed(() => this.disabled() || this.formDisabled());\n\n /** Whether the content can currently be edited at all. */\n protected readonly editable = computed(() => !this.isDisabled() && !this.readonly());\n\n /** Word count */\n protected wordCount = computed(() => {\n const text = this.markdown().trim();\n if (!text) return 0;\n return text.split(/\\s+/).length;\n });\n\n /** Character count */\n protected charCount = computed(() => this.markdown().length);\n\n /**\n * Rendered HTML from markdown — escaped by the converter and, on top of that,\n * passed through Angular's HTML sanitizer, so it is safe to assign to\n * `innerHTML` directly.\n */\n protected renderedHtml = computed(() => this.sanitizeHtml(this.markdownToHtml(this.markdown())));\n\n // CVA\n private onChange: (value: string) => void = () => { /* set by registerOnChange */ };\n private onTouched: () => void = () => { /* set by registerOnTouched */ };\n\n constructor() {\n // The rich area lives inside an `@switch`, so it comes and goes with the\n // mode. Whenever it (re)appears, it starts empty and has to be filled from\n // the markdown — including the very first time, which is after any\n // `writeValue` a form control performs before the view exists. Only the\n // element's presence is tracked: re-rendering on every markdown change would\n // wipe the caret while the user types in it.\n effect(() => {\n const el = this.richArea()?.nativeElement;\n if (el) untracked(() => this.renderRich(el));\n });\n }\n\n // -- ControlValueAccessor --\n writeValue(value: string | null | undefined): void {\n this.markdown.set(value || '');\n const el = this.richArea()?.nativeElement;\n if (el) this.renderRich(el);\n }\n\n registerOnChange(fn: (value: string) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.formDisabled.set(isDisabled);\n }\n\n // -- Mode switching --\n protected switchMode(mode: RichTextEditorMode): void {\n if (this.activeMode() === 'rich' && mode !== 'rich') {\n // Sync rich content to markdown before the rich area is torn down.\n const el = this.richArea()?.nativeElement;\n if (el) this.markdown.set(this.htmlToMarkdown(el.innerHTML));\n }\n // Entering rich mode: the effect in the constructor renders the markdown\n // into the freshly created rich area.\n this.activeMode.set(mode);\n }\n\n // -- Toolbar actions --\n protected onToolbarAction(action: ToolbarAction): void {\n if (!this.editable()) return;\n\n if (this.activeMode() === 'rich') {\n this.execRichCommand(action);\n } else {\n this.insertMarkdownSyntax(action);\n }\n }\n\n private execRichCommand(action: ToolbarAction): void {\n switch (action) {\n case 'bold':\n document.execCommand('bold');\n break;\n case 'italic':\n document.execCommand('italic');\n break;\n case 'underline':\n document.execCommand('underline');\n break;\n case 'strikethrough':\n document.execCommand('strikeThrough');\n break;\n case 'h1':\n document.execCommand('formatBlock', false, 'h1');\n break;\n case 'h2':\n document.execCommand('formatBlock', false, 'h2');\n break;\n case 'h3':\n document.execCommand('formatBlock', false, 'h3');\n break;\n case 'ul':\n document.execCommand('insertUnorderedList');\n break;\n case 'ol':\n document.execCommand('insertOrderedList');\n break;\n case 'blockquote':\n document.execCommand('formatBlock', false, 'blockquote');\n break;\n case 'code':\n document.execCommand('insertHTML', false, '<code>' + this.getSelection() + '</code>');\n break;\n case 'codeblock':\n document.execCommand('insertHTML', false, '<pre><code>' + this.getSelection() + '</code></pre>');\n break;\n case 'link': {\n const url = prompt('Enter URL:');\n if (url) document.execCommand('createLink', false, url);\n break;\n }\n case 'hr':\n document.execCommand('insertHorizontalRule');\n break;\n }\n this.syncFromRich();\n }\n\n private insertMarkdownSyntax(action: ToolbarAction): void {\n const textarea = this.editorArea()?.nativeElement;\n if (!textarea) return;\n\n const start = textarea.selectionStart;\n const end = textarea.selectionEnd;\n const text = this.markdown();\n const selected = text.substring(start, end);\n\n let insertion = '';\n let cursorOffset = 0;\n\n switch (action) {\n case 'bold':\n insertion = `**${selected || 'bold text'}**`;\n cursorOffset = selected ? insertion.length : 2;\n break;\n case 'italic':\n insertion = `*${selected || 'italic text'}*`;\n cursorOffset = selected ? insertion.length : 1;\n break;\n case 'underline':\n insertion = `<u>${selected || 'underlined text'}</u>`;\n cursorOffset = selected ? insertion.length : 3;\n break;\n case 'strikethrough':\n insertion = `~~${selected || 'strikethrough'}~~`;\n cursorOffset = selected ? insertion.length : 2;\n break;\n case 'h1':\n insertion = `# ${selected || 'Heading 1'}`;\n cursorOffset = insertion.length;\n break;\n case 'h2':\n insertion = `## ${selected || 'Heading 2'}`;\n cursorOffset = insertion.length;\n break;\n case 'h3':\n insertion = `### ${selected || 'Heading 3'}`;\n cursorOffset = insertion.length;\n break;\n case 'ul':\n insertion = `- ${selected || 'List item'}`;\n cursorOffset = insertion.length;\n break;\n case 'ol':\n insertion = `1. ${selected || 'List item'}`;\n cursorOffset = insertion.length;\n break;\n case 'blockquote':\n insertion = `> ${selected || 'Quote'}`;\n cursorOffset = insertion.length;\n break;\n case 'code':\n insertion = `\\`${selected || 'code'}\\``;\n cursorOffset = selected ? insertion.length : 1;\n break;\n case 'codeblock':\n insertion = `\\`\\`\\`\\n${selected || 'code'}\\n\\`\\`\\``;\n cursorOffset = 4;\n break;\n case 'link':\n insertion = `[${selected || 'link text'}](url)`;\n cursorOffset = selected ? insertion.length - 5 : 1;\n break;\n case 'image':\n insertion = ``;\n cursorOffset = selected ? insertion.length - 5 : 2;\n break;\n case 'hr':\n insertion = `\\n---\\n`;\n cursorOffset = insertion.length;\n break;\n }\n\n const newText = text.substring(0, start) + insertion + text.substring(end);\n this.markdown.set(newText);\n this.emitChange(newText);\n\n setTimeout(() => {\n textarea.focus();\n textarea.setSelectionRange(start + cursorOffset, start + cursorOffset);\n });\n }\n\n // -- Content input handlers --\n protected onMarkdownInput(event: Event): void {\n const value = (event.target as HTMLTextAreaElement).value;\n this.markdown.set(value);\n this.emitChange(value);\n }\n\n protected onRichInput(): void {\n this.syncFromRich();\n }\n\n protected onBlur(): void {\n this.onTouched();\n }\n\n private syncFromRich(): void {\n const el = this.richArea()?.nativeElement;\n if (!el) return;\n const md = this.htmlToMarkdown(el.innerHTML);\n this.markdown.set(md);\n this.emitChange(md);\n }\n\n /** Puts the current markdown into the rich area, as sanitised HTML. */\n private renderRich(el: HTMLDivElement): void {\n el.innerHTML = this.renderedHtml();\n }\n\n /**\n * Angular's HTML sanitizer, as the last line of defence behind the escaping\n * `markdownToHtml` does itself: whatever it might miss, this strips.\n */\n private sanitizeHtml(html: string): string {\n return this.sanitizer.sanitize(SecurityContext.HTML, html) ?? '';\n }\n\n private emitChange(value: string): void {\n this.onChange(value);\n this.contentChange.emit(value);\n }\n\n private getSelection(): string {\n return window.getSelection()?.toString() || '';\n }\n\n // -- Toolbar label helpers --\n protected getActionLabel(action: ToolbarAction): string {\n const labels: Record<ToolbarAction, string> = {\n bold: 'B',\n italic: 'I',\n underline: 'U',\n strikethrough: 'S',\n h1: 'H1',\n h2: 'H2',\n h3: 'H3',\n ul: '•',\n ol: '1.',\n blockquote: '\"',\n code: '<>',\n codeblock: '{ }',\n link: '🔗',\n image: '🖼',\n hr: '—',\n };\n return labels[action] || action;\n }\n\n protected getActionTitle(action: ToolbarAction): string {\n const titles: Record<ToolbarAction, string> = {\n bold: 'Bold',\n italic: 'Italic',\n underline: 'Underline',\n strikethrough: 'Strikethrough',\n h1: 'Heading 1',\n h2: 'Heading 2',\n h3: 'Heading 3',\n ul: 'Unordered List',\n ol: 'Ordered List',\n blockquote: 'Blockquote',\n code: 'Inline Code',\n codeblock: 'Code Block',\n link: 'Insert Link',\n image: 'Insert Image',\n hr: 'Horizontal Rule',\n };\n return this.actionTitles()[action] || titles[action] || action;\n }\n\n /**\n * Tooltip and accessible name of a toolbar button (AS-33, WCAG 2.5.3 Label\n * in Name): the title, then the visible label when it has letters or digits\n * the title doesn't contain (`Heading 1, H1`). Symbol labels keep the title.\n */\n protected getActionName(action: ToolbarAction): string {\n return nameWithText(this.getActionTitle(action), this.getActionLabel(action));\n }\n\n // -- Markdown <-> HTML conversion (simple built-in) --\n /**\n * Converts the editor's markdown subset to HTML. The source is HTML-escaped\n * *first*, so nothing in it can open a tag or an attribute; the conversions\n * below then only ever add markup of their own. Raw HTML in the markdown is\n * therefore shown as text — with one exception, `<u>`, which the toolbar's\n * underline action emits (markdown has no underline) and which is let back\n * through in escaped form only.\n */\n protected markdownToHtml(md: string): string {\n if (!md) return '';\n\n let html = escapeHtml(md);\n\n // Code blocks (must be first)\n html = html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g, '<pre><code class=\"language-$1\">$2</code></pre>');\n\n // Headings\n html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');\n html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');\n html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');\n\n // Horizontal rule\n html = html.replace(/^---$/gm, '<hr>');\n\n // Bold/Italic\n html = html.replace(/\\*\\*\\*(.+?)\\*\\*\\*/g, '<strong><em>$1</em></strong>');\n html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');\n html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');\n\n // Underline: the one raw tag the editor itself writes into the markdown.\n html = html.replace(/<u>(.+?)<\\/u>/g, '<u>$1</u>');\n\n // Inline code\n html = html.replace(/`(.+?)`/g, '<code>$1</code>');\n\n // Links and images. Only safe URLs become attributes; anything else is\n // shown as its text — a `javascript:` link that just isn't a link is far\n // better than one that runs. Quotes are already escaped (see above), so\n // the URL cannot break out of the attribute either.\n html = html.replace(/!\\[(.+?)\\]\\((.+?)\\)/g, (_m, alt: string, src: string) => {\n const url = safeUrl(src, true);\n return url ? `<img alt=\"${alt}\" src=\"${url}\" />` : alt;\n });\n html = html.replace(/\\[(.+?)\\]\\((.+?)\\)/g, (_m, text: string, href: string) => {\n const url = safeUrl(href);\n return url ? `<a href=\"${url}\">${text}</a>` : text;\n });\n\n // Blockquotes — the marker is `>` by now, the source having been escaped.\n html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');\n\n // Unordered lists\n html = html.replace(/^- (.+)$/gm, '<li>$1</li>');\n html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');\n\n // Ordered lists\n html = html.replace(/^\\d+\\. (.+)$/gm, '<li>$1</li>');\n\n // Paragraphs (lines that aren't already wrapped)\n html = html.replace(/^(?!<[a-z])((?!^\\s*$).+)$/gm, '<p>$1</p>');\n\n // Clean up empty paragraphs\n html = html.replace(/<p>\\s*<\\/p>/g, '');\n\n return html.trim();\n }\n\n private htmlToMarkdown(html: string): string {\n if (!html) return '';\n\n let md = html;\n\n // Block elements\n md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, '# $1\\n');\n md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, '## $1\\n');\n md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, '### $1\\n');\n md = md.replace(/<hr\\s*\\/?>/gi, '---\\n');\n md = md.replace(/<blockquote[^>]*>(.*?)<\\/blockquote>/gi, '> $1\\n');\n md = md.replace(/<pre><code[^>]*>([\\s\\S]*?)<\\/code><\\/pre>/gi, '```\\n$1\\n```\\n');\n\n // Lists\n md = md.replace(/<ul[^>]*>([\\s\\S]*?)<\\/ul>/gi, '$1');\n md = md.replace(/<ol[^>]*>([\\s\\S]*?)<\\/ol>/gi, '$1');\n md = md.replace(/<li[^>]*>(.*?)<\\/li>/gi, '- $1\\n');\n\n // Inline\n md = md.replace(/<strong[^>]*>(.*?)<\\/strong>/gi, '**$1**');\n md = md.replace(/<b[^>]*>(.*?)<\\/b>/gi, '**$1**');\n md = md.replace(/<em[^>]*>(.*?)<\\/em>/gi, '*$1*');\n md = md.replace(/<i[^>]*>(.*?)<\\/i>/gi, '*$1*');\n md = md.replace(/<del[^>]*>(.*?)<\\/del>/gi, '~~$1~~');\n md = md.replace(/<u[^>]*>(.*?)<\\/u>/gi, '<u>$1</u>');\n md = md.replace(/<code[^>]*>(.*?)<\\/code>/gi, '`$1`');\n md = md.replace(/<a[^>]*href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/gi, '[$2]($1)');\n md = md.replace(/<img[^>]*alt=\"([^\"]*)\"[^>]*src=\"([^\"]*)\"[^>]*\\/?>/gi, '');\n\n // Paragraphs and line breaks\n md = md.replace(/<p[^>]*>(.*?)<\\/p>/gi, '$1\\n\\n');\n md = md.replace(/<br\\s*\\/?>/gi, '\\n');\n md = md.replace(/<div[^>]*>(.*?)<\\/div>/gi, '$1\\n');\n\n // Strip remaining HTML tags\n md = md.replace(/<[^>]+>/g, '');\n\n // Decode HTML entities\n md = md.replace(/&/g, '&');\n md = md.replace(/</g, '<');\n md = md.replace(/>/g, '>');\n md = md.replace(/"/g, '\"');\n md = md.replace(/ /g, ' ');\n\n // Normalize whitespace\n md = md.replace(/\\n{3,}/g, '\\n\\n');\n\n return md.trim();\n }\n}\n","<div class=\"lc-rich-text-editor\" [class.lc-rich-text-editor--disabled]=\"isDisabled()\">\n <!-- Toolbar -->\n <div class=\"lc-rich-text-editor__toolbar\">\n <div class=\"lc-rich-text-editor__toolbar-actions\">\n @for (action of toolbar().actions; track action) {\n <button\n type=\"button\"\n class=\"lc-rich-text-editor__toolbar-btn\"\n [title]=\"getActionName(action)\"\n [attr.aria-label]=\"getActionName(action)\"\n [disabled]=\"!editable()\"\n (click)=\"onToolbarAction(action)\"\n >\n {{ getActionLabel(action) }}\n </button>\n }\n </div>\n <div class=\"lc-rich-text-editor__toolbar-modes\">\n <button\n type=\"button\"\n class=\"lc-rich-text-editor__mode-btn\"\n [class.active]=\"activeMode() === 'rich'\"\n (click)=\"switchMode('rich')\"\n [attr.title]=\"richModeTitle()\"\n >{{ richModeLabel() }}</button>\n <button\n type=\"button\"\n class=\"lc-rich-text-editor__mode-btn\"\n [class.active]=\"activeMode() === 'markdown'\"\n (click)=\"switchMode('markdown')\"\n [attr.title]=\"markdownModeTitle()\"\n >{{ markdownModeLabel() }}</button>\n <button\n type=\"button\"\n class=\"lc-rich-text-editor__mode-btn\"\n [class.active]=\"activeMode() === 'split'\"\n (click)=\"switchMode('split')\"\n [attr.title]=\"splitModeTitle()\"\n >{{ splitModeLabel() }}</button>\n </div>\n </div>\n\n <!-- Editor area -->\n <div\n class=\"lc-rich-text-editor__content\"\n [style.min-height.px]=\"minHeight()\"\n [style.max-height.px]=\"maxHeight() || null\"\n >\n @switch (activeMode()) {\n @case ('rich') {\n <!-- The rich area is filled from the markdown by the component (see the\n constructor effect); it must never receive markup Angular hasn't\n sanitised. `contenteditable` follows the disabled/readonly state —\n a greyed-out toolbar alone would still let the user type. -->\n <div\n #richArea\n class=\"lc-rich-text-editor__rich\"\n [attr.contenteditable]=\"editable() ? 'true' : 'false'\"\n [attr.aria-disabled]=\"isDisabled() ? 'true' : null\"\n [attr.aria-readonly]=\"readonly() && !isDisabled() ? 'true' : null\"\n [attr.data-placeholder]=\"placeholder()\"\n [class.lc-rich-text-editor__rich--empty]=\"!markdown()\"\n (input)=\"onRichInput()\"\n (blur)=\"onBlur()\"\n role=\"textbox\"\n aria-multiline=\"true\"\n [attr.aria-label]=\"placeholder()\"\n ></div>\n }\n @case ('markdown') {\n <textarea\n #editorArea\n class=\"lc-rich-text-editor__textarea\"\n [placeholder]=\"placeholder()\"\n [value]=\"markdown()\"\n [disabled]=\"isDisabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"onMarkdownInput($event)\"\n (blur)=\"onBlur()\"\n spellcheck=\"false\"\n [attr.aria-label]=\"markdownEditorAriaLabel()\"\n ></textarea>\n }\n @case ('split') {\n <div class=\"lc-rich-text-editor__split\">\n <textarea\n #editorArea\n class=\"lc-rich-text-editor__textarea lc-rich-text-editor__split-editor\"\n [placeholder]=\"placeholder()\"\n [value]=\"markdown()\"\n [disabled]=\"isDisabled()\"\n [readOnly]=\"readonly()\"\n (input)=\"onMarkdownInput($event)\"\n (blur)=\"onBlur()\"\n spellcheck=\"false\"\n [attr.aria-label]=\"markdownEditorAriaLabel()\"\n ></textarea>\n <div\n class=\"lc-rich-text-editor__split-preview\"\n [innerHTML]=\"renderedHtml()\"\n ></div>\n </div>\n }\n }\n </div>\n\n <!-- Footer -->\n @if (showWordCount()) {\n <div class=\"lc-rich-text-editor__footer\">\n <span>{{ wordCount() }} {{ wordsLabel() }}</span>\n <span>{{ charCount() }} {{ charactersLabel() }}</span>\n </div>\n }\n</div>\n"],"mappings":";;;;;;;AA4CA,MAAM,eAAuC;CAC3C,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAK;CACL,KAAK;AACN;AAGD,SAAS,WAAW,MAAY;CAC9B,OAAO,KAAK,QAAQ,aAAa,MAAM,aAAa,EAAE;AACxD;AAOA,SAAS,QAAQ,KAAa,QAAQ,OAAK;CACzC,MAAM,QAAQ,IAAI,KAAI;CACtB,IAAI,CAAC,OAAO,OAAO;CACnB,QAAQ,QAAQ,eAAe,KAAK,IAAI,cAAc,KAAK,KAAK,QAAQ;AAC1E;AAEA,MAAM,kBAAiC,EACrC,SAAS;CACP;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD,EACF;AA0CD,IAAa,0BAAb,MAAa,wBAAuB;CAElC,OAAO,MAA0B,QAAO,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAGxC,cAAc,MAAc,mBAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAG9C,YAAY,MAAc,KAAI,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAG9B,YAAY,MAAc,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAG5B,WAAW,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGhC,WAAW,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGhC,UAAU,MAAqB,iBAAgB,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAG/C,gBAAgB,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CASpC,eAAe,MAA8C,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAGhE,gBAAgB,MAAM,QAAO,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAG7B,gBAAgB,MAAM,aAAY,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAGlC,oBAAoB,MAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,oBAAA,CAAA,+BAAA,CAAA,CAAA;CAG/B,oBAAoB,MAAM,YAAW,GAAA,YAAA,CAAA,EAAA,WAAA,oBAAA,CAAA,+BAAA,CAAA,CAAA;CAGrC,iBAAiB,MAAM,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAG/B,iBAAiB,MAAM,cAAa,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAGpC,0BAA0B,MAAM,mBAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,0BAAA,CAAA,+BAAA,CAAA,CAAA;CAGlD,aAAa,MAAM,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAG3B,kBAAkB,MAAM,cAAa,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAG5B,gBAAgB,OAAM;CAEd,aAAa,UAA2C,cAAa,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CACrE,WAAW,UAAsC,YAAW,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAC5D,YAAY,OAAO,YAAY;CAGtC,WAAW,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGrB,aAAa,mBAAuC,KAAK,KAAI,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAGzD,eAAe,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAG1B,aAAa,eAAe,KAAK,SAAQ,KAAM,KAAK,aAAY,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAGnE,WAAW,eAAe,CAAC,KAAK,WAAU,KAAM,CAAC,KAAK,SAAQ,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAG1E,YAAY,eAAc;EAClC,MAAM,OAAO,KAAK,SAAQ,CAAE,CAAC,KAAI;EACjC,IAAI,CAAC,MAAM,OAAO;EAClB,OAAO,KAAK,MAAM,KAAK,CAAC,CAAC;CAC3B,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAGQ,YAAY,eAAe,KAAK,SAAQ,CAAE,CAAC,QAAO,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAOlD,eAAe,eAAe,KAAK,aAAa,KAAK,eAAe,KAAK,SAAQ,CAAE,CAAC,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAGxF,iBAAyC,CAAiC;CAC1E,kBAA6B,CAAkC;CAEvE,cAAA;EAOE,aAAY;GACV,MAAM,KAAK,KAAK,SAAQ,CAAE,EAAE;GAC5B,IAAI,IAAI,gBAAgB,KAAK,WAAW,EAAE,CAAC;EAC7C,CAAC;CACH;CAGA,WAAW,OAAgC;EACzC,KAAK,SAAS,IAAI,SAAS,EAAE;EAC7B,MAAM,KAAK,KAAK,SAAQ,CAAE,EAAE;EAC5B,IAAI,IAAI,KAAK,WAAW,EAAE;CAC5B;CAEA,iBAAiB,IAA2B;EAC1C,KAAK,WAAW;CAClB;CAEA,kBAAkB,IAAc;EAC9B,KAAK,YAAY;CACnB;CAEA,iBAAiB,YAAmB;EAClC,KAAK,aAAa,IAAI,UAAU;CAClC;CAGU,WAAW,MAAwB;EAC3C,IAAI,KAAK,WAAU,MAAO,UAAU,SAAS,QAAQ;GAEnD,MAAM,KAAK,KAAK,SAAQ,CAAE,EAAE;GAC5B,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,eAAe,GAAG,SAAS,CAAC;EAC7D;EAGA,KAAK,WAAW,IAAI,IAAI;CAC1B;CAGU,gBAAgB,QAAqB;EAC7C,IAAI,CAAC,KAAK,SAAQ,GAAI;EAEtB,IAAI,KAAK,WAAU,MAAO,QACxB,KAAK,gBAAgB,MAAM;OAE3B,KAAK,qBAAqB,MAAM;CAEpC;CAEQ,gBAAgB,QAAqB;EAC3C,QAAQ,QAAR;GACE,KAAK;IACH,SAAS,YAAY,MAAM;IAC3B;GACF,KAAK;IACH,SAAS,YAAY,QAAQ;IAC7B;GACF,KAAK;IACH,SAAS,YAAY,WAAW;IAChC;GACF,KAAK;IACH,SAAS,YAAY,eAAe;IACpC;GACF,KAAK;IACH,SAAS,YAAY,eAAe,OAAO,IAAI;IAC/C;GACF,KAAK;IACH,SAAS,YAAY,eAAe,OAAO,IAAI;IAC/C;GACF,KAAK;IACH,SAAS,YAAY,eAAe,OAAO,IAAI;IAC/C;GACF,KAAK;IACH,SAAS,YAAY,qBAAqB;IAC1C;GACF,KAAK;IACH,SAAS,YAAY,mBAAmB;IACxC;GACF,KAAK;IACH,SAAS,YAAY,eAAe,OAAO,YAAY;IACvD;GACF,KAAK;IACH,SAAS,YAAY,cAAc,OAAO,WAAW,KAAK,aAAY,IAAK,SAAS;IACpF;GACF,KAAK;IACH,SAAS,YAAY,cAAc,OAAO,gBAAgB,KAAK,aAAY,IAAK,eAAe;IAC/F;GACF,KAAK,QAAQ;IACX,MAAM,MAAM,OAAO,YAAY;IAC/B,IAAI,KAAK,SAAS,YAAY,cAAc,OAAO,GAAG;IACtD;GACF;GACA,KAAK,MACH,SAAS,YAAY,sBAAsB;EAE/C;EACA,KAAK,aAAY;CACnB;CAEQ,qBAAqB,QAAqB;EAChD,MAAM,WAAW,KAAK,WAAU,CAAE,EAAE;EACpC,IAAI,CAAC,UAAU;EAEf,MAAM,QAAQ,SAAS;EACvB,MAAM,MAAM,SAAS;EACrB,MAAM,OAAO,KAAK,SAAQ;EAC1B,MAAM,WAAW,KAAK,UAAU,OAAO,GAAG;EAE1C,IAAI,YAAY;EAChB,IAAI,eAAe;EAEnB,QAAQ,QAAR;GACE,KAAK;IACH,YAAY,KAAK,YAAY,YAAW;IACxC,eAAe,WAAW,UAAU,SAAS;IAC7C;GACF,KAAK;IACH,YAAY,IAAI,YAAY,cAAa;IACzC,eAAe,WAAW,UAAU,SAAS;IAC7C;GACF,KAAK;IACH,YAAY,MAAM,YAAY,kBAAiB;IAC/C,eAAe,WAAW,UAAU,SAAS;IAC7C;GACF,KAAK;IACH,YAAY,KAAK,YAAY,gBAAe;IAC5C,eAAe,WAAW,UAAU,SAAS;IAC7C;GACF,KAAK;IACH,YAAY,KAAK,YAAY;IAC7B,eAAe,UAAU;IACzB;GACF,KAAK;IACH,YAAY,MAAM,YAAY;IAC9B,eAAe,UAAU;IACzB;GACF,KAAK;IACH,YAAY,OAAO,YAAY;IAC/B,eAAe,UAAU;IACzB;GACF,KAAK;IACH,YAAY,KAAK,YAAY;IAC7B,eAAe,UAAU;IACzB;GACF,KAAK;IACH,YAAY,MAAM,YAAY;IAC9B,eAAe,UAAU;IACzB;GACF,KAAK;IACH,YAAY,KAAK,YAAY;IAC7B,eAAe,UAAU;IACzB;GACF,KAAK;IACH,YAAY,KAAK,YAAY,OAAM;IACnC,eAAe,WAAW,UAAU,SAAS;IAC7C;GACF,KAAK;IACH,YAAY,WAAW,YAAY,OAAM;IACzC,eAAe;IACf;GACF,KAAK;IACH,YAAY,IAAI,YAAY,YAAW;IACvC,eAAe,WAAW,UAAU,SAAS,IAAI;IACjD;GACF,KAAK;IACH,YAAY,KAAK,YAAY,WAAU;IACvC,eAAe,WAAW,UAAU,SAAS,IAAI;IACjD;GACF,KAAK;IACH,YAAY;IACZ,eAAe,UAAU;EAE7B;EAEA,MAAM,UAAU,KAAK,UAAU,GAAG,KAAK,IAAI,YAAY,KAAK,UAAU,GAAG;EACzE,KAAK,SAAS,IAAI,OAAO;EACzB,KAAK,WAAW,OAAO;EAEvB,iBAAgB;GACd,SAAS,MAAK;GACd,SAAS,kBAAkB,QAAQ,cAAc,QAAQ,YAAY;EACvE,CAAC;CACH;CAGU,gBAAgB,OAAY;EACpC,MAAM,QAAS,MAAM,OAA+B;EACpD,KAAK,SAAS,IAAI,KAAK;EACvB,KAAK,WAAW,KAAK;CACvB;CAEU,cAAW;EACnB,KAAK,aAAY;CACnB;CAEU,SAAM;EACd,KAAK,UAAS;CAChB;CAEQ,eAAY;EAClB,MAAM,KAAK,KAAK,SAAQ,CAAE,EAAE;EAC5B,IAAI,CAAC,IAAI;EACT,MAAM,KAAK,KAAK,eAAe,GAAG,SAAS;EAC3C,KAAK,SAAS,IAAI,EAAE;EACpB,KAAK,WAAW,EAAE;CACpB;CAGQ,WAAW,IAAkB;EACnC,GAAG,YAAY,KAAK,aAAY;CAClC;CAMQ,aAAa,MAAY;EAC/B,OAAO,KAAK,UAAU,SAAS,gBAAgB,MAAM,IAAI,KAAK;CAChE;CAEQ,WAAW,OAAa;EAC9B,KAAK,SAAS,KAAK;EACnB,KAAK,cAAc,KAAK,KAAK;CAC/B;CAEQ,eAAY;EAClB,OAAO,OAAO,aAAY,CAAE,EAAE,SAAQ,KAAM;CAC9C;CAGU,eAAe,QAAqB;EAkB5C,OAAO;GAhBL,MAAM;GACN,QAAQ;GACR,WAAW;GACX,eAAe;GACf,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,YAAY;GACZ,MAAM;GACN,WAAW;GACX,MAAM;GACN,OAAO;GACP,IAAI;EAEC,EAAO,WAAW;CAC3B;CAEU,eAAe,QAAqB;EAkB5C,OAAO,KAAK,aAAY,CAAE,CAAC,WAAW;GAhBpC,MAAM;GACN,QAAQ;GACR,WAAW;GACX,eAAe;GACf,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,YAAY;GACZ,MAAM;GACN,WAAW;GACX,MAAM;GACN,OAAO;GACP,IAAI;EAEgC,EAAO,WAAW;CAC1D;CAOU,cAAc,QAAqB;EAC3C,OAAO,aAAa,KAAK,eAAe,MAAM,GAAG,KAAK,eAAe,MAAM,CAAC;CAC9E;CAWU,eAAe,IAAU;EACjC,IAAI,CAAC,IAAI,OAAO;EAEhB,IAAI,OAAO,WAAW,EAAE;EAGxB,OAAO,KAAK,QAAQ,4BAA4B,kDAAgD;EAGhG,OAAO,KAAK,QAAQ,gBAAgB,aAAa;EACjD,OAAO,KAAK,QAAQ,eAAe,aAAa;EAChD,OAAO,KAAK,QAAQ,cAAc,aAAa;EAG/C,OAAO,KAAK,QAAQ,WAAW,MAAM;EAGrC,OAAO,KAAK,QAAQ,sBAAsB,8BAA8B;EACxE,OAAO,KAAK,QAAQ,kBAAkB,qBAAqB;EAC3D,OAAO,KAAK,QAAQ,cAAc,aAAa;EAC/C,OAAO,KAAK,QAAQ,cAAc,eAAe;EAGjD,OAAO,KAAK,QAAQ,8BAA8B,WAAW;EAG7D,OAAO,KAAK,QAAQ,YAAY,iBAAiB;EAMjD,OAAO,KAAK,QAAQ,yBAAyB,IAAI,KAAa,QAAe;GAC3E,MAAM,MAAM,QAAQ,KAAK,IAAI;GAC7B,OAAO,MAAM,aAAa,IAAG,SAAU,IAAG,QAAS;EACrD,CAAC;EACD,OAAO,KAAK,QAAQ,wBAAwB,IAAI,MAAc,SAAgB;GAC5E,MAAM,MAAM,QAAQ,IAAI;GACxB,OAAO,MAAM,YAAY,IAAG,IAAK,KAAI,QAAS;EAChD,CAAC;EAGD,OAAO,KAAK,QAAQ,iBAAiB,6BAA6B;EAGlE,OAAO,KAAK,QAAQ,cAAc,aAAa;EAC/C,OAAO,KAAK,QAAQ,uBAAuB,aAAa;EAGxD,OAAO,KAAK,QAAQ,kBAAkB,aAAa;EAGnD,OAAO,KAAK,QAAQ,+BAA+B,WAAW;EAG9D,OAAO,KAAK,QAAQ,gBAAgB,EAAE;EAEtC,OAAO,KAAK,KAAI;CAClB;CAEQ,eAAe,MAAY;EACjC,IAAI,CAAC,MAAM,OAAO;EAElB,IAAI,KAAK;EAGT,KAAK,GAAG,QAAQ,0BAA0B,QAAQ;EAClD,KAAK,GAAG,QAAQ,0BAA0B,SAAS;EACnD,KAAK,GAAG,QAAQ,0BAA0B,UAAU;EACpD,KAAK,GAAG,QAAQ,gBAAgB,OAAO;EACvC,KAAK,GAAG,QAAQ,0CAA0C,QAAQ;EAClE,KAAK,GAAG,QAAQ,+CAA+C,gBAAgB;EAG/E,KAAK,GAAG,QAAQ,+BAA+B,IAAI;EACnD,KAAK,GAAG,QAAQ,+BAA+B,IAAI;EACnD,KAAK,GAAG,QAAQ,0BAA0B,QAAQ;EAGlD,KAAK,GAAG,QAAQ,kCAAkC,QAAQ;EAC1D,KAAK,GAAG,QAAQ,wBAAwB,QAAQ;EAChD,KAAK,GAAG,QAAQ,0BAA0B,MAAM;EAChD,KAAK,GAAG,QAAQ,wBAAwB,MAAM;EAC9C,KAAK,GAAG,QAAQ,4BAA4B,QAAQ;EACpD,KAAK,GAAG,QAAQ,wBAAwB,WAAW;EACnD,KAAK,GAAG,QAAQ,8BAA8B,MAAM;EACpD,KAAK,GAAG,QAAQ,2CAA2C,UAAU;EACrE,KAAK,GAAG,QAAQ,uDAAuD,WAAW;EAGlF,KAAK,GAAG,QAAQ,wBAAwB,QAAQ;EAChD,KAAK,GAAG,QAAQ,gBAAgB,IAAI;EACpC,KAAK,GAAG,QAAQ,4BAA4B,MAAM;EAGlD,KAAK,GAAG,QAAQ,YAAY,EAAE;EAG9B,KAAK,GAAG,QAAQ,UAAU,GAAG;EAC7B,KAAK,GAAG,QAAQ,SAAS,GAAG;EAC5B,KAAK,GAAG,QAAQ,SAAS,GAAG;EAC5B,KAAK,GAAG,QAAQ,WAAW,IAAG;EAC9B,KAAK,GAAG,QAAQ,WAAW,GAAG;EAG9B,KAAK,GAAG,QAAQ,WAAW,MAAM;EAEjC,OAAO,GAAG,KAAI;CAChB;CA5fW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAuB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAvB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAuB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,aAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,mBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,mBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,gBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,gBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,yBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA,EAAA,eAAA,gBAAA;EAAA,WARvB,CACT;GACE,SAAS;GACT,aAAa,iBAAiB,uBAAuB;GACrD,OAAO;EACR,CACF;EAAA,aAAA,CAAA;GAAA,cAAA;GAAA,OAAA;GAAA,WAAA,CAAA,YAAA;GAAA,aAAA;GAAA,UAAA;EAAA,GAAA;GAAA,cAAA;GAAA,OAAA;GAAA,WAAA,CAAA,UAAA;GAAA,aAAA;GAAA,UAAA;EAAA,CAAA;EAAA,UAAA;EAAA,UC7HH;EAkHA,QAAA,CAAA,8jNAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MDCY;EAAY,CAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAYX,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAuB,YAAA,CAAA;EAfnC,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAqB,YACnB;GAAI,SACP,CAAC,YAAY;GAAC,iBAGN,wBAAwB;GAAM,WACpC,CACT;IACE,SAAS;IACT,aAAa,iBAAgB,uBAAwB;IACrD,OAAO;GACR,CACF;GAAA,UAAA;GAAA,QAAA,CAAA,8jNAAA;EAAA,CAAA;CAkEwE,CAAA;CAAA,sBAAA,CAAA;CAAA,gBAAA;EAAA,MAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,aAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,WAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,UAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,UAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,SAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,eAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,cAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,eAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,eAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,mBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,mBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,gBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,gBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,yBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,YAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,iBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,eAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,eAAA;EAAA,CAAA;EAAA,YAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,cAAY,EAAA,UAAA,KAAA,CAAA;EAAA,CAAA;EAAA,UAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CACnB,YAAU,EAAA,UAAA,KAAA,CAAA;EAAA,CAAA;CAAA;AAAA,CAAA"}
|
|
@@ -25,8 +25,13 @@ var StepperComponent = class StepperComponent {
|
|
|
25
25
|
getStepAriaLabel(index) {
|
|
26
26
|
const step = this.steps()[index];
|
|
27
27
|
if (!step) return "";
|
|
28
|
-
const
|
|
29
|
-
return
|
|
28
|
+
const template = this.stepAriaLabel();
|
|
29
|
+
return [
|
|
30
|
+
template.replace("{n}", String(index + 1)).replace("{m}", String(this.totalSteps())).replace("{label}", step.label),
|
|
31
|
+
template.trimEnd().endsWith("{label}") ? "" : step.label,
|
|
32
|
+
step.description ?? "",
|
|
33
|
+
step.optional ? this.optionalLabel() : ""
|
|
34
|
+
].filter(Boolean).join(", ");
|
|
30
35
|
}
|
|
31
36
|
getDescriptionId(index) {
|
|
32
37
|
return this.steps()[index]?.description ? `${this.instanceId}-desc-${index}` : null;
|
|
@@ -125,8 +130,8 @@ var StepperComponent = class StepperComponent {
|
|
|
125
130
|
stepChange: "stepChange"
|
|
126
131
|
},
|
|
127
132
|
ngImport: i0,
|
|
128
|
-
template: "<div\n class=\"lc-stepper\"\n [class.lc-stepper--horizontal]=\"orientation() === 'horizontal'\"\n [class.lc-stepper--vertical]=\"orientation() === 'vertical'\"\n>\n <!-- Step indicators: an ordered list of buttons (position is conveyed by the list) -->\n <ol class=\"lc-stepper__header\">\n @for (step of steps(); track $index; let i = $index; let last = $last) {\n <li class=\"lc-stepper__item\">\n <button\n type=\"button\"\n [class]=\"getStepClasses(i)\"\n [disabled]=\"!isStepNavigable(i)\"\n [attr.aria-current]=\"getStepState(i) === 'active' ? 'step' : null\"\n [attr.aria-label]=\"getStepAriaLabel(i)\"\n
|
|
129
|
-
styles: [".lc-stepper{width:100%}.lc-stepper__header{display:flex;align-items:center;gap:0;list-style:none;margin:0;padding:0}.lc-stepper--horizontal .lc-stepper__header{flex-direction:row}.lc-stepper--vertical .lc-stepper__header{flex-direction:column;align-items:flex-start}.lc-stepper__item{display:flex;align-items:center;flex:1 1 auto;min-width:0}.lc-stepper__item:last-child{flex:0 0 auto}.lc-stepper--vertical .lc-stepper__item{flex-direction:column;align-items:flex-start;flex:0 0 auto}.lc-stepper__step{appearance:none;background:none;border:0;margin:0;padding:0;font:inherit;color:inherit;text-align:left;cursor:default;display:flex;align-items:center;gap:.75rem;flex-shrink:0;border-radius:var(--border-radius-sm)}.lc-stepper__step--clickable{cursor:pointer}.lc-stepper__step:disabled{cursor:default}.lc-stepper__step:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-stepper__indicator{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:var(--border-radius-full);font-size:var(--font-size-sm, .875rem);font-weight:var(--typography-font-weight-semibold, 600);transition:all .2s ease}.lc-stepper__step--pending .lc-stepper__indicator{background-color:var(--color-surface-hover);color:var(--color-text-tertiary);border:1px solid var(--color-border)}.lc-stepper__step--active .lc-stepper__indicator{background-color:var(--color-primary-fill);color:var(--color-on-primary);box-shadow:0 0 0 4px #20849740}.lc-stepper__step--completed .lc-stepper__indicator{background-color:var(--color-success);color:var(--color-neutral-900)}.lc-stepper__check{width:1rem;height:1rem}.lc-stepper__number{line-height:1}.lc-stepper__label{display:flex;flex-direction:column}.lc-stepper__title{font-size:var(--font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-primary)}.lc-stepper__step--pending .lc-stepper__title{color:var(--color-text-tertiary)}.lc-stepper__description{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);margin-top:.125rem}.lc-stepper__optional{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);font-style:italic;margin-top:.125rem}.lc-stepper__connector{flex:1;height:.125rem;min-width:2rem;margin:0 .5rem;background-color:var(--color-border);transition:background-color .2s ease}.lc-stepper__connector--completed{background-color:var(--color-success)}.lc-stepper--vertical .lc-stepper__connector{flex:0 0 auto;width:.125rem;height:2rem;min-width:0;margin:.25rem 0 .25rem 1rem}.lc-stepper__content{margin-top:1.5rem}\n"],
|
|
133
|
+
template: "<div\n class=\"lc-stepper\"\n [class.lc-stepper--horizontal]=\"orientation() === 'horizontal'\"\n [class.lc-stepper--vertical]=\"orientation() === 'vertical'\"\n>\n <!-- Step indicators: an ordered list of buttons (position is conveyed by the list) -->\n <ol class=\"lc-stepper__header\">\n @for (step of steps(); track $index; let i = $index; let last = $last) {\n <li class=\"lc-stepper__item\">\n <button\n type=\"button\"\n [class]=\"getStepClasses(i)\"\n [disabled]=\"!isStepNavigable(i)\"\n [attr.aria-current]=\"getStepState(i) === 'active' ? 'step' : null\"\n [attr.aria-label]=\"getStepAriaLabel(i)\"\n (click)=\"goToStep(i)\"\n >\n <!-- The number is generated content (data-step), not text: as text,\n the visible label would read \"2Profile\" (WCAG 2.5.3). -->\n <span class=\"lc-stepper__indicator\" aria-hidden=\"true\">\n @if (getStepState(i) === 'completed') {\n <!-- Checkmark for completed -->\n <svg class=\"lc-stepper__check\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <path\n fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n } @else {\n <span class=\"lc-stepper__number\" [attr.data-step]=\"i + 1\"></span>\n }\n </span>\n <!-- Real spaces between the parts: the visible text reads\n \"Profile Set up your profile\", which the name contains. -->\n <span class=\"lc-stepper__label\">\n <span class=\"lc-stepper__title\">{{ step.label }}</span>\n @if (step.description) {\n &ngsp;<span class=\"lc-stepper__description\" [attr.id]=\"getDescriptionId(i)\">{{\n step.description\n }}</span>\n }\n @if (step.optional) {\n &ngsp;<span class=\"lc-stepper__optional\">{{ optionalLabel() }}</span>\n }\n </span>\n </button>\n\n @if (!last) {\n <span [class]=\"getConnectorClasses(i)\" aria-hidden=\"true\"></span>\n }\n </li>\n }\n </ol>\n\n <!-- Step content area (projected) -->\n <div class=\"lc-stepper__content\">\n <ng-content></ng-content>\n </div>\n</div>\n",
|
|
134
|
+
styles: [".lc-stepper{width:100%}.lc-stepper__header{display:flex;align-items:center;gap:0;list-style:none;margin:0;padding:0}.lc-stepper--horizontal .lc-stepper__header{flex-direction:row}.lc-stepper--vertical .lc-stepper__header{flex-direction:column;align-items:flex-start}.lc-stepper__item{display:flex;align-items:center;flex:1 1 auto;min-width:0}.lc-stepper__item:last-child{flex:0 0 auto}.lc-stepper--vertical .lc-stepper__item{flex-direction:column;align-items:flex-start;flex:0 0 auto}.lc-stepper__step{appearance:none;background:none;border:0;margin:0;padding:0;font:inherit;color:inherit;text-align:left;cursor:default;display:flex;align-items:center;gap:.75rem;flex-shrink:0;border-radius:var(--border-radius-sm)}.lc-stepper__step--clickable{cursor:pointer}.lc-stepper__step:disabled{cursor:default}.lc-stepper__step:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-stepper__indicator{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:var(--border-radius-full);font-size:var(--font-size-sm, .875rem);font-weight:var(--typography-font-weight-semibold, 600);transition:all .2s ease}.lc-stepper__step--pending .lc-stepper__indicator{background-color:var(--color-surface-hover);color:var(--color-text-tertiary);border:1px solid var(--color-border)}.lc-stepper__step--active .lc-stepper__indicator{background-color:var(--color-primary-fill);color:var(--color-on-primary);box-shadow:0 0 0 4px #20849740}.lc-stepper__step--completed .lc-stepper__indicator{background-color:var(--color-success);color:var(--color-neutral-900)}.lc-stepper__check{width:1rem;height:1rem}.lc-stepper__number{line-height:1}.lc-stepper__number:before{content:attr(data-step)}.lc-stepper__label{display:flex;flex-direction:column}.lc-stepper__title{font-size:var(--font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-primary)}.lc-stepper__step--pending .lc-stepper__title{color:var(--color-text-tertiary)}.lc-stepper__description{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);margin-top:.125rem}.lc-stepper__optional{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);font-style:italic;margin-top:.125rem}.lc-stepper__connector{flex:1;height:.125rem;min-width:2rem;margin:0 .5rem;background-color:var(--color-border);transition:background-color .2s ease}.lc-stepper__connector--completed{background-color:var(--color-success)}.lc-stepper--vertical .lc-stepper__connector{flex:0 0 auto;width:.125rem;height:2rem;min-width:0;margin:.25rem 0 .25rem 1rem}.lc-stepper__content{margin-top:1.5rem}\n"],
|
|
130
135
|
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
131
136
|
});
|
|
132
137
|
};
|
|
@@ -142,8 +147,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
142
147
|
standalone: true,
|
|
143
148
|
imports: [],
|
|
144
149
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
145
|
-
template: "<div\n class=\"lc-stepper\"\n [class.lc-stepper--horizontal]=\"orientation() === 'horizontal'\"\n [class.lc-stepper--vertical]=\"orientation() === 'vertical'\"\n>\n <!-- Step indicators: an ordered list of buttons (position is conveyed by the list) -->\n <ol class=\"lc-stepper__header\">\n @for (step of steps(); track $index; let i = $index; let last = $last) {\n <li class=\"lc-stepper__item\">\n <button\n type=\"button\"\n [class]=\"getStepClasses(i)\"\n [disabled]=\"!isStepNavigable(i)\"\n [attr.aria-current]=\"getStepState(i) === 'active' ? 'step' : null\"\n [attr.aria-label]=\"getStepAriaLabel(i)\"\n
|
|
146
|
-
styles: [".lc-stepper{width:100%}.lc-stepper__header{display:flex;align-items:center;gap:0;list-style:none;margin:0;padding:0}.lc-stepper--horizontal .lc-stepper__header{flex-direction:row}.lc-stepper--vertical .lc-stepper__header{flex-direction:column;align-items:flex-start}.lc-stepper__item{display:flex;align-items:center;flex:1 1 auto;min-width:0}.lc-stepper__item:last-child{flex:0 0 auto}.lc-stepper--vertical .lc-stepper__item{flex-direction:column;align-items:flex-start;flex:0 0 auto}.lc-stepper__step{appearance:none;background:none;border:0;margin:0;padding:0;font:inherit;color:inherit;text-align:left;cursor:default;display:flex;align-items:center;gap:.75rem;flex-shrink:0;border-radius:var(--border-radius-sm)}.lc-stepper__step--clickable{cursor:pointer}.lc-stepper__step:disabled{cursor:default}.lc-stepper__step:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-stepper__indicator{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:var(--border-radius-full);font-size:var(--font-size-sm, .875rem);font-weight:var(--typography-font-weight-semibold, 600);transition:all .2s ease}.lc-stepper__step--pending .lc-stepper__indicator{background-color:var(--color-surface-hover);color:var(--color-text-tertiary);border:1px solid var(--color-border)}.lc-stepper__step--active .lc-stepper__indicator{background-color:var(--color-primary-fill);color:var(--color-on-primary);box-shadow:0 0 0 4px #20849740}.lc-stepper__step--completed .lc-stepper__indicator{background-color:var(--color-success);color:var(--color-neutral-900)}.lc-stepper__check{width:1rem;height:1rem}.lc-stepper__number{line-height:1}.lc-stepper__label{display:flex;flex-direction:column}.lc-stepper__title{font-size:var(--font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-primary)}.lc-stepper__step--pending .lc-stepper__title{color:var(--color-text-tertiary)}.lc-stepper__description{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);margin-top:.125rem}.lc-stepper__optional{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);font-style:italic;margin-top:.125rem}.lc-stepper__connector{flex:1;height:.125rem;min-width:2rem;margin:0 .5rem;background-color:var(--color-border);transition:background-color .2s ease}.lc-stepper__connector--completed{background-color:var(--color-success)}.lc-stepper--vertical .lc-stepper__connector{flex:0 0 auto;width:.125rem;height:2rem;min-width:0;margin:.25rem 0 .25rem 1rem}.lc-stepper__content{margin-top:1.5rem}\n"]
|
|
150
|
+
template: "<div\n class=\"lc-stepper\"\n [class.lc-stepper--horizontal]=\"orientation() === 'horizontal'\"\n [class.lc-stepper--vertical]=\"orientation() === 'vertical'\"\n>\n <!-- Step indicators: an ordered list of buttons (position is conveyed by the list) -->\n <ol class=\"lc-stepper__header\">\n @for (step of steps(); track $index; let i = $index; let last = $last) {\n <li class=\"lc-stepper__item\">\n <button\n type=\"button\"\n [class]=\"getStepClasses(i)\"\n [disabled]=\"!isStepNavigable(i)\"\n [attr.aria-current]=\"getStepState(i) === 'active' ? 'step' : null\"\n [attr.aria-label]=\"getStepAriaLabel(i)\"\n (click)=\"goToStep(i)\"\n >\n <!-- The number is generated content (data-step), not text: as text,\n the visible label would read \"2Profile\" (WCAG 2.5.3). -->\n <span class=\"lc-stepper__indicator\" aria-hidden=\"true\">\n @if (getStepState(i) === 'completed') {\n <!-- Checkmark for completed -->\n <svg class=\"lc-stepper__check\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <path\n fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n } @else {\n <span class=\"lc-stepper__number\" [attr.data-step]=\"i + 1\"></span>\n }\n </span>\n <!-- Real spaces between the parts: the visible text reads\n \"Profile Set up your profile\", which the name contains. -->\n <span class=\"lc-stepper__label\">\n <span class=\"lc-stepper__title\">{{ step.label }}</span>\n @if (step.description) {\n &ngsp;<span class=\"lc-stepper__description\" [attr.id]=\"getDescriptionId(i)\">{{\n step.description\n }}</span>\n }\n @if (step.optional) {\n &ngsp;<span class=\"lc-stepper__optional\">{{ optionalLabel() }}</span>\n }\n </span>\n </button>\n\n @if (!last) {\n <span [class]=\"getConnectorClasses(i)\" aria-hidden=\"true\"></span>\n }\n </li>\n }\n </ol>\n\n <!-- Step content area (projected) -->\n <div class=\"lc-stepper__content\">\n <ng-content></ng-content>\n </div>\n</div>\n",
|
|
151
|
+
styles: [".lc-stepper{width:100%}.lc-stepper__header{display:flex;align-items:center;gap:0;list-style:none;margin:0;padding:0}.lc-stepper--horizontal .lc-stepper__header{flex-direction:row}.lc-stepper--vertical .lc-stepper__header{flex-direction:column;align-items:flex-start}.lc-stepper__item{display:flex;align-items:center;flex:1 1 auto;min-width:0}.lc-stepper__item:last-child{flex:0 0 auto}.lc-stepper--vertical .lc-stepper__item{flex-direction:column;align-items:flex-start;flex:0 0 auto}.lc-stepper__step{appearance:none;background:none;border:0;margin:0;padding:0;font:inherit;color:inherit;text-align:left;cursor:default;display:flex;align-items:center;gap:.75rem;flex-shrink:0;border-radius:var(--border-radius-sm)}.lc-stepper__step--clickable{cursor:pointer}.lc-stepper__step:disabled{cursor:default}.lc-stepper__step:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.lc-stepper__indicator{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:var(--border-radius-full);font-size:var(--font-size-sm, .875rem);font-weight:var(--typography-font-weight-semibold, 600);transition:all .2s ease}.lc-stepper__step--pending .lc-stepper__indicator{background-color:var(--color-surface-hover);color:var(--color-text-tertiary);border:1px solid var(--color-border)}.lc-stepper__step--active .lc-stepper__indicator{background-color:var(--color-primary-fill);color:var(--color-on-primary);box-shadow:0 0 0 4px #20849740}.lc-stepper__step--completed .lc-stepper__indicator{background-color:var(--color-success);color:var(--color-neutral-900)}.lc-stepper__check{width:1rem;height:1rem}.lc-stepper__number{line-height:1}.lc-stepper__number:before{content:attr(data-step)}.lc-stepper__label{display:flex;flex-direction:column}.lc-stepper__title{font-size:var(--font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-primary)}.lc-stepper__step--pending .lc-stepper__title{color:var(--color-text-tertiary)}.lc-stepper__description{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);margin-top:.125rem}.lc-stepper__optional{font-size:var(--font-size-xs, .75rem);color:var(--color-text-tertiary);font-style:italic;margin-top:.125rem}.lc-stepper__connector{flex:1;height:.125rem;min-width:2rem;margin:0 .5rem;background-color:var(--color-border);transition:background-color .2s ease}.lc-stepper__connector--completed{background-color:var(--color-success)}.lc-stepper--vertical .lc-stepper__connector{flex:0 0 auto;width:.125rem;height:2rem;min-width:0;margin:.25rem 0 .25rem 1rem}.lc-stepper__content{margin-top:1.5rem}\n"]
|
|
147
152
|
}]
|
|
148
153
|
}],
|
|
149
154
|
propDecorators: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-stepper.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/stepper/stepper.component.ts","../../../../libs/angular-ui-kit/src/lib/stepper/stepper.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n model,\n computed,\n} from '@angular/core';\n\nexport interface StepperStep {\n readonly label: string;\n readonly description?: string;\n readonly optional?: boolean;\n}\n\nexport type StepState = 'pending' | 'active' | 'completed';\n\nlet nextStepperId = 0;\n\n/**\n * Stepper component for multi-step wizard workflows.\n *\n * Features:\n * - Horizontal and vertical orientations\n * - Active, completed, and pending step states\n * - Numbered step indicators with checkmark on completion\n * - Optional step descriptions and \"optional\" labels\n * - Linear mode restricting navigation to sequential steps\n * - Two-way active step binding\n * - Content projection for step body\n * - Accessible: steps are real buttons in an ordered list, the active step\n * carries `aria-current=\"step\"`, non-navigable steps are disabled\n *\n * @example\n * ```html\n * <lc-stepper [steps]=\"steps\" [(activeStep)]=\"currentStep\">\n * <div>Step content here</div>\n * </lc-stepper>\n * ```\n */\n@Component({\n selector: 'lc-stepper',\n standalone: true,\n imports: [],\n templateUrl: './stepper.component.html',\n styleUrl: './stepper.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class StepperComponent {\n /** Step definitions */\n readonly steps = input.required<readonly StepperStep[]>();\n\n /** Active step index (0-based, two-way binding) */\n readonly activeStep = model<number>(0);\n\n /** Whether completed steps can be clicked to navigate back */\n readonly linear = input<boolean>(true);\n\n /** Orientation */\n readonly orientation = input<'horizontal' | 'vertical'>('horizontal');\n\n /** Text shown under optional steps */\n readonly optionalLabel = input('Optional');\n\n /**\n * Accessible name template for each step button. Placeholders: `{n}`\n * (1-based step number), `{m}` (step count), `{label}`. Optional steps get\n * `optionalLabel` appended.\n */\n readonly stepAriaLabel = input('Step {n} of {m}: {label}');\n\n /** Emitted when active step changes */\n readonly stepChange = output<number>();\n\n /** Per-instance id prefix so description ids stay unique across steppers */\n private readonly instanceId = `lc-stepper-${++nextStepperId}`;\n\n /** Computed total step count */\n readonly totalSteps = computed(() => this.steps().length);\n\n /** Whether we're on the first step */\n readonly isFirstStep = computed(() => this.activeStep() === 0);\n\n /** Whether we're on the last step */\n readonly isLastStep = computed(() => this.activeStep() === this.totalSteps() - 1);\n\n /** Get the state of a step */\n getStepState(index: number): StepState {\n const current = this.activeStep();\n if (index < current) return 'completed';\n if (index === current) return 'active';\n return 'pending';\n }\n\n /**\n * Whether the step at `index` can be activated by the user. In linear mode\n * only completed steps (and the active one) are reachable.\n */\n isStepNavigable(index: number): boolean {\n return !this.linear() || index <= this.activeStep();\n }\n\n /** Accessible name for the step button at `index` */\n getStepAriaLabel(index: number): string {\n const step = this.steps()[index];\n if (!step) return '';\n const name = this.stepAriaLabel()\n .replace('{n}', String(index + 1))\n .replace('{m}', String(this.totalSteps()))\n .replace('{label}', step.label);\n return step.optional ? `${name}, ${this.optionalLabel()}` : name;\n }\n\n /** Id of the description element for `aria-describedby` (or null) */\n getDescriptionId(index: number): string | null {\n return this.steps()[index]?.description ? `${this.instanceId}-desc-${index}` : null;\n }\n\n /** Navigate to a specific step (only if navigable, see `isStepNavigable`) */\n goToStep(index: number): void {\n if (index === this.activeStep() || !this.isStepNavigable(index)) {\n return;\n }\n this.activeStep.set(index);\n this.stepChange.emit(index);\n }\n\n /** Go to next step */\n next(): void {\n const current = this.activeStep();\n if (current < this.totalSteps() - 1) {\n this.activeStep.set(current + 1);\n this.stepChange.emit(current + 1);\n }\n }\n\n /** Go to previous step */\n previous(): void {\n const current = this.activeStep();\n if (current > 0) {\n this.activeStep.set(current - 1);\n this.stepChange.emit(current - 1);\n }\n }\n\n /** Get CSS classes for step indicator */\n getStepClasses(index: number): string {\n const state = this.getStepState(index);\n const classes = ['lc-stepper__step'];\n classes.push(`lc-stepper__step--${state}`);\n if (!this.linear() || state === 'completed') {\n classes.push('lc-stepper__step--clickable');\n }\n return classes.join(' ');\n }\n\n /** Get CSS classes for connector line */\n getConnectorClasses(index: number): string {\n const classes = ['lc-stepper__connector'];\n if (index < this.activeStep()) {\n classes.push('lc-stepper__connector--completed');\n }\n return classes.join(' ');\n }\n}\n","<div\n class=\"lc-stepper\"\n [class.lc-stepper--horizontal]=\"orientation() === 'horizontal'\"\n [class.lc-stepper--vertical]=\"orientation() === 'vertical'\"\n>\n <!-- Step indicators: an ordered list of buttons (position is conveyed by the list) -->\n <ol class=\"lc-stepper__header\">\n @for (step of steps(); track $index; let i = $index; let last = $last) {\n <li class=\"lc-stepper__item\">\n <button\n type=\"button\"\n [class]=\"getStepClasses(i)\"\n [disabled]=\"!isStepNavigable(i)\"\n [attr.aria-current]=\"getStepState(i) === 'active' ? 'step' : null\"\n [attr.aria-label]=\"getStepAriaLabel(i)\"\n [attr.aria-describedby]=\"getDescriptionId(i)\"\n (click)=\"goToStep(i)\"\n >\n <span class=\"lc-stepper__indicator\" aria-hidden=\"true\">\n @if (getStepState(i) === 'completed') {\n <!-- Checkmark for completed -->\n <svg class=\"lc-stepper__check\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <path\n fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n } @else {\n <span class=\"lc-stepper__number\">{{ i + 1 }}</span>\n }\n </span>\n <span class=\"lc-stepper__label\">\n <span class=\"lc-stepper__title\">{{ step.label }}</span>\n @if (step.description) {\n <span class=\"lc-stepper__description\" [attr.id]=\"getDescriptionId(i)\">{{\n step.description\n }}</span>\n }\n @if (step.optional) {\n <span class=\"lc-stepper__optional\">{{ optionalLabel() }}</span>\n }\n </span>\n </button>\n\n @if (!last) {\n <span [class]=\"getConnectorClasses(i)\" aria-hidden=\"true\"></span>\n }\n </li>\n }\n </ol>\n\n <!-- Step content area (projected) -->\n <div class=\"lc-stepper__content\">\n <ng-content></ng-content>\n </div>\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-stepper.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/stepper/stepper.component.ts","../../../../libs/angular-ui-kit/src/lib/stepper/stepper.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n model,\n computed,\n} from '@angular/core';\n\nexport interface StepperStep {\n readonly label: string;\n readonly description?: string;\n readonly optional?: boolean;\n}\n\nexport type StepState = 'pending' | 'active' | 'completed';\n\nlet nextStepperId = 0;\n\n/**\n * Stepper component for multi-step wizard workflows.\n *\n * Features:\n * - Horizontal and vertical orientations\n * - Active, completed, and pending step states\n * - Numbered step indicators with checkmark on completion\n * - Optional step descriptions and \"optional\" labels\n * - Linear mode restricting navigation to sequential steps\n * - Two-way active step binding\n * - Content projection for step body\n * - Accessible: steps are real buttons in an ordered list, the active step\n * carries `aria-current=\"step\"`, non-navigable steps are disabled\n *\n * @example\n * ```html\n * <lc-stepper [steps]=\"steps\" [(activeStep)]=\"currentStep\">\n * <div>Step content here</div>\n * </lc-stepper>\n * ```\n */\n@Component({\n selector: 'lc-stepper',\n standalone: true,\n imports: [],\n templateUrl: './stepper.component.html',\n styleUrl: './stepper.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class StepperComponent {\n /** Step definitions */\n readonly steps = input.required<readonly StepperStep[]>();\n\n /** Active step index (0-based, two-way binding) */\n readonly activeStep = model<number>(0);\n\n /** Whether completed steps can be clicked to navigate back */\n readonly linear = input<boolean>(true);\n\n /** Orientation */\n readonly orientation = input<'horizontal' | 'vertical'>('horizontal');\n\n /**\n * Text shown under optional steps; it also ends the step's accessible name.\n * @default 'Optional'\n */\n readonly optionalLabel = input('Optional');\n\n /**\n * Accessible name template for each step button. Placeholders: `{n}`\n * (1-based step number), `{m}` (step count), `{label}`. The step's visible\n * description and `optionalLabel` follow the filled template,\n * comma-separated (`Step 2 of 3: Profile, Set up your profile`). Keep\n * `{label}` last, so that the name contains the visible text (WCAG 2.5.3).\n * @default 'Step {n} of {m}: {label}'\n */\n readonly stepAriaLabel = input('Step {n} of {m}: {label}');\n\n /** Emitted when active step changes */\n readonly stepChange = output<number>();\n\n /** Per-instance id prefix so description ids stay unique across steppers */\n private readonly instanceId = `lc-stepper-${++nextStepperId}`;\n\n /** Computed total step count */\n readonly totalSteps = computed(() => this.steps().length);\n\n /** Whether we're on the first step */\n readonly isFirstStep = computed(() => this.activeStep() === 0);\n\n /** Whether we're on the last step */\n readonly isLastStep = computed(() => this.activeStep() === this.totalSteps() - 1);\n\n /** Get the state of a step */\n getStepState(index: number): StepState {\n const current = this.activeStep();\n if (index < current) return 'completed';\n if (index === current) return 'active';\n return 'pending';\n }\n\n /**\n * Whether the step at `index` can be activated by the user. In linear mode\n * only completed steps (and the active one) are reachable.\n */\n isStepNavigable(index: number): boolean {\n return !this.linear() || index <= this.activeStep();\n }\n\n /**\n * Accessible name for the step button at `index` (AS-33, WCAG 2.5.3 Label\n * in Name): the filled `stepAriaLabel`, then the visible description and\n * `optionalLabel`, comma-separated, so the name contains the visible text.\n * A template that doesn't end with `{label}` gets the label after it.\n */\n getStepAriaLabel(index: number): string {\n const step = this.steps()[index];\n if (!step) return '';\n const template = this.stepAriaLabel();\n const name = template\n .replace('{n}', String(index + 1))\n .replace('{m}', String(this.totalSteps()))\n .replace('{label}', step.label);\n const label = template.trimEnd().endsWith('{label}') ? '' : step.label;\n return [name, label, step.description ?? '', step.optional ? this.optionalLabel() : '']\n .filter(Boolean)\n .join(', ');\n }\n\n /** Id of the description element (or null); the description is part of the step's name. */\n getDescriptionId(index: number): string | null {\n return this.steps()[index]?.description ? `${this.instanceId}-desc-${index}` : null;\n }\n\n /** Navigate to a specific step (only if navigable, see `isStepNavigable`) */\n goToStep(index: number): void {\n if (index === this.activeStep() || !this.isStepNavigable(index)) {\n return;\n }\n this.activeStep.set(index);\n this.stepChange.emit(index);\n }\n\n /** Go to next step */\n next(): void {\n const current = this.activeStep();\n if (current < this.totalSteps() - 1) {\n this.activeStep.set(current + 1);\n this.stepChange.emit(current + 1);\n }\n }\n\n /** Go to previous step */\n previous(): void {\n const current = this.activeStep();\n if (current > 0) {\n this.activeStep.set(current - 1);\n this.stepChange.emit(current - 1);\n }\n }\n\n /** Get CSS classes for step indicator */\n getStepClasses(index: number): string {\n const state = this.getStepState(index);\n const classes = ['lc-stepper__step'];\n classes.push(`lc-stepper__step--${state}`);\n if (!this.linear() || state === 'completed') {\n classes.push('lc-stepper__step--clickable');\n }\n return classes.join(' ');\n }\n\n /** Get CSS classes for connector line */\n getConnectorClasses(index: number): string {\n const classes = ['lc-stepper__connector'];\n if (index < this.activeStep()) {\n classes.push('lc-stepper__connector--completed');\n }\n return classes.join(' ');\n }\n}\n","<div\n class=\"lc-stepper\"\n [class.lc-stepper--horizontal]=\"orientation() === 'horizontal'\"\n [class.lc-stepper--vertical]=\"orientation() === 'vertical'\"\n>\n <!-- Step indicators: an ordered list of buttons (position is conveyed by the list) -->\n <ol class=\"lc-stepper__header\">\n @for (step of steps(); track $index; let i = $index; let last = $last) {\n <li class=\"lc-stepper__item\">\n <button\n type=\"button\"\n [class]=\"getStepClasses(i)\"\n [disabled]=\"!isStepNavigable(i)\"\n [attr.aria-current]=\"getStepState(i) === 'active' ? 'step' : null\"\n [attr.aria-label]=\"getStepAriaLabel(i)\"\n (click)=\"goToStep(i)\"\n >\n <!-- The number is generated content (data-step), not text: as text,\n the visible label would read \"2Profile\" (WCAG 2.5.3). -->\n <span class=\"lc-stepper__indicator\" aria-hidden=\"true\">\n @if (getStepState(i) === 'completed') {\n <!-- Checkmark for completed -->\n <svg class=\"lc-stepper__check\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <path\n fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n } @else {\n <span class=\"lc-stepper__number\" [attr.data-step]=\"i + 1\"></span>\n }\n </span>\n <!-- Real spaces between the parts: the visible text reads\n \"Profile Set up your profile\", which the name contains. -->\n <span class=\"lc-stepper__label\">\n <span class=\"lc-stepper__title\">{{ step.label }}</span>\n @if (step.description) {\n &ngsp;<span class=\"lc-stepper__description\" [attr.id]=\"getDescriptionId(i)\">{{\n step.description\n }}</span>\n }\n @if (step.optional) {\n &ngsp;<span class=\"lc-stepper__optional\">{{ optionalLabel() }}</span>\n }\n </span>\n </button>\n\n @if (!last) {\n <span [class]=\"getConnectorClasses(i)\" aria-hidden=\"true\"></span>\n }\n </li>\n }\n </ol>\n\n <!-- Step content area (projected) -->\n <div class=\"lc-stepper__content\">\n <ng-content></ng-content>\n </div>\n</div>\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifecorner/angular-ui-kit",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.5",
|
|
4
4
|
"description": "LifeCorner Design System – Angular UI component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -266,6 +266,10 @@
|
|
|
266
266
|
"types": "./types/lifecorner-angular-ui-kit-input.d.ts",
|
|
267
267
|
"default": "./fesm2022/lifecorner-angular-ui-kit-input.mjs"
|
|
268
268
|
},
|
|
269
|
+
"./internal/a11y": {
|
|
270
|
+
"types": "./types/lifecorner-angular-ui-kit-internal-a11y.d.ts",
|
|
271
|
+
"default": "./fesm2022/lifecorner-angular-ui-kit-internal-a11y.mjs"
|
|
272
|
+
},
|
|
269
273
|
"./internal/chart": {
|
|
270
274
|
"types": "./types/lifecorner-angular-ui-kit-internal-chart.d.ts",
|
|
271
275
|
"default": "./fesm2022/lifecorner-angular-ui-kit-internal-chart.mjs"
|
|
@@ -12,12 +12,18 @@ import { TemplateRef } from "@angular/core";
|
|
|
12
12
|
* contain nested interactive elements (no button/link/input). Text, badges and
|
|
13
13
|
* other non-interactive nodes only.
|
|
14
14
|
*
|
|
15
|
+
* Name (WCAG 2.5.3 Label in Name): the header is named by the template's
|
|
16
|
+
* text; the accordion's `ariaLabel` (or `title`), if set, comes first through
|
|
17
|
+
* `aria-labelledby`. Separate the template's parts with whitespace
|
|
18
|
+
* (`&ngsp;`), so they read as words ("Item label Done 12:04"), and set a
|
|
19
|
+
* label only for context that the template doesn't show.
|
|
20
|
+
*
|
|
15
21
|
* @example
|
|
16
22
|
* ```html
|
|
17
23
|
* <lc-accordion variant="flat" chevronPosition="leading">
|
|
18
24
|
* <ng-template lcAccordionHeader>
|
|
19
|
-
* <span class="title">Item label</span
|
|
20
|
-
* <lc-badge variant="success" size="sm">Done</lc-badge
|
|
25
|
+
* <span class="title">Item label</span>&ngsp;
|
|
26
|
+
* <lc-badge variant="success" size="sm">Done</lc-badge>&ngsp;
|
|
21
27
|
* <span style="margin-left: auto;">12:04</span>
|
|
22
28
|
* </ng-template>
|
|
23
29
|
* <div>Body…</div>
|
|
@@ -81,8 +87,8 @@ export type AccordionChevronPosition = 'leading' | 'trailing';
|
|
|
81
87
|
* ```html
|
|
82
88
|
* <lc-accordion variant="flat" chevronPosition="leading" [lazy]="true">
|
|
83
89
|
* <ng-template lcAccordionHeader>
|
|
84
|
-
* <span class="title">Item label</span
|
|
85
|
-
* <lc-badge variant="success" size="sm">Done</lc-badge
|
|
90
|
+
* <span class="title">Item label</span>&ngsp;
|
|
91
|
+
* <lc-badge variant="success" size="sm">Done</lc-badge>&ngsp;
|
|
86
92
|
* <span style="margin-left: auto;">12:04</span>
|
|
87
93
|
* </ng-template>
|
|
88
94
|
* <ng-template lcAccordionContent>
|
|
@@ -95,13 +101,18 @@ export declare class AccordionComponent {
|
|
|
95
101
|
/**
|
|
96
102
|
* Title displayed in the accordion header. Optional: when a
|
|
97
103
|
* `lcAccordionHeader` template is projected it takes precedence, and `title`
|
|
98
|
-
* (if set)
|
|
104
|
+
* (if set) starts the header's accessible name when `ariaLabel` is empty.
|
|
105
|
+
* @default ''
|
|
99
106
|
*/
|
|
100
107
|
readonly title: import("@angular/core").InputSignal<string>;
|
|
101
108
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* `
|
|
109
|
+
* Start of the header button's accessible name; falls back to `title`. A
|
|
110
|
+
* plain `title` is appended unless contained (WCAG 2.5.3 Label in Name).
|
|
111
|
+
* With an `lcAccordionHeader` template, the name is this label (or `title`)
|
|
112
|
+
* followed by the template's text, through `aria-labelledby`. Separate the
|
|
113
|
+
* template's parts with whitespace (`&ngsp;`), and set a label only for
|
|
114
|
+
* context that the template doesn't show.
|
|
115
|
+
* @default ''
|
|
105
116
|
*/
|
|
106
117
|
readonly ariaLabel: import("@angular/core").InputSignal<string>;
|
|
107
118
|
/** Whether the accordion is expanded (two-way binding) */
|
|
@@ -147,8 +158,19 @@ export declare class AccordionComponent {
|
|
|
147
158
|
/** Latches to `true` the first time the panel is opened (for lazy bodies). */
|
|
148
159
|
private readonly hasBeenOpened;
|
|
149
160
|
constructor();
|
|
150
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Label that starts a rich header's name (`ariaLabel`, else `title`); empty
|
|
163
|
+
* without a header template.
|
|
164
|
+
*/
|
|
165
|
+
protected readonly richHeaderLabel: import("@angular/core").Signal<string>;
|
|
166
|
+
/**
|
|
167
|
+
* Accessible label of a plain header button (AS-33, WCAG 2.5.3 Label in
|
|
168
|
+
* Name): the explicit override, else the title, then the visible title
|
|
169
|
+
* unless contained. A rich header has none: it is named by its content.
|
|
170
|
+
*/
|
|
151
171
|
protected readonly headerAriaLabel: import("@angular/core").Signal<string | null>;
|
|
172
|
+
/** A labelled rich header: its hidden label, then the template's text. */
|
|
173
|
+
protected readonly headerLabelledBy: import("@angular/core").Signal<string | null>;
|
|
152
174
|
/**
|
|
153
175
|
* Whether the `lcAccordionContent` body should currently be in the DOM.
|
|
154
176
|
* - `destroyOnClose`: only while expanded.
|
|
@@ -40,13 +40,23 @@ export declare class FileUploadComponent {
|
|
|
40
40
|
*/
|
|
41
41
|
disabled: import("@angular/core").InputSignal<boolean>;
|
|
42
42
|
/**
|
|
43
|
-
* Hint text shown in the dropzone.
|
|
43
|
+
* Hint text shown in the dropzone. The drop zone's accessible name is
|
|
44
|
+
* `hint`, then the shown accepted and max lines, comma-separated (WCAG 2.5.3
|
|
45
|
+
* Label in Name).
|
|
44
46
|
* @default 'Drag & drop files here, or click to browse'
|
|
45
47
|
*/
|
|
46
48
|
hint: import("@angular/core").InputSignal<string>;
|
|
47
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Prefix of the accepted file types line (`accept`). The shown line is part
|
|
51
|
+
* of the drop zone's accessible name, after `hint`.
|
|
52
|
+
* @default 'Accepted:'
|
|
53
|
+
*/
|
|
48
54
|
acceptedLabel: import("@angular/core").InputSignal<string>;
|
|
49
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Prefix of the maximum file size line (`maxSizeBytes`). The shown line is
|
|
57
|
+
* part of the drop zone's accessible name, after `hint`.
|
|
58
|
+
* @default 'Max:'
|
|
59
|
+
*/
|
|
50
60
|
maxSizeLabel: import("@angular/core").InputSignal<string>;
|
|
51
61
|
/** Accessible name of a selected file's remove button. */
|
|
52
62
|
removeFileAriaLabel: import("@angular/core").InputSignal<string>;
|
|
@@ -65,6 +75,11 @@ export declare class FileUploadComponent {
|
|
|
65
75
|
protected selectedFiles: import("@angular/core").WritableSignal<FileUploadFile[]>;
|
|
66
76
|
protected readonly fileInputRef: import("@angular/core").Signal<ElementRef<HTMLInputElement> | undefined>;
|
|
67
77
|
protected dropzoneClasses: import("@angular/core").Signal<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Name of the drop zone (AS-33, WCAG 2.5.3 Label in Name): `hint`, then the
|
|
80
|
+
* accepted and max lines it shows, comma-separated.
|
|
81
|
+
*/
|
|
82
|
+
protected readonly dropzoneAriaLabel: import("@angular/core").Signal<string>;
|
|
68
83
|
protected onDragOver(event: DragEvent): void;
|
|
69
84
|
protected onDragLeave(event: DragEvent): void;
|
|
70
85
|
protected onDrop(event: DragEvent): void;
|
|
@@ -48,7 +48,12 @@ export declare class GalleryComponent implements OnDestroy {
|
|
|
48
48
|
readonly size: import("@angular/core").InputSignal<GallerySize>;
|
|
49
49
|
/** Custom column count (overrides size preset) */
|
|
50
50
|
readonly columns: import("@angular/core").InputSignal<number | null>;
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Whether to show captions below thumbnails. A shown caption is part of the
|
|
53
|
+
* item's accessible name: `alt` (or `imageFallbackLabel` and its number),
|
|
54
|
+
* then the caption (WCAG 2.5.3 Label in Name).
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
52
57
|
readonly showCaptions: import("@angular/core").InputSignal<boolean>;
|
|
53
58
|
/** Whether clicking a thumbnail opens the lightbox */
|
|
54
59
|
readonly enableLightbox: import("@angular/core").InputSignal<boolean>;
|
|
@@ -74,7 +79,11 @@ export declare class GalleryComponent implements OnDestroy {
|
|
|
74
79
|
readonly previousAriaLabel: import("@angular/core").InputSignal<string>;
|
|
75
80
|
/** Accessible name of the next-image button. */
|
|
76
81
|
readonly nextAriaLabel: import("@angular/core").InputSignal<string>;
|
|
77
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Accessible name of a thumbnail without `alt`, followed by its number. A
|
|
84
|
+
* shown caption follows (`Bild 3, Sonnenuntergang am See`).
|
|
85
|
+
* @default 'Image'
|
|
86
|
+
*/
|
|
78
87
|
readonly imageFallbackLabel: import("@angular/core").InputSignal<string>;
|
|
79
88
|
/** Emitted when lightbox opens with item index */
|
|
80
89
|
readonly lightboxOpen: import("@angular/core").OutputEmitterRef<number>;
|
|
@@ -134,6 +143,12 @@ export declare class GalleryComponent implements OnDestroy {
|
|
|
134
143
|
protected onImageLoad(index: number): void;
|
|
135
144
|
protected isLoaded(index: number): boolean;
|
|
136
145
|
protected getThumbnail(item: GalleryItem): string;
|
|
146
|
+
/**
|
|
147
|
+
* Name of a thumbnail (AS-33, WCAG 2.5.3 Label in Name): `alt` (or
|
|
148
|
+
* `imageFallbackLabel` and its number), then the shown caption unless one
|
|
149
|
+
* contains the other.
|
|
150
|
+
*/
|
|
151
|
+
protected itemAriaLabel(item: GalleryItem, index: number): string;
|
|
137
152
|
protected trackByIndex(index: number): number;
|
|
138
153
|
/**
|
|
139
154
|
* Keyboard handling while the lightbox is open. Bound on the lightbox itself
|
|
@@ -231,7 +231,12 @@ export declare class HeaderComponent {
|
|
|
231
231
|
readonly themeToDarkAriaLabel: import("@angular/core").InputSignal<string>;
|
|
232
232
|
/** Label of the theme button while the dark theme is active. */
|
|
233
233
|
readonly themeToLightAriaLabel: import("@angular/core").InputSignal<string>;
|
|
234
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Start of the context button's accessible name. The visible `contextLabel`
|
|
236
|
+
* and `contextName` follow it (WCAG 2.5.3 Label in Name), unless the label
|
|
237
|
+
* already contains them: `Context info, Tenant Acme Corp`.
|
|
238
|
+
* @default 'Context info'
|
|
239
|
+
*/
|
|
235
240
|
readonly contextAriaLabel: import("@angular/core").InputSignal<string>;
|
|
236
241
|
/** Label of the Profile entry in the account menu. */
|
|
237
242
|
readonly profileLabel: import("@angular/core").InputSignal<string>;
|
|
@@ -262,6 +267,12 @@ export declare class HeaderComponent {
|
|
|
262
267
|
* as axe-core 4.14 does); an empty label becomes the initials alone.
|
|
263
268
|
*/
|
|
264
269
|
protected readonly avatarTriggerAriaLabel: import("@angular/core").Signal<string>;
|
|
270
|
+
/**
|
|
271
|
+
* Name of the context button (AS-33, WCAG 2.5.3 Label in Name):
|
|
272
|
+
* `contextAriaLabel`, a comma and the visible `contextLabel` and
|
|
273
|
+
* `contextName`, so a voice-control user can say what they see.
|
|
274
|
+
*/
|
|
275
|
+
protected readonly contextButtonAriaLabel: import("@angular/core").Signal<string>;
|
|
265
276
|
/**
|
|
266
277
|
* Get menu items for profile dropdown
|
|
267
278
|
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessible names that contain the visible text (WCAG 2.5.3 Label in Name,
|
|
3
|
+
* AS-33). The containment test follows axe-core 4.14's
|
|
4
|
+
* `label-content-name-mismatch`: both strings are compatibility-decomposed,
|
|
5
|
+
* parentheticals are dropped, the rest is lower-cased and split into words of
|
|
6
|
+
* letters and digits, and the visible text's words must occur as one
|
|
7
|
+
* contiguous run in the name's words.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The words of `s` as axe-core 4.14 compares them for Label in Name.
|
|
11
|
+
* `toLowerCase`, not `toLocaleLowerCase`: server and browser must agree.
|
|
12
|
+
*/
|
|
13
|
+
export declare function labelWords(s: string): string[];
|
|
14
|
+
/** Whether `needle` occurs in `haystack` as one contiguous run of words. */
|
|
15
|
+
export declare function containsWords(haystack: readonly string[], needle: readonly string[]): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Accessible name that contains the visible `text` (WCAG 2.5.3): `label`,
|
|
18
|
+
* then `, text`, unless one already contains the other.
|
|
19
|
+
*
|
|
20
|
+
* - Text without letters or digits, or one non-digit character (a symbol or
|
|
21
|
+
* an icon glyph, which axe reports as "incomplete"): `label`.
|
|
22
|
+
* - The label already contains the text: `label`, unchanged.
|
|
23
|
+
* - An empty label: `text`.
|
|
24
|
+
* - The text contains the label: `text`, so nothing is read twice.
|
|
25
|
+
* - Otherwise `label, text`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function nameWithText(label: string, text: string): string;
|