@lifecorner/angular-ui-kit 3.11.3 → 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.
Files changed (33) hide show
  1. package/fesm2022/lifecorner-angular-ui-kit-accordion.mjs +6 -3
  2. package/fesm2022/lifecorner-angular-ui-kit-accordion.mjs.map +1 -1
  3. package/fesm2022/lifecorner-angular-ui-kit-avatar.mjs +12 -12
  4. package/fesm2022/lifecorner-angular-ui-kit-avatar.mjs.map +1 -1
  5. package/fesm2022/lifecorner-angular-ui-kit-calendar.mjs +11 -6
  6. package/fesm2022/lifecorner-angular-ui-kit-calendar.mjs.map +1 -1
  7. package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs +7 -2
  8. package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs.map +1 -1
  9. package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs +7 -2
  10. package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs.map +1 -1
  11. package/fesm2022/lifecorner-angular-ui-kit-header.mjs +13 -3
  12. package/fesm2022/lifecorner-angular-ui-kit-header.mjs.map +1 -1
  13. package/fesm2022/lifecorner-angular-ui-kit-internal-a11y.mjs +20 -0
  14. package/fesm2022/lifecorner-angular-ui-kit-internal-a11y.mjs.map +1 -0
  15. package/fesm2022/lifecorner-angular-ui-kit-menu.mjs +2 -2
  16. package/fesm2022/lifecorner-angular-ui-kit-menu.mjs.map +1 -1
  17. package/fesm2022/lifecorner-angular-ui-kit-pagination.mjs +5 -2
  18. package/fesm2022/lifecorner-angular-ui-kit-pagination.mjs.map +1 -1
  19. package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs +6 -2
  20. package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs.map +1 -1
  21. package/fesm2022/lifecorner-angular-ui-kit-stepper.mjs +11 -6
  22. package/fesm2022/lifecorner-angular-ui-kit-stepper.mjs.map +1 -1
  23. package/package.json +5 -1
  24. package/types/lifecorner-angular-ui-kit-accordion.d.ts +31 -9
  25. package/types/lifecorner-angular-ui-kit-avatar.d.ts +7 -0
  26. package/types/lifecorner-angular-ui-kit-calendar.d.ts +1 -0
  27. package/types/lifecorner-angular-ui-kit-file-upload.d.ts +18 -3
  28. package/types/lifecorner-angular-ui-kit-gallery.d.ts +17 -2
  29. package/types/lifecorner-angular-ui-kit-header.d.ts +34 -3
  30. package/types/lifecorner-angular-ui-kit-internal-a11y.d.ts +27 -0
  31. package/types/lifecorner-angular-ui-kit-pagination.d.ts +15 -2
  32. package/types/lifecorner-angular-ui-kit-rich-text-editor.d.ts +13 -1
  33. package/types/lifecorner-angular-ui-kit-stepper.d.ts +16 -5
@@ -4,6 +4,7 @@ import { CommonModule } from "@angular/common";
4
4
  import { NG_VALUE_ACCESSOR } from "@angular/forms";
5
5
  import { DomSanitizer } from "@angular/platform-browser";
6
6
  import { isSafeImageUrl, isSafeLinkUrl } from "@lifecorner/angular-ui-kit/internal/url";
7
+ import { nameWithText } from "@lifecorner/angular-ui-kit/internal/a11y";
7
8
  const HTML_ESCAPES = {
8
9
  "&": "&",
9
10
  "<": "&lt;",
@@ -298,6 +299,9 @@ var RichTextEditorComponent = class RichTextEditorComponent {
298
299
  hr: "Horizontal Rule"
299
300
  }[action] || action;
300
301
  }
302
+ getActionName(action) {
303
+ return nameWithText(this.getActionTitle(action), this.getActionLabel(action));
304
+ }
301
305
  markdownToHtml(md) {
302
306
  if (!md) return "";
303
307
  let html = escapeHtml(md);
@@ -523,7 +527,7 @@ var RichTextEditorComponent = class RichTextEditorComponent {
523
527
  isSignal: true
524
528
  }],
525
529
  ngImport: i0,
526
- template: "<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",
530
+ template: "<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",
527
531
  styles: [".lc-rich-text-editor{border:1px solid var(--color-border);border-radius:var(--border-radius-md);background-color:var(--color-surface);overflow:hidden;font-family:var(--typography-font-family)}.lc-rich-text-editor--disabled{opacity:.6;pointer-events:none}.lc-rich-text-editor__toolbar{display:flex;align-items:center;justify-content:space-between;padding:var(--lc-density-padding-xs, var(--spacing-2)) var(--lc-density-padding-sm, var(--spacing-3));background-color:var(--color-surface);border-bottom:1px solid var(--color-divider);flex-wrap:wrap;gap:var(--lc-density-gap-xs, var(--spacing-1))}.lc-rich-text-editor__toolbar-actions{display:flex;align-items:center;gap:2px;flex-wrap:wrap}.lc-rich-text-editor__toolbar-btn{appearance:none;border:none;background:transparent;color:var(--color-text-secondary);width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:var(--border-radius-sm);font-size:var(--typography-font-size-sm);font-weight:var(--typography-font-weight-semibold);cursor:pointer;transition:background-color .15s ease,color .15s ease}.lc-rich-text-editor__toolbar-btn:hover:not(:disabled){background-color:var(--color-surface-hover);color:var(--color-text-primary)}.lc-rich-text-editor__toolbar-btn:disabled{cursor:not-allowed;opacity:.4}.lc-rich-text-editor__toolbar-modes{display:flex;align-items:center;gap:2px;border-left:1px solid var(--color-divider);padding-left:var(--spacing-2);margin-left:var(--spacing-2)}.lc-rich-text-editor__mode-btn{appearance:none;border:none;background:transparent;color:var(--color-text-secondary);padding:var(--spacing-1) var(--spacing-2);border-radius:var(--border-radius-sm);font-size:var(--typography-font-size-xs);font-weight:var(--typography-font-weight-medium);cursor:pointer;transition:background-color .15s ease,color .15s ease}.lc-rich-text-editor__mode-btn:hover{background-color:var(--color-surface-hover);color:var(--color-text-primary)}.lc-rich-text-editor__mode-btn.active{background-color:var(--color-primary);color:#fff}.lc-rich-text-editor__content{overflow-y:auto}.lc-rich-text-editor__rich{padding:var(--lc-density-padding-md, var(--spacing-4));min-height:100%;outline:none;font-size:var(--typography-font-size-base);line-height:var(--typography-line-height-relaxed, 1.7);color:var(--color-text-primary)}.lc-rich-text-editor__rich--empty:before{content:attr(data-placeholder);color:var(--color-text-secondary);opacity:.6;pointer-events:none}.lc-rich-text-editor__rich h1,.lc-rich-text-editor__rich h2,.lc-rich-text-editor__rich h3{margin-top:var(--spacing-4);margin-bottom:var(--spacing-2);font-weight:var(--typography-font-weight-bold);color:var(--color-text-primary)}.lc-rich-text-editor__rich h1{font-size:var(--typography-font-size-2xl)}.lc-rich-text-editor__rich h2{font-size:var(--typography-font-size-xl)}.lc-rich-text-editor__rich h3{font-size:var(--typography-font-size-lg)}.lc-rich-text-editor__rich p{margin-bottom:var(--spacing-3)}.lc-rich-text-editor__rich ul,.lc-rich-text-editor__rich ol{padding-left:var(--spacing-6);margin-bottom:var(--spacing-3)}.lc-rich-text-editor__rich blockquote{border-left:4px solid var(--color-primary);padding-left:var(--spacing-4);margin:var(--spacing-3) 0;color:var(--color-text-secondary);font-style:italic}.lc-rich-text-editor__rich code{background-color:var(--color-surface-hover);padding:2px var(--spacing-1);border-radius:var(--border-radius-sm);font-family:var(--typography-font-family-mono, monospace);font-size:.9em}.lc-rich-text-editor__rich pre{background-color:var(--color-surface-hover);padding:var(--spacing-3);border-radius:var(--border-radius-md);overflow-x:auto;margin:var(--spacing-3) 0}.lc-rich-text-editor__rich pre code{background:none;padding:0}.lc-rich-text-editor__rich a{color:var(--color-primary);text-decoration:underline}.lc-rich-text-editor__rich hr{border:none;border-top:1px solid var(--color-divider);margin:var(--spacing-4) 0}.lc-rich-text-editor__rich img{max-width:100%;border-radius:var(--border-radius-md)}.lc-rich-text-editor__textarea{width:100%;height:100%;min-height:inherit;border:none;outline:none;resize:vertical;padding:var(--lc-density-padding-md, var(--spacing-4));font-family:var(--typography-font-family-mono, monospace);font-size:var(--typography-font-size-sm);line-height:var(--typography-line-height-relaxed, 1.7);color:var(--color-text-primary);background-color:var(--color-surface);tab-size:2}.lc-rich-text-editor__split{display:grid;grid-template-columns:1fr 1fr;height:100%;min-height:inherit}.lc-rich-text-editor__split-editor{border-right:1px solid var(--color-divider);min-height:inherit;resize:none}.lc-rich-text-editor__split-preview{padding:var(--lc-density-padding-md, var(--spacing-4));overflow-y:auto;font-size:var(--typography-font-size-base);line-height:var(--typography-line-height-relaxed, 1.7);color:var(--color-text-primary)}.lc-rich-text-editor__split-preview h1,.lc-rich-text-editor__split-preview h2,.lc-rich-text-editor__split-preview h3{margin-top:var(--spacing-4);margin-bottom:var(--spacing-2);font-weight:var(--typography-font-weight-bold)}.lc-rich-text-editor__split-preview h1{font-size:var(--typography-font-size-2xl)}.lc-rich-text-editor__split-preview h2{font-size:var(--typography-font-size-xl)}.lc-rich-text-editor__split-preview h3{font-size:var(--typography-font-size-lg)}.lc-rich-text-editor__split-preview p{margin-bottom:var(--spacing-3)}.lc-rich-text-editor__split-preview ul,.lc-rich-text-editor__split-preview ol{padding-left:var(--spacing-6);margin-bottom:var(--spacing-3)}.lc-rich-text-editor__split-preview blockquote{border-left:4px solid var(--color-primary);padding-left:var(--spacing-4);margin:var(--spacing-3) 0;color:var(--color-text-secondary);font-style:italic}.lc-rich-text-editor__split-preview code{background-color:var(--color-surface-hover);padding:2px var(--spacing-1);border-radius:var(--border-radius-sm);font-family:var(--typography-font-family-mono, monospace);font-size:.9em}.lc-rich-text-editor__split-preview pre{background-color:var(--color-surface-hover);padding:var(--spacing-3);border-radius:var(--border-radius-md);overflow-x:auto}.lc-rich-text-editor__split-preview pre code{background:none;padding:0}.lc-rich-text-editor__split-preview a{color:var(--color-primary);text-decoration:underline}.lc-rich-text-editor__split-preview hr{border:none;border-top:1px solid var(--color-divider);margin:var(--spacing-4) 0}.lc-rich-text-editor__footer{display:flex;justify-content:flex-end;gap:var(--lc-density-gap-md, var(--spacing-4));padding:var(--lc-density-padding-xs, var(--spacing-2)) var(--lc-density-padding-sm, var(--spacing-3));border-top:1px solid var(--color-divider);font-size:var(--typography-font-size-xs);color:var(--color-text-secondary)}\n"],
528
532
  dependencies: [{
529
533
  kind: "ngmodule",
@@ -549,7 +553,7 @@ i0.ɵɵngDeclareClassMetadata({
549
553
  useExisting: forwardRef(() => RichTextEditorComponent),
550
554
  multi: true
551
555
  }],
552
- template: "<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",
556
+ template: "<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",
553
557
  styles: [".lc-rich-text-editor{border:1px solid var(--color-border);border-radius:var(--border-radius-md);background-color:var(--color-surface);overflow:hidden;font-family:var(--typography-font-family)}.lc-rich-text-editor--disabled{opacity:.6;pointer-events:none}.lc-rich-text-editor__toolbar{display:flex;align-items:center;justify-content:space-between;padding:var(--lc-density-padding-xs, var(--spacing-2)) var(--lc-density-padding-sm, var(--spacing-3));background-color:var(--color-surface);border-bottom:1px solid var(--color-divider);flex-wrap:wrap;gap:var(--lc-density-gap-xs, var(--spacing-1))}.lc-rich-text-editor__toolbar-actions{display:flex;align-items:center;gap:2px;flex-wrap:wrap}.lc-rich-text-editor__toolbar-btn{appearance:none;border:none;background:transparent;color:var(--color-text-secondary);width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:var(--border-radius-sm);font-size:var(--typography-font-size-sm);font-weight:var(--typography-font-weight-semibold);cursor:pointer;transition:background-color .15s ease,color .15s ease}.lc-rich-text-editor__toolbar-btn:hover:not(:disabled){background-color:var(--color-surface-hover);color:var(--color-text-primary)}.lc-rich-text-editor__toolbar-btn:disabled{cursor:not-allowed;opacity:.4}.lc-rich-text-editor__toolbar-modes{display:flex;align-items:center;gap:2px;border-left:1px solid var(--color-divider);padding-left:var(--spacing-2);margin-left:var(--spacing-2)}.lc-rich-text-editor__mode-btn{appearance:none;border:none;background:transparent;color:var(--color-text-secondary);padding:var(--spacing-1) var(--spacing-2);border-radius:var(--border-radius-sm);font-size:var(--typography-font-size-xs);font-weight:var(--typography-font-weight-medium);cursor:pointer;transition:background-color .15s ease,color .15s ease}.lc-rich-text-editor__mode-btn:hover{background-color:var(--color-surface-hover);color:var(--color-text-primary)}.lc-rich-text-editor__mode-btn.active{background-color:var(--color-primary);color:#fff}.lc-rich-text-editor__content{overflow-y:auto}.lc-rich-text-editor__rich{padding:var(--lc-density-padding-md, var(--spacing-4));min-height:100%;outline:none;font-size:var(--typography-font-size-base);line-height:var(--typography-line-height-relaxed, 1.7);color:var(--color-text-primary)}.lc-rich-text-editor__rich--empty:before{content:attr(data-placeholder);color:var(--color-text-secondary);opacity:.6;pointer-events:none}.lc-rich-text-editor__rich h1,.lc-rich-text-editor__rich h2,.lc-rich-text-editor__rich h3{margin-top:var(--spacing-4);margin-bottom:var(--spacing-2);font-weight:var(--typography-font-weight-bold);color:var(--color-text-primary)}.lc-rich-text-editor__rich h1{font-size:var(--typography-font-size-2xl)}.lc-rich-text-editor__rich h2{font-size:var(--typography-font-size-xl)}.lc-rich-text-editor__rich h3{font-size:var(--typography-font-size-lg)}.lc-rich-text-editor__rich p{margin-bottom:var(--spacing-3)}.lc-rich-text-editor__rich ul,.lc-rich-text-editor__rich ol{padding-left:var(--spacing-6);margin-bottom:var(--spacing-3)}.lc-rich-text-editor__rich blockquote{border-left:4px solid var(--color-primary);padding-left:var(--spacing-4);margin:var(--spacing-3) 0;color:var(--color-text-secondary);font-style:italic}.lc-rich-text-editor__rich code{background-color:var(--color-surface-hover);padding:2px var(--spacing-1);border-radius:var(--border-radius-sm);font-family:var(--typography-font-family-mono, monospace);font-size:.9em}.lc-rich-text-editor__rich pre{background-color:var(--color-surface-hover);padding:var(--spacing-3);border-radius:var(--border-radius-md);overflow-x:auto;margin:var(--spacing-3) 0}.lc-rich-text-editor__rich pre code{background:none;padding:0}.lc-rich-text-editor__rich a{color:var(--color-primary);text-decoration:underline}.lc-rich-text-editor__rich hr{border:none;border-top:1px solid var(--color-divider);margin:var(--spacing-4) 0}.lc-rich-text-editor__rich img{max-width:100%;border-radius:var(--border-radius-md)}.lc-rich-text-editor__textarea{width:100%;height:100%;min-height:inherit;border:none;outline:none;resize:vertical;padding:var(--lc-density-padding-md, var(--spacing-4));font-family:var(--typography-font-family-mono, monospace);font-size:var(--typography-font-size-sm);line-height:var(--typography-line-height-relaxed, 1.7);color:var(--color-text-primary);background-color:var(--color-surface);tab-size:2}.lc-rich-text-editor__split{display:grid;grid-template-columns:1fr 1fr;height:100%;min-height:inherit}.lc-rich-text-editor__split-editor{border-right:1px solid var(--color-divider);min-height:inherit;resize:none}.lc-rich-text-editor__split-preview{padding:var(--lc-density-padding-md, var(--spacing-4));overflow-y:auto;font-size:var(--typography-font-size-base);line-height:var(--typography-line-height-relaxed, 1.7);color:var(--color-text-primary)}.lc-rich-text-editor__split-preview h1,.lc-rich-text-editor__split-preview h2,.lc-rich-text-editor__split-preview h3{margin-top:var(--spacing-4);margin-bottom:var(--spacing-2);font-weight:var(--typography-font-weight-bold)}.lc-rich-text-editor__split-preview h1{font-size:var(--typography-font-size-2xl)}.lc-rich-text-editor__split-preview h2{font-size:var(--typography-font-size-xl)}.lc-rich-text-editor__split-preview h3{font-size:var(--typography-font-size-lg)}.lc-rich-text-editor__split-preview p{margin-bottom:var(--spacing-3)}.lc-rich-text-editor__split-preview ul,.lc-rich-text-editor__split-preview ol{padding-left:var(--spacing-6);margin-bottom:var(--spacing-3)}.lc-rich-text-editor__split-preview blockquote{border-left:4px solid var(--color-primary);padding-left:var(--spacing-4);margin:var(--spacing-3) 0;color:var(--color-text-secondary);font-style:italic}.lc-rich-text-editor__split-preview code{background-color:var(--color-surface-hover);padding:2px var(--spacing-1);border-radius:var(--border-radius-sm);font-family:var(--typography-font-family-mono, monospace);font-size:.9em}.lc-rich-text-editor__split-preview pre{background-color:var(--color-surface-hover);padding:var(--spacing-3);border-radius:var(--border-radius-md);overflow-x:auto}.lc-rich-text-editor__split-preview pre code{background:none;padding:0}.lc-rich-text-editor__split-preview a{color:var(--color-primary);text-decoration:underline}.lc-rich-text-editor__split-preview hr{border:none;border-top:1px solid var(--color-divider);margin:var(--spacing-4) 0}.lc-rich-text-editor__footer{display:flex;justify-content:flex-end;gap:var(--lc-density-gap-md, var(--spacing-4));padding:var(--lc-density-padding-xs, var(--spacing-2)) var(--lc-density-padding-sm, var(--spacing-3));border-top:1px solid var(--color-divider);font-size:var(--typography-font-size-xs);color:var(--color-text-secondary)}\n"]
554
558
  }]
555
559
  }],
@@ -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 '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\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 = `![${selected || 'alt text'}](url)`;\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(/&lt;u&gt;(.+?)&lt;\\/u&gt;/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 `&gt;` by now, the source having been escaped.\n html = html.replace(/^&gt; (.+)$/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, '![$1]($2)');\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(/&amp;/g, '&');\n md = md.replace(/&lt;/g, '<');\n md = md.replace(/&gt;/g, '>');\n md = md.replace(/&quot;/g, '\"');\n md = md.replace(/&nbsp;/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 '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\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 = `![${selected || 'alt text'}](url)`;\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(/&lt;u&gt;(.+?)&lt;\\/u&gt;/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 `&gt;` by now, the source having been escaped.\n html = html.replace(/^&gt; (.+)$/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, '![$1]($2)');\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(/&amp;/g, '&');\n md = md.replace(/&lt;/g, '<');\n md = md.replace(/&gt;/g, '>');\n md = md.replace(/&quot;/g, '\"');\n md = md.replace(/&nbsp;/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":"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"}
@@ -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 name = this.stepAriaLabel().replace("{n}", String(index + 1)).replace("{m}", String(this.totalSteps())).replace("{label}", step.label);
29
- return step.optional ? `${name}, ${this.optionalLabel()}` : name;
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 [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",
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 [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",
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",
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) becomes the header's accessible label fallback.
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
- * Explicit accessible label for the header button. Use this when the header
103
- * is a rich template with no meaningful plain-text title. Falls back to
104
- * `title` when omitted.
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
- /** Accessible label for the header button (explicit override, else title). */
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.
@@ -18,6 +18,13 @@ export type AvatarLoading = 'eager' | 'lazy';
18
18
  export type AvatarColor = 'default' | 'member-1' | 'member-2' | 'member-3' | 'member-4' | 'member-5' | 'member-6';
19
19
  /** The six member colours in their order (`AvatarColor` without `default`). */
20
20
  export declare const AVATAR_MEMBER_COLORS: readonly AvatarColor[];
21
+ /**
22
+ * The initials `lc-avatar` shows for `name`: first and last word's first
23
+ * letter, upper case; one word: its first letter; empty: `?`. Kit-internal
24
+ * (used by `lc-header`, so the account button's name holds exactly the text
25
+ * the avatar shows, AS-33); no semver guarantee.
26
+ */
27
+ export declare function ɵavatarInitials(name: string | undefined): string;
21
28
  /**
22
29
  * Avatar component for circular user representation.
23
30
  *
@@ -55,6 +55,7 @@ export declare class CalendarComponent {
55
55
  protected readonly weekDays: import("@angular/core").Signal<{
56
56
  date: Date;
57
57
  label: string;
58
+ ariaLabel: string;
58
59
  isToday: boolean;
59
60
  events: {
60
61
  start: Date;