@lifecorner/angular-ui-kit 3.25.1 → 3.27.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.
- package/fesm2022/lifecorner-angular-ui-kit-button.mjs +18 -2
- package/fesm2022/lifecorner-angular-ui-kit-button.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-callout.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-callout.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-chat.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-chat.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-choice-prompt.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-choice-prompt.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-color-picker.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-color-picker.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-confirm-dialog.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-confirm-dialog.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-copy-field.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-copy-field.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-date-range-picker.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-date-range-picker.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-dependency-viewer.mjs +47 -9
- package/fesm2022/lifecorner-angular-ui-kit-dependency-viewer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-description-list.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-description-list.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-document-viewer.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-document-viewer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs +8 -4
- package/fesm2022/lifecorner-angular-ui-kit-file-upload.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-filter-bar.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-filter-bar.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-gantt-chart.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-gantt-chart.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-header.mjs +3 -2
- package/fesm2022/lifecorner-angular-ui-kit-header.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-kanban-board.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-kanban-board.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-list.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-list.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs +1 -0
- package/fesm2022/lifecorner-angular-ui-kit-next-step-card.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-number-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-number-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-scatter-plot.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-scatter-plot.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-search-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-search-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-select.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-select.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-split-pane.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-split-pane.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-table.mjs +3 -2
- package/fesm2022/lifecorner-angular-ui-kit-table.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-tabs.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-tabs.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-toggle-group.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-toggle-group.mjs.map +1 -1
- package/package.json +1 -1
- package/src/styles/_reset.scss +4 -2
- package/src/styles/focus-indicators.spec.ts +199 -0
- package/types/lifecorner-angular-ui-kit-button.d.ts +20 -0
- package/types/lifecorner-angular-ui-kit-dependency-viewer.d.ts +33 -4
- package/types/lifecorner-angular-ui-kit-description-list.d.ts +4 -0
- package/types/lifecorner-angular-ui-kit-file-upload.d.ts +11 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-chat.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/chat/chat.component.ts","../../../../libs/angular-ui-kit/src/lib/chat/chat.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n Injector,\n NgZone,\n TemplateRef,\n ContentChild,\n ViewChild,\n afterNextRender,\n computed,\n effect,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { MarkdownComponent } from '@lifecorner/angular-ui-kit/markdown';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport {\n ICON_CHECK_CIRCLE,\n ICON_CIRCLE_X,\n ICON_EXCLAMATION_TRIANGLE,\n ICON_INFORMATION_CIRCLE,\n} from '@lifecorner/angular-ui-kit/icons';\n\nexport type ChatMessageRole = 'user' | 'agent' | 'system';\n\n/**\n * Semantic status of a chat message — orthogonal to `role` (role = *who*\n * speaks; status = *what kind* of message). Reuses the semantic vocabulary of\n * `lc-alert` / `lc-callout` and drives the rail dot/accent colour + a status\n * icon. `'default'` (or omitted) reproduces the role-coloured behaviour exactly.\n */\nexport type ChatMessageStatus = 'default' | 'info' | 'success' | 'warning' | 'error';\n\n/**\n * Controls which messages are rendered as Markdown via `<lc-markdown>`.\n * - `false` (default): plain text rendering (legacy behavior)\n * - `true` / `'all'`: render every role as Markdown\n * - `'agent'`: only agent messages are rendered as Markdown (recommended)\n */\nexport type ChatRenderMarkdown = boolean | 'agent' | 'all';\n\n/**\n * Represents a file attached to a chat message or pending in the input.\n *\n * The `file` reference is only available for attachments produced by the\n * input (i.e. emitted via `messageSend` / `fileAttach`). Attachments\n * coming from the server or message history typically rely on `url` only.\n */\nexport interface ChatAttachment {\n id: string;\n name: string;\n /** MIME type, e.g. `image/png`, `application/pdf`. */\n type?: string;\n /** Size in bytes. */\n size?: number;\n /** Object URL or remote URL to preview / download the file. */\n url?: string;\n /** Raw browser File reference (set for newly uploaded files). */\n file?: File;\n}\n\nexport interface ChatMessage {\n id: string;\n role: ChatMessageRole;\n content: string;\n timestamp?: Date | string;\n avatar?: string;\n name?: string;\n streaming?: boolean;\n /**\n * Semantic status — colours the rail dot/accent and shows a status icon.\n * Independent of `role`: a `role: 'agent'` message can be `status: 'error'`\n * (failed reply) or `status: 'success'` (tool finished). Defaults to\n * `'default'`, which reproduces today's role-coloured behaviour exactly.\n * An `'error'` message never pulses and is announced assertively (`role=\"alert\"`).\n */\n status?: ChatMessageStatus;\n /** Optional file attachments associated with the message. */\n attachments?: ChatAttachment[];\n /** Arbitrary data passed to a custom messageTemplate. */\n data?: Record<string, unknown>;\n}\n\nexport interface ChatSendEvent {\n content: string;\n /** Files attached to the outgoing message, if any. */\n attachments?: ChatAttachment[];\n}\n\nexport interface ChatFileAttachEvent {\n attachments: ChatAttachment[];\n /** Files rejected because of `accept` / `maxFileSize` constraints. */\n rejected: { file: File; reason: 'type' | 'size' }[];\n}\n\n@Component({\n selector: 'lc-chat',\n standalone: true,\n imports: [NgTemplateOutlet, MarkdownComponent, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_INFORMATION_CIRCLE, ICON_CHECK_CIRCLE, ICON_EXCLAMATION_TRIANGLE, ICON_CIRCLE_X])],\n templateUrl: './chat.component.html',\n styleUrls: ['./chat.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Chat component for conversational user interfaces.\n *\n * Compact, document-style layout: agent/system turns flow as full-width text on\n * a left timeline rail, while user turns render as right-aligned, accent-tinted\n * bubbles (no name label) with the timestamp on a muted line beneath. Spacing is\n * a single space-efficient scale (tunable via the `--lc-chat-*` custom\n * properties); there is no separate roomy variant.\n *\n * Features:\n * - User, agent, and system message roles\n * - Right-aligned user bubbles with an optional avatar: `msg.avatar` image, or a\n * monogram built from `msg.name` initials (gated by `showAvatars`)\n * - Agent/system turns on a left rail with a status dot, avatar or semantic icon\n * - Streaming cursor indicator for AI responses\n * - Typing indicator with animated dots\n * - Auto-scroll to the latest message (new turns, streamed tokens, history\n * loads) — only while the reader is at the bottom; scrolling up pauses it\n * - Optional avatars and timestamps\n * - Configurable header with title\n * - Send on Enter with Shift+Enter for newline\n * - Optional file upload (`allowFileUpload`) with `accept`/`maxFileSize`\n * constraints, preview chips, and image/file thumbnails inside messages.\n *\n * @example Basic\n * ```html\n * <lc-chat [messages]=\"messages\" title=\"Support\" (messageSend)=\"onSend($event)\" />\n * ```\n *\n * @example With file upload\n * ```html\n * <lc-chat\n * [messages]=\"messages\"\n * [allowFileUpload]=\"true\"\n * accept=\"image/*,.pdf\"\n * [maxFileSize]=\"5 * 1024 * 1024\"\n * (messageSend)=\"onSend($event)\"\n * (fileAttach)=\"onAttach($event)\" />\n * ```\n */\nexport class ChatComponent {\n private static nextId = 0;\n /** Id of the composer textarea (links the visually-hidden label). */\n protected readonly inputId = `lc-chat-input-${++ChatComponent.nextId}`;\n\n /** Messages to display. */\n messages = input<ChatMessage[]>([]);\n\n /** Placeholder text for the input. */\n placeholder = input<string>('Nachricht eingeben…');\n\n /** Accessible name of the composer textarea (visually hidden label). */\n inputLabel = input<string>('Nachricht');\n\n /** Title displayed in the header. */\n title = input<string>('Chat');\n\n /** Whether the agent is currently streaming / typing. */\n isStreaming = input<boolean>(false);\n\n /** Show the header bar. */\n showHeader = input<boolean>(true);\n\n /**\n * Draw the chat's own border + rounded corners. Defaults to `true` (card\n * style). Set `false` to render flush/edge-to-edge — e.g. when the chat is\n * the full-height body of an `<lc-page-layout>` and should sit directly under\n * a page header without a nested box.\n */\n bordered = input<boolean>(true);\n\n /**\n * Where messages stack when they don't fill the height.\n * - `'top'` (default): messages grow from the top down.\n * - `'bottom'`: messages anchor to the bottom (empty space above), like most\n * messaging apps. Still scrolls to reveal the top once they overflow.\n */\n messageAnchor = input<'top' | 'bottom'>('top');\n\n /**\n * Width of the message thread and composer.\n * - `'full'` (default): messages span the full available width.\n * - `'narrow'`: a centered, readable column (~46rem) like a document-style chat.\n */\n contentWidth = input<'narrow' | 'full'>('full');\n\n /** Disable the input. */\n disabled = input<boolean>(false);\n\n /** Show avatars next to messages. */\n showAvatars = input<boolean>(true);\n\n /** Show timestamps. */\n showTimestamps = input<boolean>(true);\n\n /**\n * Render message content as Markdown using `<lc-markdown variant=\"chat\">`.\n * Defaults to `'agent'` so agent replies get rich formatting while user\n * input stays as plain text. Pass `false` to opt out, or `true`/`'all'`\n * to render every role as Markdown.\n */\n renderMarkdown = input<ChatRenderMarkdown>('agent');\n\n /** Enable the file-upload (paperclip) button in the input area. */\n allowFileUpload = input<boolean>(false);\n\n /**\n * Comma-separated list of accepted file types, forwarded to the native\n * `<input accept>` attribute. Examples: `image/*`, `.pdf,.docx`.\n */\n accept = input<string>('');\n\n /** Allow selecting multiple files at once. Defaults to `true`. */\n multiple = input<boolean>(true);\n\n /** Maximum file size in bytes. `0` disables the check. */\n maxFileSize = input<number>(0);\n\n /** BCP 47 locale of the message times (hour and minute). */\n locale = input('de-DE');\n\n /** Header badge while a message is streaming. */\n streamingLabel = input('Streaming…');\n\n /** Accessible name of the attach button (`allowFileUpload`). */\n attachAriaLabel = input('Datei anhängen');\n\n /** Accessible name of the send button. */\n sendAriaLabel = input('Send');\n\n /** Screen-reader prefix of messages with status `info`. */\n statusInfoLabel = input('Info:');\n\n /** Screen-reader prefix of messages with status `success`. */\n statusSuccessLabel = input('Erfolg:');\n\n /** Screen-reader prefix of messages with status `warning`. */\n statusWarningLabel = input('Warnung:');\n\n /** Screen-reader prefix of messages with status `error`. */\n statusErrorLabel = input('Fehler:');\n\n /** Accessible name prefix of a pending attachment's remove button, followed by the file name. */\n removeAttachmentAriaLabel = input('Entfernen:');\n\n /** Emits when user sends a message. */\n messageSend = output<ChatSendEvent>();\n\n /** Emits whenever files are added to the pending attachments list. */\n fileAttach = output<ChatFileAttachEvent>();\n\n /** Custom template for rendering message content. Context: { $implicit: ChatMessage } */\n @ContentChild('messageTemplate') messageTemplate?: TemplateRef<{ $implicit: ChatMessage }>;\n\n protected readonly inputValue = signal('');\n protected readonly pendingAttachments = signal<ChatAttachment[]>([]);\n private attachmentCounter = 0;\n\n private readonly injector = inject(Injector);\n private readonly destroyRef = inject(DestroyRef);\n private readonly ngZone = inject(NgZone);\n\n /**\n * Whether the reader is (near) the bottom of the thread. Auto-scroll only\n * follows new content while this is true, so scrolling up to re-read is\n * never fought. Plain field: updated from a passive scroll listener outside\n * the zone, read untracked by the follow effect.\n */\n private atBottom = true;\n\n /** Distance from the bottom (px) still counted as \"at the bottom\". */\n private static readonly AT_BOTTOM_THRESHOLD = 32;\n\n private readonly scrollContainer = viewChild.required<ElementRef<HTMLDivElement>>('scrollContainer');\n @ViewChild('fileInput') private fileInput?: ElementRef<HTMLInputElement>;\n @ViewChild('composerInput') private composerInput?: ElementRef<HTMLTextAreaElement>;\n\n /** Upper bound for the auto-growing composer before it starts scrolling (px). */\n private static readonly INPUT_MAX_HEIGHT = 192;\n\n protected readonly formattedMessages = computed(() =>\n this.messages().map(m => ({\n ...m,\n timestamp: m.timestamp instanceof Date ? m.timestamp : m.timestamp ? new Date(m.timestamp) : undefined,\n }))\n );\n\n /**\n * Text for the visually-hidden live region: the most recent *completed*\n * non-user message. Streaming turns are announced once they finish (a\n * token-by-token live region would be unusable); user turns were typed by\n * the reader; semantic-status messages carry their own `aria-live`/`alert`.\n */\n protected readonly announcement = computed(() => {\n const msgs = this.messages();\n for (let i = msgs.length - 1; i >= 0; i--) {\n const m = msgs[i];\n if (m.role === 'user' || m.streaming || this.isSemanticStatus(m.status)) continue;\n return m.name ? `${m.name}: ${m.content}` : m.content;\n }\n return '';\n });\n\n constructor() {\n // Passive scroll tracking outside the zone — no CD per scroll frame.\n afterNextRender(() => {\n const el = this.scrollContainer().nativeElement;\n const onScroll = () => {\n this.atBottom =\n el.scrollHeight - el.scrollTop - el.clientHeight < ChatComponent.AT_BOTTOM_THRESHOLD;\n };\n this.ngZone.runOutsideAngular(() => el.addEventListener('scroll', onScroll, { passive: true }));\n this.destroyRef.onDestroy(() => el.removeEventListener('scroll', onScroll));\n });\n\n // Follow the conversation: a new turn, a streamed token on the last turn\n // (the parent hands in a new `messages` array), a history load or the\n // typing indicator appearing all grow the thread — scroll after render,\n // but only if the reader was already at the bottom.\n effect(() => {\n this.messages();\n this.isStreaming();\n if (!this.atBottom) return;\n afterNextRender(() => this.scrollToBottom(), { injector: this.injector });\n });\n }\n\n protected onInput(event: Event): void {\n const el = event.target as HTMLTextAreaElement;\n this.inputValue.set(el.value);\n this.autoGrow(el);\n }\n\n /** Resize the textarea to fit its content so the composer hugs the text. */\n private autoGrow(el: HTMLTextAreaElement): void {\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, ChatComponent.INPUT_MAX_HEIGHT)}px`;\n }\n\n protected onKeydown(event: KeyboardEvent): void {\n // Enter while an IME composition is open commits the composition, not the message.\n if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {\n event.preventDefault();\n this.send();\n }\n }\n\n protected send(): void {\n const content = this.inputValue().trim();\n const attachments = this.pendingAttachments();\n if ((!content && attachments.length === 0) || this.disabled()) return;\n this.messageSend.emit({\n content,\n attachments: attachments.length ? attachments : undefined,\n });\n this.inputValue.set('');\n this.pendingAttachments.set([]);\n // Sending re-engages follow mode: the reader wants to see their own turn\n // and the reply, even if they had scrolled up before.\n this.atBottom = true;\n afterNextRender(() => this.scrollToBottom(), { injector: this.injector });\n // Collapse the auto-grown textarea back to a single row.\n const el = this.composerInput?.nativeElement;\n if (el) el.style.height = 'auto';\n }\n\n protected openFilePicker(): void {\n if (this.disabled() || !this.allowFileUpload()) return;\n this.fileInput?.nativeElement.click();\n }\n\n protected onFilesSelected(event: Event): void {\n const input = event.target as HTMLInputElement;\n if (!input.files || input.files.length === 0) return;\n this.addFiles(Array.from(input.files));\n // Reset so the same file can be re-selected later.\n input.value = '';\n }\n\n protected removeAttachment(id: string): void {\n this.pendingAttachments.update(list => {\n const removed = list.find(a => a.id === id);\n if (removed?.url && removed.file) {\n URL.revokeObjectURL(removed.url);\n }\n return list.filter(a => a.id !== id);\n });\n }\n\n protected formatBytes(size: number | undefined): string {\n if (!size && size !== 0) return '';\n if (size < 1024) return `${size} B`;\n if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;\n return `${(size / (1024 * 1024)).toFixed(1)} MB`;\n }\n\n protected isImage(att: ChatAttachment): boolean {\n return !!att.type?.startsWith('image/');\n }\n\n private addFiles(files: File[]): void {\n const max = this.maxFileSize();\n const acceptList = this.accept()\n .split(',')\n .map(s => s.trim())\n .filter(Boolean);\n\n const accepted: ChatAttachment[] = [];\n const rejected: { file: File; reason: 'type' | 'size' }[] = [];\n\n for (const file of files) {\n if (max > 0 && file.size > max) {\n rejected.push({ file, reason: 'size' });\n continue;\n }\n if (acceptList.length > 0 && !this.matchesAccept(file, acceptList)) {\n rejected.push({ file, reason: 'type' });\n continue;\n }\n accepted.push({\n id: `att-${Date.now()}-${this.attachmentCounter++}`,\n name: file.name,\n type: file.type,\n size: file.size,\n url: URL.createObjectURL(file),\n file,\n });\n }\n\n if (accepted.length) {\n this.pendingAttachments.update(list => [...list, ...accepted]);\n }\n if (accepted.length || rejected.length) {\n this.fileAttach.emit({ attachments: accepted, rejected });\n }\n }\n\n private matchesAccept(file: File, acceptList: string[]): boolean {\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return acceptList.some(rule => {\n const r = rule.toLowerCase();\n if (r.startsWith('.')) return name.endsWith(r);\n if (r.endsWith('/*')) return type.startsWith(r.slice(0, -1));\n return type === r;\n });\n }\n\n protected formatTime(date: Date | undefined): string {\n if (!date) return '';\n return date.toLocaleTimeString(this.locale(), { hour: '2-digit', minute: '2-digit' });\n }\n\n /**\n * Up to two uppercase initials from a name (first + last word), used for the\n * user avatar monogram when no `avatar` image is provided. Returns `''` for an\n * empty/whitespace name so the caller can skip rendering.\n */\n protected initials(name: string | undefined): string {\n if (!name) return '';\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '';\n const first = parts[0][0] ?? '';\n const last = parts.length > 1 ? parts[parts.length - 1][0] ?? '' : '';\n return (first + last).toUpperCase();\n }\n\n /**\n * Whether to show an avatar on a user turn: only when avatars are enabled and\n * the message carries either an image or a name we can build a monogram from.\n * Keeps the labelless bubble clean when there's nothing to show.\n */\n protected showUserAvatar(msg: ChatMessage): boolean {\n return (\n msg.role === 'user' &&\n this.showAvatars() &&\n !!(msg.avatar || this.initials(msg.name))\n );\n }\n\n protected shouldRenderMarkdown(role: ChatMessageRole): boolean {\n const mode = this.renderMarkdown();\n if (mode === false) return false;\n if (mode === true || mode === 'all') return true;\n return role === mode;\n }\n\n /** Whether a message carries a semantic (non-`default`) status. */\n protected isSemanticStatus(status: ChatMessageStatus | undefined): boolean {\n return !!status && status !== 'default';\n }\n\n /** Rail icon for a semantic status — reuses the `lc-alert` icon names. */\n protected statusIcon(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return 'information-circle';\n case 'success':\n return 'check-circle';\n case 'warning':\n return 'exclamation-triangle';\n case 'error':\n return 'x-circle';\n default:\n return '';\n }\n }\n\n /** Semantic token colour for a status dot/icon. */\n protected statusColor(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return 'var(--color-info-500)';\n case 'success':\n return 'var(--color-success-500)';\n case 'warning':\n return 'var(--color-warning-500)';\n case 'error':\n return 'var(--color-error-500)';\n default:\n return 'currentColor';\n }\n }\n\n /** Visually-hidden prefix so colour/icon is never the only signal. */\n protected statusLabel(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return this.statusInfoLabel();\n case 'success':\n return this.statusSuccessLabel();\n case 'warning':\n return this.statusWarningLabel();\n case 'error':\n return this.statusErrorLabel();\n default:\n return '';\n }\n }\n\n /** ARIA live priority: assertive for `error`, polite for other statuses. */\n protected statusAriaLive(status: ChatMessageStatus | undefined): 'assertive' | 'polite' | null {\n if (status === 'error') return 'assertive';\n if (this.isSemanticStatus(status)) return 'polite';\n return null;\n }\n\n private scrollToBottom(): void {\n const el = this.scrollContainer().nativeElement;\n el.scrollTop = el.scrollHeight;\n }\n}\n","<div\n class=\"lc-chat\"\n [class.lc-chat--borderless]=\"!bordered()\"\n [class.lc-chat--width-narrow]=\"contentWidth() === 'narrow'\"\n [class.lc-chat--width-full]=\"contentWidth() === 'full'\"\n>\n @if (showHeader()) {\n <div class=\"lc-chat__header\">\n <span class=\"lc-chat__title\">{{ title() }}</span>\n @if (isStreaming()) {\n <span class=\"lc-chat__streaming-badge\">{{ streamingLabel() }}</span>\n }\n </div>\n }\n\n <div\n class=\"lc-chat__messages\"\n [class.lc-chat__messages--anchor-bottom]=\"messageAnchor() === 'bottom'\"\n #scrollContainer\n >\n <div class=\"lc-chat__thread\">\n @for (msg of formattedMessages(); track msg.id) {\n <div\n class=\"lc-chat__message\"\n [class.lc-chat__message--user]=\"msg.role === 'user'\"\n [class.lc-chat__message--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__message--system]=\"msg.role === 'system'\"\n [class.lc-chat__message--avatar]=\"showUserAvatar(msg)\"\n [class.lc-chat__message--streaming]=\"msg.streaming\"\n [attr.role]=\"msg.status === 'error' ? 'alert' : null\"\n [attr.aria-live]=\"statusAriaLive(msg.status)\"\n >\n @if ((msg.role !== 'user' && (showAvatars() || isSemanticStatus(msg.status))) || showUserAvatar(msg)) {\n <div class=\"lc-chat__marker\" [class.lc-chat__marker--user]=\"msg.role === 'user'\">\n @if (isSemanticStatus(msg.status) && msg.role !== 'user') {\n <span class=\"lc-chat__status-icon\">\n <lc-icon [name]=\"statusIcon(msg.status)\" size=\"xs\" [color]=\"statusColor(msg.status)\" [decorative]=\"true\" />\n </span>\n } @else if (msg.avatar) {\n <img [src]=\"msg.avatar\" [alt]=\"msg.name || msg.role\" class=\"lc-chat__avatar-img\" />\n } @else if (msg.role === 'user') {\n <span class=\"lc-chat__monogram\" [attr.aria-label]=\"msg.name\">{{ initials(msg.name) }}</span>\n } @else {\n <span\n class=\"lc-chat__dot\"\n [class.lc-chat__dot--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__dot--system]=\"msg.role === 'system'\"\n ></span>\n }\n </div>\n }\n\n <div class=\"lc-chat__bubble\" [class.lc-chat__bubble--streaming]=\"msg.streaming\">\n @if (msg.name && msg.role !== 'user') {\n @if (showTimestamps() && msg.timestamp) {\n <div class=\"lc-chat__meta lc-chat__meta--agent\">\n <span class=\"lc-chat__name\">{{ msg.name }}</span>\n <span class=\"lc-chat__time\">{{ formatTime(msg.timestamp) }}</span>\n </div>\n } @else {\n <div class=\"lc-chat__name\">{{ msg.name }}</div>\n }\n }\n <div class=\"lc-chat__content\">\n @if (isSemanticStatus(msg.status)) {\n <span class=\"lc-chat__sr-only\">{{ statusLabel(msg.status) }} </span>\n }\n @if (messageTemplate) {\n <ng-container *ngTemplateOutlet=\"messageTemplate; context: { $implicit: msg }\" />\n } @else if (shouldRenderMarkdown(msg.role)) {\n <lc-markdown [content]=\"msg.content\" variant=\"chat\" />\n } @else {\n {{ msg.content }}\n }\n <span class=\"lc-chat__cursor\" [class.lc-chat__cursor--visible]=\"msg.streaming\"></span>\n </div>\n @if (msg.attachments?.length) {\n <div class=\"lc-chat__attachments\">\n @for (att of msg.attachments; track att.id) {\n @if (isImage(att) && att.url) {\n <a class=\"lc-chat__attachment lc-chat__attachment--image\" [href]=\"att.url\" target=\"_blank\" rel=\"noopener\">\n <img [src]=\"att.url\" [alt]=\"att.name\" />\n </a>\n } @else {\n <a\n class=\"lc-chat__attachment lc-chat__attachment--file\"\n [href]=\"att.url || null\"\n [attr.download]=\"att.url ? att.name : null\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n <span class=\"lc-chat__attachment-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__attachment-size\">{{ formatBytes(att.size) }}</span>\n }\n </a>\n }\n }\n </div>\n }\n @if (showTimestamps() && msg.timestamp && msg.role !== 'user' && !msg.name) {\n <div class=\"lc-chat__time lc-chat__time--tight\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n @if (showTimestamps() && msg.timestamp && msg.role === 'user') {\n <div class=\"lc-chat__time lc-chat__time--user\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n }\n\n @if (isStreaming() && !(formattedMessages().length && formattedMessages()[formattedMessages().length - 1].streaming)) {\n <div class=\"lc-chat__message lc-chat__message--agent\">\n @if (showAvatars()) {\n <div class=\"lc-chat__marker\">\n <span class=\"lc-chat__dot lc-chat__dot--agent\"></span>\n </div>\n }\n <div class=\"lc-chat__bubble\">\n <div class=\"lc-chat__typing\">\n <span></span><span></span><span></span>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Announces the latest completed non-user turn (see `announcement`). -->\n <div class=\"lc-chat__sr-only lc-chat__announcer\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</div>\n\n <div class=\"lc-chat__input-area\">\n <div class=\"lc-chat__composer\">\n @if (allowFileUpload() && pendingAttachments().length) {\n <div class=\"lc-chat__pending-attachments\">\n @for (att of pendingAttachments(); track att.id) {\n <div class=\"lc-chat__pending-attachment\" [class.lc-chat__pending-attachment--image]=\"isImage(att)\">\n @if (isImage(att) && att.url) {\n <img class=\"lc-chat__pending-thumb\" [src]=\"att.url\" [alt]=\"att.name\" />\n } @else {\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n }\n <span class=\"lc-chat__pending-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__pending-size\">{{ formatBytes(att.size) }}</span>\n }\n <button\n type=\"button\"\n class=\"lc-chat__pending-remove\"\n (click)=\"removeAttachment(att.id)\"\n [attr.aria-label]=\"removeAttachmentAriaLabel() + ' ' + att.name\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M3 3l6 6M9 3l-6 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n }\n\n <label class=\"lc-chat__sr-only\" [for]=\"inputId\">{{ inputLabel() }}</label>\n <textarea\n #composerInput\n [id]=\"inputId\"\n class=\"lc-chat__input\"\n [placeholder]=\"placeholder()\"\n [value]=\"inputValue()\"\n [disabled]=\"disabled()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n rows=\"1\"\n ></textarea>\n\n <div class=\"lc-chat__composer-actions\">\n @if (allowFileUpload()) {\n <input\n #fileInput\n type=\"file\"\n class=\"lc-chat__file-input\"\n [accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n (change)=\"onFilesSelected($event)\"\n />\n <button\n type=\"button\"\n class=\"lc-chat__attach-btn\"\n [disabled]=\"disabled()\"\n (click)=\"openFilePicker()\"\n [attr.aria-label]=\"attachAriaLabel()\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M14.5 9.5l-5 5a3 3 0 0 1-4.243-4.243l6.364-6.364a2 2 0 0 1 2.829 2.829l-6.364 6.364a1 1 0 1 1-1.415-1.415L11.5 6.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </button>\n }\n <button\n class=\"lc-chat__send-btn\"\n [disabled]=\"(!inputValue().trim() && !pendingAttachments().length) || disabled()\"\n (click)=\"send()\"\n [attr.aria-label]=\"sendAriaLabel()\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M3 10l7-7m0 0l7 7m-7-7v14\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" transform=\"rotate(90 10 10)\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-chat.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/chat/chat.component.ts","../../../../libs/angular-ui-kit/src/lib/chat/chat.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n Injector,\n NgZone,\n TemplateRef,\n ContentChild,\n ViewChild,\n afterNextRender,\n computed,\n effect,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { MarkdownComponent } from '@lifecorner/angular-ui-kit/markdown';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport {\n ICON_CHECK_CIRCLE,\n ICON_CIRCLE_X,\n ICON_EXCLAMATION_TRIANGLE,\n ICON_INFORMATION_CIRCLE,\n} from '@lifecorner/angular-ui-kit/icons';\n\nexport type ChatMessageRole = 'user' | 'agent' | 'system';\n\n/**\n * Semantic status of a chat message — orthogonal to `role` (role = *who*\n * speaks; status = *what kind* of message). Reuses the semantic vocabulary of\n * `lc-alert` / `lc-callout` and drives the rail dot/accent colour + a status\n * icon. `'default'` (or omitted) reproduces the role-coloured behaviour exactly.\n */\nexport type ChatMessageStatus = 'default' | 'info' | 'success' | 'warning' | 'error';\n\n/**\n * Controls which messages are rendered as Markdown via `<lc-markdown>`.\n * - `false` (default): plain text rendering (legacy behavior)\n * - `true` / `'all'`: render every role as Markdown\n * - `'agent'`: only agent messages are rendered as Markdown (recommended)\n */\nexport type ChatRenderMarkdown = boolean | 'agent' | 'all';\n\n/**\n * Represents a file attached to a chat message or pending in the input.\n *\n * The `file` reference is only available for attachments produced by the\n * input (i.e. emitted via `messageSend` / `fileAttach`). Attachments\n * coming from the server or message history typically rely on `url` only.\n */\nexport interface ChatAttachment {\n id: string;\n name: string;\n /** MIME type, e.g. `image/png`, `application/pdf`. */\n type?: string;\n /** Size in bytes. */\n size?: number;\n /** Object URL or remote URL to preview / download the file. */\n url?: string;\n /** Raw browser File reference (set for newly uploaded files). */\n file?: File;\n}\n\nexport interface ChatMessage {\n id: string;\n role: ChatMessageRole;\n content: string;\n timestamp?: Date | string;\n avatar?: string;\n name?: string;\n streaming?: boolean;\n /**\n * Semantic status — colours the rail dot/accent and shows a status icon.\n * Independent of `role`: a `role: 'agent'` message can be `status: 'error'`\n * (failed reply) or `status: 'success'` (tool finished). Defaults to\n * `'default'`, which reproduces today's role-coloured behaviour exactly.\n * An `'error'` message never pulses and is announced assertively (`role=\"alert\"`).\n */\n status?: ChatMessageStatus;\n /** Optional file attachments associated with the message. */\n attachments?: ChatAttachment[];\n /** Arbitrary data passed to a custom messageTemplate. */\n data?: Record<string, unknown>;\n}\n\nexport interface ChatSendEvent {\n content: string;\n /** Files attached to the outgoing message, if any. */\n attachments?: ChatAttachment[];\n}\n\nexport interface ChatFileAttachEvent {\n attachments: ChatAttachment[];\n /** Files rejected because of `accept` / `maxFileSize` constraints. */\n rejected: { file: File; reason: 'type' | 'size' }[];\n}\n\n@Component({\n selector: 'lc-chat',\n standalone: true,\n imports: [NgTemplateOutlet, MarkdownComponent, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_INFORMATION_CIRCLE, ICON_CHECK_CIRCLE, ICON_EXCLAMATION_TRIANGLE, ICON_CIRCLE_X])],\n templateUrl: './chat.component.html',\n styleUrls: ['./chat.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Chat component for conversational user interfaces.\n *\n * Compact, document-style layout: agent/system turns flow as full-width text on\n * a left timeline rail, while user turns render as right-aligned, accent-tinted\n * bubbles (no name label) with the timestamp on a muted line beneath. Spacing is\n * a single space-efficient scale (tunable via the `--lc-chat-*` custom\n * properties); there is no separate roomy variant.\n *\n * Features:\n * - User, agent, and system message roles\n * - Right-aligned user bubbles with an optional avatar: `msg.avatar` image, or a\n * monogram built from `msg.name` initials (gated by `showAvatars`)\n * - Agent/system turns on a left rail with a status dot, avatar or semantic icon\n * - Streaming cursor indicator for AI responses\n * - Typing indicator with animated dots\n * - Auto-scroll to the latest message (new turns, streamed tokens, history\n * loads) — only while the reader is at the bottom; scrolling up pauses it\n * - Optional avatars and timestamps\n * - Configurable header with title\n * - Send on Enter with Shift+Enter for newline\n * - Optional file upload (`allowFileUpload`) with `accept`/`maxFileSize`\n * constraints, preview chips, and image/file thumbnails inside messages.\n *\n * @example Basic\n * ```html\n * <lc-chat [messages]=\"messages\" title=\"Support\" (messageSend)=\"onSend($event)\" />\n * ```\n *\n * @example With file upload\n * ```html\n * <lc-chat\n * [messages]=\"messages\"\n * [allowFileUpload]=\"true\"\n * accept=\"image/*,.pdf\"\n * [maxFileSize]=\"5 * 1024 * 1024\"\n * (messageSend)=\"onSend($event)\"\n * (fileAttach)=\"onAttach($event)\" />\n * ```\n */\nexport class ChatComponent {\n private static nextId = 0;\n /** Id of the composer textarea (links the visually-hidden label). */\n protected readonly inputId = `lc-chat-input-${++ChatComponent.nextId}`;\n\n /** Messages to display. */\n messages = input<ChatMessage[]>([]);\n\n /** Placeholder text for the input. */\n placeholder = input<string>('Nachricht eingeben…');\n\n /** Accessible name of the composer textarea (visually hidden label). */\n inputLabel = input<string>('Nachricht');\n\n /** Title displayed in the header. */\n title = input<string>('Chat');\n\n /** Whether the agent is currently streaming / typing. */\n isStreaming = input<boolean>(false);\n\n /** Show the header bar. */\n showHeader = input<boolean>(true);\n\n /**\n * Draw the chat's own border + rounded corners. Defaults to `true` (card\n * style). Set `false` to render flush/edge-to-edge — e.g. when the chat is\n * the full-height body of an `<lc-page-layout>` and should sit directly under\n * a page header without a nested box.\n */\n bordered = input<boolean>(true);\n\n /**\n * Where messages stack when they don't fill the height.\n * - `'top'` (default): messages grow from the top down.\n * - `'bottom'`: messages anchor to the bottom (empty space above), like most\n * messaging apps. Still scrolls to reveal the top once they overflow.\n */\n messageAnchor = input<'top' | 'bottom'>('top');\n\n /**\n * Width of the message thread and composer.\n * - `'full'` (default): messages span the full available width.\n * - `'narrow'`: a centered, readable column (~46rem) like a document-style chat.\n */\n contentWidth = input<'narrow' | 'full'>('full');\n\n /** Disable the input. */\n disabled = input<boolean>(false);\n\n /** Show avatars next to messages. */\n showAvatars = input<boolean>(true);\n\n /** Show timestamps. */\n showTimestamps = input<boolean>(true);\n\n /**\n * Render message content as Markdown using `<lc-markdown variant=\"chat\">`.\n * Defaults to `'agent'` so agent replies get rich formatting while user\n * input stays as plain text. Pass `false` to opt out, or `true`/`'all'`\n * to render every role as Markdown.\n */\n renderMarkdown = input<ChatRenderMarkdown>('agent');\n\n /** Enable the file-upload (paperclip) button in the input area. */\n allowFileUpload = input<boolean>(false);\n\n /**\n * Comma-separated list of accepted file types, forwarded to the native\n * `<input accept>` attribute. Examples: `image/*`, `.pdf,.docx`.\n */\n accept = input<string>('');\n\n /** Allow selecting multiple files at once. Defaults to `true`. */\n multiple = input<boolean>(true);\n\n /** Maximum file size in bytes. `0` disables the check. */\n maxFileSize = input<number>(0);\n\n /** BCP 47 locale of the message times (hour and minute). */\n locale = input('de-DE');\n\n /** Header badge while a message is streaming. */\n streamingLabel = input('Streaming…');\n\n /** Accessible name of the attach button (`allowFileUpload`). */\n attachAriaLabel = input('Datei anhängen');\n\n /** Accessible name of the send button. */\n sendAriaLabel = input('Send');\n\n /** Screen-reader prefix of messages with status `info`. */\n statusInfoLabel = input('Info:');\n\n /** Screen-reader prefix of messages with status `success`. */\n statusSuccessLabel = input('Erfolg:');\n\n /** Screen-reader prefix of messages with status `warning`. */\n statusWarningLabel = input('Warnung:');\n\n /** Screen-reader prefix of messages with status `error`. */\n statusErrorLabel = input('Fehler:');\n\n /** Accessible name prefix of a pending attachment's remove button, followed by the file name. */\n removeAttachmentAriaLabel = input('Entfernen:');\n\n /** Emits when user sends a message. */\n messageSend = output<ChatSendEvent>();\n\n /** Emits whenever files are added to the pending attachments list. */\n fileAttach = output<ChatFileAttachEvent>();\n\n /** Custom template for rendering message content. Context: { $implicit: ChatMessage } */\n @ContentChild('messageTemplate') messageTemplate?: TemplateRef<{ $implicit: ChatMessage }>;\n\n protected readonly inputValue = signal('');\n protected readonly pendingAttachments = signal<ChatAttachment[]>([]);\n private attachmentCounter = 0;\n\n private readonly injector = inject(Injector);\n private readonly destroyRef = inject(DestroyRef);\n private readonly ngZone = inject(NgZone);\n\n /**\n * Whether the reader is (near) the bottom of the thread. Auto-scroll only\n * follows new content while this is true, so scrolling up to re-read is\n * never fought. Plain field: updated from a passive scroll listener outside\n * the zone, read untracked by the follow effect.\n */\n private atBottom = true;\n\n /** Distance from the bottom (px) still counted as \"at the bottom\". */\n private static readonly AT_BOTTOM_THRESHOLD = 32;\n\n private readonly scrollContainer = viewChild.required<ElementRef<HTMLDivElement>>('scrollContainer');\n @ViewChild('fileInput') private fileInput?: ElementRef<HTMLInputElement>;\n @ViewChild('composerInput') private composerInput?: ElementRef<HTMLTextAreaElement>;\n\n /** Upper bound for the auto-growing composer before it starts scrolling (px). */\n private static readonly INPUT_MAX_HEIGHT = 192;\n\n protected readonly formattedMessages = computed(() =>\n this.messages().map(m => ({\n ...m,\n timestamp: m.timestamp instanceof Date ? m.timestamp : m.timestamp ? new Date(m.timestamp) : undefined,\n }))\n );\n\n /**\n * Text for the visually-hidden live region: the most recent *completed*\n * non-user message. Streaming turns are announced once they finish (a\n * token-by-token live region would be unusable); user turns were typed by\n * the reader; semantic-status messages carry their own `aria-live`/`alert`.\n */\n protected readonly announcement = computed(() => {\n const msgs = this.messages();\n for (let i = msgs.length - 1; i >= 0; i--) {\n const m = msgs[i];\n if (m.role === 'user' || m.streaming || this.isSemanticStatus(m.status)) continue;\n return m.name ? `${m.name}: ${m.content}` : m.content;\n }\n return '';\n });\n\n constructor() {\n // Passive scroll tracking outside the zone — no CD per scroll frame.\n afterNextRender(() => {\n const el = this.scrollContainer().nativeElement;\n const onScroll = () => {\n this.atBottom =\n el.scrollHeight - el.scrollTop - el.clientHeight < ChatComponent.AT_BOTTOM_THRESHOLD;\n };\n this.ngZone.runOutsideAngular(() => el.addEventListener('scroll', onScroll, { passive: true }));\n this.destroyRef.onDestroy(() => el.removeEventListener('scroll', onScroll));\n });\n\n // Follow the conversation: a new turn, a streamed token on the last turn\n // (the parent hands in a new `messages` array), a history load or the\n // typing indicator appearing all grow the thread — scroll after render,\n // but only if the reader was already at the bottom.\n effect(() => {\n this.messages();\n this.isStreaming();\n if (!this.atBottom) return;\n afterNextRender(() => this.scrollToBottom(), { injector: this.injector });\n });\n }\n\n protected onInput(event: Event): void {\n const el = event.target as HTMLTextAreaElement;\n this.inputValue.set(el.value);\n this.autoGrow(el);\n }\n\n /** Resize the textarea to fit its content so the composer hugs the text. */\n private autoGrow(el: HTMLTextAreaElement): void {\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, ChatComponent.INPUT_MAX_HEIGHT)}px`;\n }\n\n protected onKeydown(event: KeyboardEvent): void {\n // Enter while an IME composition is open commits the composition, not the message.\n if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {\n event.preventDefault();\n this.send();\n }\n }\n\n protected send(): void {\n const content = this.inputValue().trim();\n const attachments = this.pendingAttachments();\n if ((!content && attachments.length === 0) || this.disabled()) return;\n this.messageSend.emit({\n content,\n attachments: attachments.length ? attachments : undefined,\n });\n this.inputValue.set('');\n this.pendingAttachments.set([]);\n // Sending re-engages follow mode: the reader wants to see their own turn\n // and the reply, even if they had scrolled up before.\n this.atBottom = true;\n afterNextRender(() => this.scrollToBottom(), { injector: this.injector });\n // Collapse the auto-grown textarea back to a single row.\n const el = this.composerInput?.nativeElement;\n if (el) el.style.height = 'auto';\n }\n\n protected openFilePicker(): void {\n if (this.disabled() || !this.allowFileUpload()) return;\n this.fileInput?.nativeElement.click();\n }\n\n protected onFilesSelected(event: Event): void {\n const input = event.target as HTMLInputElement;\n if (!input.files || input.files.length === 0) return;\n this.addFiles(Array.from(input.files));\n // Reset so the same file can be re-selected later.\n input.value = '';\n }\n\n protected removeAttachment(id: string): void {\n this.pendingAttachments.update(list => {\n const removed = list.find(a => a.id === id);\n if (removed?.url && removed.file) {\n URL.revokeObjectURL(removed.url);\n }\n return list.filter(a => a.id !== id);\n });\n }\n\n protected formatBytes(size: number | undefined): string {\n if (!size && size !== 0) return '';\n if (size < 1024) return `${size} B`;\n if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;\n return `${(size / (1024 * 1024)).toFixed(1)} MB`;\n }\n\n protected isImage(att: ChatAttachment): boolean {\n return !!att.type?.startsWith('image/');\n }\n\n private addFiles(files: File[]): void {\n const max = this.maxFileSize();\n const acceptList = this.accept()\n .split(',')\n .map(s => s.trim())\n .filter(Boolean);\n\n const accepted: ChatAttachment[] = [];\n const rejected: { file: File; reason: 'type' | 'size' }[] = [];\n\n for (const file of files) {\n if (max > 0 && file.size > max) {\n rejected.push({ file, reason: 'size' });\n continue;\n }\n if (acceptList.length > 0 && !this.matchesAccept(file, acceptList)) {\n rejected.push({ file, reason: 'type' });\n continue;\n }\n accepted.push({\n id: `att-${Date.now()}-${this.attachmentCounter++}`,\n name: file.name,\n type: file.type,\n size: file.size,\n url: URL.createObjectURL(file),\n file,\n });\n }\n\n if (accepted.length) {\n this.pendingAttachments.update(list => [...list, ...accepted]);\n }\n if (accepted.length || rejected.length) {\n this.fileAttach.emit({ attachments: accepted, rejected });\n }\n }\n\n private matchesAccept(file: File, acceptList: string[]): boolean {\n const name = file.name.toLowerCase();\n const type = file.type.toLowerCase();\n return acceptList.some(rule => {\n const r = rule.toLowerCase();\n if (r.startsWith('.')) return name.endsWith(r);\n if (r.endsWith('/*')) return type.startsWith(r.slice(0, -1));\n return type === r;\n });\n }\n\n protected formatTime(date: Date | undefined): string {\n if (!date) return '';\n return date.toLocaleTimeString(this.locale(), { hour: '2-digit', minute: '2-digit' });\n }\n\n /**\n * Up to two uppercase initials from a name (first + last word), used for the\n * user avatar monogram when no `avatar` image is provided. Returns `''` for an\n * empty/whitespace name so the caller can skip rendering.\n */\n protected initials(name: string | undefined): string {\n if (!name) return '';\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '';\n const first = parts[0][0] ?? '';\n const last = parts.length > 1 ? parts[parts.length - 1][0] ?? '' : '';\n return (first + last).toUpperCase();\n }\n\n /**\n * Whether to show an avatar on a user turn: only when avatars are enabled and\n * the message carries either an image or a name we can build a monogram from.\n * Keeps the labelless bubble clean when there's nothing to show.\n */\n protected showUserAvatar(msg: ChatMessage): boolean {\n return (\n msg.role === 'user' &&\n this.showAvatars() &&\n !!(msg.avatar || this.initials(msg.name))\n );\n }\n\n protected shouldRenderMarkdown(role: ChatMessageRole): boolean {\n const mode = this.renderMarkdown();\n if (mode === false) return false;\n if (mode === true || mode === 'all') return true;\n return role === mode;\n }\n\n /** Whether a message carries a semantic (non-`default`) status. */\n protected isSemanticStatus(status: ChatMessageStatus | undefined): boolean {\n return !!status && status !== 'default';\n }\n\n /** Rail icon for a semantic status — reuses the `lc-alert` icon names. */\n protected statusIcon(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return 'information-circle';\n case 'success':\n return 'check-circle';\n case 'warning':\n return 'exclamation-triangle';\n case 'error':\n return 'x-circle';\n default:\n return '';\n }\n }\n\n /** Semantic token colour for a status dot/icon. */\n protected statusColor(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return 'var(--color-info-500)';\n case 'success':\n return 'var(--color-success-500)';\n case 'warning':\n return 'var(--color-warning-500)';\n case 'error':\n return 'var(--color-error-500)';\n default:\n return 'currentColor';\n }\n }\n\n /** Visually-hidden prefix so colour/icon is never the only signal. */\n protected statusLabel(status: ChatMessageStatus | undefined): string {\n switch (status) {\n case 'info':\n return this.statusInfoLabel();\n case 'success':\n return this.statusSuccessLabel();\n case 'warning':\n return this.statusWarningLabel();\n case 'error':\n return this.statusErrorLabel();\n default:\n return '';\n }\n }\n\n /** ARIA live priority: assertive for `error`, polite for other statuses. */\n protected statusAriaLive(status: ChatMessageStatus | undefined): 'assertive' | 'polite' | null {\n if (status === 'error') return 'assertive';\n if (this.isSemanticStatus(status)) return 'polite';\n return null;\n }\n\n private scrollToBottom(): void {\n const el = this.scrollContainer().nativeElement;\n el.scrollTop = el.scrollHeight;\n }\n}\n","<div\n class=\"lc-chat\"\n [class.lc-chat--borderless]=\"!bordered()\"\n [class.lc-chat--width-narrow]=\"contentWidth() === 'narrow'\"\n [class.lc-chat--width-full]=\"contentWidth() === 'full'\"\n>\n @if (showHeader()) {\n <div class=\"lc-chat__header\">\n <span class=\"lc-chat__title\">{{ title() }}</span>\n @if (isStreaming()) {\n <span class=\"lc-chat__streaming-badge\">{{ streamingLabel() }}</span>\n }\n </div>\n }\n\n <div\n class=\"lc-chat__messages\"\n [class.lc-chat__messages--anchor-bottom]=\"messageAnchor() === 'bottom'\"\n #scrollContainer\n >\n <div class=\"lc-chat__thread\">\n @for (msg of formattedMessages(); track msg.id) {\n <div\n class=\"lc-chat__message\"\n [class.lc-chat__message--user]=\"msg.role === 'user'\"\n [class.lc-chat__message--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__message--system]=\"msg.role === 'system'\"\n [class.lc-chat__message--avatar]=\"showUserAvatar(msg)\"\n [class.lc-chat__message--streaming]=\"msg.streaming\"\n [attr.role]=\"msg.status === 'error' ? 'alert' : null\"\n [attr.aria-live]=\"statusAriaLive(msg.status)\"\n >\n @if ((msg.role !== 'user' && (showAvatars() || isSemanticStatus(msg.status))) || showUserAvatar(msg)) {\n <div class=\"lc-chat__marker\" [class.lc-chat__marker--user]=\"msg.role === 'user'\">\n @if (isSemanticStatus(msg.status) && msg.role !== 'user') {\n <span class=\"lc-chat__status-icon\">\n <lc-icon [name]=\"statusIcon(msg.status)\" size=\"xs\" [color]=\"statusColor(msg.status)\" [decorative]=\"true\" />\n </span>\n } @else if (msg.avatar) {\n <img [src]=\"msg.avatar\" [alt]=\"msg.name || msg.role\" class=\"lc-chat__avatar-img\" />\n } @else if (msg.role === 'user') {\n <span class=\"lc-chat__monogram\" [attr.aria-label]=\"msg.name\">{{ initials(msg.name) }}</span>\n } @else {\n <span\n class=\"lc-chat__dot\"\n [class.lc-chat__dot--agent]=\"msg.role === 'agent'\"\n [class.lc-chat__dot--system]=\"msg.role === 'system'\"\n ></span>\n }\n </div>\n }\n\n <div class=\"lc-chat__bubble\" [class.lc-chat__bubble--streaming]=\"msg.streaming\">\n @if (msg.name && msg.role !== 'user') {\n @if (showTimestamps() && msg.timestamp) {\n <div class=\"lc-chat__meta lc-chat__meta--agent\">\n <span class=\"lc-chat__name\">{{ msg.name }}</span>\n <span class=\"lc-chat__time\">{{ formatTime(msg.timestamp) }}</span>\n </div>\n } @else {\n <div class=\"lc-chat__name\">{{ msg.name }}</div>\n }\n }\n <div class=\"lc-chat__content\">\n @if (isSemanticStatus(msg.status)) {\n <span class=\"lc-chat__sr-only\">{{ statusLabel(msg.status) }} </span>\n }\n @if (messageTemplate) {\n <ng-container *ngTemplateOutlet=\"messageTemplate; context: { $implicit: msg }\" />\n } @else if (shouldRenderMarkdown(msg.role)) {\n <lc-markdown [content]=\"msg.content\" variant=\"chat\" />\n } @else {\n {{ msg.content }}\n }\n <span class=\"lc-chat__cursor\" [class.lc-chat__cursor--visible]=\"msg.streaming\"></span>\n </div>\n @if (msg.attachments?.length) {\n <div class=\"lc-chat__attachments\">\n @for (att of msg.attachments; track att.id) {\n @if (isImage(att) && att.url) {\n <a class=\"lc-chat__attachment lc-chat__attachment--image\" [href]=\"att.url\" target=\"_blank\" rel=\"noopener\">\n <img [src]=\"att.url\" [alt]=\"att.name\" />\n </a>\n } @else {\n <a\n class=\"lc-chat__attachment lc-chat__attachment--file\"\n [href]=\"att.url || null\"\n [attr.download]=\"att.url ? att.name : null\"\n target=\"_blank\"\n rel=\"noopener\"\n >\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n <span class=\"lc-chat__attachment-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__attachment-size\">{{ formatBytes(att.size) }}</span>\n }\n </a>\n }\n }\n </div>\n }\n @if (showTimestamps() && msg.timestamp && msg.role !== 'user' && !msg.name) {\n <div class=\"lc-chat__time lc-chat__time--tight\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n @if (showTimestamps() && msg.timestamp && msg.role === 'user') {\n <div class=\"lc-chat__time lc-chat__time--user\">{{ formatTime(msg.timestamp) }}</div>\n }\n </div>\n }\n\n @if (isStreaming() && !(formattedMessages().length && formattedMessages()[formattedMessages().length - 1].streaming)) {\n <div class=\"lc-chat__message lc-chat__message--agent\">\n @if (showAvatars()) {\n <div class=\"lc-chat__marker\">\n <span class=\"lc-chat__dot lc-chat__dot--agent\"></span>\n </div>\n }\n <div class=\"lc-chat__bubble\">\n <div class=\"lc-chat__typing\">\n <span></span><span></span><span></span>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Announces the latest completed non-user turn (see `announcement`). -->\n <div class=\"lc-chat__sr-only lc-chat__announcer\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</div>\n\n <div class=\"lc-chat__input-area\">\n <div class=\"lc-chat__composer\">\n @if (allowFileUpload() && pendingAttachments().length) {\n <div class=\"lc-chat__pending-attachments\">\n @for (att of pendingAttachments(); track att.id) {\n <div class=\"lc-chat__pending-attachment\" [class.lc-chat__pending-attachment--image]=\"isImage(att)\">\n @if (isImage(att) && att.url) {\n <img class=\"lc-chat__pending-thumb\" [src]=\"att.url\" [alt]=\"att.name\" />\n } @else {\n <svg class=\"lc-chat__attachment-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M6 2.5h6L16 6.5v10A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3H6V2.5Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <path d=\"M12 2.5V6.5H16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n </svg>\n }\n <span class=\"lc-chat__pending-name\">{{ att.name }}</span>\n @if (att.size) {\n <span class=\"lc-chat__pending-size\">{{ formatBytes(att.size) }}</span>\n }\n <button\n type=\"button\"\n class=\"lc-chat__pending-remove\"\n (click)=\"removeAttachment(att.id)\"\n [attr.aria-label]=\"removeAttachmentAriaLabel() + ' ' + att.name\"\n >\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M3 3l6 6M9 3l-6 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n </button>\n </div>\n }\n </div>\n }\n\n <label class=\"lc-chat__sr-only\" [for]=\"inputId\">{{ inputLabel() }}</label>\n <textarea\n #composerInput\n [id]=\"inputId\"\n class=\"lc-chat__input\"\n [placeholder]=\"placeholder()\"\n [value]=\"inputValue()\"\n [disabled]=\"disabled()\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n rows=\"1\"\n ></textarea>\n\n <div class=\"lc-chat__composer-actions\">\n @if (allowFileUpload()) {\n <input\n #fileInput\n type=\"file\"\n class=\"lc-chat__file-input\"\n [accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n (change)=\"onFilesSelected($event)\"\n />\n <button\n type=\"button\"\n class=\"lc-chat__attach-btn\"\n [disabled]=\"disabled()\"\n (click)=\"openFilePicker()\"\n [attr.aria-label]=\"attachAriaLabel()\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M14.5 9.5l-5 5a3 3 0 0 1-4.243-4.243l6.364-6.364a2 2 0 0 1 2.829 2.829l-6.364 6.364a1 1 0 1 1-1.415-1.415L11.5 6.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </button>\n }\n <button\n class=\"lc-chat__send-btn\"\n [disabled]=\"(!inputValue().trim() && !pendingAttachments().length) || disabled()\"\n (click)=\"send()\"\n [attr.aria-label]=\"sendAriaLabel()\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\">\n <path d=\"M3 10l7-7m0 0l7 7m-7-7v14\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" transform=\"rotate(90 10 10)\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n</div>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-choice-prompt.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/choice-prompt/choice-prompt.component.ts","../../../../libs/angular-ui-kit/src/lib/choice-prompt/choice-prompt.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n computed,\n effect,\n input,\n isDevMode,\n output,\n signal,\n viewChild,\n viewChildren,\n} from '@angular/core';\nimport { ButtonComponent } from '@lifecorner/angular-ui-kit/button';\n\n/** One selectable answer of an `lc-choice-prompt`. */\nexport interface ChoicePromptOption {\n id: string;\n label: string;\n /** Optional second line inside the option button. */\n description?: string;\n /** Marks the recommended answer: badge + first position. */\n recommended?: boolean;\n}\n\n/** A subordinate exit of the prompt (e.g. \"Selbst gelöst\" / \"Ignorieren\"). */\nexport interface ChoicePromptSecondaryAction {\n id: string;\n label: string;\n tooltip?: string;\n}\n\n/** Payload of the `(decided)` output — exactly one of the two fields is set. */\nexport interface ChoicePromptDecision {\n optionId?: string;\n customText?: string;\n}\n\nexport type ChoicePromptSize = 'sm' | 'md';\n\n/**\n * Inline decision prompt: predefined answer options (with a marked\n * recommendation) plus an optional free-text answer — no modal, directly in\n * the flow.\n *\n * Features:\n * - Options as buttons; `recommended` carries the badge and is sorted first\n * - Free-text lane behind a toggle link (`allowCustom`), submit via button or\n * Enter; empty text is never submittable\n * - `busy` after a decision: the clicked option shows a spinner, everything\n * else is disabled — prevents double submits. Focus stays on the triggering\n * element when `busy` ends.\n * - `disabled` requires a `disabledReason` (tooltip); missing reason logs a\n * dev warning\n * - Subordinate exits via `secondaryActions`\n * - `role=\"group\"` labelled via `promptLabel` (the question itself lives in\n * the surrounding layout)\n *\n * @example\n * ```html\n * <lc-choice-prompt\n * promptLabel=\"Wie soll der Konflikt gelöst werden?\"\n * [options]=\"[\n * { id: 'a', label: 'Variante A übernehmen', recommended: true },\n * { id: 'b', label: 'Beide behalten' },\n * ]\"\n * [busy]=\"saving()\"\n * (decided)=\"resolve($event)\"\n * />\n * ```\n */\n@Component({\n selector: 'lc-choice-prompt',\n standalone: true,\n imports: [ButtonComponent],\n templateUrl: './choice-prompt.component.html',\n styleUrls: ['./choice-prompt.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ChoicePromptComponent {\n /** Answer options. The recommended option is sorted to the front. */\n options = input.required<ChoicePromptOption[]>();\n\n /**\n * Whether the free-text lane is offered.\n * @default true\n */\n allowCustom = input<boolean>(true);\n\n /** Label of the link that opens the free-text lane. */\n customToggleLabel = input<string>('Eigene Antwort …');\n\n /** Placeholder of the free-text input. */\n customPlaceholder = input<string>('');\n\n /** Label of the free-text submit button. */\n submitLabel = input<string>('Übernehmen');\n\n /**\n * Set after a decision while it is being processed: the chosen element shows\n * a spinner, every other interaction is blocked (including Enter in the\n * free-text input).\n * @default false\n */\n busy = input<boolean>(false);\n\n /** Disables the whole prompt. Requires `disabledReason`. */\n disabled = input<boolean>(false);\n\n /** Why the prompt is disabled — shown as tooltip. */\n disabledReason = input<string>('');\n\n /** Subordinate exits, rendered as quiet text buttons after the options. */\n secondaryActions = input<ChoicePromptSecondaryAction[]>([]);\n\n /**\n * `sm` for dense lists (inbox rows).\n * @default 'md'\n */\n size = input<ChoicePromptSize>('md');\n\n /** Accessible name of the group — the question shown in the surroundings. */\n promptLabel = input<string>('');\n\n /** Label of the recommendation badge. */\n recommendedLabel = input<string>('Empfehlung');\n\n /** Fires immediately on option click, or on free-text submit. */\n decided = output<ChoicePromptDecision>();\n\n /** Fires with the id of a clicked secondary action. */\n secondaryAction = output<string>();\n\n /** Whether the free-text lane is open. */\n protected customOpen = signal(false);\n\n /** Current free-text value. */\n protected customText = signal('');\n\n /** Option that triggered the pending decision (spinner + focus anchor). */\n protected pendingOptionId = signal<string | null>(null);\n\n /** Whether the pending decision came from the free-text lane. */\n protected pendingCustom = signal(false);\n\n private customInput = viewChild<ElementRef<HTMLInputElement>>('customInput');\n private optionButtons = viewChildren<ElementRef<HTMLButtonElement>>('optionButton');\n private submitButton = viewChild<ButtonComponent>('submitButton');\n\n /** Options with the recommended one(s) first, original order otherwise. */\n protected sortedOptions = computed(() => {\n const options = this.options();\n return [...options.filter((o) => o.recommended), ...options.filter((o) => !o.recommended)];\n });\n\n /** No interaction while busy or disabled. */\n protected locked = computed(() => this.busy() || this.disabled());\n\n protected canSubmitCustom = computed(\n () => this.customText().trim().length > 0 && !this.locked(),\n );\n\n protected disabledTitle = computed(() =>\n this.disabled() && this.disabledReason() ? this.disabledReason() : null,\n );\n\n protected groupClasses = computed(() =>\n ['lc-choice-prompt', `lc-choice-prompt--${this.size()}`].join(' '),\n );\n\n constructor() {\n effect(() => {\n if (isDevMode() && this.disabled() && !this.disabledReason()) {\n console.warn(\n '[lc-choice-prompt] `disabled` is set without a `disabledReason`. ' +\n 'A disabled prompt must explain why (shown as tooltip).',\n );\n }\n });\n\n // When busy ends, focus returns to the element that triggered the\n // decision, then the pending markers reset.\n let wasBusy = false;\n effect(() => {\n const busy = this.busy();\n if (wasBusy && !busy) {\n this.restoreFocus();\n this.pendingOptionId.set(null);\n this.pendingCustom.set(false);\n }\n wasBusy = busy;\n });\n }\n\n protected isPendingOption(id: string): boolean {\n return this.busy() && this.pendingOptionId() === id;\n }\n\n protected chooseOption(option: ChoicePromptOption): void {\n if (this.locked()) return;\n this.pendingOptionId.set(option.id);\n this.pendingCustom.set(false);\n this.decided.emit({ optionId: option.id });\n }\n\n protected openCustom(): void {\n if (this.locked()) return;\n this.customOpen.set(true);\n // Focus the input once it is rendered.\n setTimeout(() => this.customInput()?.nativeElement.focus());\n }\n\n protected onCustomInput(event: Event): void {\n this.customText.set((event.target as HTMLInputElement).value);\n }\n\n protected submitCustom(): void {\n const text = this.customText().trim();\n if (!text || this.locked()) return;\n this.pendingCustom.set(true);\n this.pendingOptionId.set(null);\n this.decided.emit({ customText: text });\n }\n\n protected triggerSecondary(action: ChoicePromptSecondaryAction): void {\n if (this.locked()) return;\n this.secondaryAction.emit(action.id);\n }\n\n private restoreFocus(): void {\n const pendingId = this.pendingOptionId();\n if (pendingId !== null) {\n const button = this.optionButtons().find(\n (ref) => ref.nativeElement.dataset['optionId'] === pendingId,\n );\n button?.nativeElement.focus();\n } else if (this.pendingCustom()) {\n this.submitButton()?.focus();\n }\n }\n}\n","<div\n [class]=\"groupClasses()\"\n role=\"group\"\n [attr.aria-label]=\"promptLabel() || null\"\n [attr.aria-busy]=\"busy() ? 'true' : null\"\n>\n <div class=\"lc-choice-prompt__options\">\n @for (option of sortedOptions(); track option.id) {\n <!-- The busy (clicked) option keeps focus: aria-disabled instead of the\n native attribute, the click handler is guarded via locked(). -->\n <button\n #optionButton\n type=\"button\"\n class=\"lc-choice-prompt__option\"\n [class.lc-choice-prompt__option--recommended]=\"option.recommended\"\n [class.lc-choice-prompt__option--busy]=\"isPendingOption(option.id)\"\n [attr.data-option-id]=\"option.id\"\n [disabled]=\"locked() && !isPendingOption(option.id)\"\n [attr.aria-disabled]=\"locked() ? 'true' : null\"\n [attr.title]=\"disabledTitle()\"\n (click)=\"chooseOption(option)\"\n >\n <span class=\"lc-choice-prompt__option-main\">\n @if (isPendingOption(option.id)) {\n <span class=\"lc-choice-prompt__spinner\" aria-hidden=\"true\"></span>\n }\n <span class=\"lc-choice-prompt__option-label\">{{ option.label }}</span>\n @if (option.recommended) {\n <span class=\"lc-choice-prompt__badge\">{{ recommendedLabel() }}</span>\n }\n </span>\n @if (option.description) {\n <span class=\"lc-choice-prompt__option-description\">{{ option.description }}</span>\n }\n </button>\n }\n\n @if (allowCustom() && !customOpen()) {\n <button\n type=\"button\"\n class=\"lc-choice-prompt__custom-toggle\"\n [disabled]=\"locked()\"\n [attr.title]=\"disabledTitle()\"\n (click)=\"openCustom()\"\n >\n {{ customToggleLabel() }}\n </button>\n }\n\n @for (action of secondaryActions(); track action.id) {\n <button\n type=\"button\"\n class=\"lc-choice-prompt__secondary\"\n [disabled]=\"locked()\"\n [attr.title]=\"disabledTitle() ?? action.tooltip ?? null\"\n (click)=\"triggerSecondary(action)\"\n >\n {{ action.label }}\n </button>\n }\n </div>\n\n @if (allowCustom() && customOpen()) {\n <div class=\"lc-choice-prompt__custom\">\n <input\n #customInput\n type=\"text\"\n class=\"lc-choice-prompt__input\"\n [placeholder]=\"customPlaceholder()\"\n [value]=\"customText()\"\n [disabled]=\"locked()\"\n [attr.title]=\"disabledTitle()\"\n [attr.aria-label]=\"customToggleLabel()\"\n (input)=\"onCustomInput($event)\"\n (keydown.enter)=\"submitCustom()\"\n />\n <lc-button\n #submitButton\n variant=\"primary\"\n [size]=\"size() === 'sm' ? 'xs' : 'sm'\"\n [loading]=\"busy() && pendingCustom()\"\n [disabled]=\"!canSubmitCustom()\"\n [disabledReason]=\"disabledTitle() ?? ''\"\n (clicked)=\"submitCustom()\"\n >\n {{ submitLabel() }}\n </lc-button>\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-choice-prompt.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/choice-prompt/choice-prompt.component.ts","../../../../libs/angular-ui-kit/src/lib/choice-prompt/choice-prompt.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n computed,\n effect,\n input,\n isDevMode,\n output,\n signal,\n viewChild,\n viewChildren,\n} from '@angular/core';\nimport { ButtonComponent } from '@lifecorner/angular-ui-kit/button';\n\n/** One selectable answer of an `lc-choice-prompt`. */\nexport interface ChoicePromptOption {\n id: string;\n label: string;\n /** Optional second line inside the option button. */\n description?: string;\n /** Marks the recommended answer: badge + first position. */\n recommended?: boolean;\n}\n\n/** A subordinate exit of the prompt (e.g. \"Selbst gelöst\" / \"Ignorieren\"). */\nexport interface ChoicePromptSecondaryAction {\n id: string;\n label: string;\n tooltip?: string;\n}\n\n/** Payload of the `(decided)` output — exactly one of the two fields is set. */\nexport interface ChoicePromptDecision {\n optionId?: string;\n customText?: string;\n}\n\nexport type ChoicePromptSize = 'sm' | 'md';\n\n/**\n * Inline decision prompt: predefined answer options (with a marked\n * recommendation) plus an optional free-text answer — no modal, directly in\n * the flow.\n *\n * Features:\n * - Options as buttons; `recommended` carries the badge and is sorted first\n * - Free-text lane behind a toggle link (`allowCustom`), submit via button or\n * Enter; empty text is never submittable\n * - `busy` after a decision: the clicked option shows a spinner, everything\n * else is disabled — prevents double submits. Focus stays on the triggering\n * element when `busy` ends.\n * - `disabled` requires a `disabledReason` (tooltip); missing reason logs a\n * dev warning\n * - Subordinate exits via `secondaryActions`\n * - `role=\"group\"` labelled via `promptLabel` (the question itself lives in\n * the surrounding layout)\n *\n * @example\n * ```html\n * <lc-choice-prompt\n * promptLabel=\"Wie soll der Konflikt gelöst werden?\"\n * [options]=\"[\n * { id: 'a', label: 'Variante A übernehmen', recommended: true },\n * { id: 'b', label: 'Beide behalten' },\n * ]\"\n * [busy]=\"saving()\"\n * (decided)=\"resolve($event)\"\n * />\n * ```\n */\n@Component({\n selector: 'lc-choice-prompt',\n standalone: true,\n imports: [ButtonComponent],\n templateUrl: './choice-prompt.component.html',\n styleUrls: ['./choice-prompt.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ChoicePromptComponent {\n /** Answer options. The recommended option is sorted to the front. */\n options = input.required<ChoicePromptOption[]>();\n\n /**\n * Whether the free-text lane is offered.\n * @default true\n */\n allowCustom = input<boolean>(true);\n\n /** Label of the link that opens the free-text lane. */\n customToggleLabel = input<string>('Eigene Antwort …');\n\n /** Placeholder of the free-text input. */\n customPlaceholder = input<string>('');\n\n /** Label of the free-text submit button. */\n submitLabel = input<string>('Übernehmen');\n\n /**\n * Set after a decision while it is being processed: the chosen element shows\n * a spinner, every other interaction is blocked (including Enter in the\n * free-text input).\n * @default false\n */\n busy = input<boolean>(false);\n\n /** Disables the whole prompt. Requires `disabledReason`. */\n disabled = input<boolean>(false);\n\n /** Why the prompt is disabled — shown as tooltip. */\n disabledReason = input<string>('');\n\n /** Subordinate exits, rendered as quiet text buttons after the options. */\n secondaryActions = input<ChoicePromptSecondaryAction[]>([]);\n\n /**\n * `sm` for dense lists (inbox rows).\n * @default 'md'\n */\n size = input<ChoicePromptSize>('md');\n\n /** Accessible name of the group — the question shown in the surroundings. */\n promptLabel = input<string>('');\n\n /** Label of the recommendation badge. */\n recommendedLabel = input<string>('Empfehlung');\n\n /** Fires immediately on option click, or on free-text submit. */\n decided = output<ChoicePromptDecision>();\n\n /** Fires with the id of a clicked secondary action. */\n secondaryAction = output<string>();\n\n /** Whether the free-text lane is open. */\n protected customOpen = signal(false);\n\n /** Current free-text value. */\n protected customText = signal('');\n\n /** Option that triggered the pending decision (spinner + focus anchor). */\n protected pendingOptionId = signal<string | null>(null);\n\n /** Whether the pending decision came from the free-text lane. */\n protected pendingCustom = signal(false);\n\n private customInput = viewChild<ElementRef<HTMLInputElement>>('customInput');\n private optionButtons = viewChildren<ElementRef<HTMLButtonElement>>('optionButton');\n private submitButton = viewChild<ButtonComponent>('submitButton');\n\n /** Options with the recommended one(s) first, original order otherwise. */\n protected sortedOptions = computed(() => {\n const options = this.options();\n return [...options.filter((o) => o.recommended), ...options.filter((o) => !o.recommended)];\n });\n\n /** No interaction while busy or disabled. */\n protected locked = computed(() => this.busy() || this.disabled());\n\n protected canSubmitCustom = computed(\n () => this.customText().trim().length > 0 && !this.locked(),\n );\n\n protected disabledTitle = computed(() =>\n this.disabled() && this.disabledReason() ? this.disabledReason() : null,\n );\n\n protected groupClasses = computed(() =>\n ['lc-choice-prompt', `lc-choice-prompt--${this.size()}`].join(' '),\n );\n\n constructor() {\n effect(() => {\n if (isDevMode() && this.disabled() && !this.disabledReason()) {\n console.warn(\n '[lc-choice-prompt] `disabled` is set without a `disabledReason`. ' +\n 'A disabled prompt must explain why (shown as tooltip).',\n );\n }\n });\n\n // When busy ends, focus returns to the element that triggered the\n // decision, then the pending markers reset.\n let wasBusy = false;\n effect(() => {\n const busy = this.busy();\n if (wasBusy && !busy) {\n this.restoreFocus();\n this.pendingOptionId.set(null);\n this.pendingCustom.set(false);\n }\n wasBusy = busy;\n });\n }\n\n protected isPendingOption(id: string): boolean {\n return this.busy() && this.pendingOptionId() === id;\n }\n\n protected chooseOption(option: ChoicePromptOption): void {\n if (this.locked()) return;\n this.pendingOptionId.set(option.id);\n this.pendingCustom.set(false);\n this.decided.emit({ optionId: option.id });\n }\n\n protected openCustom(): void {\n if (this.locked()) return;\n this.customOpen.set(true);\n // Focus the input once it is rendered.\n setTimeout(() => this.customInput()?.nativeElement.focus());\n }\n\n protected onCustomInput(event: Event): void {\n this.customText.set((event.target as HTMLInputElement).value);\n }\n\n protected submitCustom(): void {\n const text = this.customText().trim();\n if (!text || this.locked()) return;\n this.pendingCustom.set(true);\n this.pendingOptionId.set(null);\n this.decided.emit({ customText: text });\n }\n\n protected triggerSecondary(action: ChoicePromptSecondaryAction): void {\n if (this.locked()) return;\n this.secondaryAction.emit(action.id);\n }\n\n private restoreFocus(): void {\n const pendingId = this.pendingOptionId();\n if (pendingId !== null) {\n const button = this.optionButtons().find(\n (ref) => ref.nativeElement.dataset['optionId'] === pendingId,\n );\n button?.nativeElement.focus();\n } else if (this.pendingCustom()) {\n this.submitButton()?.focus();\n }\n }\n}\n","<div\n [class]=\"groupClasses()\"\n role=\"group\"\n [attr.aria-label]=\"promptLabel() || null\"\n [attr.aria-busy]=\"busy() ? 'true' : null\"\n>\n <div class=\"lc-choice-prompt__options\">\n @for (option of sortedOptions(); track option.id) {\n <!-- The busy (clicked) option keeps focus: aria-disabled instead of the\n native attribute, the click handler is guarded via locked(). -->\n <button\n #optionButton\n type=\"button\"\n class=\"lc-choice-prompt__option\"\n [class.lc-choice-prompt__option--recommended]=\"option.recommended\"\n [class.lc-choice-prompt__option--busy]=\"isPendingOption(option.id)\"\n [attr.data-option-id]=\"option.id\"\n [disabled]=\"locked() && !isPendingOption(option.id)\"\n [attr.aria-disabled]=\"locked() ? 'true' : null\"\n [attr.title]=\"disabledTitle()\"\n (click)=\"chooseOption(option)\"\n >\n <span class=\"lc-choice-prompt__option-main\">\n @if (isPendingOption(option.id)) {\n <span class=\"lc-choice-prompt__spinner\" aria-hidden=\"true\"></span>\n }\n <span class=\"lc-choice-prompt__option-label\">{{ option.label }}</span>\n @if (option.recommended) {\n <span class=\"lc-choice-prompt__badge\">{{ recommendedLabel() }}</span>\n }\n </span>\n @if (option.description) {\n <span class=\"lc-choice-prompt__option-description\">{{ option.description }}</span>\n }\n </button>\n }\n\n @if (allowCustom() && !customOpen()) {\n <button\n type=\"button\"\n class=\"lc-choice-prompt__custom-toggle\"\n [disabled]=\"locked()\"\n [attr.title]=\"disabledTitle()\"\n (click)=\"openCustom()\"\n >\n {{ customToggleLabel() }}\n </button>\n }\n\n @for (action of secondaryActions(); track action.id) {\n <button\n type=\"button\"\n class=\"lc-choice-prompt__secondary\"\n [disabled]=\"locked()\"\n [attr.title]=\"disabledTitle() ?? action.tooltip ?? null\"\n (click)=\"triggerSecondary(action)\"\n >\n {{ action.label }}\n </button>\n }\n </div>\n\n @if (allowCustom() && customOpen()) {\n <div class=\"lc-choice-prompt__custom\">\n <input\n #customInput\n type=\"text\"\n class=\"lc-choice-prompt__input\"\n [placeholder]=\"customPlaceholder()\"\n [value]=\"customText()\"\n [disabled]=\"locked()\"\n [attr.title]=\"disabledTitle()\"\n [attr.aria-label]=\"customToggleLabel()\"\n (input)=\"onCustomInput($event)\"\n (keydown.enter)=\"submitCustom()\"\n />\n <lc-button\n #submitButton\n variant=\"primary\"\n [size]=\"size() === 'sm' ? 'xs' : 'sm'\"\n [loading]=\"busy() && pendingCustom()\"\n [disabled]=\"!canSubmitCustom()\"\n [disabledReason]=\"disabledTitle() ?? ''\"\n (clicked)=\"submitCustom()\"\n >\n {{ submitLabel() }}\n </lc-button>\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
@@ -182,7 +182,7 @@ var ColorPickerComponent = class ColorPickerComponent {
|
|
|
182
182
|
}],
|
|
183
183
|
ngImport: i0,
|
|
184
184
|
template: "<div [class]=\"pickerClasses()\">\n @if (label()) {\n <label class=\"color-picker__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"color-picker__preview-row\">\n <input\n type=\"color\"\n [id]=\"inputId\"\n class=\"color-picker__native\"\n [value]=\"value()\"\n [disabled]=\"isDisabled()\"\n (input)=\"onNativeInput($event)\"\n (blur)=\"onBlur()\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n />\n <span\n class=\"color-picker__swatch-preview\"\n [style.background-color]=\"value()\"\n aria-hidden=\"true\"\n ></span>\n @if (showInput()) {\n <input\n type=\"text\"\n class=\"color-picker__hex-input\"\n [value]=\"value()\"\n [disabled]=\"isDisabled()\"\n maxlength=\"7\"\n (input)=\"onHexInput($event)\"\n (blur)=\"onBlur()\"\n [attr.aria-label]=\"(label() ? label() + ' – ' : '') + hexAriaLabel()\"\n />\n }\n </div>\n @if (swatches().length > 0) {\n <div\n class=\"color-picker__swatches\"\n role=\"radiogroup\"\n [attr.aria-label]=\"(label() ? label() + ' – ' : '') + swatchesAriaLabel()\"\n >\n @for (swatch of swatches(); track swatch; let i = $index) {\n <button\n #swatchBtn\n type=\"button\"\n class=\"color-picker__swatch\"\n role=\"radio\"\n [class.color-picker__swatch--selected]=\"swatch === value()\"\n [class.color-picker__swatch--light]=\"swatch === '#ffffff'\"\n [style.background-color]=\"swatch\"\n [disabled]=\"isDisabled()\"\n [tabindex]=\"i === focusableSwatchIndex() ? 0 : -1\"\n [attr.aria-label]=\"swatch\"\n [attr.aria-checked]=\"swatch === value()\"\n (click)=\"selectSwatch(swatch)\"\n (keydown)=\"onSwatchKeydown($event, i)\"\n (blur)=\"onBlur()\"\n ></button>\n }\n </div>\n }\n</div>\n",
|
|
185
|
-
styles: [".color-picker{display:inline-flex;flex-direction:column;gap:.5rem}.color-picker__label{font-size:var(--typography-font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-secondary)}.color-picker__preview-row{display:flex;align-items:center;gap:.5rem}.color-picker__native{width:2.5rem;height:2.5rem;padding:0;border:1px solid var(--color-border);border-radius:var(--border-radius-md);cursor:pointer;background:none}.color-picker__native::-webkit-color-swatch-wrapper{padding:2px}.color-picker__native::-webkit-color-swatch{border:none;border-radius:var(--border-radius-sm)}.color-picker__native::-moz-color-swatch{border:none;border-radius:var(--border-radius-sm)}.color-picker__native:disabled{cursor:not-allowed;opacity:.5}.color-picker__swatch-preview{display:block;width:1.5rem;height:1.5rem;border-radius:50%;border:2px solid var(--color-border);flex-shrink:0}.color-picker__hex-input{width:5.5rem;padding:.375rem .5rem;border:1px solid var(--color-border);border-radius:var(--border-radius-md);font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family-mono, monospace);color:var(--color-text-primary);background:var(--color-surface-2)}.color-picker__hex-input:focus{outline:none;border-color:var(--color-primary-500);box-shadow:0 0 0 3px #20849733}.color-picker__hex-input:disabled{cursor:not-allowed;opacity:.5}.color-picker__swatches{display:flex;flex-wrap:wrap;gap:.375rem;max-width:16rem}.color-picker__swatch{width:1.5rem;height:1.5rem;border-radius:var(--border-radius-sm);border:2px solid transparent;cursor:pointer;transition:transform .1s ease,border-color .1s ease;padding:0}.color-picker__swatch:hover:not(:disabled){transform:scale(1.15)}.color-picker__swatch:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.color-picker__swatch--selected{border-color:var(--color-primary-500);box-shadow:0 0 0 2px var(--color-surface),0 0 0 4px var(--color-primary-500)}.color-picker__swatch--light{border-color:var(--color-border)}.color-picker__swatch:disabled{cursor:not-allowed;opacity:.5}.color-picker--disabled{opacity:.5;pointer-events:none}\n"],
|
|
185
|
+
styles: [".color-picker{display:inline-flex;flex-direction:column;gap:.5rem}.color-picker__label{font-size:var(--typography-font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-secondary)}.color-picker__preview-row{display:flex;align-items:center;gap:.5rem}.color-picker__native{width:2.5rem;height:2.5rem;padding:0;border:1px solid var(--color-border);border-radius:var(--border-radius-md);cursor:pointer;background:none}.color-picker__native::-webkit-color-swatch-wrapper{padding:2px}.color-picker__native::-webkit-color-swatch{border:none;border-radius:var(--border-radius-sm)}.color-picker__native::-moz-color-swatch{border:none;border-radius:var(--border-radius-sm)}.color-picker__native:disabled{cursor:not-allowed;opacity:.5}.color-picker__native:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.color-picker__swatch-preview{display:block;width:1.5rem;height:1.5rem;border-radius:50%;border:2px solid var(--color-border);flex-shrink:0}.color-picker__hex-input{width:5.5rem;padding:.375rem .5rem;border:1px solid var(--color-border);border-radius:var(--border-radius-md);font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family-mono, monospace);color:var(--color-text-primary);background:var(--color-surface-2)}.color-picker__hex-input:focus{outline:none;border-color:var(--color-primary-500);box-shadow:0 0 0 3px #20849733}.color-picker__hex-input:disabled{cursor:not-allowed;opacity:.5}.color-picker__swatches{display:flex;flex-wrap:wrap;gap:.375rem;max-width:16rem}.color-picker__swatch{width:1.5rem;height:1.5rem;border-radius:var(--border-radius-sm);border:2px solid transparent;cursor:pointer;transition:transform .1s ease,border-color .1s ease;padding:0}.color-picker__swatch:hover:not(:disabled){transform:scale(1.15)}.color-picker__swatch:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.color-picker__swatch--selected{border-color:var(--color-primary-500);box-shadow:0 0 0 2px var(--color-surface),0 0 0 4px var(--color-primary-500)}.color-picker__swatch--light{border-color:var(--color-border)}.color-picker__swatch:disabled{cursor:not-allowed;opacity:.5}.color-picker--disabled{opacity:.5;pointer-events:none}@media(forced-colors:active){.color-picker__native:focus-visible{outline-color:Highlight}}\n"],
|
|
186
186
|
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
187
187
|
});
|
|
188
188
|
};
|
|
@@ -203,7 +203,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
203
203
|
multi: true
|
|
204
204
|
}],
|
|
205
205
|
template: "<div [class]=\"pickerClasses()\">\n @if (label()) {\n <label class=\"color-picker__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"color-picker__preview-row\">\n <input\n type=\"color\"\n [id]=\"inputId\"\n class=\"color-picker__native\"\n [value]=\"value()\"\n [disabled]=\"isDisabled()\"\n (input)=\"onNativeInput($event)\"\n (blur)=\"onBlur()\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n />\n <span\n class=\"color-picker__swatch-preview\"\n [style.background-color]=\"value()\"\n aria-hidden=\"true\"\n ></span>\n @if (showInput()) {\n <input\n type=\"text\"\n class=\"color-picker__hex-input\"\n [value]=\"value()\"\n [disabled]=\"isDisabled()\"\n maxlength=\"7\"\n (input)=\"onHexInput($event)\"\n (blur)=\"onBlur()\"\n [attr.aria-label]=\"(label() ? label() + ' – ' : '') + hexAriaLabel()\"\n />\n }\n </div>\n @if (swatches().length > 0) {\n <div\n class=\"color-picker__swatches\"\n role=\"radiogroup\"\n [attr.aria-label]=\"(label() ? label() + ' – ' : '') + swatchesAriaLabel()\"\n >\n @for (swatch of swatches(); track swatch; let i = $index) {\n <button\n #swatchBtn\n type=\"button\"\n class=\"color-picker__swatch\"\n role=\"radio\"\n [class.color-picker__swatch--selected]=\"swatch === value()\"\n [class.color-picker__swatch--light]=\"swatch === '#ffffff'\"\n [style.background-color]=\"swatch\"\n [disabled]=\"isDisabled()\"\n [tabindex]=\"i === focusableSwatchIndex() ? 0 : -1\"\n [attr.aria-label]=\"swatch\"\n [attr.aria-checked]=\"swatch === value()\"\n (click)=\"selectSwatch(swatch)\"\n (keydown)=\"onSwatchKeydown($event, i)\"\n (blur)=\"onBlur()\"\n ></button>\n }\n </div>\n }\n</div>\n",
|
|
206
|
-
styles: [".color-picker{display:inline-flex;flex-direction:column;gap:.5rem}.color-picker__label{font-size:var(--typography-font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-secondary)}.color-picker__preview-row{display:flex;align-items:center;gap:.5rem}.color-picker__native{width:2.5rem;height:2.5rem;padding:0;border:1px solid var(--color-border);border-radius:var(--border-radius-md);cursor:pointer;background:none}.color-picker__native::-webkit-color-swatch-wrapper{padding:2px}.color-picker__native::-webkit-color-swatch{border:none;border-radius:var(--border-radius-sm)}.color-picker__native::-moz-color-swatch{border:none;border-radius:var(--border-radius-sm)}.color-picker__native:disabled{cursor:not-allowed;opacity:.5}.color-picker__swatch-preview{display:block;width:1.5rem;height:1.5rem;border-radius:50%;border:2px solid var(--color-border);flex-shrink:0}.color-picker__hex-input{width:5.5rem;padding:.375rem .5rem;border:1px solid var(--color-border);border-radius:var(--border-radius-md);font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family-mono, monospace);color:var(--color-text-primary);background:var(--color-surface-2)}.color-picker__hex-input:focus{outline:none;border-color:var(--color-primary-500);box-shadow:0 0 0 3px #20849733}.color-picker__hex-input:disabled{cursor:not-allowed;opacity:.5}.color-picker__swatches{display:flex;flex-wrap:wrap;gap:.375rem;max-width:16rem}.color-picker__swatch{width:1.5rem;height:1.5rem;border-radius:var(--border-radius-sm);border:2px solid transparent;cursor:pointer;transition:transform .1s ease,border-color .1s ease;padding:0}.color-picker__swatch:hover:not(:disabled){transform:scale(1.15)}.color-picker__swatch:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.color-picker__swatch--selected{border-color:var(--color-primary-500);box-shadow:0 0 0 2px var(--color-surface),0 0 0 4px var(--color-primary-500)}.color-picker__swatch--light{border-color:var(--color-border)}.color-picker__swatch:disabled{cursor:not-allowed;opacity:.5}.color-picker--disabled{opacity:.5;pointer-events:none}\n"]
|
|
206
|
+
styles: [".color-picker{display:inline-flex;flex-direction:column;gap:.5rem}.color-picker__label{font-size:var(--typography-font-size-sm, .875rem);font-weight:var(--typography-font-weight-medium, 500);color:var(--color-text-secondary)}.color-picker__preview-row{display:flex;align-items:center;gap:.5rem}.color-picker__native{width:2.5rem;height:2.5rem;padding:0;border:1px solid var(--color-border);border-radius:var(--border-radius-md);cursor:pointer;background:none}.color-picker__native::-webkit-color-swatch-wrapper{padding:2px}.color-picker__native::-webkit-color-swatch{border:none;border-radius:var(--border-radius-sm)}.color-picker__native::-moz-color-swatch{border:none;border-radius:var(--border-radius-sm)}.color-picker__native:disabled{cursor:not-allowed;opacity:.5}.color-picker__native:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.color-picker__swatch-preview{display:block;width:1.5rem;height:1.5rem;border-radius:50%;border:2px solid var(--color-border);flex-shrink:0}.color-picker__hex-input{width:5.5rem;padding:.375rem .5rem;border:1px solid var(--color-border);border-radius:var(--border-radius-md);font-size:var(--typography-font-size-sm, .875rem);font-family:var(--typography-font-family-mono, monospace);color:var(--color-text-primary);background:var(--color-surface-2)}.color-picker__hex-input:focus{outline:none;border-color:var(--color-primary-500);box-shadow:0 0 0 3px #20849733}.color-picker__hex-input:disabled{cursor:not-allowed;opacity:.5}.color-picker__swatches{display:flex;flex-wrap:wrap;gap:.375rem;max-width:16rem}.color-picker__swatch{width:1.5rem;height:1.5rem;border-radius:var(--border-radius-sm);border:2px solid transparent;cursor:pointer;transition:transform .1s ease,border-color .1s ease;padding:0}.color-picker__swatch:hover:not(:disabled){transform:scale(1.15)}.color-picker__swatch:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:2px}.color-picker__swatch--selected{border-color:var(--color-primary-500);box-shadow:0 0 0 2px var(--color-surface),0 0 0 4px var(--color-primary-500)}.color-picker__swatch--light{border-color:var(--color-border)}.color-picker__swatch:disabled{cursor:not-allowed;opacity:.5}.color-picker--disabled{opacity:.5;pointer-events:none}@media(forced-colors:active){.color-picker__native:focus-visible{outline-color:Highlight}}\n"]
|
|
207
207
|
}]
|
|
208
208
|
}],
|
|
209
209
|
propDecorators: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-color-picker.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/color-picker/color-picker.component.ts","../../../../libs/angular-ui-kit/src/lib/color-picker/color-picker.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n input,\n output,\n computed,\n signal,\n forwardRef,\n viewChildren,\n} from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\nconst DEFAULT_SWATCHES = [\n '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6',\n '#3b82f6', '#6366f1', '#8b5cf6', '#ec4899', '#f43f5e',\n '#0ea5e9', '#10b981', '#a855f7', '#64748b', '#000000',\n '#ffffff',\n];\n\n@Component({\n selector: 'lc-color-picker',\n standalone: true,\n templateUrl: './color-picker.component.html',\n styleUrls: ['./color-picker.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => ColorPickerComponent),\n multi: true,\n },\n ],\n})\n/**\n * Color picker component for selecting colors from swatches or custom input.\n *\n * Features:\n * - Predefined color swatch palette (a radiogroup: arrow keys move between swatches)\n * - Native browser color picker integration\n * - Hex color text input with validation\n * - Disabled state support\n * - ControlValueAccessor integration for reactive forms\n *\n * @example\n * ```html\n * <lc-color-picker label=\"Brand Color\" [showInput]=\"true\" />\n * ```\n */\nexport class ColorPickerComponent implements ControlValueAccessor {\n private static nextId = 0;\n\n /** Label text */\n label = input<string>();\n\n /** Predefined color swatches */\n swatches = input<string[]>(DEFAULT_SWATCHES);\n\n /** Whether to show the hex input field */\n showInput = input<boolean>(true);\n\n /** Whether the picker is disabled */\n disabled = input<boolean>(false);\n\n /** Accessible name of the picker when no visible `label` is set. */\n ariaLabel = input('Color picker');\n\n /** Accessible name of the hex field (after `label` and a dash when set). */\n hexAriaLabel = input('Hex color value');\n\n /** Accessible name of the swatch group (after `label` and a dash when set). */\n swatchesAriaLabel = input('Color swatches');\n\n /** Emits the selected color hex value */\n colorChange = output<string>();\n\n /** Per-instance id wiring the label to the native colour input. */\n readonly inputId = `lc-color-picker-${++ColorPickerComponent.nextId}`;\n\n protected value = signal('#3b82f6');\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 private readonly swatchButtons = viewChildren<ElementRef<HTMLButtonElement>>('swatchBtn');\n\n /**\n * The swatch carrying the group's single tab stop (roving tabindex): the\n * selected one, or the first when the current colour is not in the palette.\n */\n protected readonly focusableSwatchIndex = computed(() => {\n const index = this.swatches().indexOf(this.value());\n return index >= 0 ? index : 0;\n });\n\n protected pickerClasses = computed(() => {\n const classes = ['color-picker'];\n if (this.isDisabled()) classes.push('color-picker--disabled');\n return classes.join(' ');\n });\n\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onChange: (val: string) => void = () => {};\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onTouched: () => void = () => {};\n\n protected selectSwatch(color: string): void {\n if (this.isDisabled()) return;\n this.setValue(color);\n }\n\n /** Arrow keys / Home / End move selection (and focus) through the palette like a radiogroup. */\n protected onSwatchKeydown(event: KeyboardEvent, index: number): void {\n if (this.isDisabled()) return;\n const count = this.swatches().length;\n let next: number;\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowDown':\n next = (index + 1) % count;\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n next = (index - 1 + count) % count;\n break;\n case 'Home':\n next = 0;\n break;\n case 'End':\n next = count - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.selectSwatch(this.swatches()[next]);\n this.swatchButtons()[next]?.nativeElement.focus();\n }\n\n protected onNativeInput(event: Event): void {\n const target = event.target as HTMLInputElement;\n this.setValue(target.value);\n }\n\n protected onHexInput(event: Event): void {\n const target = event.target as HTMLInputElement;\n let val = target.value.trim();\n if (!val.startsWith('#')) val = '#' + val;\n if (/^#[0-9a-fA-F]{6}$/.test(val)) {\n this.setValue(val);\n }\n }\n\n protected onBlur(): void {\n this.onTouched();\n }\n\n private setValue(color: string): void {\n this.value.set(color);\n this.colorChange.emit(color);\n this.onChange(color);\n }\n\n // ControlValueAccessor\n writeValue(val: string): void {\n this.value.set(val ?? '#3b82f6');\n }\n\n registerOnChange(fn: (val: 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","<div [class]=\"pickerClasses()\">\n @if (label()) {\n <label class=\"color-picker__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"color-picker__preview-row\">\n <input\n type=\"color\"\n [id]=\"inputId\"\n class=\"color-picker__native\"\n [value]=\"value()\"\n [disabled]=\"isDisabled()\"\n (input)=\"onNativeInput($event)\"\n (blur)=\"onBlur()\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n />\n <span\n class=\"color-picker__swatch-preview\"\n [style.background-color]=\"value()\"\n aria-hidden=\"true\"\n ></span>\n @if (showInput()) {\n <input\n type=\"text\"\n class=\"color-picker__hex-input\"\n [value]=\"value()\"\n [disabled]=\"isDisabled()\"\n maxlength=\"7\"\n (input)=\"onHexInput($event)\"\n (blur)=\"onBlur()\"\n [attr.aria-label]=\"(label() ? label() + ' – ' : '') + hexAriaLabel()\"\n />\n }\n </div>\n @if (swatches().length > 0) {\n <div\n class=\"color-picker__swatches\"\n role=\"radiogroup\"\n [attr.aria-label]=\"(label() ? label() + ' – ' : '') + swatchesAriaLabel()\"\n >\n @for (swatch of swatches(); track swatch; let i = $index) {\n <button\n #swatchBtn\n type=\"button\"\n class=\"color-picker__swatch\"\n role=\"radio\"\n [class.color-picker__swatch--selected]=\"swatch === value()\"\n [class.color-picker__swatch--light]=\"swatch === '#ffffff'\"\n [style.background-color]=\"swatch\"\n [disabled]=\"isDisabled()\"\n [tabindex]=\"i === focusableSwatchIndex() ? 0 : -1\"\n [attr.aria-label]=\"swatch\"\n [attr.aria-checked]=\"swatch === value()\"\n (click)=\"selectSwatch(swatch)\"\n (keydown)=\"onSwatchKeydown($event, i)\"\n (blur)=\"onBlur()\"\n ></button>\n }\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-color-picker.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/color-picker/color-picker.component.ts","../../../../libs/angular-ui-kit/src/lib/color-picker/color-picker.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n input,\n output,\n computed,\n signal,\n forwardRef,\n viewChildren,\n} from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\nconst DEFAULT_SWATCHES = [\n '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6',\n '#3b82f6', '#6366f1', '#8b5cf6', '#ec4899', '#f43f5e',\n '#0ea5e9', '#10b981', '#a855f7', '#64748b', '#000000',\n '#ffffff',\n];\n\n@Component({\n selector: 'lc-color-picker',\n standalone: true,\n templateUrl: './color-picker.component.html',\n styleUrls: ['./color-picker.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => ColorPickerComponent),\n multi: true,\n },\n ],\n})\n/**\n * Color picker component for selecting colors from swatches or custom input.\n *\n * Features:\n * - Predefined color swatch palette (a radiogroup: arrow keys move between swatches)\n * - Native browser color picker integration\n * - Hex color text input with validation\n * - Disabled state support\n * - ControlValueAccessor integration for reactive forms\n *\n * @example\n * ```html\n * <lc-color-picker label=\"Brand Color\" [showInput]=\"true\" />\n * ```\n */\nexport class ColorPickerComponent implements ControlValueAccessor {\n private static nextId = 0;\n\n /** Label text */\n label = input<string>();\n\n /** Predefined color swatches */\n swatches = input<string[]>(DEFAULT_SWATCHES);\n\n /** Whether to show the hex input field */\n showInput = input<boolean>(true);\n\n /** Whether the picker is disabled */\n disabled = input<boolean>(false);\n\n /** Accessible name of the picker when no visible `label` is set. */\n ariaLabel = input('Color picker');\n\n /** Accessible name of the hex field (after `label` and a dash when set). */\n hexAriaLabel = input('Hex color value');\n\n /** Accessible name of the swatch group (after `label` and a dash when set). */\n swatchesAriaLabel = input('Color swatches');\n\n /** Emits the selected color hex value */\n colorChange = output<string>();\n\n /** Per-instance id wiring the label to the native colour input. */\n readonly inputId = `lc-color-picker-${++ColorPickerComponent.nextId}`;\n\n protected value = signal('#3b82f6');\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 private readonly swatchButtons = viewChildren<ElementRef<HTMLButtonElement>>('swatchBtn');\n\n /**\n * The swatch carrying the group's single tab stop (roving tabindex): the\n * selected one, or the first when the current colour is not in the palette.\n */\n protected readonly focusableSwatchIndex = computed(() => {\n const index = this.swatches().indexOf(this.value());\n return index >= 0 ? index : 0;\n });\n\n protected pickerClasses = computed(() => {\n const classes = ['color-picker'];\n if (this.isDisabled()) classes.push('color-picker--disabled');\n return classes.join(' ');\n });\n\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onChange: (val: string) => void = () => {};\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n private onTouched: () => void = () => {};\n\n protected selectSwatch(color: string): void {\n if (this.isDisabled()) return;\n this.setValue(color);\n }\n\n /** Arrow keys / Home / End move selection (and focus) through the palette like a radiogroup. */\n protected onSwatchKeydown(event: KeyboardEvent, index: number): void {\n if (this.isDisabled()) return;\n const count = this.swatches().length;\n let next: number;\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowDown':\n next = (index + 1) % count;\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n next = (index - 1 + count) % count;\n break;\n case 'Home':\n next = 0;\n break;\n case 'End':\n next = count - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.selectSwatch(this.swatches()[next]);\n this.swatchButtons()[next]?.nativeElement.focus();\n }\n\n protected onNativeInput(event: Event): void {\n const target = event.target as HTMLInputElement;\n this.setValue(target.value);\n }\n\n protected onHexInput(event: Event): void {\n const target = event.target as HTMLInputElement;\n let val = target.value.trim();\n if (!val.startsWith('#')) val = '#' + val;\n if (/^#[0-9a-fA-F]{6}$/.test(val)) {\n this.setValue(val);\n }\n }\n\n protected onBlur(): void {\n this.onTouched();\n }\n\n private setValue(color: string): void {\n this.value.set(color);\n this.colorChange.emit(color);\n this.onChange(color);\n }\n\n // ControlValueAccessor\n writeValue(val: string): void {\n this.value.set(val ?? '#3b82f6');\n }\n\n registerOnChange(fn: (val: 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","<div [class]=\"pickerClasses()\">\n @if (label()) {\n <label class=\"color-picker__label\" [for]=\"inputId\">{{ label() }}</label>\n }\n <div class=\"color-picker__preview-row\">\n <input\n type=\"color\"\n [id]=\"inputId\"\n class=\"color-picker__native\"\n [value]=\"value()\"\n [disabled]=\"isDisabled()\"\n (input)=\"onNativeInput($event)\"\n (blur)=\"onBlur()\"\n [attr.aria-label]=\"label() ? null : ariaLabel()\"\n />\n <span\n class=\"color-picker__swatch-preview\"\n [style.background-color]=\"value()\"\n aria-hidden=\"true\"\n ></span>\n @if (showInput()) {\n <input\n type=\"text\"\n class=\"color-picker__hex-input\"\n [value]=\"value()\"\n [disabled]=\"isDisabled()\"\n maxlength=\"7\"\n (input)=\"onHexInput($event)\"\n (blur)=\"onBlur()\"\n [attr.aria-label]=\"(label() ? label() + ' – ' : '') + hexAriaLabel()\"\n />\n }\n </div>\n @if (swatches().length > 0) {\n <div\n class=\"color-picker__swatches\"\n role=\"radiogroup\"\n [attr.aria-label]=\"(label() ? label() + ' – ' : '') + swatchesAriaLabel()\"\n >\n @for (swatch of swatches(); track swatch; let i = $index) {\n <button\n #swatchBtn\n type=\"button\"\n class=\"color-picker__swatch\"\n role=\"radio\"\n [class.color-picker__swatch--selected]=\"swatch === value()\"\n [class.color-picker__swatch--light]=\"swatch === '#ffffff'\"\n [style.background-color]=\"swatch\"\n [disabled]=\"isDisabled()\"\n [tabindex]=\"i === focusableSwatchIndex() ? 0 : -1\"\n [attr.aria-label]=\"swatch\"\n [attr.aria-checked]=\"swatch === value()\"\n (click)=\"selectSwatch(swatch)\"\n (keydown)=\"onSwatchKeydown($event, i)\"\n (blur)=\"onBlur()\"\n ></button>\n }\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-confirm-dialog.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/confirm-dialog/confirm-dialog.component.ts","../../../../libs/angular-ui-kit/src/lib/confirm-dialog/confirm-dialog.component.html","../../../../libs/angular-ui-kit/src/lib/confirm-dialog/confirm.service.ts"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n signal,\n computed,\n effect,\n} from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_EXCLAMATION_CIRCLE, ICON_EXCLAMATION_TRIANGLE } from '@lifecorner/angular-ui-kit/icons';\nimport { ButtonComponent } from '@lifecorner/angular-ui-kit/button';\nimport { InputComponent } from '@lifecorner/angular-ui-kit/input';\nimport { ModalComponent, ModalPlacement } from '@lifecorner/angular-ui-kit/modal';\n\nexport type ConfirmDialogVariant = 'default' | 'destructive' | 'warning';\n\n/** Size of the confirm and cancel buttons (`lg` = 48.5 px, a 44 px target). */\nexport type ConfirmDialogButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface RequireTextConfig {\n prompt: string;\n expected: string;\n}\n\n/**\n * Confirm dialog component for confirming user actions.\n *\n * Builds on `<lc-modal>` to provide a standardized confirmation pattern\n * with optional destructive variant and text-matching confirmation.\n *\n * The dialog is named by its title (`aria-labelledby`); `[slot='preview']`\n * shows what the action affects below the message; `confirmLoading` keeps the\n * dialog open with a busy confirm button while the action runs.\n *\n * @example\n * ```html\n * <lc-confirm-dialog\n * [open]=\"showConfirm()\"\n * variant=\"destructive\"\n * title=\"Delete project?\"\n * message=\"This cannot be undone.\"\n * (confirmed)=\"onDelete()\"\n * (cancelled)=\"showConfirm.set(false)\"\n * />\n * ```\n */\n@Component({\n selector: 'lc-confirm-dialog',\n standalone: true,\n imports: [FormsModule, IconComponent, ButtonComponent, ModalComponent, InputComponent],\n viewProviders: [ɵprovideKitIcons([ICON_EXCLAMATION_TRIANGLE, ICON_EXCLAMATION_CIRCLE])],\n templateUrl: './confirm-dialog.component.html',\n styleUrls: ['./confirm-dialog.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ConfirmDialogComponent {\n private static nextId = 0;\n\n /** Whether the dialog is open */\n readonly open = input<boolean>(false);\n\n /** Dialog variant */\n readonly variant = input<ConfirmDialogVariant>('default');\n\n /** Dialog title */\n readonly title = input.required<string>();\n\n /** Dialog message */\n readonly message = input.required<string>();\n\n /** Confirm button label */\n readonly confirmLabel = input<string>('Confirm');\n\n /** Cancel button label */\n readonly cancelLabel = input<string>('Cancel');\n\n /** Icon name (auto-set by variant if not provided) */\n readonly icon = input<string>();\n\n /** Require text match to enable confirm */\n readonly requireText = input<RequireTextConfig>();\n\n /** Accessible name of the close button in the header. */\n readonly closeAriaLabel = input('Close modal');\n\n /**\n * Shows a spinner on the confirm button and blocks a second confirm while\n * the action runs; the app closes the dialog when it is done.\n * @default false\n */\n readonly confirmLoading = input<boolean>(false);\n\n /**\n * Size of the confirm and cancel buttons: `md` (3.2) or `lg` (48.5 px, the\n * 44 px touch target), or `sm`.\n * @default 'md'\n */\n readonly buttonSize = input<ConfirmDialogButtonSize>('md');\n\n /**\n * Where the dialog sits (passed to `lc-modal`): `center` (3.2), `auto` (a\n * bottom sheet below 640 px) or `sheet`.\n * @default 'center'\n */\n readonly placement = input<ModalPlacement>('center');\n\n /** Id of the title, which names the dialog. */\n protected readonly titleId = `lc-confirm-title-${++ConfirmDialogComponent.nextId}`;\n\n /** Emitted when user confirms */\n readonly confirmed = output<void>();\n\n /** Emitted when user cancels */\n readonly cancelled = output<void>();\n\n /** Internal text input value */\n protected inputValue = signal('');\n\n /** Resolved icon name */\n protected resolvedIcon = computed(() => {\n const icon = this.icon();\n if (icon) return icon;\n switch (this.variant()) {\n case 'destructive':\n return 'exclamation-triangle';\n case 'warning':\n return 'exclamation-circle';\n default:\n // Tabler name — 'question-mark-circle' has no alias and rendered a placeholder\n return 'help-circle';\n }\n });\n\n /** Resolved icon color */\n protected iconColor = computed(() => {\n switch (this.variant()) {\n case 'destructive':\n return 'var(--color-error-default)';\n case 'warning':\n return 'var(--color-warning-default)';\n default:\n return 'var(--color-primary-600)';\n }\n });\n\n /** Confirm button variant */\n protected confirmButtonVariant = computed(() => {\n switch (this.variant()) {\n case 'destructive':\n return 'danger';\n case 'warning':\n return 'warning';\n default:\n return 'primary';\n }\n });\n\n /** Whether confirm is allowed (text match check) */\n protected confirmAllowed = computed(() => {\n const req = this.requireText();\n if (!req) return true;\n return this.inputValue() === req.expected;\n });\n\n constructor() {\n // Reset input when dialog opens\n effect(() => {\n if (this.open()) {\n this.inputValue.set('');\n }\n });\n }\n\n /**\n * Confirm via button click or Enter inside the require-text input.\n * Enter is deliberately NOT handled on the whole dialog: the focus trap\n * lands on the close/cancel buttons first, and Enter there must activate\n * *that* button, never the (possibly destructive) confirm action.\n */\n protected onConfirm(): void {\n if (!this.confirmAllowed() || this.confirmLoading()) return;\n this.confirmed.emit();\n }\n\n protected onCancel(): void {\n this.cancelled.emit();\n }\n\n protected onModalClose(): void {\n this.cancelled.emit();\n }\n\n protected onInputChange(value: string | number): void {\n this.inputValue.set(String(value));\n }\n}\n","<lc-modal\n [open]=\"open()\"\n size=\"sm\"\n [placement]=\"placement()\"\n [showCloseButton]=\"true\"\n [closeAriaLabel]=\"closeAriaLabel()\"\n [ariaLabelledBy]=\"titleId\"\n (openChange)=\"$event ? null : onModalClose()\"\n>\n <div slot=\"header\">\n <div class=\"lc-confirm__header\">\n <lc-icon\n [name]=\"resolvedIcon()\"\n size=\"md\"\n [color]=\"iconColor()\"\n [decorative]=\"true\"\n />\n <h2 [id]=\"titleId\">{{ title() }}</h2>\n </div>\n </div>\n\n <div slot=\"body\">\n <p class=\"lc-confirm__message\">{{ message() }}</p>\n\n <div class=\"lc-confirm__preview\">\n <ng-content select=\"[slot='preview']\" />\n </div>\n\n @if (requireText(); as req) {\n <div class=\"lc-confirm__require-text\">\n <!-- lc-input renders the prompt as an associated <label for> -->\n <lc-input\n [label]=\"req.prompt\"\n [placeholder]=\"req.expected\"\n (valueChange)=\"onInputChange($event)\"\n (enterPressed)=\"onConfirm()\"\n />\n </div>\n }\n </div>\n\n <div slot=\"footer\" class=\"lc-confirm__footer\">\n <lc-button\n variant=\"ghost\"\n [size]=\"buttonSize()\"\n (clicked)=\"onCancel()\"\n >\n {{ cancelLabel() }}\n </lc-button>\n <lc-button\n [variant]=\"confirmButtonVariant()\"\n [size]=\"buttonSize()\"\n [disabled]=\"!confirmAllowed()\"\n [loading]=\"confirmLoading()\"\n (clicked)=\"onConfirm()\"\n >\n {{ confirmLabel() }}\n </lc-button>\n </div>\n</lc-modal>\n","import {\n Injectable,\n ApplicationRef,\n createComponent,\n EnvironmentInjector,\n inject,\n} from '@angular/core';\nimport {\n ConfirmDialogButtonSize,\n ConfirmDialogComponent,\n ConfirmDialogVariant,\n RequireTextConfig,\n} from './confirm-dialog.component';\nimport type { ModalPlacement } from '@lifecorner/angular-ui-kit/modal';\n\nexport interface ConfirmOptions {\n title: string;\n message: string;\n confirmLabel?: string;\n cancelLabel?: string;\n variant?: ConfirmDialogVariant;\n icon?: string;\n requireText?: RequireTextConfig;\n /** Accessible name of the close button (3.3). */\n closeAriaLabel?: string;\n /** Size of both buttons (3.3): `lg` for a 44 px touch target. */\n buttonSize?: ConfirmDialogButtonSize;\n /** `center`, `auto` (a bottom sheet below 640 px) or `sheet` (3.3). */\n placement?: ModalPlacement;\n}\n\n/**\n * Imperative confirm dialog service.\n *\n * @example\n * ```ts\n * const ok = await this.confirmService.confirm({\n * title: 'Delete item?',\n * message: 'This cannot be undone.',\n * });\n * if (ok) { ... }\n * ```\n */\n@Injectable({ providedIn: 'root' })\nexport class ConfirmService {\n private readonly appRef = inject(ApplicationRef);\n private readonly injector = inject(EnvironmentInjector);\n\n /**\n * Show a confirmation dialog and return a promise that resolves\n * to true (confirmed) or false (cancelled).\n */\n confirm(opts: ConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n // Create a host element\n const hostEl = document.createElement('div');\n document.body.appendChild(hostEl);\n\n const componentRef = createComponent(ConfirmDialogComponent, {\n hostElement: hostEl,\n environmentInjector: this.injector,\n });\n\n // Set inputs\n componentRef.setInput('open', true);\n componentRef.setInput('title', opts.title);\n componentRef.setInput('message', opts.message);\n if (opts.confirmLabel) componentRef.setInput('confirmLabel', opts.confirmLabel);\n if (opts.cancelLabel) componentRef.setInput('cancelLabel', opts.cancelLabel);\n if (opts.variant) componentRef.setInput('variant', opts.variant);\n if (opts.icon) componentRef.setInput('icon', opts.icon);\n if (opts.requireText) componentRef.setInput('requireText', opts.requireText);\n if (opts.closeAriaLabel) componentRef.setInput('closeAriaLabel', opts.closeAriaLabel);\n if (opts.buttonSize) componentRef.setInput('buttonSize', opts.buttonSize);\n if (opts.placement) componentRef.setInput('placement', opts.placement);\n\n const cleanup = () => {\n this.appRef.detachView(componentRef.hostView);\n componentRef.destroy();\n hostEl.remove();\n };\n\n componentRef.instance.confirmed.subscribe(() => {\n cleanup();\n resolve(true);\n });\n\n componentRef.instance.cancelled.subscribe(() => {\n cleanup();\n resolve(false);\n });\n\n this.appRef.attachView(componentRef.hostView);\n });\n }\n\n /**\n * Show a destructive confirmation dialog.\n * Shorthand for `confirm({ ...opts, variant: 'destructive' })`.\n */\n destructive(opts: Omit<ConfirmOptions, 'variant'>): Promise<boolean> {\n return this.confirm({ ...opts, variant: 'destructive' });\n }\n\n /**\n * Show a warning confirmation dialog.\n * Shorthand for `confirm({ ...opts, variant: 'warning' })`.\n */\n warning(opts: Omit<ConfirmOptions, 'variant'>): Promise<boolean> {\n return this.confirm({ ...opts, variant: 'warning' });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-confirm-dialog.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/confirm-dialog/confirm-dialog.component.ts","../../../../libs/angular-ui-kit/src/lib/confirm-dialog/confirm-dialog.component.html","../../../../libs/angular-ui-kit/src/lib/confirm-dialog/confirm.service.ts"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n signal,\n computed,\n effect,\n} from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_EXCLAMATION_CIRCLE, ICON_EXCLAMATION_TRIANGLE } from '@lifecorner/angular-ui-kit/icons';\nimport { ButtonComponent } from '@lifecorner/angular-ui-kit/button';\nimport { InputComponent } from '@lifecorner/angular-ui-kit/input';\nimport { ModalComponent, ModalPlacement } from '@lifecorner/angular-ui-kit/modal';\n\nexport type ConfirmDialogVariant = 'default' | 'destructive' | 'warning';\n\n/** Size of the confirm and cancel buttons (`lg` = 48.5 px, a 44 px target). */\nexport type ConfirmDialogButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface RequireTextConfig {\n prompt: string;\n expected: string;\n}\n\n/**\n * Confirm dialog component for confirming user actions.\n *\n * Builds on `<lc-modal>` to provide a standardized confirmation pattern\n * with optional destructive variant and text-matching confirmation.\n *\n * The dialog is named by its title (`aria-labelledby`); `[slot='preview']`\n * shows what the action affects below the message; `confirmLoading` keeps the\n * dialog open with a busy confirm button while the action runs.\n *\n * @example\n * ```html\n * <lc-confirm-dialog\n * [open]=\"showConfirm()\"\n * variant=\"destructive\"\n * title=\"Delete project?\"\n * message=\"This cannot be undone.\"\n * (confirmed)=\"onDelete()\"\n * (cancelled)=\"showConfirm.set(false)\"\n * />\n * ```\n */\n@Component({\n selector: 'lc-confirm-dialog',\n standalone: true,\n imports: [FormsModule, IconComponent, ButtonComponent, ModalComponent, InputComponent],\n viewProviders: [ɵprovideKitIcons([ICON_EXCLAMATION_TRIANGLE, ICON_EXCLAMATION_CIRCLE])],\n templateUrl: './confirm-dialog.component.html',\n styleUrls: ['./confirm-dialog.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ConfirmDialogComponent {\n private static nextId = 0;\n\n /** Whether the dialog is open */\n readonly open = input<boolean>(false);\n\n /** Dialog variant */\n readonly variant = input<ConfirmDialogVariant>('default');\n\n /** Dialog title */\n readonly title = input.required<string>();\n\n /** Dialog message */\n readonly message = input.required<string>();\n\n /** Confirm button label */\n readonly confirmLabel = input<string>('Confirm');\n\n /** Cancel button label */\n readonly cancelLabel = input<string>('Cancel');\n\n /** Icon name (auto-set by variant if not provided) */\n readonly icon = input<string>();\n\n /** Require text match to enable confirm */\n readonly requireText = input<RequireTextConfig>();\n\n /** Accessible name of the close button in the header. */\n readonly closeAriaLabel = input('Close modal');\n\n /**\n * Shows a spinner on the confirm button and blocks a second confirm while\n * the action runs; the app closes the dialog when it is done.\n * @default false\n */\n readonly confirmLoading = input<boolean>(false);\n\n /**\n * Size of the confirm and cancel buttons: `md` (3.2) or `lg` (48.5 px, the\n * 44 px touch target), or `sm`.\n * @default 'md'\n */\n readonly buttonSize = input<ConfirmDialogButtonSize>('md');\n\n /**\n * Where the dialog sits (passed to `lc-modal`): `center` (3.2), `auto` (a\n * bottom sheet below 640 px) or `sheet`.\n * @default 'center'\n */\n readonly placement = input<ModalPlacement>('center');\n\n /** Id of the title, which names the dialog. */\n protected readonly titleId = `lc-confirm-title-${++ConfirmDialogComponent.nextId}`;\n\n /** Emitted when user confirms */\n readonly confirmed = output<void>();\n\n /** Emitted when user cancels */\n readonly cancelled = output<void>();\n\n /** Internal text input value */\n protected inputValue = signal('');\n\n /** Resolved icon name */\n protected resolvedIcon = computed(() => {\n const icon = this.icon();\n if (icon) return icon;\n switch (this.variant()) {\n case 'destructive':\n return 'exclamation-triangle';\n case 'warning':\n return 'exclamation-circle';\n default:\n // Tabler name — 'question-mark-circle' has no alias and rendered a placeholder\n return 'help-circle';\n }\n });\n\n /** Resolved icon color */\n protected iconColor = computed(() => {\n switch (this.variant()) {\n case 'destructive':\n return 'var(--color-error-default)';\n case 'warning':\n return 'var(--color-warning-default)';\n default:\n return 'var(--color-primary-600)';\n }\n });\n\n /** Confirm button variant */\n protected confirmButtonVariant = computed(() => {\n switch (this.variant()) {\n case 'destructive':\n return 'danger';\n case 'warning':\n return 'warning';\n default:\n return 'primary';\n }\n });\n\n /** Whether confirm is allowed (text match check) */\n protected confirmAllowed = computed(() => {\n const req = this.requireText();\n if (!req) return true;\n return this.inputValue() === req.expected;\n });\n\n constructor() {\n // Reset input when dialog opens\n effect(() => {\n if (this.open()) {\n this.inputValue.set('');\n }\n });\n }\n\n /**\n * Confirm via button click or Enter inside the require-text input.\n * Enter is deliberately NOT handled on the whole dialog: the focus trap\n * lands on the close/cancel buttons first, and Enter there must activate\n * *that* button, never the (possibly destructive) confirm action.\n */\n protected onConfirm(): void {\n if (!this.confirmAllowed() || this.confirmLoading()) return;\n this.confirmed.emit();\n }\n\n protected onCancel(): void {\n this.cancelled.emit();\n }\n\n protected onModalClose(): void {\n this.cancelled.emit();\n }\n\n protected onInputChange(value: string | number): void {\n this.inputValue.set(String(value));\n }\n}\n","<lc-modal\n [open]=\"open()\"\n size=\"sm\"\n [placement]=\"placement()\"\n [showCloseButton]=\"true\"\n [closeAriaLabel]=\"closeAriaLabel()\"\n [ariaLabelledBy]=\"titleId\"\n (openChange)=\"$event ? null : onModalClose()\"\n>\n <div slot=\"header\">\n <div class=\"lc-confirm__header\">\n <lc-icon\n [name]=\"resolvedIcon()\"\n size=\"md\"\n [color]=\"iconColor()\"\n [decorative]=\"true\"\n />\n <h2 [id]=\"titleId\">{{ title() }}</h2>\n </div>\n </div>\n\n <div slot=\"body\">\n <p class=\"lc-confirm__message\">{{ message() }}</p>\n\n <div class=\"lc-confirm__preview\">\n <ng-content select=\"[slot='preview']\" />\n </div>\n\n @if (requireText(); as req) {\n <div class=\"lc-confirm__require-text\">\n <!-- lc-input renders the prompt as an associated <label for> -->\n <lc-input\n [label]=\"req.prompt\"\n [placeholder]=\"req.expected\"\n (valueChange)=\"onInputChange($event)\"\n (enterPressed)=\"onConfirm()\"\n />\n </div>\n }\n </div>\n\n <div slot=\"footer\" class=\"lc-confirm__footer\">\n <lc-button\n variant=\"ghost\"\n [size]=\"buttonSize()\"\n (clicked)=\"onCancel()\"\n >\n {{ cancelLabel() }}\n </lc-button>\n <lc-button\n [variant]=\"confirmButtonVariant()\"\n [size]=\"buttonSize()\"\n [disabled]=\"!confirmAllowed()\"\n [loading]=\"confirmLoading()\"\n (clicked)=\"onConfirm()\"\n >\n {{ confirmLabel() }}\n </lc-button>\n </div>\n</lc-modal>\n","import {\n Injectable,\n ApplicationRef,\n createComponent,\n EnvironmentInjector,\n inject,\n} from '@angular/core';\nimport {\n ConfirmDialogButtonSize,\n ConfirmDialogComponent,\n ConfirmDialogVariant,\n RequireTextConfig,\n} from './confirm-dialog.component';\nimport type { ModalPlacement } from '@lifecorner/angular-ui-kit/modal';\n\nexport interface ConfirmOptions {\n title: string;\n message: string;\n confirmLabel?: string;\n cancelLabel?: string;\n variant?: ConfirmDialogVariant;\n icon?: string;\n requireText?: RequireTextConfig;\n /** Accessible name of the close button (3.3). */\n closeAriaLabel?: string;\n /** Size of both buttons (3.3): `lg` for a 44 px touch target. */\n buttonSize?: ConfirmDialogButtonSize;\n /** `center`, `auto` (a bottom sheet below 640 px) or `sheet` (3.3). */\n placement?: ModalPlacement;\n}\n\n/**\n * Imperative confirm dialog service.\n *\n * @example\n * ```ts\n * const ok = await this.confirmService.confirm({\n * title: 'Delete item?',\n * message: 'This cannot be undone.',\n * });\n * if (ok) { ... }\n * ```\n */\n@Injectable({ providedIn: 'root' })\nexport class ConfirmService {\n private readonly appRef = inject(ApplicationRef);\n private readonly injector = inject(EnvironmentInjector);\n\n /**\n * Show a confirmation dialog and return a promise that resolves\n * to true (confirmed) or false (cancelled).\n */\n confirm(opts: ConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n // Create a host element\n const hostEl = document.createElement('div');\n document.body.appendChild(hostEl);\n\n const componentRef = createComponent(ConfirmDialogComponent, {\n hostElement: hostEl,\n environmentInjector: this.injector,\n });\n\n // Set inputs\n componentRef.setInput('open', true);\n componentRef.setInput('title', opts.title);\n componentRef.setInput('message', opts.message);\n if (opts.confirmLabel) componentRef.setInput('confirmLabel', opts.confirmLabel);\n if (opts.cancelLabel) componentRef.setInput('cancelLabel', opts.cancelLabel);\n if (opts.variant) componentRef.setInput('variant', opts.variant);\n if (opts.icon) componentRef.setInput('icon', opts.icon);\n if (opts.requireText) componentRef.setInput('requireText', opts.requireText);\n if (opts.closeAriaLabel) componentRef.setInput('closeAriaLabel', opts.closeAriaLabel);\n if (opts.buttonSize) componentRef.setInput('buttonSize', opts.buttonSize);\n if (opts.placement) componentRef.setInput('placement', opts.placement);\n\n const cleanup = () => {\n this.appRef.detachView(componentRef.hostView);\n componentRef.destroy();\n hostEl.remove();\n };\n\n componentRef.instance.confirmed.subscribe(() => {\n cleanup();\n resolve(true);\n });\n\n componentRef.instance.cancelled.subscribe(() => {\n cleanup();\n resolve(false);\n });\n\n this.appRef.attachView(componentRef.hostView);\n });\n }\n\n /**\n * Show a destructive confirmation dialog.\n * Shorthand for `confirm({ ...opts, variant: 'destructive' })`.\n */\n destructive(opts: Omit<ConfirmOptions, 'variant'>): Promise<boolean> {\n return this.confirm({ ...opts, variant: 'destructive' });\n }\n\n /**\n * Show a warning confirmation dialog.\n * Shorthand for `confirm({ ...opts, variant: 'warning' })`.\n */\n warning(opts: Omit<ConfirmOptions, 'variant'>): Promise<boolean> {\n return this.confirm({ ...opts, variant: 'warning' });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecorner-angular-ui-kit-copy-field.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/copy-field/copy-field.component.ts","../../../../libs/angular-ui-kit/src/lib/copy-field/copy-field.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { ButtonComponent } from '@lifecorner/angular-ui-kit/button';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHECK, ICON_CLIPBOARD } from '@lifecorner/angular-ui-kit/icons';\nimport { InputComponent } from '@lifecorner/angular-ui-kit/input';\n\n/** `stacked`: the button below the value at full width (phones); `inline`: beside it. */\nexport type CopyFieldLayout = 'stacked' | 'inline';\n\n/** How long the button says \"Copied\" (ms). */\nconst CONFIRMATION_MS = 2000;\n\n/**\n * A value to copy — an invitation link, a token, a command: the value\n * read-only and wrapping (never clipped), a 48.5 px copy button, and one\n * polite announcement after copying. If the clipboard refuses, the value is\n * selected so the person can copy it by hand.\n *\n * @example\n * ```html\n * <lc-copy-field label=\"Einladungslink\" [value]=\"link\" copyLabel=\"Kopieren\"\n * copiedLabel=\"Kopiert\" copiedAnnouncement=\"Link kopiert\" />\n * ```\n */\n@Component({\n selector: 'lc-copy-field',\n imports: [InputComponent, ButtonComponent, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_CHECK, ICON_CLIPBOARD])],\n templateUrl: './copy-field.component.html',\n styleUrl: './copy-field.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CopyFieldComponent {\n /** Label of the value (its accessible name). */\n readonly label = input<string>('');\n\n /** The value to copy. */\n readonly value = input<string>('');\n\n /** Help below the value. */\n readonly helperText = input<string>();\n\n /** `stacked` (default): button below at full width; `inline`: button beside the value. */\n readonly layout = input<CopyFieldLayout>('stacked');\n\n /** Show the clipboard (and, after copying, check) icon in the button. */\n readonly showIcon = input<boolean>(true);\n\n /** Button label. */\n readonly copyLabel = input('Copy');\n\n /** Button label for two seconds after copying. */\n readonly copiedLabel = input('Copied');\n\n /** Spoken once after copying (polite live region). */\n readonly copiedAnnouncement = input('Copied to the clipboard');\n\n /** Emitted with the value after it was copied. */\n readonly copied = output<string>();\n\n /** Emitted when the clipboard refused; the value is then selected for copying by hand. */\n readonly copyFailed = output<void>();\n\n protected readonly isCopied = signal(false);\n\n private readonly field = viewChild(InputComponent);\n private timer: ReturnType<typeof setTimeout> | undefined;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => clearTimeout(this.timer));\n }\n\n /** Copies the value; on refusal selects it instead. */\n async copy(): Promise<void> {\n const value = this.value();\n try {\n const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard;\n if (!clipboard) throw new Error('Clipboard API unavailable');\n await clipboard.writeText(value);\n } catch {\n this.copyFailed.emit();\n this.field()?.focus();\n this.field()?.select();\n return;\n }\n this.isCopied.set(true);\n this.copied.emit(value);\n clearTimeout(this.timer);\n this.timer = setTimeout(() => this.isCopied.set(false), CONFIRMATION_MS);\n }\n}\n","<div\n class=\"lc-copy-field\"\n [class.lc-copy-field--stacked]=\"layout() === 'stacked'\"\n [class.lc-copy-field--inline]=\"layout() === 'inline'\"\n>\n <div class=\"lc-copy-field__value\">\n <lc-input\n [label]=\"label()\"\n [value]=\"value()\"\n [helperText]=\"helperText()\"\n size=\"lg\"\n [readonly]=\"true\"\n [readonlyWrap]=\"true\"\n />\n </div>\n\n <div class=\"lc-copy-field__action\">\n <lc-button variant=\"outline\" size=\"lg\" [fullWidth]=\"layout() === 'stacked'\" (clicked)=\"copy()\">\n @if (showIcon()) {\n <lc-icon slot=\"icon-before\" [name]=\"isCopied() ? 'check' : 'clipboard-document'\" size=\"sm\" [decorative]=\"true\" />\n }\n {{ isCopied() ? copiedLabel() : copyLabel() }}\n </lc-button>\n </div>\n\n <span class=\"lc-copy-field__live\" role=\"status\" aria-live=\"polite\">{{ isCopied() ? copiedAnnouncement() : '' }}</span>\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecorner-angular-ui-kit-copy-field.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/copy-field/copy-field.component.ts","../../../../libs/angular-ui-kit/src/lib/copy-field/copy-field.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { ButtonComponent } from '@lifecorner/angular-ui-kit/button';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHECK, ICON_CLIPBOARD } from '@lifecorner/angular-ui-kit/icons';\nimport { InputComponent } from '@lifecorner/angular-ui-kit/input';\n\n/** `stacked`: the button below the value at full width (phones); `inline`: beside it. */\nexport type CopyFieldLayout = 'stacked' | 'inline';\n\n/** How long the button says \"Copied\" (ms). */\nconst CONFIRMATION_MS = 2000;\n\n/**\n * A value to copy — an invitation link, a token, a command: the value\n * read-only and wrapping (never clipped), a 48.5 px copy button, and one\n * polite announcement after copying. If the clipboard refuses, the value is\n * selected so the person can copy it by hand.\n *\n * @example\n * ```html\n * <lc-copy-field label=\"Einladungslink\" [value]=\"link\" copyLabel=\"Kopieren\"\n * copiedLabel=\"Kopiert\" copiedAnnouncement=\"Link kopiert\" />\n * ```\n */\n@Component({\n selector: 'lc-copy-field',\n imports: [InputComponent, ButtonComponent, IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_CHECK, ICON_CLIPBOARD])],\n templateUrl: './copy-field.component.html',\n styleUrl: './copy-field.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class CopyFieldComponent {\n /** Label of the value (its accessible name). */\n readonly label = input<string>('');\n\n /** The value to copy. */\n readonly value = input<string>('');\n\n /** Help below the value. */\n readonly helperText = input<string>();\n\n /** `stacked` (default): button below at full width; `inline`: button beside the value. */\n readonly layout = input<CopyFieldLayout>('stacked');\n\n /** Show the clipboard (and, after copying, check) icon in the button. */\n readonly showIcon = input<boolean>(true);\n\n /** Button label. */\n readonly copyLabel = input('Copy');\n\n /** Button label for two seconds after copying. */\n readonly copiedLabel = input('Copied');\n\n /** Spoken once after copying (polite live region). */\n readonly copiedAnnouncement = input('Copied to the clipboard');\n\n /** Emitted with the value after it was copied. */\n readonly copied = output<string>();\n\n /** Emitted when the clipboard refused; the value is then selected for copying by hand. */\n readonly copyFailed = output<void>();\n\n protected readonly isCopied = signal(false);\n\n private readonly field = viewChild(InputComponent);\n private timer: ReturnType<typeof setTimeout> | undefined;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => clearTimeout(this.timer));\n }\n\n /** Copies the value; on refusal selects it instead. */\n async copy(): Promise<void> {\n const value = this.value();\n try {\n const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard;\n if (!clipboard) throw new Error('Clipboard API unavailable');\n await clipboard.writeText(value);\n } catch {\n this.copyFailed.emit();\n this.field()?.focus();\n this.field()?.select();\n return;\n }\n this.isCopied.set(true);\n this.copied.emit(value);\n clearTimeout(this.timer);\n this.timer = setTimeout(() => this.isCopied.set(false), CONFIRMATION_MS);\n }\n}\n","<div\n class=\"lc-copy-field\"\n [class.lc-copy-field--stacked]=\"layout() === 'stacked'\"\n [class.lc-copy-field--inline]=\"layout() === 'inline'\"\n>\n <div class=\"lc-copy-field__value\">\n <lc-input\n [label]=\"label()\"\n [value]=\"value()\"\n [helperText]=\"helperText()\"\n size=\"lg\"\n [readonly]=\"true\"\n [readonlyWrap]=\"true\"\n />\n </div>\n\n <div class=\"lc-copy-field__action\">\n <lc-button variant=\"outline\" size=\"lg\" [fullWidth]=\"layout() === 'stacked'\" (clicked)=\"copy()\">\n @if (showIcon()) {\n <lc-icon slot=\"icon-before\" [name]=\"isCopied() ? 'check' : 'clipboard-document'\" size=\"sm\" [decorative]=\"true\" />\n }\n {{ isCopied() ? copiedLabel() : copyLabel() }}\n </lc-button>\n </div>\n\n <span class=\"lc-copy-field__live\" role=\"status\" aria-live=\"polite\">{{ isCopied() ? copiedAnnouncement() : '' }}</span>\n</div>\n"],"mappings":"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"}
|