@lifecorner/angular-ui-kit 3.8.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/fesm2022/lifecorner-angular-ui-kit-app-shell.mjs +4 -4
  2. package/fesm2022/lifecorner-angular-ui-kit-app-shell.mjs.map +1 -1
  3. package/fesm2022/lifecorner-angular-ui-kit-button.mjs +39 -6
  4. package/fesm2022/lifecorner-angular-ui-kit-button.mjs.map +1 -1
  5. package/fesm2022/lifecorner-angular-ui-kit-choice-prompt.mjs +2 -1
  6. package/fesm2022/lifecorner-angular-ui-kit-choice-prompt.mjs.map +1 -1
  7. package/fesm2022/lifecorner-angular-ui-kit-confirm-dialog.mjs +2 -1
  8. package/fesm2022/lifecorner-angular-ui-kit-confirm-dialog.mjs.map +1 -1
  9. package/fesm2022/lifecorner-angular-ui-kit-copy-field.mjs +2 -1
  10. package/fesm2022/lifecorner-angular-ui-kit-copy-field.mjs.map +1 -1
  11. package/fesm2022/lifecorner-angular-ui-kit-header.mjs +2 -1
  12. package/fesm2022/lifecorner-angular-ui-kit-header.mjs.map +1 -1
  13. package/fesm2022/lifecorner-angular-ui-kit-list.mjs +2 -1
  14. package/fesm2022/lifecorner-angular-ui-kit-list.mjs.map +1 -1
  15. package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs +2 -1
  16. package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs.map +1 -1
  17. package/fesm2022/lifecorner-angular-ui-kit-page-header.mjs +2 -2
  18. package/fesm2022/lifecorner-angular-ui-kit-page-header.mjs.map +1 -1
  19. package/fesm2022/lifecorner-angular-ui-kit-skeleton.mjs +2 -2
  20. package/fesm2022/lifecorner-angular-ui-kit-skeleton.mjs.map +1 -1
  21. package/fesm2022/lifecorner-angular-ui-kit-table.mjs +2 -1
  22. package/fesm2022/lifecorner-angular-ui-kit-table.mjs.map +1 -1
  23. package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs +75 -8
  24. package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs.map +1 -1
  25. package/package.json +1 -1
  26. package/src/styles/_horizon.scss +46 -12
  27. package/src/styles/horizon.shell.spec.ts +67 -0
  28. package/src/styles/horizon.spec.ts +6 -4
  29. package/types/lifecorner-angular-ui-kit-app-shell.d.ts +15 -4
  30. package/types/lifecorner-angular-ui-kit-button.d.ts +30 -0
  31. package/types/lifecorner-angular-ui-kit-page-header.d.ts +6 -1
  32. package/types/lifecorner-angular-ui-kit-skeleton.d.ts +7 -1
  33. package/types/lifecorner-angular-ui-kit-textarea.d.ts +87 -5
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-textarea.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/textarea/textarea.component.ts","../../../../libs/angular-ui-kit/src/lib/textarea/textarea.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n ElementRef,\n afterRenderEffect,\n computed,\n forwardRef,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\nlet nextUniqueId = 0;\n\n/**\n * Textarea component for multi-line text input.\n *\n * Features:\n * - Variant styles (outline, filled)\n * - Size presets (xs, sm, md, lg)\n * - Auto-resize with configurable min/max rows\n * - Character count display with maxLength\n * - Validation error message and helper text\n * - Disabled and readonly states\n * - ControlValueAccessor integration for reactive forms\n *\n * @example\n * ```html\n * <lc-textarea placeholder=\"Enter message\" [autoResize]=\"true\" [(ngModel)]=\"message\" />\n * ```\n */\n@Component({\n selector: 'lc-textarea',\n standalone: true,\n imports: [],\n templateUrl: './textarea.component.html',\n styleUrl: './textarea.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => TextareaComponent),\n multi: true,\n },\n ],\n})\nexport class TextareaComponent implements ControlValueAccessor {\n private readonly textareaElement = viewChild<ElementRef<HTMLTextAreaElement>>('textarea');\n\n /**\n * Visual variant of the textarea\n */\n readonly variant = input<'outline' | 'filled'>('outline');\n\n /**\n * Size of the textarea\n */\n readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');\n\n /**\n * Whether the textarea is disabled\n */\n readonly disabled = input<boolean>(false);\n\n /**\n * Whether the textarea is in error state\n */\n readonly error = input<boolean>(false);\n\n /**\n * Whether the textarea is required\n */\n readonly required = input<boolean>(false);\n\n /**\n * Whether the textarea is readonly\n */\n readonly readonly = input<boolean>(false);\n\n /**\n * Placeholder text\n */\n readonly placeholder = input<string>('');\n\n /**\n * Label text displayed above the textarea\n */\n readonly label = input<string>('');\n\n /**\n * Helper text displayed below the textarea\n */\n readonly helperText = input<string>('');\n\n /**\n * Error message displayed when error is true\n */\n readonly errorMessage = input<string>('');\n\n /**\n * ARIA label for accessibility\n */\n readonly ariaLabel = input<string | undefined>(undefined);\n\n /**\n * Number of visible text rows\n */\n readonly rows = input<number>(3);\n\n /**\n * Maximum number of characters allowed\n */\n readonly maxLength = input<number | undefined>(undefined);\n\n /**\n * Whether to show character count\n */\n readonly showCharacterCount = input<boolean>(false);\n\n /**\n * Whether to automatically resize based on content\n */\n readonly autoResize = input<boolean>(false);\n\n /**\n * Minimum number of rows for auto-resize\n */\n readonly minRows = input<number>(3);\n\n /**\n * Maximum number of rows for auto-resize\n */\n readonly maxRows = input<number | undefined>(undefined);\n\n /**\n * Emitted when value changes\n */\n readonly valueChange = output<string>();\n\n /** Per-instance id: links label, error and helper text to the control. */\n readonly textareaId = `lc-textarea-${nextUniqueId++}`;\n readonly errorId = `${this.textareaId}-error`;\n readonly helperId = `${this.textareaId}-helper`;\n\n // Internal state\n readonly value = signal<string>('');\n\n // Internal disabled state (for ControlValueAccessor)\n private readonly formDisabled = signal<boolean>(false);\n\n /**\n * Computed disabled state from both input and form control\n */\n readonly isDisabled = computed(() => this.disabled() || this.formDisabled());\n\n // Computed values\n readonly currentCharacterCount = computed(() => this.value().length);\n\n readonly characterCountText = computed(() => {\n const current = this.currentCharacterCount();\n const max = this.maxLength();\n return max !== undefined ? `${current} / ${max}` : `${current}`;\n });\n\n readonly isOverLimit = computed(() => {\n const max = this.maxLength();\n return max !== undefined && this.currentCharacterCount() > max;\n });\n\n /** Whether an error message is shown (drives aria-describedby) */\n readonly showsError = computed(() => this.error() && !!this.errorMessage());\n\n /** Whether the helper text is shown (error message takes precedence) */\n readonly showsHelper = computed(() => !this.showsError() && !!this.helperText());\n\n readonly describedBy = computed(() => {\n const ids: string[] = [];\n if (this.showsError()) ids.push(this.errorId);\n if (this.showsHelper()) ids.push(this.helperId);\n return ids.length ? ids.join(' ') : null;\n });\n\n /**\n * Computed classes for the textarea element\n */\n readonly textareaClasses = computed(() => {\n const classes = ['lc-textarea', `lc-textarea--${this.variant()}`, `lc-textarea--${this.size()}`];\n\n if (this.isDisabled()) {\n classes.push('lc-textarea--disabled');\n }\n\n if (this.error()) {\n classes.push('lc-textarea--error');\n }\n\n if (this.readonly()) {\n classes.push('lc-textarea--readonly');\n }\n\n if (this.autoResize()) {\n classes.push('lc-textarea--auto-resize');\n }\n\n return classes.join(' ');\n });\n\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onChange: (value: string) => void = () => {};\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onTouched: () => void = () => {};\n\n constructor() {\n // Re-measure after every render in which the value or the resize\n // constraints changed — this covers typing, form writes and input changes\n // without timers, and runs once the DOM already holds the new value.\n afterRenderEffect(() => {\n this.value();\n this.minRows();\n this.maxRows();\n if (this.autoResize()) {\n this.adjustHeight();\n }\n });\n }\n\n /**\n * Handle input changes\n */\n onInput(value: string): void {\n if (this.isDisabled() || this.readonly()) {\n return;\n }\n\n this.value.set(value);\n this.onChange(value);\n this.valueChange.emit(value);\n }\n\n /**\n * Handle blur event\n */\n onBlur(): void {\n this.onTouched();\n }\n\n // ControlValueAccessor implementation\n writeValue(value: string | null | undefined): void {\n this.value.set(value || '');\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 // Private helper methods\n private adjustHeight(): void {\n const textarea = this.textareaElement()?.nativeElement;\n if (!textarea) {\n return;\n }\n\n // Reset height to get accurate scrollHeight\n textarea.style.height = 'auto';\n\n const minHeight = this.rowsToHeight(textarea, this.minRows());\n const maxHeight = this.rowsToHeight(textarea, this.maxRows());\n let newHeight = textarea.scrollHeight;\n\n // Apply constraints\n if (minHeight !== undefined) {\n newHeight = Math.max(newHeight, minHeight);\n }\n if (maxHeight !== undefined) {\n newHeight = Math.min(newHeight, maxHeight);\n }\n\n textarea.style.height = `${newHeight}px`;\n }\n\n /** Pixel height for `rows` lines incl. vertical padding; undefined when it cannot be measured. */\n private rowsToHeight(textarea: HTMLTextAreaElement, rows: number | undefined): number | undefined {\n if (rows === undefined) {\n return undefined;\n }\n\n const style = getComputedStyle(textarea);\n const lineHeight = parseFloat(style.lineHeight);\n const paddingTop = parseFloat(style.paddingTop) || 0;\n const paddingBottom = parseFloat(style.paddingBottom) || 0;\n\n // 'normal' / unmeasurable line-height (e.g. jsdom) → no constraint\n if (!Number.isFinite(lineHeight)) {\n return undefined;\n }\n\n return lineHeight * rows + paddingTop + paddingBottom;\n }\n\n protected getInputValue(event: Event): string {\n return (event.target as HTMLTextAreaElement).value;\n }\n}\n","<div class=\"lc-textarea-wrapper\">\n @if (label()) {\n <label [for]=\"textareaId\" class=\"lc-textarea-label\">\n {{ label() }}\n @if (required()) {\n <span class=\"lc-textarea-required-indicator\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"lc-textarea-input-wrapper\">\n <textarea\n #textarea\n [id]=\"textareaId\"\n [class]=\"textareaClasses()\"\n [value]=\"value()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n [attr.rows]=\"autoResize() ? null : rows()\"\n [attr.maxlength]=\"maxLength() ?? null\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"error() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput(getInputValue($event))\"\n (blur)=\"onBlur()\"\n ></textarea>\n </div>\n\n <div class=\"lc-textarea-footer\">\n <div class=\"lc-textarea-messages\">\n @if (showsError()) {\n <span [id]=\"errorId\" class=\"lc-textarea-error-message\" role=\"alert\">{{ errorMessage() }}</span>\n } @else if (showsHelper()) {\n <span [id]=\"helperId\" class=\"lc-textarea-helper-text\">{{ helperText() }}</span>\n }\n </div>\n\n @if (showCharacterCount()) {\n <span\n class=\"lc-textarea-character-count\"\n [class.lc-textarea-character-count--over-limit]=\"isOverLimit()\"\n aria-live=\"polite\"\n >\n {{ characterCountText() }}\n </span>\n }\n </div>\n</div>\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-textarea.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/textarea/textarea.component.ts","../../../../libs/angular-ui-kit/src/lib/textarea/textarea.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n ElementRef,\n OutputRef,\n PLATFORM_ID,\n afterRenderEffect,\n computed,\n forwardRef,\n inject,\n input,\n model,\n signal,\n viewChild,\n} from '@angular/core';\nimport { isPlatformServer } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\nlet nextUniqueId = 0;\n\n/**\n * Textarea component for multi-line text input.\n *\n * Features:\n * - Variant styles (outline, filled)\n * - Size presets (xs, sm, md, lg)\n * - Auto-resize with configurable min/max rows\n * - Character count display with maxLength\n * - Validation error message and helper text\n * - Disabled and readonly states\n * - ControlValueAccessor integration for reactive forms\n * - Plain form posts without JavaScript: `name`, and a `value` that is\n * prerendered into the native `<textarea>`\n *\n * @example\n * ```html\n * <lc-textarea placeholder=\"Enter message\" [autoResize]=\"true\" [(ngModel)]=\"message\" />\n * <lc-textarea label=\"Message\" name=\"message\" [required]=\"true\" [value]=\"draft\" />\n * ```\n */\n@Component({\n selector: 'lc-textarea',\n standalone: true,\n imports: [],\n templateUrl: './textarea.component.html',\n styleUrl: './textarea.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => TextareaComponent),\n multi: true,\n },\n ],\n})\nexport class TextareaComponent implements ControlValueAccessor {\n private readonly textareaElement = viewChild<ElementRef<HTMLTextAreaElement>>('textarea');\n\n private readonly isServer = isPlatformServer(inject(PLATFORM_ID));\n\n /**\n * Visual variant of the textarea\n * @default 'outline'\n */\n readonly variant = input<'outline' | 'filled'>('outline');\n\n /**\n * Size of the textarea\n * @default 'md'\n */\n readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');\n\n /**\n * Whether the textarea is disabled\n * @default false\n */\n readonly disabled = input<boolean>(false);\n\n /**\n * Whether the textarea is in error state\n * @default false\n */\n readonly error = input<boolean>(false);\n\n /**\n * Whether the textarea is required\n * @default false\n */\n readonly required = input<boolean>(false);\n\n /**\n * Show the `*` after the label of a `required` field. `false` keeps the\n * required semantics (`required`, `aria-required`) without the asterisk —\n * for forms where every field is required.\n * @default true\n */\n readonly requiredIndicator = input<boolean>(true);\n\n /**\n * Accessible name of the `*` marker, for API parity with `lc-input`.\n * `lc-textarea` hides the `*` from assistive technology, because the native\n * `required` state is announced already and naming the `*` would say\n * \"required\" twice.\n * @default 'required'\n */\n readonly requiredAriaLabel = input<string>('required');\n\n /**\n * Whether the textarea is readonly\n * @default false\n */\n readonly readonly = input<boolean>(false);\n\n /**\n * Placeholder text\n * @default ''\n */\n readonly placeholder = input<string>('');\n\n /**\n * Label text displayed above the textarea\n * @default ''\n */\n readonly label = input<string>('');\n\n /**\n * Helper text displayed below the textarea\n * @default ''\n */\n readonly helperText = input<string>('');\n\n /**\n * Error message displayed when error is true\n * @default ''\n */\n readonly errorMessage = input<string>('');\n\n /**\n * ARIA label for accessibility\n * @default undefined\n */\n readonly ariaLabel = input<string | undefined>(undefined);\n\n /**\n * Native `name` of the inner `<textarea>`, so a form posts the text without\n * JavaScript. Unset = no attribute. A `disabled` textarea is not posted\n * (HTML); use `readonly` to post a fixed text.\n * @default undefined\n */\n readonly name = input<string>();\n\n /**\n * Number of visible text rows\n * @default 3\n */\n readonly rows = input<number>(3);\n\n /**\n * Maximum number of characters allowed\n * @default undefined\n */\n readonly maxLength = input<number | undefined>(undefined);\n\n /**\n * Whether to show character count\n * @default false\n */\n readonly showCharacterCount = input<boolean>(false);\n\n /**\n * Whether to automatically resize based on content\n * @default false\n */\n readonly autoResize = input<boolean>(false);\n\n /**\n * Minimum number of rows for auto-resize\n * @default 3\n */\n readonly minRows = input<number>(3);\n\n /**\n * Maximum number of rows for auto-resize\n * @default undefined\n */\n readonly maxRows = input<number | undefined>(undefined);\n\n /**\n * Current text, two-way bindable (`[value]` / `[(value)]`). It pre-fills the\n * native textarea, in prerendered HTML too, so a server-rendered page can\n * keep a visitor's text. Emits `valueChange` whenever it changes: once per\n * keystroke that changes the text, and when a bound form control or code\n * sets a different text. With a form control, the control's value wins;\n * don't bind both.\n * @default ''\n */\n readonly value = model<string>('');\n\n /**\n * The model's change output as a class property, so code that subscribes to\n * it (`textarea.valueChange.subscribe(…)`, `outputToObservable(…)`) keeps\n * compiling as in 3.9. Template bindings `(valueChange)` use the model.\n */\n readonly valueChange: OutputRef<string> = this.value;\n\n /** Per-instance id: links label, error and helper text to the control. */\n readonly textareaId = `lc-textarea-${nextUniqueId++}`;\n readonly errorId = `${this.textareaId}-error`;\n readonly helperId = `${this.textareaId}-helper`;\n\n /**\n * The native textarea's value. On the server a leading line break gets one\n * extra `\\n`: the HTML parser drops the first line break after `<textarea>`,\n * and the server's DOM serialiser doesn't add the one the HTML\n * serialisation rules require, so the prerendered page would lose it.\n */\n protected readonly renderedValue = computed(() => {\n const value = this.value();\n return this.isServer && /^[\\r\\n]/.test(value) ? `\\n${value}` : value;\n });\n\n // Internal disabled state (for ControlValueAccessor)\n private readonly formDisabled = signal<boolean>(false);\n\n /**\n * Computed disabled state from both input and form control\n */\n readonly isDisabled = computed(() => this.disabled() || this.formDisabled());\n\n // Computed values\n readonly currentCharacterCount = computed(() => this.value().length);\n\n readonly characterCountText = computed(() => {\n const current = this.currentCharacterCount();\n const max = this.maxLength();\n return max !== undefined ? `${current} / ${max}` : `${current}`;\n });\n\n readonly isOverLimit = computed(() => {\n const max = this.maxLength();\n return max !== undefined && this.currentCharacterCount() > max;\n });\n\n /** Whether an error message is shown (drives aria-describedby) */\n readonly showsError = computed(() => this.error() && !!this.errorMessage());\n\n /** Whether the helper text is shown (error message takes precedence) */\n readonly showsHelper = computed(() => !this.showsError() && !!this.helperText());\n\n readonly describedBy = computed(() => {\n const ids: string[] = [];\n if (this.showsError()) ids.push(this.errorId);\n if (this.showsHelper()) ids.push(this.helperId);\n return ids.length ? ids.join(' ') : null;\n });\n\n /**\n * Computed classes for the textarea element\n */\n readonly textareaClasses = computed(() => {\n const classes = ['lc-textarea', `lc-textarea--${this.variant()}`, `lc-textarea--${this.size()}`];\n\n if (this.isDisabled()) {\n classes.push('lc-textarea--disabled');\n }\n\n if (this.error()) {\n classes.push('lc-textarea--error');\n }\n\n if (this.readonly()) {\n classes.push('lc-textarea--readonly');\n }\n\n if (this.autoResize()) {\n classes.push('lc-textarea--auto-resize');\n }\n\n return classes.join(' ');\n });\n\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onChange: (value: string) => void = () => {};\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onTouched: () => void = () => {};\n\n constructor() {\n // Re-measure after every render in which the value or the resize\n // constraints changed — this covers typing, form writes and input changes\n // without timers, and runs once the DOM already holds the new value.\n afterRenderEffect(() => {\n this.value();\n this.minRows();\n this.maxRows();\n if (this.autoResize()) {\n this.adjustHeight();\n }\n });\n }\n\n /**\n * Handle input changes\n */\n onInput(value: string): void {\n if (this.isDisabled() || this.readonly()) {\n return;\n }\n\n // model.set() emits `valueChange` exactly once per actual change\n this.value.set(value);\n this.onChange(value);\n }\n\n /**\n * Handle blur event\n */\n onBlur(): void {\n this.onTouched();\n }\n\n // ControlValueAccessor implementation\n writeValue(value: string | null | undefined): void {\n this.value.set(value || '');\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 // Private helper methods\n private adjustHeight(): void {\n const textarea = this.textareaElement()?.nativeElement;\n if (!textarea) {\n return;\n }\n\n // Reset height to get accurate scrollHeight\n textarea.style.height = 'auto';\n\n const minHeight = this.rowsToHeight(textarea, this.minRows());\n const maxHeight = this.rowsToHeight(textarea, this.maxRows());\n let newHeight = textarea.scrollHeight;\n\n // Apply constraints\n if (minHeight !== undefined) {\n newHeight = Math.max(newHeight, minHeight);\n }\n if (maxHeight !== undefined) {\n newHeight = Math.min(newHeight, maxHeight);\n }\n\n textarea.style.height = `${newHeight}px`;\n }\n\n /** Pixel height for `rows` lines incl. vertical padding; undefined when it cannot be measured. */\n private rowsToHeight(textarea: HTMLTextAreaElement, rows: number | undefined): number | undefined {\n if (rows === undefined) {\n return undefined;\n }\n\n const style = getComputedStyle(textarea);\n const lineHeight = parseFloat(style.lineHeight);\n const paddingTop = parseFloat(style.paddingTop) || 0;\n const paddingBottom = parseFloat(style.paddingBottom) || 0;\n\n // 'normal' / unmeasurable line-height (e.g. jsdom) → no constraint\n if (!Number.isFinite(lineHeight)) {\n return undefined;\n }\n\n return lineHeight * rows + paddingTop + paddingBottom;\n }\n\n protected getInputValue(event: Event): string {\n return (event.target as HTMLTextAreaElement).value;\n }\n}\n","<div class=\"lc-textarea-wrapper\">\n @if (label()) {\n <label [for]=\"textareaId\" class=\"lc-textarea-label\">\n {{ label() }}\n @if (required() && requiredIndicator()) {\n <span class=\"lc-textarea-required-indicator\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"lc-textarea-input-wrapper\">\n <textarea\n #textarea\n [id]=\"textareaId\"\n [class]=\"textareaClasses()\"\n [value]=\"renderedValue()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [required]=\"required()\"\n [attr.name]=\"name() ?? null\"\n [attr.rows]=\"autoResize() ? null : rows()\"\n [attr.maxlength]=\"maxLength() ?? null\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"error() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n (input)=\"onInput(getInputValue($event))\"\n (blur)=\"onBlur()\"\n ></textarea>\n </div>\n\n <div class=\"lc-textarea-footer\">\n <div class=\"lc-textarea-messages\">\n @if (showsError()) {\n <span [id]=\"errorId\" class=\"lc-textarea-error-message\" role=\"alert\">{{ errorMessage() }}</span>\n } @else if (showsHelper()) {\n <span [id]=\"helperId\" class=\"lc-textarea-helper-text\">{{ helperText() }}</span>\n }\n </div>\n\n @if (showCharacterCount()) {\n <span\n class=\"lc-textarea-character-count\"\n [class.lc-textarea-character-count--over-limit]=\"isOverLimit()\"\n aria-live=\"polite\"\n >\n {{ characterCountText() }}\n </span>\n }\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.8.0",
3
+ "version": "3.10.0",
4
4
  "description": "LifeCorner Design System – Angular UI component library",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,9 +26,11 @@
26
26
  *
27
27
  * Inputs (custom properties, set on the block or any ancestor):
28
28
  * --lc-horizon-land height of the land below the line
29
- * (default Today's: 112 px phone, 100 · 120 · 140 · 184 px
30
- * from 1024 · 1280 · 1440 · 1920; with the compact footer
31
- * on the land add its height, e.g. 136 px on the phone)
29
+ * (default Today's: 112 px phone, 72 · 80 · 96 · 128 px
30
+ * from 1024 · 1280 · 1440 · 1920 (3.9, LK-12); with the
31
+ * compact footer on the land add its height, e.g. 136 px
32
+ * on the phone. Inside lc-app-shell with a [shell-footer]
33
+ * the footer's height is added below it by the shell.)
32
34
  * --lc-horizon-sun-radius the sun's radius (58 px phone, 70 · 90 · 120 · 166 px)
33
35
  * --lc-horizon-sun-x the sun's centre from the block's left edge (72%)
34
36
  * --lc-horizon-figure-size the figure's size in .lc-horizon__figure
@@ -45,6 +47,9 @@
45
47
  * <lc-character class="lc-horizon__figure" pose="comforts" />
46
48
  * </div>
47
49
  *
50
+ * In lc-app-shell (3.9, AS-29) the attribute `shell-horizon-figure` alone does
51
+ * the same: the shell projects the element as a direct child of its horizon.
52
+ *
48
53
  * Colours: --color-background-low, --color-horizon-line, --color-land-1…3,
49
54
  * --color-sun-1…3, --color-sun-ring, --color-sun-halo (theme tokens).
50
55
  */
@@ -94,27 +99,28 @@
94
99
  linear-gradient(180deg, transparent, var(--color-background-low))
95
100
  0 0 / 100% calc(100% - var(--_lc-horizon-land)) no-repeat;
96
101
 
97
- // Desktop steps of Today's horizon (app-shell 12.2). A page sets its own
98
- // geometry through --lc-horizon-land / --lc-horizon-sun-radius.
102
+ // Desktop steps of Today's horizon (app-shell 12.2; the land 72 · 80 · 96 ·
103
+ // 128 px since 3.9, LK-12). A page sets its own geometry through
104
+ // --lc-horizon-land / --lc-horizon-sun-radius.
99
105
  @media (min-width: 1024px) {
100
- --_lc-horizon-land: var(--lc-horizon-land, 100px);
106
+ --_lc-horizon-land: var(--lc-horizon-land, 72px);
101
107
  --_lc-horizon-r: var(--lc-horizon-sun-radius, 70px);
102
108
  --_lc-horizon-fig: var(--lc-horizon-figure-size, 96px);
103
109
  --_lc-horizon-fig-gap: var(--lc-horizon-figure-gap, 24px);
104
110
  }
105
111
 
106
112
  @media (min-width: 1280px) {
107
- --_lc-horizon-land: var(--lc-horizon-land, 120px);
113
+ --_lc-horizon-land: var(--lc-horizon-land, 80px);
108
114
  --_lc-horizon-r: var(--lc-horizon-sun-radius, 90px);
109
115
  }
110
116
 
111
117
  @media (min-width: 1440px) {
112
- --_lc-horizon-land: var(--lc-horizon-land, 140px);
118
+ --_lc-horizon-land: var(--lc-horizon-land, 96px);
113
119
  --_lc-horizon-r: var(--lc-horizon-sun-radius, 120px);
114
120
  }
115
121
 
116
122
  @media (min-width: 1920px) {
117
- --_lc-horizon-land: var(--lc-horizon-land, 184px);
123
+ --_lc-horizon-land: var(--lc-horizon-land, 128px);
118
124
  --_lc-horizon-r: var(--lc-horizon-sun-radius, 166px);
119
125
  --_lc-horizon-fig: var(--lc-horizon-figure-size, 128px);
120
126
  }
@@ -135,7 +141,7 @@
135
141
  // Edu on the horizon line (3.8, G-4): left of the sun, the feet on the line.
136
142
  // Only a block WITH the figure gets the taller minimum; without it the block
137
143
  // renders exactly as before.
138
- .lc-horizon:has(> .lc-horizon__figure) {
144
+ @mixin figure-min-height {
139
145
  min-height: calc(var(--_lc-horizon-land) + max(var(--_lc-horizon-r) + 22px, var(--_lc-horizon-fig) + 8px));
140
146
  }
141
147
 
@@ -143,7 +149,7 @@
143
149
  // Its right edge sits the gap left of the sun's left edge; the box bottom lies
144
150
  // 12.5 % of its size below the line, where the standing poses' feet end
145
151
  // (y 87.5 of 100). Px, not rem: at 200 % text the figure keeps its size.
146
- .lc-horizon__figure {
152
+ @mixin figure {
147
153
  --lc-character-size: var(--_lc-horizon-fig);
148
154
 
149
155
  position: absolute;
@@ -160,8 +166,36 @@
160
166
  }
161
167
 
162
168
  // A <picture> or an element holding an inline <svg>.
163
- .lc-horizon__figure > :is(img, svg) {
169
+ @mixin figure-fill {
164
170
  display: block;
165
171
  width: 100%;
166
172
  height: 100%;
167
173
  }
174
+
175
+ .lc-horizon:has(> .lc-horizon__figure) {
176
+ @include figure-min-height;
177
+ }
178
+
179
+ .lc-horizon__figure {
180
+ @include figure;
181
+ }
182
+
183
+ .lc-horizon__figure > :is(img, svg) {
184
+ @include figure-fill;
185
+ }
186
+
187
+ // lc-app-shell's figure slot (3.9, AS-29): the attribute alone places the
188
+ // figure, with the same rules and the same specificity as .lc-horizon__figure
189
+ // (`:where` adds none). Here, not in the shell's own stylesheet: the slot's
190
+ // element belongs to the page's view, and `:has()` must see it.
191
+ .lc-horizon:has(> [shell-horizon-figure]) {
192
+ @include figure-min-height;
193
+ }
194
+
195
+ .lc-horizon > :where([shell-horizon-figure]) {
196
+ @include figure;
197
+ }
198
+
199
+ .lc-horizon > :where([shell-horizon-figure]) > :is(img, svg) {
200
+ @include figure-fill;
201
+ }
@@ -0,0 +1,67 @@
1
+ import { compile } from 'sass';
2
+ import { resolve } from 'path';
3
+
4
+ /**
5
+ * The horizon in 3.9 (request 006): the slimmer land defaults (LK-12, AC-1,
6
+ * AC-3, AC-8) and lc-app-shell's figure slot `[shell-horizon-figure]` (AS-29,
7
+ * AC-6, AC-7), read from the compiled CSS. The 3.8 figure rules are pinned by
8
+ * horizon.figure.spec.ts.
9
+ */
10
+ const css = '\n' + compile(resolve(__dirname, '_horizon.scss')).css;
11
+
12
+ /** The declarations of the top-level rule whose selector is exactly `selector` (compiled rules start a line). */
13
+ function rule(selector: string): string {
14
+ const start = css.indexOf(`\n${selector} {`);
15
+ expect(start).toBeGreaterThan(-1);
16
+ return css.slice(start + selector.length + 3, css.indexOf('}', start)).trim();
17
+ }
18
+
19
+ /** The declarations of `selector` inside `@media (forced-colors: active)`. */
20
+ function forcedColors(selector: string): string {
21
+ const match = new RegExp(`@media \\(forced-colors: active\\) \\{\\s*${selector.replace(/[.*+?^${}()|[\]\\>]/g, '\\$&')} \\{([^}]*)\\}`).exec(css);
22
+ expect(match).not.toBeNull();
23
+ return (match?.[1] ?? '').trim();
24
+ }
25
+
26
+ describe('.lc-horizon land defaults (LK-12)', () => {
27
+ it('AC-1: 112 px on the phone, 72 · 80 · 96 · 128 px from 1024 · 1280 · 1440 · 1920', () => {
28
+ const lands = [...css.matchAll(/--_lc-horizon-land: var\(--lc-horizon-land, (\d+)px\)/g)].map((m) => Number(m[1]));
29
+ expect(lands).toEqual([112, 72, 80, 96, 128]);
30
+ });
31
+
32
+ it('AC-3, AC-8: --lc-horizon-land wins at every width (every step is only its fallback)', () => {
33
+ const steps = [...css.matchAll(/--_lc-horizon-land:\s*([^;]+);/g)].map((m) => m[1]);
34
+ expect(steps).toHaveLength(5);
35
+ for (const step of steps) expect(step).toMatch(/^var\(--lc-horizon-land, \d+px\)$/);
36
+ });
37
+ });
38
+
39
+ describe('[shell-horizon-figure] (AS-29)', () => {
40
+ it('places the shell figure with exactly the rules of .lc-horizon__figure', () => {
41
+ expect(rule('.lc-horizon > :where([shell-horizon-figure])')).toBe(rule('.lc-horizon__figure'));
42
+ expect(rule('.lc-horizon > :where([shell-horizon-figure]) > :is(img, svg)')).toBe(rule('.lc-horizon__figure > :is(img, svg)'));
43
+ });
44
+
45
+ it('raises the minimum above the line with a figure child, as .lc-horizon__figure does', () => {
46
+ expect(rule('.lc-horizon:has(> [shell-horizon-figure])')).toBe(rule('.lc-horizon:has(> .lc-horizon__figure)'));
47
+ });
48
+
49
+ it('AC-7: sizes and places the figure, never a pointer target, hidden in forced colours', () => {
50
+ const figure = rule('.lc-horizon > :where([shell-horizon-figure])');
51
+ expect(figure).toMatch(/position:\s*absolute/);
52
+ expect(figure).toMatch(/--lc-character-size:\s*var\(--_lc-horizon-fig\)/);
53
+ expect(figure).toMatch(/pointer-events:\s*none/);
54
+ expect(forcedColors('.lc-horizon > :where([shell-horizon-figure])')).toBe(forcedColors('.lc-horizon__figure'));
55
+ });
56
+
57
+ it('keeps the specificity of .lc-horizon__figure: :where() adds none to the attribute', () => {
58
+ // (0,1,0) like `.lc-horizon__figure`, (0,2,0) for the :has() rule: every
59
+ // selector that names the attribute, in order (the third is in forced colours).
60
+ expect(css.match(/^[^{}\n]*shell-horizon-figure[^{}\n]*(?= \{)/gm)).toEqual([
61
+ '.lc-horizon:has(> [shell-horizon-figure])',
62
+ '.lc-horizon > :where([shell-horizon-figure])',
63
+ ' .lc-horizon > :where([shell-horizon-figure])',
64
+ '.lc-horizon > :where([shell-horizon-figure]) > :is(img, svg)',
65
+ ]);
66
+ });
67
+ });
@@ -49,11 +49,13 @@ describe('.lc-horizon (LK-1)', () => {
49
49
  expect(base).toMatch(/--_lc-horizon-x:\s*var\(--lc-horizon-sun-x, 72%\)/);
50
50
  });
51
51
 
52
+ // The land steps changed in 3.9 (LK-12, request 006): 100 · 120 · 140 · 184 px
53
+ // became 72 · 80 · 96 · 128 px; the sun radii are unchanged.
52
54
  it.each([
53
- [1024, '100px', '70px'],
54
- [1280, '120px', '90px'],
55
- [1440, '140px', '120px'],
56
- [1920, '184px', '166px'],
55
+ [1024, '72px', '70px'],
56
+ [1280, '80px', '90px'],
57
+ [1440, '96px', '120px'],
58
+ [1920, '128px', '166px'],
57
59
  ])('uses Today’s desktop geometry from %s px', (width, land, radius) => {
58
60
  const step = block(`@media (min-width: ${width}px) {\n .lc-horizon {`);
59
61
  expect(step).toContain(`var(--lc-horizon-land, ${land})`);
@@ -20,8 +20,17 @@ import * as i0 from "@angular/core";
20
20
  * reserves its height at the end of the page (16 + 72 + 24 px plus the
21
21
  * safe-area inset; `--lc-app-shell-bottom-clearance`), so it never covers
22
22
  * content. With the horizon, the bar rests on the land instead.
23
- * - `[shell-footer]` — e.g. a compact `lc-footer`; on the horizon's land when
24
- * `horizon` is on, else after the page.
23
+ * - `[shell-footer]` — e.g. a compact `lc-footer`; after the page, or with
24
+ * `horizon` in a row of its own below the horizon, painted in the land's
25
+ * colour (`--color-land-3`): the land grows by the footer's height, so
26
+ * `--lc-horizon-land` stays the land above the footer and no page overrides
27
+ * it (3.9, LK-12).
28
+ * - `[shell-horizon-figure]` — Edu (or any decorative figure) on the horizon
29
+ * line, left of the sun: `lc-character`, `<img>`, `<picture>` or an element
30
+ * holding an inline `<svg>`. Projected as a direct child of `.lc-horizon`,
31
+ * sized and placed by the horizon (`--lc-horizon-figure-size` / `-gap`).
32
+ * Decorative: the horizon is `aria-hidden`, so give an `<img>` `alt=""` and
33
+ * nothing focusable. Shown only when `horizon` is on (3.9, AS-29).
25
34
  *
26
35
  * `horizon` closes the page with the "Weite" horizon (`.lc-horizon` from the
27
36
  * kit's styles): full-bleed, below the side navigation and the page, filling
@@ -47,7 +56,9 @@ export declare class AppShellComponent {
47
56
  /**
48
57
  * Close the page with the horizon (`.lc-horizon`): full width, below the
49
58
  * side navigation and the page, filling the rest of the screen; the footer
50
- * and the floating bar rest on its land.
59
+ * (in a row of its own, which adds to the land) and the floating bar rest
60
+ * on its land, and `[shell-horizon-figure]` stands on its line.
61
+ * @default false
51
62
  */
52
63
  readonly horizon: import("@angular/core").InputSignal<boolean>;
53
64
  protected readonly shellClasses: import("@angular/core").Signal<string>;
@@ -58,7 +69,7 @@ export declare class AppShellComponent {
58
69
  "required": false;
59
70
  "isSignal": true;
60
71
  };
61
- }, {}, never, ["[shell-header]", "[shell-nav]", "*", "[shell-footer]", "[shell-bottom-nav]"], true, never>;
72
+ }, {}, never, ["[shell-header]", "[shell-nav]", "*", "[shell-horizon-figure]", "[shell-footer]", "[shell-bottom-nav]"], true, never>;
62
73
  }
63
74
  /**
64
75
  * A page inside `lc-app-shell`: the `<main>` landmark with the page's content
@@ -30,6 +30,16 @@ export declare class ButtonComponent {
30
30
  * @default false
31
31
  */
32
32
  readonly inline: import("@angular/core").InputSignal<boolean>;
33
+ /**
34
+ * Drop the inline padding so a `variant="link"` button sits flush with the
35
+ * content edge: the label starts at the edge, and the button keeps its height
36
+ * and a 44 × 44 px minimum target. At the end edge of a row, set the CSS
37
+ * hook `--lc-btn-flush-align: end` (default `start`) on the host or an
38
+ * ancestor: every line of the label then ends at the edge. Ignored on other
39
+ * variants and with `iconOnly` (3.9, AS-26).
40
+ * @default false
41
+ */
42
+ readonly flush: import("@angular/core").InputSignal<boolean>;
33
43
  readonly clicked: import("@angular/core").OutputEmitterRef<void>;
34
44
  readonly focused: import("@angular/core").OutputEmitterRef<void>;
35
45
  readonly blurred: import("@angular/core").OutputEmitterRef<void>;
@@ -102,6 +112,11 @@ export declare class ButtonComponent {
102
112
  "required": false;
103
113
  "isSignal": true;
104
114
  };
115
+ "flush": {
116
+ "alias": "flush";
117
+ "required": false;
118
+ "isSignal": true;
119
+ };
105
120
  }, {
106
121
  "clicked": "clicked";
107
122
  "focused": "focused";
@@ -148,6 +163,16 @@ export declare class ButtonLinkComponent {
148
163
  * size, weight and line-height). Pair with `variant="link"`.
149
164
  */
150
165
  readonly inline: import("@angular/core").InputSignal<boolean>;
166
+ /**
167
+ * Drop the inline padding so a `variant="link"` link sits flush with the
168
+ * content edge: the label starts at the edge, and the link keeps its height
169
+ * and a 44 × 44 px minimum target. At the end edge of a row, set the CSS
170
+ * hook `--lc-btn-flush-align: end` (default `start`) on the link or an
171
+ * ancestor: every line of the label then ends at the edge. Ignored on other
172
+ * variants and with `iconOnly` (3.9, AS-26).
173
+ * @default false
174
+ */
175
+ readonly flush: import("@angular/core").InputSignal<boolean>;
151
176
  /** Classes on the anchor; merged with the consumer's own `class`. */
152
177
  protected readonly hostClasses: import("@angular/core").Signal<string>;
153
178
  static ɵfac: i0.ɵɵFactoryDeclaration<ButtonLinkComponent, never>;
@@ -177,5 +202,10 @@ export declare class ButtonLinkComponent {
177
202
  "required": false;
178
203
  "isSignal": true;
179
204
  };
205
+ "flush": {
206
+ "alias": "flush";
207
+ "required": false;
208
+ "isSignal": true;
209
+ };
180
210
  }, {}, never, ["[slot='icon-before']", "*", "[slot='icon-after']"], true, never>;
181
211
  }
@@ -2,6 +2,9 @@ import * as i0 from "@angular/core";
2
2
  /**
3
3
  * Visual size of the page header.
4
4
  * - `compact` — dense data-tool pages and section headings (18 px)
5
+ * - `secondary` — a second title under the page's main one, e.g. the other
6
+ * language of a bilingual page (20 px, `--font-size-xl`;
7
+ * the `compact` spacing; 3.9, AS-25)
5
8
  * - `default` — standard app pages (24 px)
6
9
  * - `comfortable` — landing-page hero feel (30 px)
7
10
  * - `title` — the "Weite" page title: 32 px on phones, 40 / 44 / 48 px
@@ -11,7 +14,7 @@ import * as i0 from "@angular/core";
11
14
  * - `auth` — the sign-in and registration `<h1>`: 30 px, 34 px from
12
15
  * 1024 px (`--font-size-auth-title`)
13
16
  */
14
- export type PageHeaderSize = 'compact' | 'default' | 'comfortable' | 'title' | 'display' | 'auth';
17
+ export type PageHeaderSize = 'compact' | 'secondary' | 'default' | 'comfortable' | 'title' | 'display' | 'auth';
15
18
  /**
16
19
  * Page header — top-of-page title block with optional subtitle,
17
20
  * breadcrumbs, badge, description and an actions area.
@@ -88,6 +91,8 @@ export declare class PageHeaderComponent {
88
91
  /**
89
92
  * Visual size of the page header.
90
93
  * - `compact` — dense data-tool pages and section headings (18 px)
94
+ * - `secondary` — a second title under the main one, e.g. the other
95
+ * language of a bilingual page (20 px, compact spacing)
91
96
  * - `default` — standard app pages (24 px)
92
97
  * - `comfortable` — landing-page hero feel (30 px)
93
98
  * - `title` — page title on the responsive title scale, 32–48 px
@@ -9,6 +9,9 @@ export type SkeletonVariant = 'line' | 'circle' | 'rect';
9
9
  * - Colours from `--color-skeleton` / `--color-skeleton-highlight`, overridable
10
10
  * with the `--lc-skeleton-bg` / `--lc-skeleton-highlight` hooks
11
11
  * - Custom width, height, and border radius
12
+ * - `--lc-skeleton-height` hook: the height from CSS (a container query, `cqi`),
13
+ * winning over the `height` input; set it on the `lc-skeleton` element (on a
14
+ * wrapper it reaches every skeleton inside)
12
15
  * - Composable for building complex loading layouts
13
16
  * - Dark mode compatible
14
17
  *
@@ -24,7 +27,10 @@ export declare class SkeletonComponent {
24
27
  readonly variant: import("@angular/core").InputSignal<SkeletonVariant>;
25
28
  /** Width (CSS value). Defaults to '100%' for line/rect, '40px' for circle. */
26
29
  readonly width: import("@angular/core").InputSignal<string | undefined>;
27
- /** Height (CSS value). Defaults to '0.875rem' for line, '40px' for circle, '100px' for rect. */
30
+ /**
31
+ * Height (CSS value). Defaults to '0.875rem' for line, '40px' for circle, '100px' for rect.
32
+ * The `--lc-skeleton-height` custom property, when set, wins over it.
33
+ */
28
34
  readonly height: import("@angular/core").InputSignal<string | undefined>;
29
35
  /** Border radius override (CSS value). */
30
36
  readonly borderRadius: import("@angular/core").InputSignal<string | undefined>;