@tolle_/tolle-ui 18.2.29 → 18.3.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/esm2022/lib/accordion-item.component.mjs +5 -11
- package/esm2022/lib/accordion.component.mjs +4 -9
- package/esm2022/lib/alert-dialog-dynamic.component.mjs +12 -12
- package/esm2022/lib/alert-dialog.component.mjs +82 -64
- package/esm2022/lib/alert-dialog.service.mjs +38 -6
- package/esm2022/lib/alert.component.mjs +5 -11
- package/esm2022/lib/aspect-ratio.component.mjs +119 -0
- package/esm2022/lib/attachment.component.mjs +334 -0
- package/esm2022/lib/avatar-fallback.component.mjs +3 -3
- package/esm2022/lib/avatar.component.mjs +4 -9
- package/esm2022/lib/badge.component.mjs +4 -9
- package/esm2022/lib/breadcrumb-item.component.mjs +3 -3
- package/esm2022/lib/breadcrumb-link.component.mjs +3 -3
- package/esm2022/lib/breadcrumb-separator.component.mjs +3 -3
- package/esm2022/lib/breadcrumb.component.mjs +3 -3
- package/esm2022/lib/bubble.component.mjs +301 -0
- package/esm2022/lib/button.component.mjs +5 -11
- package/esm2022/lib/calendar.component.mjs +5 -11
- package/esm2022/lib/card.component.mjs +14 -43
- package/esm2022/lib/carousel/carousel.component.mjs +5 -12
- package/esm2022/lib/chain-of-thought.component.mjs +380 -0
- package/esm2022/lib/chart-pie.component.mjs +386 -0
- package/esm2022/lib/chart.component.mjs +1146 -0
- package/esm2022/lib/chart.service.mjs +555 -0
- package/esm2022/lib/checkbox.component.mjs +5 -11
- package/esm2022/lib/checkpoint.component.mjs +134 -0
- package/esm2022/lib/collapsible.component.mjs +4 -11
- package/esm2022/lib/combobox.component.mjs +372 -0
- package/esm2022/lib/command-dialog.component.mjs +164 -0
- package/esm2022/lib/command.component.mjs +514 -0
- package/esm2022/lib/command.service.mjs +130 -0
- package/esm2022/lib/confirmation.component.mjs +219 -0
- package/esm2022/lib/context-menu-trigger.directive.mjs +36 -0
- package/esm2022/lib/context-menu.component.mjs +460 -0
- package/esm2022/lib/context-menu.service.mjs +125 -0
- package/esm2022/lib/context.component.mjs +377 -0
- package/esm2022/lib/conversation.component.mjs +442 -0
- package/esm2022/lib/country-selector.component.mjs +5 -11
- package/esm2022/lib/data-table.component.mjs +59 -12
- package/esm2022/lib/date-picker.component.mjs +251 -198
- package/esm2022/lib/date-range-picker.component.mjs +224 -116
- package/esm2022/lib/date-time-picker.component.mjs +506 -0
- package/esm2022/lib/direction.component.mjs +79 -0
- package/esm2022/lib/dropdown-item.component.mjs +3 -3
- package/esm2022/lib/dropdown-label.component.mjs +3 -3
- package/esm2022/lib/dropdown-menu.component.mjs +3 -3
- package/esm2022/lib/dropdown-separator.component.mjs +3 -3
- package/esm2022/lib/dropdown-trigger.directive.mjs +6 -4
- package/esm2022/lib/empty-state.component.mjs +4 -9
- package/esm2022/lib/field.component.mjs +394 -0
- package/esm2022/lib/hover-card.component.mjs +6 -16
- package/esm2022/lib/inline-citation.component.mjs +263 -0
- package/esm2022/lib/input-group.component.mjs +418 -0
- package/esm2022/lib/input.component.mjs +5 -11
- package/esm2022/lib/item.component.mjs +243 -0
- package/esm2022/lib/kbd.component.mjs +82 -0
- package/esm2022/lib/label.component.mjs +4 -9
- package/esm2022/lib/marker.component.mjs +135 -0
- package/esm2022/lib/masked-input.component.mjs +5 -11
- package/esm2022/lib/menubar.component.mjs +683 -0
- package/esm2022/lib/message-scroller.component.mjs +353 -0
- package/esm2022/lib/message-scroller.service.mjs +157 -0
- package/esm2022/lib/message.component.mjs +228 -0
- package/esm2022/lib/modal-ref.mjs +36 -7
- package/esm2022/lib/modal.component.mjs +42 -28
- package/esm2022/lib/modal.mjs +2 -29
- package/esm2022/lib/modal.service.mjs +26 -4
- package/esm2022/lib/model-selector.component.mjs +310 -0
- package/esm2022/lib/multi-select.component.mjs +155 -72
- package/esm2022/lib/native-select.component.mjs +201 -0
- package/esm2022/lib/navigation-menu.component.mjs +642 -0
- package/esm2022/lib/otp-group.component.mjs +3 -3
- package/esm2022/lib/otp-slot.component.mjs +4 -9
- package/esm2022/lib/otp.component.mjs +5 -11
- package/esm2022/lib/pagination.component.mjs +7 -10
- package/esm2022/lib/plan.component.mjs +335 -0
- package/esm2022/lib/popover-content.component.mjs +4 -9
- package/esm2022/lib/popover.component.mjs +15 -13
- package/esm2022/lib/progress.component.mjs +4 -9
- package/esm2022/lib/prompt-input.component.mjs +526 -0
- package/esm2022/lib/queue.component.mjs +280 -0
- package/esm2022/lib/radio-group.component.mjs +6 -13
- package/esm2022/lib/radio-item.component.mjs +4 -9
- package/esm2022/lib/range-calendar.component.mjs +5 -11
- package/esm2022/lib/reasoning.component.mjs +335 -0
- package/esm2022/lib/resizable-panel.component.mjs +5 -5
- package/esm2022/lib/resizable.component.mjs +219 -0
- package/esm2022/lib/segment.component.mjs +19 -9
- package/esm2022/lib/select-group.component.mjs +4 -8
- package/esm2022/lib/select-item.component.mjs +4 -9
- package/esm2022/lib/select-separator.component.mjs +4 -8
- package/esm2022/lib/select.component.mjs +133 -49
- package/esm2022/lib/separator.component.mjs +4 -9
- package/esm2022/lib/sheet.component.mjs +38 -56
- package/esm2022/lib/shimmer.component.mjs +84 -0
- package/esm2022/lib/sidebar.component.mjs +11 -1
- package/esm2022/lib/skeleton.component.mjs +4 -9
- package/esm2022/lib/sources.component.mjs +452 -0
- package/esm2022/lib/spinner.component.mjs +68 -0
- package/esm2022/lib/suggestion.component.mjs +207 -0
- package/esm2022/lib/switch.component.mjs +5 -11
- package/esm2022/lib/table.component.mjs +213 -0
- package/esm2022/lib/tabs.component.mjs +4 -11
- package/esm2022/lib/tag-input.component.mjs +5 -11
- package/esm2022/lib/task.component.mjs +359 -0
- package/esm2022/lib/textarea.component.mjs +5 -11
- package/esm2022/lib/theme.service.mjs +17 -6
- package/esm2022/lib/time-picker.component.mjs +923 -0
- package/esm2022/lib/toast.service.mjs +3 -2
- package/esm2022/lib/toaster.component.mjs +76 -39
- package/esm2022/lib/toggle-group.component.mjs +4 -9
- package/esm2022/lib/toggle.component.mjs +5 -11
- package/esm2022/lib/tool.component.mjs +462 -0
- package/esm2022/lib/typography.component.mjs +83 -0
- package/esm2022/lib/utils/color.mjs +140 -19
- package/esm2022/public-api.mjs +61 -1
- package/fesm2022/tolle-ui.mjs +16642 -1105
- package/fesm2022/tolle-ui.mjs.map +1 -1
- package/lib/alert-dialog-dynamic.component.d.ts +5 -0
- package/lib/alert-dialog.component.d.ts +23 -2
- package/lib/aspect-ratio.component.d.ts +20 -0
- package/lib/attachment.component.d.ts +101 -0
- package/lib/bubble.component.d.ts +112 -0
- package/lib/chain-of-thought.component.d.ts +166 -0
- package/lib/chart-pie.component.d.ts +108 -0
- package/lib/chart.component.d.ts +369 -0
- package/lib/chart.service.d.ts +222 -0
- package/lib/checkpoint.component.d.ts +51 -0
- package/lib/combobox.component.d.ts +90 -0
- package/lib/command-dialog.component.d.ts +53 -0
- package/lib/command.component.d.ts +189 -0
- package/lib/command.service.d.ts +67 -0
- package/lib/confirmation.component.d.ts +70 -0
- package/lib/context-menu-trigger.directive.d.ts +12 -0
- package/lib/context-menu.component.d.ts +41 -0
- package/lib/context-menu.service.d.ts +38 -0
- package/lib/context.component.d.ts +134 -0
- package/lib/conversation.component.d.ts +165 -0
- package/lib/data-table.component.d.ts +26 -1
- package/lib/date-picker.component.d.ts +72 -11
- package/lib/date-range-picker.component.d.ts +63 -6
- package/lib/date-time-picker.component.d.ts +119 -0
- package/lib/direction.component.d.ts +45 -0
- package/lib/field.component.d.ts +201 -0
- package/lib/inline-citation.component.d.ts +103 -0
- package/lib/input-group.component.d.ts +150 -0
- package/lib/item.component.d.ts +154 -0
- package/lib/kbd.component.d.ts +50 -0
- package/lib/marker.component.d.ts +58 -0
- package/lib/menubar.component.d.ts +247 -0
- package/lib/message-scroller.component.d.ts +159 -0
- package/lib/message-scroller.service.d.ts +73 -0
- package/lib/message.component.d.ts +128 -0
- package/lib/modal-ref.d.ts +6 -1
- package/lib/modal.component.d.ts +7 -4
- package/lib/modal.d.ts +9 -3
- package/lib/model-selector.component.d.ts +103 -0
- package/lib/multi-select.component.d.ts +66 -14
- package/lib/native-select.component.d.ts +57 -0
- package/lib/navigation-menu.component.d.ts +230 -0
- package/lib/plan.component.d.ts +120 -0
- package/lib/popover.component.d.ts +1 -0
- package/lib/prompt-input.component.d.ts +194 -0
- package/lib/queue.component.d.ts +106 -0
- package/lib/reasoning.component.d.ts +127 -0
- package/lib/resizable.component.d.ts +43 -0
- package/lib/segment.component.d.ts +13 -3
- package/lib/select.component.d.ts +37 -5
- package/lib/sheet.component.d.ts +3 -0
- package/lib/shimmer.component.d.ts +38 -0
- package/lib/sidebar.component.d.ts +2 -0
- package/lib/sources.component.d.ts +156 -0
- package/lib/spinner.component.d.ts +29 -0
- package/lib/suggestion.component.d.ts +96 -0
- package/lib/table.component.d.ts +106 -0
- package/lib/task.component.d.ts +151 -0
- package/lib/theme.service.d.ts +9 -0
- package/lib/time-picker.component.d.ts +243 -0
- package/lib/toast.service.d.ts +3 -1
- package/lib/toaster.component.d.ts +26 -9
- package/lib/tool.component.d.ts +158 -0
- package/lib/typography.component.d.ts +32 -0
- package/lib/utils/color.d.ts +19 -5
- package/package.json +3 -2
- package/preset.js +7 -1
- package/public-api.d.ts +50 -0
- package/registry/docs-content.json +8480 -1542
- package/registry/llms-full.txt +2679 -319
- package/registry/llms.txt +43 -4
- package/registry/manifest.json +9090 -1742
- package/registry/r/accordion-item.json +1 -1
- package/registry/r/accordion.json +1 -1
- package/registry/r/alert-dialog-dynamic.json +1 -1
- package/registry/r/alert-dialog.json +1 -1
- package/registry/r/alert.json +1 -1
- package/registry/r/attachment.json +23 -0
- package/registry/r/avatar-fallback.json +1 -1
- package/registry/r/avatar.json +1 -1
- package/registry/r/badge.json +1 -1
- package/registry/r/breadcrumb-item.json +1 -1
- package/registry/r/breadcrumb-link.json +1 -1
- package/registry/r/breadcrumb-separator.json +1 -1
- package/registry/r/breadcrumb.json +1 -1
- package/registry/r/bubble.json +23 -0
- package/registry/r/button.json +1 -1
- package/registry/r/calendar.json +1 -1
- package/registry/r/card.json +1 -1
- package/registry/r/carousel.json +1 -1
- package/registry/r/chain-of-thought.json +23 -0
- package/registry/r/chart-pie.json +30 -0
- package/registry/r/chart.json +28 -0
- package/registry/r/checkbox.json +1 -1
- package/registry/r/checkpoint.json +23 -0
- package/registry/r/collapsible.json +1 -1
- package/registry/r/combobox.json +31 -0
- package/registry/r/command-dialog.json +21 -0
- package/registry/r/command.json +26 -0
- package/registry/r/confirmation.json +23 -0
- package/registry/r/context-menu-trigger.json +1 -1
- package/registry/r/context-menu.json +1 -1
- package/registry/r/context.json +26 -0
- package/registry/r/conversation.json +23 -0
- package/registry/r/country-selector.json +1 -1
- package/registry/r/data-table.json +1 -1
- package/registry/r/date-picker.json +4 -4
- package/registry/r/date-range-picker.json +3 -3
- package/registry/r/date-time-picker.json +28 -0
- package/registry/r/direction.json +16 -0
- package/registry/r/dropdown-item.json +1 -1
- package/registry/r/dropdown-label.json +1 -1
- package/registry/r/dropdown-menu.json +1 -1
- package/registry/r/dropdown-separator.json +1 -1
- package/registry/r/dropdown-trigger.json +1 -1
- package/registry/r/empty-state.json +1 -1
- package/registry/r/field.json +23 -0
- package/registry/r/hover-card.json +1 -1
- package/registry/r/inline-citation.json +26 -0
- package/registry/r/input-group.json +23 -0
- package/registry/r/input.json +1 -1
- package/registry/r/item.json +23 -0
- package/registry/r/kbd.json +23 -0
- package/registry/r/label.json +1 -1
- package/registry/r/marker.json +23 -0
- package/registry/r/masked-input.json +1 -1
- package/registry/r/menubar.json +24 -0
- package/registry/r/message-scroller.json +26 -0
- package/registry/r/message.json +23 -0
- package/registry/r/modal.json +3 -3
- package/registry/r/model-selector.json +32 -0
- package/registry/r/multi-select.json +2 -2
- package/registry/r/native-select.json +23 -0
- package/registry/r/navigation-menu.json +24 -0
- package/registry/r/otp-group.json +1 -1
- package/registry/r/otp-slot.json +1 -1
- package/registry/r/otp.json +1 -1
- package/registry/r/pagination.json +3 -2
- package/registry/r/plan.json +23 -0
- package/registry/r/popover-content.json +1 -1
- package/registry/r/popover.json +1 -1
- package/registry/r/progress.json +1 -1
- package/registry/r/prompt-input.json +23 -0
- package/registry/r/queue.json +23 -0
- package/registry/r/radio-group.json +1 -1
- package/registry/r/radio-item.json +1 -1
- package/registry/r/range-calendar.json +1 -1
- package/registry/r/reasoning.json +23 -0
- package/registry/r/resizable-panel.json +1 -1
- package/registry/r/segment.json +1 -1
- package/registry/r/select-group.json +1 -1
- package/registry/r/select-item.json +1 -1
- package/registry/r/select-separator.json +1 -1
- package/registry/r/select.json +4 -3
- package/registry/r/separator.json +1 -1
- package/registry/r/sheet.json +1 -1
- package/registry/r/shimmer.json +23 -0
- package/registry/r/sidebar.json +1 -1
- package/registry/r/skeleton.json +1 -1
- package/registry/r/sources.json +23 -0
- package/registry/r/spinner.json +23 -0
- package/registry/r/suggestion.json +23 -0
- package/registry/r/switch.json +1 -1
- package/registry/r/table.json +21 -0
- package/registry/r/tabs.json +1 -1
- package/registry/r/tag-input.json +1 -1
- package/registry/r/task.json +23 -0
- package/registry/r/textarea.json +1 -1
- package/registry/r/time-picker.json +24 -0
- package/registry/r/toaster.json +2 -2
- package/registry/r/toggle-group.json +1 -1
- package/registry/r/toggle.json +1 -1
- package/registry/r/tool.json +23 -0
- package/registry/r/typography.json +23 -0
- package/registry/registry.json +915 -22
- package/theme.css +121 -8
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
"name": "date-range-picker",
|
|
3
3
|
"type": "registry:ui",
|
|
4
4
|
"title": "Date Range Picker",
|
|
5
|
-
"description": "Date Range Picker component.",
|
|
5
|
+
"description": "Date Range Picker component with size and invalid options.",
|
|
6
6
|
"category": "date-time",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"input",
|
|
9
8
|
"range-calendar"
|
|
10
9
|
],
|
|
11
10
|
"dependencies": [
|
|
12
11
|
"@floating-ui/dom",
|
|
12
|
+
"class-variance-authority",
|
|
13
13
|
"date-fns"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
|
16
16
|
{
|
|
17
17
|
"path": "date-range-picker.component.ts",
|
|
18
18
|
"target": "ui/date-range-picker.component.ts",
|
|
19
|
-
"content": "import {\n Component, Input, forwardRef, ElementRef, ViewChild, ChangeDetectorRef, OnDestroy, HostListener\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport { computePosition, flip, shift, offset, autoUpdate, size } from '@floating-ui/dom';\nimport { cn } from './utils/cn';\nimport { RangeCalendarComponent } from './range-calendar.component';\nimport { format } from 'date-fns';\nimport { InputComponent } from './input.component';\nimport { DateRange } from './types/date-range';\n\n@Component({\n selector: 'tolle-date-range-picker',\n standalone: true,\n imports: [CommonModule, FormsModule, RangeCalendarComponent, InputComponent],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => DateRangePickerComponent),\n multi: true\n }\n ],\n template: `\n <div class=\"relative w-full\">\n <tolle-input\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [ngModel]=\"displayValue\"\n [class]=\"class\"\n >\n <div suffix class=\"flex items-center gap-1.5\">\n <button\n type=\"button\"\n tabindex=\"-1\"\n aria-label=\"Clear date range\"\n *ngIf=\"(value.start || value.end) && !disabled\"\n (click)=\"clear($event)\"\n class=\"ri-close-line cursor-pointer text-muted-foreground hover:text-foreground transition-colors\"\n ></button>\n\n <button\n #trigger\n type=\"button\"\n (click)=\"togglePopover($event)\"\n aria-label=\"Open calendar\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"isOpen\"\n class=\"ri-calendar-line cursor-pointer text-muted-foreground hover:text-primary transition-colors bg-transparent border-0 p-0\"\n ></button>\n </div>\n </tolle-input>\n\n <div\n #popover\n *ngIf=\"isOpen\"\n role=\"dialog\"\n class=\"fixed z-50\"\n style=\"visibility: hidden;\"\n >\n <tolle-range-calendar class=\"shadow-lg\"\n [ngModel]=\"value\"\n (rangeSelect)=\"onCalendarSelect($event)\"\n [disablePastDates]=\"disablePastDates\"\n [numberOfMonths]=\"numberOfMonths\"\n ></tolle-range-calendar>\n </div>\n </div>\n `\n})\nexport class DateRangePickerComponent implements ControlValueAccessor, OnDestroy {\n @Input() disabled = false;\n @Input() placeholder = 'Pick a date range';\n @Input() class = '';\n @Input() disablePastDates = false;\n @Input() size: 'xs' | 'sm' | 'default' | 'lg' = 'default';\n /** Consecutive months shown in the popover (e.g. `2` for a two-month picker). @default 1 */\n @Input() numberOfMonths = 1;\n\n @ViewChild('trigger') trigger!: ElementRef;\n @ViewChild('popover') popover!: ElementRef;\n\n value: DateRange = { start: null, end: null };\n isOpen = false;\n cleanupAutoUpdate?: () => void;\n\n private _outsideClickHandler = (event: MouseEvent) => {\n if (!this.trigger.nativeElement.contains(event.target) && !this.popover?.nativeElement.contains(event.target)) {\n this.close();\n }\n };\n\n constructor(private cdr: ChangeDetectorRef) {}\n\n get displayValue(): string {\n if (!this.value.start) return '';\n\n const startStr = format(this.value.start, 'MMM dd, yyyy');\n if (!this.value.end) return startStr;\n\n const endStr = format(this.value.end, 'MMM dd, yyyy');\n return `${startStr} - ${endStr}`;\n }\n\n onCalendarSelect(range: DateRange) {\n this.value = range;\n this.onChange(this.value);\n\n // Close only if range is complete\n if (range.start && range.end) {\n setTimeout(() => this.close(), 150);\n }\n }\n\n togglePopover(event: MouseEvent) {\n event.stopPropagation();\n if (this.disabled) return;\n this.isOpen ? this.close() : this.open();\n }\n\n @HostListener('keydown.escape') onEscape() { if (this.isOpen) this.close(); }\n\n open() {\n this.isOpen = true;\n // Render the popover synchronously so its DOM node + @ViewChild('popover') exist\n // before we position it. Otherwise, with zone eventCoalescing (Angular defers change\n // detection to the next animation frame), updatePosition() can run first, find no\n // popover, and the calendar stays hidden — regardless of where the picker is used.\n this.cdr.detectChanges();\n if (this.trigger?.nativeElement) {\n this.trigger.nativeElement.focus();\n }\n const schedule = typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : (fn: FrameRequestCallback) => setTimeout(fn, 0);\n schedule(() => {\n this.updatePosition();\n document.addEventListener('mousedown', this._outsideClickHandler);\n });\n }\n\n close() {\n this.isOpen = false;\n if (this.cleanupAutoUpdate) {\n this.cleanupAutoUpdate();\n this.cleanupAutoUpdate = undefined;\n }\n document.removeEventListener('mousedown', this._outsideClickHandler);\n }\n\n clear(event: MouseEvent) {\n event.stopPropagation();\n this.value = { start: null, end: null };\n this.onChange(this.value);\n this.cdr.markForCheck();\n }\n\n // --- Floating UI Positioning with Fixed Strategy ---\n private updatePosition() {\n if (!this.trigger || !this.popover) return;\n\n this.cleanupAutoUpdate = autoUpdate(\n this.trigger.nativeElement,\n this.popover.nativeElement,\n () => {\n computePosition(this.trigger.nativeElement, this.popover.nativeElement, {\n placement: 'bottom-end',\n strategy: 'fixed', // Use fixed to escape column layout\n middleware: [\n offset(4),\n flip({\n fallbackAxisSideDirection: 'start',\n padding: 8\n }),\n shift({ padding: 8 }),\n size({\n apply({ rects, elements, availableHeight }) {\n // Constrain popover to available space\n Object.assign(elements.floating.style, {\n maxHeight: `${Math.min(400, availableHeight)}px`,\n minWidth: `${Math.max(rects.reference.width, 320)}px`, // Calendar minimum width\n });\n }\n })\n ],\n }).then(({ x, y, placement }) => {\n Object.assign(this.popover.nativeElement.style, {\n left: `${x}px`,\n top: `${y}px`,\n visibility: 'visible',\n });\n\n // Optional: Add placement class for styling\n this.popover.nativeElement.classList.remove('calendar-top', 'calendar-bottom');\n if (placement.includes('top')) {\n this.popover.nativeElement.classList.add('calendar-top');\n } else {\n this.popover.nativeElement.classList.add('calendar-bottom');\n }\n });\n }\n );\n }\n\n ngOnDestroy() {\n if (this.cleanupAutoUpdate) this.cleanupAutoUpdate();\n document.removeEventListener('mousedown', this._outsideClickHandler);\n }\n\n @HostListener('window:resize')\n onWindowResize() {\n if (this.isOpen) {\n this.close(); // Close on resize for simplicity\n }\n }\n\n @HostListener('window:scroll')\n onWindowScroll() {\n // Floating-UI's autoUpdate handles scroll repositioning\n }\n\n // --- Control Value Accessor ---\n onChange: any = () => {};\n onTouched: any = () => {};\n\n writeValue(val: DateRange | null): void {\n if (val) {\n this.value = { ...val };\n } else {\n this.value = { start: null, end: null };\n }\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; }\n\n protected cn = cn;\n}\n"
|
|
19
|
+
"content": "import { Component, Input, Output, EventEmitter, forwardRef, ElementRef, ViewChild, ChangeDetectorRef, ChangeDetectionStrategy, OnDestroy, HostListener, OnChanges, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport { computePosition, flip, shift, offset, autoUpdate, size as sizeMiddleware } from '@floating-ui/dom';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\nimport { RangeCalendarComponent } from './range-calendar.component';\nimport { format } from 'date-fns';\nimport { DateRange } from './types/date-range';\n\n/**\n * The trigger button — same shape as `tolle-date-time-picker`'s and\n * `tolle-date-picker`'s, so all three read as one family: a bordered field\n * that opens a floating panel, not a typeable text input.\n */\nconst dateRangePickerTriggerVariants = cva(\n 'flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background text-foreground shadow-sm transition-colors ' +\n 'focus:outline-none focus-visible:border-primary/80 focus-visible:ring-4 focus-visible:ring-ring/30 ' +\n 'disabled:cursor-not-allowed disabled:opacity-50',\n {\n variants: {\n size: {\n xs: 'h-8 px-2 text-xs',\n sm: 'h-9 px-3 text-sm',\n default: 'h-10 px-3 text-sm',\n lg: 'h-11 px-4 text-base',\n },\n invalid: {\n true: 'border-destructive focus-visible:border-destructive/80 focus-visible:ring-destructive/30',\n false: '',\n },\n },\n defaultVariants: { size: 'default', invalid: false },\n }\n);\n\n/** Variant props for the date range picker. */\nexport type DateRangePickerProps = VariantProps<typeof dateRangePickerTriggerVariants>;\n\n/**\n * A button trigger paired with a `tolle-range-calendar` in a floating panel,\n * for picking a start and an end date.\n *\n * The value is a `DateRange` — `{ start, end }` — and a half-made range is a\n * legal intermediate state: picking a start emits `{ start, end: null }` and\n * leaves the panel open, so a consumer can react to the first click. The panel\n * only closes once both ends exist.\n *\n * Implements `ControlValueAccessor`, so it works with `ngModel` and reactive\n * forms. Use `tolle-date-picker` for a single date — this trigger is\n * deliberately the same shape as that one and as `tolle-date-time-picker`.\n */\n@Component({\n selector: 'tolle-date-range-picker',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule, FormsModule, RangeCalendarComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => DateRangePickerComponent),\n multi: true\n }\n ],\n template: `\n <button\n #trigger\n type=\"button\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n [disabled]=\"disabled\"\n [class]=\"computedTriggerClass\"\n (click)=\"toggle()\"\n (keydown)=\"onTriggerKeyDown($event)\"\n >\n <span [class]=\"cn('truncate', displayValue ? 'text-foreground' : 'text-muted-foreground')\">\n {{ displayValue || placeholder }}\n </span>\n <i class=\"ri-calendar-line shrink-0 text-muted-foreground\" aria-hidden=\"true\"></i>\n </button>\n\n <div\n #popover\n *ngIf=\"isOpen\"\n role=\"dialog\"\n [attr.aria-label]=\"'Choose a date range'\"\n class=\"fixed left-0 top-0 z-50\"\n style=\"visibility: hidden\"\n >\n <div class=\"rounded-md border border-border bg-popover text-popover-foreground shadow-md\">\n <div class=\"p-3\">\n <tolle-range-calendar\n [bordered]=\"false\"\n [ngModel]=\"value\"\n (rangeSelect)=\"onCalendarSelect($event)\"\n [disablePastDates]=\"disablePastDates\"\n [numberOfMonths]=\"numberOfMonths\"\n ></tolle-range-calendar>\n </div>\n\n <div class=\"flex items-center justify-between gap-2 border-t border-border px-3 py-2\">\n <button type=\"button\" [class]=\"footerButtonClass\" (click)=\"clearFromFooter()\">\n Clear\n </button>\n <button\n type=\"button\"\n [class]=\"cn(footerButtonClass, 'bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground')\"\n (click)=\"done()\"\n >\n Done\n </button>\n </div>\n </div>\n </div>\n `\n})\nexport class DateRangePickerComponent implements OnChanges, ControlValueAccessor, OnDestroy {\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Disables the control. @default false */\n @Input() disabled = false;\n /** Text shown on the trigger when no range is selected. @default 'Pick a date range' */\n @Input() placeholder = 'Pick a date range';\n /** Extra Tailwind classes merged onto the trigger via `cn()` (last-wins). @default '' */\n @Input() class = '';\n /** Disables every day before today in the calendar. @default false */\n @Input() disablePastDates = false;\n /** Height and text size of the trigger. @default 'default' */\n @Input() size: DateRangePickerProps['size'] = 'default';\n /** Consecutive months shown in the popover (e.g. `2` for a two-month picker). @default 1 */\n @Input() numberOfMonths = 1;\n /** Applies the destructive border and focus ring. @default false */\n @Input() invalid = false;\n /** Accessible name when there is no associated visible label. @default '' */\n @Input() ariaLabel = '';\n\n /** Emitted with the selected range, including the half-made `{ start, end: null }` state. */\n @Output() valueChange = new EventEmitter<DateRange>();\n /** Emitted when the calendar panel opens. */\n @Output() opened = new EventEmitter<void>();\n /** Emitted when the calendar panel closes. */\n @Output() closed = new EventEmitter<void>();\n\n @ViewChild('trigger') trigger!: ElementRef;\n @ViewChild('popover') popover!: ElementRef;\n\n value: DateRange = { start: null, end: null };\n isOpen = false;\n cleanupAutoUpdate?: () => void;\n\n /** Footer buttons, styled exactly as the date-time picker's. */\n readonly footerButtonClass = cn(\n 'rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors',\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n );\n\n /** Footer Clear — empties the range and leaves the panel open, like the reference. */\n clearFromFooter(): void {\n this.value = { start: null, end: null };\n this.emit();\n this.cdr.markForCheck();\n }\n\n /** Footer Done — commits what is already selected and closes. */\n done(): void {\n this.close();\n }\n\n private _outsideClickHandler = (event: MouseEvent) => {\n if (!this.trigger.nativeElement.contains(event.target) && !this.popover?.nativeElement.contains(event.target)) {\n this.close();\n }\n };\n\n constructor(private cdr: ChangeDetectorRef) {}\n\n get computedTriggerClass(): string {\n return cn(dateRangePickerTriggerVariants({ size: this.size, invalid: this.invalid }), this.class);\n }\n\n get displayValue(): string {\n if (!this.value.start) return '';\n\n const startStr = format(this.value.start, 'MMM d, yyyy');\n if (!this.value.end) return startStr;\n\n const endStr = format(this.value.end, 'MMM d, yyyy');\n return startStr + ' - ' + endStr;\n }\n\n onCalendarSelect(range: DateRange) {\n this.value = range;\n this.emit();\n\n // Close only if range is complete\n if (range.start && range.end) {\n setTimeout(() => this.close(), 150);\n }\n this.cdr.markForCheck();\n }\n\n toggle(): void {\n this.isOpen ? this.close() : this.open();\n }\n\n onTriggerKeyDown(event: KeyboardEvent): void {\n if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key) && !this.isOpen) {\n event.preventDefault();\n this.open();\n }\n }\n\n @HostListener('keydown.escape') onEscape() { if (this.isOpen) this.close(); }\n\n open() {\n if (this.disabled || this.isOpen) return;\n this.isOpen = true;\n this.opened.emit();\n // Render the popover synchronously so its DOM node + @ViewChild('popover') exist\n // before we position it. Otherwise, with zone eventCoalescing (Angular defers change\n // detection to the next animation frame), updatePosition() can run first, find no\n // popover, and the calendar stays hidden — regardless of where the picker is used.\n this.cdr.detectChanges();\n if (this.trigger?.nativeElement) {\n this.trigger.nativeElement.focus();\n }\n const schedule = typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : (fn: FrameRequestCallback) => setTimeout(fn, 0);\n schedule(() => {\n this.updatePosition();\n document.addEventListener('pointerdown', this._outsideClickHandler, true);\n });\n }\n\n close() {\n if (!this.isOpen) return;\n this.isOpen = false;\n this.closed.emit();\n this.teardown();\n // Both the outside-click handler and the deferred close after a completed\n // range run outside any template binding, so nothing else marks this OnPush\n // view dirty.\n this.cdr.markForCheck();\n }\n\n /** Reports the current range to the form and to `valueChange`. */\n private emit() {\n this.onChange(this.value);\n this.valueChange.emit(this.value);\n }\n\n // --- Floating UI Positioning with Fixed Strategy ---\n private updatePosition() {\n if (!this.trigger || !this.popover) return;\n\n this.cleanupAutoUpdate = autoUpdate(\n this.trigger.nativeElement,\n this.popover.nativeElement,\n () => {\n computePosition(this.trigger.nativeElement, this.popover.nativeElement, {\n placement: 'bottom-start',\n strategy: 'fixed', // Use fixed to escape column layout\n middleware: [\n offset(4),\n flip({\n fallbackAxisSideDirection: 'start',\n padding: 8\n }),\n shift({ padding: 8 }),\n sizeMiddleware({\n apply({ rects, elements, availableHeight }) {\n // Constrain popover to available space\n Object.assign(elements.floating.style, {\n maxHeight: `${Math.min(400, availableHeight)}px`,\n minWidth: `${Math.max(rects.reference.width, 320)}px`, // Calendar minimum width\n });\n }\n })\n ],\n }).then(({ x, y, placement }) => {\n Object.assign(this.popover.nativeElement.style, {\n left: `${x}px`,\n top: `${y}px`,\n visibility: 'visible',\n });\n\n // Optional: Add placement class for styling\n this.popover.nativeElement.classList.remove('calendar-top', 'calendar-bottom');\n if (placement.includes('top')) {\n this.popover.nativeElement.classList.add('calendar-top');\n } else {\n this.popover.nativeElement.classList.add('calendar-bottom');\n }\n });\n }\n );\n }\n\n private teardown() {\n if (this.cleanupAutoUpdate) {\n this.cleanupAutoUpdate();\n this.cleanupAutoUpdate = undefined;\n }\n document.removeEventListener('pointerdown', this._outsideClickHandler, true);\n }\n\n ngOnDestroy() {\n this.teardown();\n }\n\n @HostListener('window:resize')\n onWindowResize() {\n if (this.isOpen) {\n this.close(); // Close on resize for simplicity\n }\n }\n\n @HostListener('window:scroll')\n onWindowScroll() {\n // Floating-UI's autoUpdate handles scroll repositioning\n }\n\n // --- Control Value Accessor ---\n onChange: any = () => {};\n onTouched: any = () => {};\n\n writeValue(val: DateRange | null): void {\n if (val) {\n this.value = { ...val };\n } else {\n this.value = { start: null, end: null };\n }\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.cdr.markForCheck();\n }\n\n protected cn = cn;\n}\n"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "types/date-range.ts",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "date-time-picker",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Date Time Picker",
|
|
5
|
+
"description": "Date Time Picker component with size and invalid options.",
|
|
6
|
+
"category": "date-time",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"calendar",
|
|
9
|
+
"time-picker"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [
|
|
12
|
+
"@floating-ui/dom",
|
|
13
|
+
"class-variance-authority",
|
|
14
|
+
"date-fns"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"path": "date-time-picker.component.ts",
|
|
19
|
+
"target": "ui/date-time-picker.component.ts",
|
|
20
|
+
"content": "import { Component, Input, Output, EventEmitter, forwardRef, ElementRef, ViewChild, ChangeDetectorRef, ChangeDetectionStrategy, OnChanges, OnDestroy, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { format, isSameDay, isValid, startOfDay } from 'date-fns';\nimport { cn } from './utils/cn';\nimport { CalendarComponent } from './calendar.component';\nimport { TimeColumnsComponent, TimeParts, to12Hour } from './time-picker.component';\n\nconst SECONDS_PER_DAY = 24 * 3600;\n\nconst dateTimePickerTriggerVariants = cva(\n 'flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background text-foreground shadow-sm transition-colors ' +\n 'focus:outline-none focus-visible:border-primary/80 focus-visible:ring-4 focus-visible:ring-ring/30 ' +\n 'disabled:cursor-not-allowed disabled:opacity-50',\n {\n variants: {\n size: {\n xs: 'h-8 px-2 text-xs',\n sm: 'h-9 px-3 text-sm',\n default: 'h-10 px-3 text-sm',\n lg: 'h-11 px-4 text-base',\n },\n invalid: {\n true: 'border-destructive focus-visible:border-destructive/80 focus-visible:ring-destructive/30',\n false: '',\n },\n },\n defaultVariants: { size: 'default', invalid: false },\n }\n);\n\n/** Variant props for the date-time picker trigger. */\nexport type DateTimePickerProps = VariantProps<typeof dateTimePickerTriggerVariants>;\n\n/**\n * One control for a date *and* a time: `tolle-calendar` on the left, the\n * `tolle-time-columns` strip on the right, in a single floating panel with a\n * Clear / Done footer.\n *\n * The value is a `Date`, because a date-time genuinely is a moment. Changing\n * the date preserves the chosen time and changing the time preserves the\n * chosen date — the two halves are only ever recombined, never reset.\n *\n * Implements `ControlValueAccessor`, so it works with `ngModel` and reactive\n * forms. Use `tolle-date-picker` for a date alone and `tolle-time-picker` for a\n * time alone.\n * @new\n */\n@Component({\n selector: 'tolle-date-time-picker',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule, FormsModule, CalendarComponent, TimeColumnsComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => DateTimePickerComponent),\n multi: true,\n },\n ],\n template: `\n <button\n #trigger\n type=\"button\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? panelId : null\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.data-state]=\"isOpen ? 'open' : 'closed'\"\n [disabled]=\"disabled\"\n [class]=\"computedTriggerClass\"\n (click)=\"toggle()\"\n (keydown)=\"onTriggerKeyDown($event)\"\n >\n <span [class]=\"cn('truncate', displayValue ? 'text-foreground' : 'text-muted-foreground')\">\n {{ displayValue || placeholder }}\n </span>\n <i class=\"ri-calendar-schedule-line shrink-0 text-muted-foreground\" aria-hidden=\"true\"></i>\n </button>\n\n <div\n *ngIf=\"isOpen\"\n #panel\n role=\"dialog\"\n [id]=\"panelId\"\n [attr.aria-label]=\"ariaLabel || 'Choose a date and time'\"\n class=\"fixed left-0 top-0 z-[9999]\"\n style=\"visibility: hidden\"\n >\n <div class=\"rounded-md border border-border bg-popover text-popover-foreground shadow-md\">\n <div class=\"flex flex-col sm:flex-row\">\n <div class=\"p-3\">\n <tolle-calendar\n [bordered]=\"false\"\n [showQuickActions]=\"false\"\n [minDate]=\"calendarMin\"\n [maxDate]=\"calendarMax\"\n [ngModel]=\"value\"\n (dateSelect)=\"onDateSelect($event)\"\n ></tolle-calendar>\n </div>\n\n <div class=\"border-t border-border p-3 sm:border-l sm:border-t-0\">\n <tolle-time-columns\n #columns\n [hour]=\"hour\"\n [minute]=\"minute\"\n [second]=\"second\"\n [use12Hours]=\"use12Hours\"\n [showSeconds]=\"showSeconds\"\n [minuteStep]=\"minuteStep\"\n [minSeconds]=\"minSeconds\"\n [maxSeconds]=\"maxSeconds\"\n [size]=\"size\"\n (timeChange)=\"onTimeChange($event)\"\n ></tolle-time-columns>\n </div>\n </div>\n\n <div class=\"flex items-center justify-between gap-2 border-t border-border px-3 py-2\">\n <button\n type=\"button\"\n [class]=\"footerButtonClass\"\n (click)=\"clear()\"\n >\n Clear\n </button>\n <button\n type=\"button\"\n [class]=\"cn(footerButtonClass, 'bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground')\"\n (click)=\"done()\"\n >\n Done\n </button>\n </div>\n </div>\n </div>\n `,\n})\nexport class DateTimePickerComponent implements ControlValueAccessor, OnChanges, OnDestroy {\n /** Text shown on the trigger when nothing is selected. @default 'Select date and time' */\n @Input() placeholder = 'Select date and time';\n /** Disables the control. @default false */\n @Input() disabled = false;\n /** Renders a 12-hour face with an AM/PM column. @default false */\n @Input() use12Hours = false;\n /** Adds a seconds column. @default false */\n @Input() showSeconds = false;\n /** Interval between minute entries, in minutes. @default 1 */\n @Input() minuteStep = 1;\n /** Earliest selectable moment; earlier days and times are disabled. */\n @Input() min?: Date;\n /** Latest selectable moment; later days and times are disabled. */\n @Input() max?: Date;\n /** date-fns pattern used to render the value on the trigger. @default 'MMM d, yyyy h:mm a' */\n @Input() displayFormat = 'MMM d, yyyy h:mm a';\n /** Height and text size of the trigger and entries. @default 'default' */\n @Input() size: DateTimePickerProps['size'] = 'default';\n /** Applies the destructive border and focus ring. @default false */\n @Input() invalid = false;\n /** Accessible name when there is no associated visible label. @default '' */\n @Input() ariaLabel = '';\n /** Extra Tailwind classes merged onto the trigger via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted with the chosen moment, or `null` when the value is cleared. */\n @Output() valueChange = new EventEmitter<Date | null>();\n /** Emitted when the panel opens. */\n @Output() opened = new EventEmitter<void>();\n /** Emitted when the panel closes. */\n @Output() closed = new EventEmitter<void>();\n\n @ViewChild('trigger') triggerEl?: ElementRef<HTMLElement>;\n @ViewChild('panel') panelEl?: ElementRef<HTMLElement>;\n @ViewChild('columns') columnsCmp?: TimeColumnsComponent;\n\n readonly panelId = 'date-time-picker-panel-' + Math.random().toString(36).slice(2, 11);\n\n value: Date | null = null;\n isOpen = false;\n\n /**\n * Day-only copies of `min` / `max` for the calendar. The calendar compares\n * whole `Date`s, so passing a bound that carries a time would disable its own\n * day — 09:00 on the min day would look \"before\" a 09:30 minimum.\n */\n calendarMin?: Date;\n calendarMax?: Date;\n\n readonly footerButtonClass = cn(\n 'rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors',\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n );\n\n protected cn = cn;\n\n private cleanup?: () => void;\n private onChange: (value: Date | null) => void = () => {};\n private onTouched: () => void = () => {};\n\n constructor(private cdr: ChangeDetectorRef) {}\n\n ngOnChanges(): void {\n this.calendarMin = this.min ? startOfDay(this.min) : undefined;\n this.calendarMax = this.max ? startOfDay(this.max) : undefined;\n \n // A bound `class` input is written through Angular's styling path,\n // which does not mark an OnPush view dirty on its own.\n this.cdr.markForCheck();\n }\n\n private readonly outsideClickHandler = (event: MouseEvent) => {\n const target = event.target as Node;\n if (\n !this.triggerEl?.nativeElement.contains(target) &&\n !this.panelEl?.nativeElement.contains(target)\n ) {\n this.close();\n }\n };\n\n private readonly escapeHandler = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && this.isOpen) {\n event.preventDefault();\n this.close();\n this.triggerEl?.nativeElement.focus();\n }\n };\n\n /* ---------------- derived ---------------- */\n\n get hour(): number | null {\n return this.value ? this.value.getHours() : null;\n }\n\n get minute(): number | null {\n return this.value ? this.value.getMinutes() : null;\n }\n\n get second(): number | null {\n return this.value ? this.value.getSeconds() : null;\n }\n\n get displayValue(): string {\n if (!this.value) return '';\n try {\n return format(this.value, this.displayFormat);\n } catch {\n // A bad pattern should not blank the control; fall back to something readable.\n const { hour12, meridiem } = to12Hour(this.value.getHours());\n return (\n format(this.value, 'MMM d, yyyy') +\n ' ' +\n hour12 +\n ':' +\n String(this.value.getMinutes()).padStart(2, '0') +\n ' ' +\n meridiem\n );\n }\n }\n\n /**\n * The time bound only bites on the boundary day itself: on the min day the\n * columns start at the min time, on any later day they start at midnight.\n */\n get minSeconds(): number {\n if (!this.min) return 0;\n const day = this.value ?? this.min;\n if (!isSameDay(day, this.min)) return 0;\n return this.min.getHours() * 3600 + this.min.getMinutes() * 60 + this.min.getSeconds();\n }\n\n get maxSeconds(): number {\n if (!this.max) return SECONDS_PER_DAY - 1;\n const day = this.value ?? this.max;\n if (!isSameDay(day, this.max)) return SECONDS_PER_DAY - 1;\n return this.max.getHours() * 3600 + this.max.getMinutes() * 60 + this.max.getSeconds();\n }\n\n get computedTriggerClass(): string {\n return cn(\n dateTimePickerTriggerVariants({ size: this.size, invalid: this.invalid }),\n this.class\n );\n }\n\n /* ---------------- open / close ---------------- */\n\n toggle(): void {\n this.isOpen ? this.close() : this.open();\n }\n\n open(): void {\n if (this.disabled || this.isOpen) return;\n this.isOpen = true;\n this.opened.emit();\n this.cdr.markForCheck();\n\n setTimeout(() => {\n this.updatePosition();\n this.columnsCmp?.scrollSelectedIntoView();\n document.addEventListener('pointerdown', this.outsideClickHandler, true);\n document.addEventListener('keydown', this.escapeHandler, true);\n });\n }\n\n close(): void {\n if (!this.isOpen) return;\n this.isOpen = false;\n this.closed.emit();\n this.onTouched();\n this.teardown();\n this.cdr.markForCheck();\n }\n\n onTriggerKeyDown(event: KeyboardEvent): void {\n if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key) && !this.isOpen) {\n event.preventDefault();\n this.open();\n }\n }\n\n /* ---------------- value ---------------- */\n\n /**\n * Applies a newly picked day, carrying the existing time across. With no\n * time chosen yet the day starts at midnight.\n */\n onDateSelect(date: Date | null): void {\n if (!date) {\n this.clear();\n return;\n }\n\n const previous = this.value;\n this.commit(\n new Date(\n date.getFullYear(),\n date.getMonth(),\n date.getDate(),\n previous ? previous.getHours() : 0,\n previous ? previous.getMinutes() : 0,\n previous ? previous.getSeconds() : 0,\n 0\n )\n );\n }\n\n /**\n * Applies a newly picked time, carrying the existing day across. With no day\n * chosen yet the time lands on today.\n */\n onTimeChange(parts: TimeParts): void {\n const base = this.value ?? new Date();\n this.commit(\n new Date(\n base.getFullYear(),\n base.getMonth(),\n base.getDate(),\n parts.hour,\n parts.minute,\n parts.second,\n 0\n )\n );\n }\n\n clear(): void {\n this.commit(null);\n // The calendar keeps its own selection and ignores a null written through\n // the CVA, so closing is what actually returns it to a clean state — it is\n // rebuilt from scratch the next time the panel opens.\n this.close();\n }\n\n done(): void {\n this.close();\n this.triggerEl?.nativeElement.focus();\n }\n\n private commit(next: Date | null): void {\n this.value = next;\n this.onChange(next);\n this.valueChange.emit(next);\n this.cdr.markForCheck();\n }\n\n private updatePosition(): void {\n const trigger = this.triggerEl?.nativeElement;\n const panel = this.panelEl?.nativeElement;\n if (!trigger || !panel) return;\n\n this.cleanup = autoUpdate(trigger, panel, () => {\n computePosition(trigger, panel, {\n strategy: 'fixed',\n placement: 'bottom-start',\n middleware: [offset(4), flip(), shift({ padding: 8 })],\n }).then(({ x, y }) => {\n Object.assign(panel.style, {\n left: x + 'px',\n top: y + 'px',\n visibility: 'visible',\n });\n });\n });\n }\n\n private teardown(): void {\n this.cleanup?.();\n this.cleanup = undefined;\n document.removeEventListener('pointerdown', this.outsideClickHandler, true);\n document.removeEventListener('keydown', this.escapeHandler, true);\n }\n\n ngOnDestroy(): void {\n this.teardown();\n }\n\n /* ---------------- ControlValueAccessor ---------------- */\n\n writeValue(value: Date | string | null | undefined): void {\n if (value instanceof Date && isValid(value)) {\n this.value = value;\n } else if (typeof value === 'string' && value) {\n const parsed = new Date(value);\n this.value = isValid(parsed) ? parsed : null;\n } else {\n this.value = null;\n }\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: (value: Date | null) => 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.disabled = isDisabled;\n if (isDisabled) this.close();\n this.cdr.markForCheck();\n }\n}\n"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "utils/cn.ts",
|
|
24
|
+
"target": "ui/utils/cn.ts",
|
|
25
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "direction",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Direction",
|
|
5
|
+
"description": "Direction component.",
|
|
6
|
+
"category": "utilities",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "direction.component.ts",
|
|
12
|
+
"target": "ui/direction.component.ts",
|
|
13
|
+
"content": "import {\n Directive,\n Injectable,\n Input,\n Output,\n EventEmitter,\n HostBinding,\n OnChanges,\n inject,\n} from '@angular/core';\nimport { BehaviorSubject } from 'rxjs';\n\n/** Writing direction of a subtree: explicit, or inferred from its text. */\nexport type Direction = 'ltr' | 'rtl' | 'auto';\n\n/**\n * Publishes the application's current writing direction so components can adapt\n * without reading the DOM.\n *\n * Direction is not an input of most components, so an OnPush view would never be\n * marked dirty by a change. Subscribe to `direction$` and call `markForCheck()`\n * on emission wherever the rendered output depends on it.\n */\n@Injectable({ providedIn: 'root' })\nexport class DirectionService {\n private directionSource = new BehaviorSubject<Direction>('ltr');\n\n /** Emits the current writing direction, starting with the present value. */\n readonly direction$ = this.directionSource.asObservable();\n\n /** The current writing direction. */\n get direction(): Direction {\n return this.directionSource.value;\n }\n\n /** Whether the current direction is explicitly right-to-left. */\n get isRtl(): boolean {\n return this.directionSource.value === 'rtl';\n }\n\n /** Sets the current direction, emitting only when it actually changes. */\n setDirection(direction: Direction): void {\n if (this.directionSource.value !== direction) this.directionSource.next(direction);\n }\n}\n\n/**\n * Sets the `dir` attribute on its host and publishes that direction to\n * `DirectionService`, so a whole chat surface can be flipped to RTL from one\n * place. Put it on the outermost element of the subtree you want to flip.\n * @new\n */\n@Directive({\n selector: '[tolleDirection]',\n standalone: true,\n})\nexport class DirectionDirective implements OnChanges {\n /** Writing direction applied to the host's `dir` attribute. @default 'ltr' */\n @Input() tolleDirection: Direction = 'ltr';\n /** Whether to publish this direction to the app-wide `DirectionService`. @default true */\n @Input() tolleDirectionPublish = true;\n\n /** Emitted with the direction whenever it changes. */\n @Output() directionChange = new EventEmitter<Direction>();\n\n private readonly service = inject(DirectionService);\n\n /** The resolved `dir` attribute value written onto the host. */\n @HostBinding('attr.dir')\n get dir(): Direction {\n return this.tolleDirection ?? 'ltr';\n }\n\n ngOnChanges(): void {\n const direction = this.dir;\n if (this.tolleDirectionPublish) this.service.setDirection(direction);\n this.directionChange.emit(direction);\n }\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"path": "dropdown-item.component.ts",
|
|
12
12
|
"target": "ui/dropdown-item.component.ts",
|
|
13
|
-
"content": "import {Component, Input} from '@angular/core';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-dropdown-item',\n standalone: true,\n imports: [],\n template: `\n <div\n role=\"menuitem\"\n tabindex=\"-1\"\n [attr.data-disabled]=\"disabled ? '' : null\"\n [attr.aria-disabled]=\"disabled || null\"\n [class]=\"cn('relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', class)\">\n <ng-content></ng-content>\n </div>\n `,\n styles:
|
|
13
|
+
"content": "import {Component, Input} from '@angular/core';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-dropdown-item',\n standalone: true,\n imports: [],\n template: `\n <div\n role=\"menuitem\"\n tabindex=\"-1\"\n [attr.data-disabled]=\"disabled ? '' : null\"\n [attr.aria-disabled]=\"disabled || null\"\n [class]=\"cn('relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', class)\">\n <ng-content></ng-content>\n </div>\n `,\n styles: `:host { display: block; }`\n})\nexport class DropdownItemComponent {\n @Input() class: string = '';\n @Input() disabled = false;\n protected readonly cn = cn;\n}\n"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"path": "utils/cn.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"path": "dropdown-label.component.ts",
|
|
12
12
|
"target": "ui/dropdown-label.component.ts",
|
|
13
|
-
"content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-dropdown-label',\n standalone: true,\n imports: [],\n template: `\n <div class=\"px-2 py-1.5 text-sm font-semibold text-muted-foreground\">\n <ng-content></ng-content>\n </div>\n `,\n styles:
|
|
13
|
+
"content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-dropdown-label',\n standalone: true,\n imports: [],\n template: `\n <div class=\"px-2 py-1.5 text-sm font-semibold text-muted-foreground\">\n <ng-content></ng-content>\n </div>\n `,\n styles: `:host { display: block; }`\n})\nexport class DropdownLabelComponent {\n\n}\n"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"path": "dropdown-menu.component.ts",
|
|
12
12
|
"target": "ui/dropdown-menu.component.ts",
|
|
13
|
-
"content": "import {Component, TemplateRef, ViewChild} from '@angular/core';\n\n@Component({\n selector: 'tolle-dropdown-menu',\n standalone: true,\n imports: [],\n template: `\n <ng-template #menuTemplate>\n <div role=\"menu\" aria-orientation=\"vertical\" class=\"z-[1000] min-w-[12rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in zoom-in-95\">\n <ng-content></ng-content>\n </div>\n </ng-template>\n `,\n styles:
|
|
13
|
+
"content": "import {Component, TemplateRef, ViewChild} from '@angular/core';\n\n@Component({\n selector: 'tolle-dropdown-menu',\n standalone: true,\n imports: [],\n template: `\n <ng-template #menuTemplate>\n <div role=\"menu\" aria-orientation=\"vertical\" class=\"z-[1000] min-w-[12rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in zoom-in-95\">\n <ng-content></ng-content>\n </div>\n </ng-template>\n `,\n styles: `:host { display: block; }`\n})\nexport class DropdownMenuComponent {\n @ViewChild('menuTemplate') templateRef!: TemplateRef<any>;\n}\n"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"path": "dropdown-separator.component.ts",
|
|
12
12
|
"target": "ui/dropdown-separator.component.ts",
|
|
13
|
-
"content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-dropdown-separator',\n standalone: true,\n imports: [],\n template: `<div role=\"separator\" aria-hidden=\"true\" class=\"-mx-1 my-1 h-px bg-border\"></div>`,\n styles:
|
|
13
|
+
"content": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'tolle-dropdown-separator',\n standalone: true,\n imports: [],\n template: `<div role=\"separator\" aria-hidden=\"true\" class=\"-mx-1 my-1 h-px bg-border\"></div>`,\n styles: `:host { display: block; }`\n})\nexport class DropdownSeparatorComponent {\n\n}\n"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"path": "dropdown-trigger.directive.ts",
|
|
16
16
|
"target": "ui/dropdown-trigger.directive.ts",
|
|
17
|
-
"content": "import { Directive, Input, ElementRef, OnDestroy, HostListener, ViewContainerRef } from '@angular/core';\nimport { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom';\nimport { DropdownMenuComponent } from './dropdown-menu.component';\n\n@Directive({\n selector: '[tolleDropdownTrigger]',\n standalone: true,\n host: {\n '[attr.aria-haspopup]': \"'menu'\",\n '[attr.aria-expanded]': 'isOpen'\n }\n})\nexport class DropdownTriggerDirective implements OnDestroy {\n @Input('tolleDropdownTrigger') menu!: DropdownMenuComponent;\n\n private cleanup?: () => void;\n protected isOpen = false;\n private menuElement?: HTMLElement;\n\n constructor(\n private el: ElementRef,\n private vcr: ViewContainerRef\n ) {}\n\n @HostListener('click')\n toggle() {\n this.isOpen ? this.close() : this.open();\n }\n\n private open() {\n this.isOpen = true;\n\n // Create the menu view\n const view = this.vcr.createEmbeddedView(this.menu.templateRef);\n this.menuElement = view.rootNodes[0] as HTMLElement;\n document.body.appendChild(this.menuElement);\n\n // Floating UI positioning logic\n this.cleanup = autoUpdate(this.el.nativeElement, this.menuElement, () => {\n computePosition(this.el.nativeElement, this.menuElement!, {\n placement: 'bottom-end',\n middleware: [\n offset(4), // Space between trigger and menu\n flip(), // Flip to top if space is tight\n shift({ padding: 8 }) // Prevent menu from hitting screen edges\n ]\n }).then(({ x, y }) => {\n Object.assign(this.menuElement!.style, {\n left: `${x}px`,\n top: `${y}px`,\n position: 'absolute'\n });\n });\n });\n\n // Close on outside click\n setTimeout(() => {\n document.addEventListener('
|
|
17
|
+
"content": "import { Directive, Input, ElementRef, OnDestroy, HostListener, ViewContainerRef } from '@angular/core';\nimport { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom';\nimport { DropdownMenuComponent } from './dropdown-menu.component';\n\n@Directive({\n selector: '[tolleDropdownTrigger]',\n standalone: true,\n host: {\n '[attr.aria-haspopup]': \"'menu'\",\n '[attr.aria-expanded]': 'isOpen'\n }\n})\nexport class DropdownTriggerDirective implements OnDestroy {\n @Input('tolleDropdownTrigger') menu!: DropdownMenuComponent;\n\n private cleanup?: () => void;\n protected isOpen = false;\n private menuElement?: HTMLElement;\n\n constructor(\n private el: ElementRef,\n private vcr: ViewContainerRef\n ) {}\n\n @HostListener('click')\n toggle() {\n this.isOpen ? this.close() : this.open();\n }\n\n private open() {\n this.isOpen = true;\n\n // Create the menu view\n const view = this.vcr.createEmbeddedView(this.menu.templateRef);\n this.menuElement = view.rootNodes[0] as HTMLElement;\n document.body.appendChild(this.menuElement);\n\n // Floating UI positioning logic\n this.cleanup = autoUpdate(this.el.nativeElement, this.menuElement, () => {\n computePosition(this.el.nativeElement, this.menuElement!, {\n placement: 'bottom-end',\n middleware: [\n offset(4), // Space between trigger and menu\n flip(), // Flip to top if space is tight\n shift({ padding: 8 }) // Prevent menu from hitting screen edges\n ]\n }).then(({ x, y }) => {\n Object.assign(this.menuElement!.style, {\n left: `${x}px`,\n top: `${y}px`,\n position: 'absolute'\n });\n });\n });\n\n // Close on outside click. Capture phase so container components that call\n // stopPropagation() on their root (modal, sheet, country-selector) cannot\n // prevent the event from reaching us.\n setTimeout(() => {\n document.addEventListener('pointerdown', this.outsideClick, true);\n });\n }\n\n private close() {\n this.isOpen = false;\n this.cleanup?.();\n this.menuElement?.remove();\n document.removeEventListener('pointerdown', this.outsideClick, true);\n }\n\n private outsideClick = (event: MouseEvent) => {\n if (!this.el.nativeElement.contains(event.target) && !this.menuElement?.contains(event.target as Node)) {\n this.close();\n }\n };\n\n ngOnDestroy() {\n this.close();\n }\n}\n"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"path": "empty-state.component.ts",
|
|
14
14
|
"target": "ui/empty-state.component.ts",
|
|
15
|
-
"content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst emptyStateVariants = cva(\n \"flex flex-col items-center justify-center text-center animate-in fade-in duration-500\",\n {\n variants: {\n variant: {\n default: \"min-h-[400px] rounded-md border border-dashed border-border p-8 bg-background/50\",\n minimal: \"p-4 min-h-[200px]\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\ntype EmptyStateVariants = VariantProps<typeof emptyStateVariants>;\n\n@Component({\n selector: 'tolle-empty-state',\n standalone: true,\n imports: [CommonModule],\n template: `\n <div [class]=\"cn(emptyStateVariants({ variant }), class)\">\n\n <div [class]=\"cn(\n 'flex items-center justify-center rounded-full bg-muted',\n variant === 'minimal' ? 'h-12 w-12' : 'h-20 w-20'\n )\">\n <ng-content select=\"[icon]\">\n <i aria-hidden=\"true\" [class]=\"cn(\n 'ri-inbox-line text-muted-foreground/60',\n variant === 'minimal' ? 'text-xl' : 'text-3xl'\n )\"></i>\n </ng-content>\n </div>\n\n <h3 [class]=\"cn(\n 'font-semibold text-foreground',\n variant === 'minimal' ? 'mt-2 text-sm' : 'mt-4 text-lg'\n )\">\n {{ title }}\n </h3>\n\n <p *ngIf=\"description\" [class]=\"cn(\n 'text-muted-foreground',\n variant === 'minimal' ? 'mt-1 text-xs' : 'mb-6 mt-2 max-w-sm text-sm'\n )\">\n {{ description }}\n </p>\n\n <div *ngIf=\"variant !== 'minimal'\" class=\"flex items-center justify-center gap-3\">\n <ng-content select=\"[actions]\"></ng-content>\n </div>\n </div>\n `\n})\nexport class EmptyStateComponent {\n @Input() variant: EmptyStateVariants['variant'] = 'default';\n @Input() title: string = 'No items found';\n @Input() description?: string;\n @Input() class: string = '';\n\n protected emptyStateVariants = emptyStateVariants;\n protected cn = cn;\n}\n"
|
|
15
|
+
"content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst emptyStateVariants = cva(\n \"flex flex-col items-center justify-center text-center animate-in fade-in duration-500\",\n {\n variants: {\n variant: {\n default: \"min-h-[400px] rounded-md border border-dashed border-border p-8 bg-background/50\",\n minimal: \"p-4 min-h-[200px]\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\ntype EmptyStateVariants = VariantProps<typeof emptyStateVariants>;\n\n@Component({\n selector: 'tolle-empty-state',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `\n <div [class]=\"cn(emptyStateVariants({ variant }), class)\">\n\n <div [class]=\"cn(\n 'flex items-center justify-center rounded-full bg-muted',\n variant === 'minimal' ? 'h-12 w-12' : 'h-20 w-20'\n )\">\n <ng-content select=\"[icon]\">\n <i aria-hidden=\"true\" [class]=\"cn(\n 'ri-inbox-line text-muted-foreground/60',\n variant === 'minimal' ? 'text-xl' : 'text-3xl'\n )\"></i>\n </ng-content>\n </div>\n\n <h3 [class]=\"cn(\n 'font-semibold text-foreground',\n variant === 'minimal' ? 'mt-2 text-sm' : 'mt-4 text-lg'\n )\">\n {{ title }}\n </h3>\n\n <p *ngIf=\"description\" [class]=\"cn(\n 'text-muted-foreground',\n variant === 'minimal' ? 'mt-1 text-xs' : 'mb-6 mt-2 max-w-sm text-sm'\n )\">\n {{ description }}\n </p>\n\n <div *ngIf=\"variant !== 'minimal'\" class=\"flex items-center justify-center gap-3\">\n <ng-content select=\"[actions]\"></ng-content>\n </div>\n </div>\n `\n})\nexport class EmptyStateComponent {\n @Input() variant: EmptyStateVariants['variant'] = 'default';\n @Input() title: string = 'No items found';\n @Input() description?: string;\n @Input() class: string = '';\n\n protected emptyStateVariants = emptyStateVariants;\n protected cn = cn;\n}\n"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"path": "utils/cn.ts",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "field",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Field",
|
|
5
|
+
"description": "Field component with orientation options.",
|
|
6
|
+
"category": "forms",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "field.component.ts",
|
|
14
|
+
"target": "ui/field.component.ts",
|
|
15
|
+
"content": "import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef, OnChanges, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst fieldVariants = cva('group/field flex w-full gap-2 data-[invalid=true]:text-destructive', {\n variants: {\n orientation: {\n vertical: 'flex-col',\n horizontal: 'flex-row items-center justify-between',\n // shadcn uses container queries here, but this preset is Tailwind 3.4 with no\n // container-queries plugin — `@md/...` classes would be silently dropped and the\n // field would never go horizontal. Viewport breakpoints work with the stack we have.\n responsive: 'flex-col md:flex-row md:items-center md:justify-between',\n },\n },\n defaultVariants: { orientation: 'vertical' },\n});\n\nexport type FieldProps = VariantProps<typeof fieldVariants>;\n\n/**\n * Wraps a label, a control and its help/error text into one accessible form\n * field. Composes *around* the existing controls (`tolle-input`,\n * `tolle-select`, …) — it does not replace them.\n *\n * Use `tolle-item` instead when displaying content rather than a form control.\n * @new\n */\n@Component({\n selector: 'tolle-field',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div role=\"group\" [attr.data-invalid]=\"invalid || null\" [class]=\"computedClass\">\n <ng-content></ng-content>\n </div>\n `,\n})\nexport class FieldComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Layout direction; `responsive` goes horizontal at the `@md` container width. @default 'vertical' */\n @Input() orientation: FieldProps['orientation'] = 'vertical';\n /** Marks the field invalid, turning on the destructive styling for descendants. @default false */\n @Input() invalid = false;\n /** Extra Tailwind classes merged onto the field via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass() {\n return cn(fieldVariants({ orientation: this.orientation }), this.class);\n }\n}\n\n/**\n * Label for a field's control. Set `for` to the control's `id` so clicking the\n * label focuses it.\n */\n@Component({\n selector: 'tolle-field-label',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <label\n [attr.for]=\"for || null\"\n [class]=\"cn(\n 'flex items-center gap-2 text-sm font-medium leading-none text-foreground',\n 'group-data-[invalid=true]/field:text-destructive',\n 'peer-disabled:cursor-not-allowed peer-disabled:opacity-50',\n class\n )\"\n >\n <ng-content></ng-content>\n <span *ngIf=\"required\" class=\"text-destructive\" aria-hidden=\"true\">*</span>\n </label>\n `,\n})\nexport class FieldLabelComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Id of the control this label describes; wire it to the control's `id`. @default '' */\n @Input() for = '';\n /** Appends a required marker (*) after the label text. @default false */\n @Input() required = false;\n /** Extra Tailwind classes merged onto the label via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Column that groups the control with its description and error text. */\n@Component({\n selector: 'tolle-field-content',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"cn('flex flex-1 flex-col gap-1.5', class)\"><ng-content></ng-content></div>`,\n})\nexport class FieldContentComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the content column via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/**\n * Helper text below a control. Give it an `id` and point the control's\n * `aria-describedby` at it.\n */\n@Component({\n selector: 'tolle-field-description',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <p [id]=\"id || null\" [class]=\"cn('text-balance text-xs text-muted-foreground', class)\">\n <ng-content></ng-content>\n </p>\n `,\n})\nexport class FieldDescriptionComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Id to reference from the control's `aria-describedby`. @default '' */\n @Input() id = '';\n /** Extra Tailwind classes merged onto the description via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/**\n * Error text for a field. Renders nothing when there is no error, so it is safe\n * to leave in the template permanently. Announces via `role=\"alert\"`.\n */\n@Component({\n selector: 'tolle-field-error',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div\n *ngIf=\"messages.length\"\n [id]=\"id || null\"\n role=\"alert\"\n [class]=\"cn('text-xs font-medium text-destructive', class)\"\n >\n <ng-container *ngIf=\"messages.length === 1\">{{ messages[0] }}</ng-container>\n <ul *ngIf=\"messages.length > 1\" class=\"ml-4 list-disc space-y-0.5\">\n <li *ngFor=\"let message of messages\">{{ message }}</li>\n </ul>\n </div>\n `,\n})\nexport class FieldErrorComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /**\n * Validation message(s). Accepts a single string, an array of strings, or an\n * Angular `ValidationErrors`-style object whose values are messages.\n * @default ''\n */\n @Input() errors: string | string[] | Record<string, any> | null = '';\n /** Id to reference from the control's `aria-describedby`. @default '' */\n @Input() id = '';\n /** Extra Tailwind classes merged onto the error text via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n\n /** Normalises the three accepted `errors` shapes to a flat list of strings. */\n get messages(): string[] {\n const e = this.errors;\n if (!e) return [];\n if (typeof e === 'string') return e ? [e] : [];\n if (Array.isArray(e)) return e.filter(Boolean).map(String);\n return Object.values(e)\n .filter((v) => v !== false && v !== null && v !== undefined)\n .map((v) => (typeof v === 'string' ? v : String(v)));\n }\n}\n\n/** Stacks several fields with consistent spacing. */\n@Component({\n selector: 'tolle-field-group',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"cn('flex w-full flex-col gap-6', class)\"><ng-content></ng-content></div>`,\n})\nexport class FieldGroupComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the group via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Semantic `<fieldset>` grouping related fields under a legend. */\n@Component({\n selector: 'tolle-field-set',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<fieldset [class]=\"cn('flex w-full flex-col gap-4 border-0 p-0', class)\"><ng-content></ng-content></fieldset>`,\n})\nexport class FieldSetComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the fieldset via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\nconst fieldLegendVariants = cva('mb-1 font-medium text-foreground', {\n variants: {\n variant: {\n legend: 'text-base',\n label: 'text-sm leading-none',\n },\n },\n defaultVariants: { variant: 'legend' },\n});\n\nexport type FieldLegendProps = VariantProps<typeof fieldLegendVariants>;\n\n/** `<legend>` for a `tolle-field-set`. */\n@Component({\n selector: 'tolle-field-legend',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<legend [class]=\"computedClass\"><ng-content></ng-content></legend>`,\n})\nexport class FieldLegendComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Type scale of the legend. @default 'legend' */\n @Input() variant: FieldLegendProps['variant'] = 'legend';\n /** Extra Tailwind classes merged onto the legend via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass() {\n return cn(fieldLegendVariants({ variant: this.variant }), this.class);\n }\n}\n\n/** Divider between sections of a form. */\n@Component({\n selector: 'tolle-field-separator',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div role=\"separator\" [class]=\"cn('h-px w-full bg-border', class)\"></div>`,\n})\nexport class FieldSeparatorComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the separator via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Title for a group of fields that is not a `<legend>`. */\n@Component({\n selector: 'tolle-field-title',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"cn('text-sm font-medium leading-none text-foreground', class)\"><ng-content></ng-content></div>`,\n})\nexport class FieldTitleComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the title via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"path": "hover-card.component.ts",
|
|
14
14
|
"target": "ui/hover-card.component.ts",
|
|
15
|
-
"content": "import { Component, Input, Output, EventEmitter, inject, TemplateRef, ViewChild, ViewContainerRef, OnDestroy, ContentChild, forwardRef, ElementRef, AfterViewInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-hover-card',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`\n})\nexport class HoverCardComponent implements OnDestroy {\n @Input() openDelay = 700;\n @Input() closeDelay = 300;\n @Input() placement: 'top' | 'bottom' | 'left' | 'right' = 'bottom';\n\n private openTimeout?: any;\n private closeTimeout?: any;\n\n isOpen = false;\n\n @ContentChild(forwardRef(() => HoverCardContentComponent)) contentComponent?: HoverCardContentComponent;\n\n open(triggerElement: HTMLElement) {\n if (this.closeTimeout) {\n clearTimeout(this.closeTimeout);\n this.closeTimeout = undefined;\n }\n\n if (this.isOpen) return;\n\n this.openTimeout = setTimeout(() => {\n this.show(triggerElement);\n }, this.openDelay);\n }\n\n close() {\n if (this.openTimeout) {\n clearTimeout(this.openTimeout);\n this.openTimeout = undefined;\n }\n\n this.closeTimeout = setTimeout(() => {\n this.hide();\n }, this.closeDelay);\n }\n\n private show(triggerElement: HTMLElement) {\n this.isOpen = true;\n setTimeout(() => {\n if (this.contentComponent) {\n this.contentComponent.show(triggerElement, this.placement);\n }\n });\n }\n\n private hide() {\n this.isOpen = false;\n if (this.contentComponent) {\n this.contentComponent.hide();\n }\n }\n\n ngOnDestroy() {\n this.hide();\n if (this.openTimeout) clearTimeout(this.openTimeout);\n if (this.closeTimeout) clearTimeout(this.closeTimeout);\n }\n}\n\n@Component({\n selector: 'tolle-hover-card-trigger',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: {\n '(mouseenter)': 'onMouseEnter()',\n '(mouseleave)': 'onMouseLeave()',\n '(focusin)': 'onMouseEnter()',\n '(focusout)': 'onMouseLeave()',\n '[class]': '\"cursor-default inline-block\"'\n }\n})\nexport class HoverCardTriggerComponent {\n private hoverCard = inject(HoverCardComponent);\n private elementRef = inject(ElementRef);\n\n onMouseEnter() {\n this.hoverCard.open(this.elementRef.nativeElement);\n }\n\n onMouseLeave() {\n this.hoverCard.close();\n }\n}\n\n@Component({\n selector: 'tolle-hover-card-content',\n standalone: true,\n imports: [CommonModule],\n template: `\n <div *ngIf=\"isVisible\"\n #content\n [class]=\"computedClass\"\n (mouseenter)=\"onMouseEnter()\"\n (mouseleave)=\"onMouseLeave()\">\n <ng-content></ng-content>\n </div>\n `\n})\nexport class HoverCardContentComponent implements OnDestroy {\n @Input() class: string = '';\n @ViewChild('content') contentEl?: ElementRef;\n\n isVisible = false;\n private cleanup?: () => void;\n private hoverCard = inject(HoverCardComponent);\n\n show(triggerElement: HTMLElement, placement: any) {\n this.isVisible = true;\n setTimeout(() => {\n if (this.contentEl) {\n this.updatePosition(triggerElement, this.contentEl.nativeElement, placement);\n }\n });\n }\n\n hide() {\n this.isVisible = false;\n if (this.cleanup) {\n this.cleanup();\n this.cleanup = undefined;\n }\n }\n\n private updatePosition(trigger: HTMLElement, content: HTMLElement, placement: any) {\n this.cleanup = autoUpdate(trigger, content, () => {\n computePosition(trigger, content, {\n placement,\n middleware: [offset(8), flip(), shift({ padding: 8 })],\n }).then(({ x, y }) => {\n Object.assign(content.style, {\n position: 'fixed',\n left: `${x}px`,\n top: `${y}px`,\n zIndex: '9999'\n });\n });\n });\n }\n\n onMouseEnter() {\n this.hoverCard.open(null as any);\n }\n\n onMouseLeave() {\n this.hoverCard.close();\n }\n\n get computedClass() {\n return cn(\n \"w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n this.class\n );\n }\n\n ngOnDestroy() {\n if (this.cleanup) this.cleanup();\n }\n}\n"
|
|
15
|
+
"content": "import { Component, Input, Output, EventEmitter, inject, TemplateRef, ViewChild, ViewContainerRef, OnDestroy, ContentChild, forwardRef, ElementRef, AfterViewInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-hover-card',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`\n})\nexport class HoverCardComponent implements OnDestroy {\n @Input() openDelay = 700;\n @Input() closeDelay = 300;\n @Input() placement: 'top' | 'bottom' | 'left' | 'right' = 'bottom';\n\n private openTimeout?: any;\n private closeTimeout?: any;\n\n isOpen = false;\n\n @ContentChild(forwardRef(() => HoverCardContentComponent)) contentComponent?: HoverCardContentComponent;\n\n open(triggerElement: HTMLElement) {\n if (this.closeTimeout) {\n clearTimeout(this.closeTimeout);\n this.closeTimeout = undefined;\n }\n\n if (this.isOpen) return;\n\n this.openTimeout = setTimeout(() => {\n this.show(triggerElement);\n }, this.openDelay);\n }\n\n close() {\n if (this.openTimeout) {\n clearTimeout(this.openTimeout);\n this.openTimeout = undefined;\n }\n\n this.closeTimeout = setTimeout(() => {\n this.hide();\n }, this.closeDelay);\n }\n\n private show(triggerElement: HTMLElement) {\n this.isOpen = true;\n setTimeout(() => {\n if (this.contentComponent) {\n this.contentComponent.show(triggerElement, this.placement);\n }\n });\n }\n\n private hide() {\n this.isOpen = false;\n if (this.contentComponent) {\n this.contentComponent.hide();\n }\n }\n\n ngOnDestroy() {\n this.hide();\n if (this.openTimeout) clearTimeout(this.openTimeout);\n if (this.closeTimeout) clearTimeout(this.closeTimeout);\n }\n}\n\n@Component({\n selector: 'tolle-hover-card-trigger',\n standalone: true,\n imports: [CommonModule],\n template: `<ng-content></ng-content>`,\n host: {\n '(mouseenter)': 'onMouseEnter()',\n '(mouseleave)': 'onMouseLeave()',\n '(focusin)': 'onMouseEnter()',\n '(focusout)': 'onMouseLeave()',\n '[class]': '\"cursor-default inline-block\"'\n }\n})\nexport class HoverCardTriggerComponent {\n private hoverCard = inject(HoverCardComponent);\n private elementRef = inject(ElementRef);\n\n onMouseEnter() {\n this.hoverCard.open(this.elementRef.nativeElement);\n }\n\n onMouseLeave() {\n this.hoverCard.close();\n }\n}\n\n@Component({\n selector: 'tolle-hover-card-content',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `\n <div *ngIf=\"isVisible\"\n #content\n [class]=\"computedClass\"\n (mouseenter)=\"onMouseEnter()\"\n (mouseleave)=\"onMouseLeave()\">\n <ng-content></ng-content>\n </div>\n `\n})\nexport class HoverCardContentComponent implements OnDestroy {\n @Input() class: string = '';\n @ViewChild('content') contentEl?: ElementRef;\n\n isVisible = false;\n private cleanup?: () => void;\n private hoverCard = inject(HoverCardComponent);\n\n show(triggerElement: HTMLElement, placement: any) {\n this.isVisible = true;\n setTimeout(() => {\n if (this.contentEl) {\n this.updatePosition(triggerElement, this.contentEl.nativeElement, placement);\n }\n });\n }\n\n hide() {\n this.isVisible = false;\n if (this.cleanup) {\n this.cleanup();\n this.cleanup = undefined;\n }\n }\n\n private updatePosition(trigger: HTMLElement, content: HTMLElement, placement: any) {\n this.cleanup = autoUpdate(trigger, content, () => {\n computePosition(trigger, content, {\n placement,\n middleware: [offset(8), flip(), shift({ padding: 8 })],\n }).then(({ x, y }) => {\n Object.assign(content.style, {\n position: 'fixed',\n left: `${x}px`,\n top: `${y}px`,\n zIndex: '9999'\n });\n });\n });\n }\n\n onMouseEnter() {\n this.hoverCard.open(null as any);\n }\n\n onMouseLeave() {\n this.hoverCard.close();\n }\n\n get computedClass() {\n return cn(\n \"w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n this.class\n );\n }\n\n ngOnDestroy() {\n if (this.cleanup) this.cleanup();\n }\n}\n"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"path": "utils/cn.ts",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "inline-citation",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Inline Citation",
|
|
5
|
+
"description": "Inline Citation component with variant and size options.",
|
|
6
|
+
"category": "ai",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"hover-card"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@floating-ui/dom",
|
|
12
|
+
"class-variance-authority"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "inline-citation.component.ts",
|
|
17
|
+
"target": "ui/inline-citation.component.ts",
|
|
18
|
+
"content": "import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, inject, OnChanges } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\nimport {\n HoverCardComponent,\n HoverCardTriggerComponent,\n HoverCardContentComponent,\n} from './hover-card.component';\n\nconst inlineCitationVariants = cva(\n 'cursor-pointer rounded-sm align-super font-medium leading-none transition-colors',\n {\n variants: {\n variant: {\n default: 'text-primary hover:text-primary/80',\n muted: 'text-muted-foreground hover:text-foreground',\n subtle: 'text-foreground/60 hover:text-foreground',\n },\n size: {\n sm: 'text-[9px]',\n default: 'text-[10px]',\n },\n },\n defaultVariants: { variant: 'default', size: 'default' },\n }\n);\n\nexport type InlineCitationProps = VariantProps<typeof inlineCitationVariants>;\n\n/**\n * A superscript citation marker for prose. Hovering the marker reveals the\n * projected source card, positioned by the existing `tolle-hover-card`.\n *\n * ```html\n * The model was released in March.<tolle-inline-citation [index]=\"1\">\n * <tolle-inline-citation-card\n * title=\"Release notes\"\n * url=\"https://example.com/notes\"\n * snippet=\"Shipped on 14 March.\"\n * ></tolle-inline-citation-card>\n * </tolle-inline-citation>\n * ```\n * @new\n */\n@Component({\n selector: 'tolle-inline-citation',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule, HoverCardComponent, HoverCardTriggerComponent, HoverCardContentComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <tolle-hover-card\n [openDelay]=\"openDelay\"\n [closeDelay]=\"closeDelay\"\n [placement]=\"placement\"\n (mouseenter)=\"scheduleRefresh(openDelay)\"\n (mouseleave)=\"scheduleRefresh(closeDelay)\"\n >\n <tolle-hover-card-trigger>\n <sup\n role=\"button\"\n tabindex=\"0\"\n [attr.aria-label]=\"ariaLabel || 'Citation ' + index\"\n [class]=\"computedClass\"\n >{{ index }}</sup\n >\n </tolle-hover-card-trigger>\n <tolle-hover-card-content [class]=\"contentClass\">\n <ng-content></ng-content>\n </tolle-hover-card-content>\n </tolle-hover-card>\n `,\n})\nexport class InlineCitationComponent implements OnChanges, OnDestroy {\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Number rendered in the superscript marker. @default 1 */\n @Input() index: number | string = 1;\n /** Colour treatment of the marker. @default 'default' */\n @Input() variant: InlineCitationProps['variant'] = 'default';\n /** Text size of the marker. @default 'default' */\n @Input() size: InlineCitationProps['size'] = 'default';\n /** Side of the marker the source card opens on. @default 'top' */\n @Input() placement: 'top' | 'bottom' | 'left' | 'right' = 'top';\n /** Milliseconds to wait before opening the card. @default 150 */\n @Input() openDelay = 150;\n /** Milliseconds to wait before closing the card. @default 200 */\n @Input() closeDelay = 200;\n /** Accessible label for the marker; falls back to \"Citation {index}\". @default '' */\n @Input() ariaLabel = '';\n /** Extra Tailwind classes merged onto the marker via `cn()` (last-wins). */\n @Input() class = '';\n /** Extra Tailwind classes merged onto the hover card panel via `cn()` (last-wins). */\n @Input() cardClass = '';\n\n private readonly cdr = inject(ChangeDetectorRef);\n private refreshTimer?: ReturnType<typeof setTimeout>;\n\n get computedClass() {\n return cn(inlineCitationVariants({ variant: this.variant, size: this.size }), this.class);\n }\n\n get contentClass() {\n return cn('w-72 p-0', this.cardClass);\n }\n\n /**\n * `tolle-hover-card` flips its own visibility from a `setTimeout`, which never\n * marks this OnPush view dirty. Re-check just after the delay elapses so the\n * card actually appears (and disappears) inside an OnPush host.\n */\n scheduleRefresh(delay: number): void {\n clearTimeout(this.refreshTimer);\n this.refreshTimer = setTimeout(() => this.cdr.markForCheck(), delay + 32);\n }\n\n ngOnDestroy(): void {\n clearTimeout(this.refreshTimer);\n }\n}\n\n/** The source preview shown inside a citation's hover card. */\n@Component({\n selector: 'tolle-inline-citation-card',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div [class]=\"computedClass\">\n <p class=\"truncate text-sm font-medium text-popover-foreground\">{{ title }}</p>\n <a\n *ngIf=\"url\"\n [href]=\"url\"\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n class=\"mt-0.5 flex items-center gap-1 truncate text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline\"\n >\n <i class=\"ri-external-link-line shrink-0\" aria-hidden=\"true\"></i>\n <span class=\"truncate\">{{ displayUrl }}</span>\n </a>\n <p *ngIf=\"snippet\" class=\"mt-2 line-clamp-3 text-xs leading-relaxed text-muted-foreground\">{{ snippet }}</p>\n <ng-content></ng-content>\n </div>\n `,\n})\nexport class InlineCitationCardComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Title of the cited source. @default '' */\n @Input() title = '';\n /** Link to the cited source. @default '' */\n @Input() url = '';\n /** Short excerpt from the source. @default '' */\n @Input() snippet = '';\n /** Extra Tailwind classes merged onto the card via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass() {\n return cn('flex flex-col p-3', this.class);\n }\n\n /** The URL without its scheme, which reads better in a narrow card. */\n get displayUrl(): string {\n return this.url.replace(/^https?:\\/\\//, '');\n }\n}\n\n/** A pulled quote from the cited source, shown inside the citation card. */\n@Component({\n selector: 'tolle-inline-citation-quote',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <blockquote [class]=\"computedClass\">\n <ng-content></ng-content>\n <footer *ngIf=\"cite\" class=\"mt-1 text-[11px] not-italic text-muted-foreground/80\">— {{ cite }}</footer>\n </blockquote>\n `,\n})\nexport class InlineCitationQuoteComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Attribution shown beneath the quote. @default '' */\n @Input() cite = '';\n /** Extra Tailwind classes merged onto the quote via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass() {\n return cn(\n 'mt-2 border-l-2 border-border pl-3 text-xs italic leading-relaxed text-muted-foreground',\n this.class\n );\n }\n}\n"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "utils/cn.ts",
|
|
22
|
+
"target": "ui/utils/cn.ts",
|
|
23
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "input-group",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Input Group",
|
|
5
|
+
"description": "Input Group component with size and invalid and stacked options.",
|
|
6
|
+
"category": "forms",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "input-group.component.ts",
|
|
14
|
+
"target": "ui/input-group.component.ts",
|
|
15
|
+
"content": "import { Component, Input, Output, EventEmitter, forwardRef, ChangeDetectorRef } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst inputGroupVariants = cva(\n 'group/input-group relative flex w-full items-center rounded-md border border-input bg-background shadow-sm transition-colors ' +\n 'focus-within:border-primary/80 focus-within:ring-4 focus-within:ring-ring/30 ' +\n 'has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50',\n {\n variants: {\n size: {\n xs: 'h-8 gap-1 px-1.5 text-xs',\n sm: 'h-9 gap-1.5 px-2 text-sm',\n default: 'h-10 gap-2 px-2 text-sm',\n lg: 'h-11 gap-2 px-3 text-base',\n },\n invalid: {\n true: 'border-destructive focus-within:border-destructive/80 focus-within:ring-destructive/30',\n false: '',\n },\n // `block-start` / `block-end` addons stack, so the group can no longer be a\n // fixed-height row.\n stacked: {\n true: 'h-auto flex-col items-stretch gap-0 px-0 py-0',\n false: '',\n },\n },\n defaultVariants: { size: 'default', invalid: false, stacked: false },\n }\n);\n\nexport type InputGroupProps = VariantProps<typeof inputGroupVariants>;\n\n/**\n * Wraps an input with leading/trailing addons — icons, text, buttons or a\n * spinner — inside a single bordered control that shows one shared focus ring.\n *\n * Put a `tolle-input-group-input` (or `tolle-input-group-textarea`) inside,\n * alongside any number of `tolle-input-group-addon` elements.\n * @new\n */\n@Component({\n selector: 'tolle-input-group',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n template: `<div [class]=\"computedClass\"><ng-content></ng-content></div>`,\n})\nexport class InputGroupComponent {\n /** Height and text size of the group; match it to the input inside. @default 'default' */\n @Input() size: InputGroupProps['size'] = 'default';\n /** Applies the destructive border and focus ring. @default false */\n @Input() invalid = false;\n /** Switches to a stacked column layout for `block-start`/`block-end` addons. @default false */\n @Input() stacked = false;\n /** Extra Tailwind classes merged onto the group via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass() {\n return cn(\n inputGroupVariants({ size: this.size, invalid: this.invalid, stacked: this.stacked }),\n this.class\n );\n }\n}\n\nconst inputGroupAddonVariants = cva('flex shrink-0 items-center gap-1.5 text-muted-foreground', {\n variants: {\n align: {\n 'inline-start': 'order-first',\n 'inline-end': 'order-last',\n 'block-start': 'order-first w-full border-b border-border px-3 py-1.5',\n 'block-end': 'order-last w-full border-t border-border px-3 py-1.5',\n },\n },\n defaultVariants: { align: 'inline-start' },\n});\n\nexport type InputGroupAddonProps = VariantProps<typeof inputGroupAddonVariants>;\n\n/**\n * A slot beside (or above/below) the input for icons, text or buttons.\n * Use `block-start` / `block-end` together with the group's `stacked` input.\n */\n@Component({\n selector: 'tolle-input-group-addon',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n template: `<div [class]=\"computedClass\"><ng-content></ng-content></div>`,\n})\nexport class InputGroupAddonComponent {\n /** Where the addon sits relative to the input. @default 'inline-start' */\n @Input() align: InputGroupAddonProps['align'] = 'inline-start';\n /** Extra Tailwind classes merged onto the addon via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass() {\n return cn(inputGroupAddonVariants({ align: this.align }), this.class);\n }\n}\n\nconst inputGroupButtonVariants = cva(\n 'inline-flex shrink-0 items-center justify-center gap-1 rounded font-medium transition-colors ' +\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ' +\n 'disabled:pointer-events-none disabled:opacity-50',\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n outline: 'border border-border bg-transparent hover:bg-accent hover:text-accent-foreground',\n ghost: 'bg-transparent hover:bg-accent hover:text-accent-foreground',\n destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n },\n size: {\n xs: 'h-6 px-2 text-xs',\n sm: 'h-7 px-2.5 text-sm',\n 'icon-xs': 'h-6 w-6 p-0 text-xs',\n 'icon-sm': 'h-7 w-7 p-0 text-sm',\n },\n },\n defaultVariants: { variant: 'ghost', size: 'icon-sm' },\n }\n);\n\nexport type InputGroupButtonProps = VariantProps<typeof inputGroupButtonVariants>;\n\n/** A compact button sized to sit inside an input group addon. */\n@Component({\n selector: 'tolle-input-group-button',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n template: `\n <button\n [type]=\"type\"\n [disabled]=\"disabled\"\n [attr.aria-label]=\"ariaLabel || null\"\n [class]=\"computedClass\"\n (click)=\"clicked.emit($event)\"\n >\n <ng-content></ng-content>\n </button>\n `,\n})\nexport class InputGroupButtonComponent {\n /** Visual style of the button. @default 'ghost' */\n @Input() variant: InputGroupButtonProps['variant'] = 'ghost';\n /** Size of the button; the `icon-*` sizes render a square. @default 'icon-sm' */\n @Input() size: InputGroupButtonProps['size'] = 'icon-sm';\n /** Native button type. @default 'button' */\n @Input() type: 'button' | 'submit' | 'reset' = 'button';\n /** Disables the button. @default false */\n @Input() disabled = false;\n /** Accessible name — required when the button shows only an icon. @default '' */\n @Input() ariaLabel = '';\n /** Extra Tailwind classes merged onto the button via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted with the click event when the button is pressed. */\n @Output() clicked = new EventEmitter<MouseEvent>();\n\n get computedClass() {\n return cn(inputGroupButtonVariants({ variant: this.variant, size: this.size }), this.class);\n }\n}\n\n/** Plain text inside an addon — a unit, prefix, or character counter. */\n@Component({\n selector: 'tolle-input-group-text',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n template: `<span [class]=\"cn('select-none whitespace-nowrap text-sm text-muted-foreground', class)\"><ng-content></ng-content></span>`,\n})\nexport class InputGroupTextComponent {\n /** Extra Tailwind classes merged onto the text via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/**\n * The input inside a `tolle-input-group`. Deliberately borderless and\n * unfocusable-looking — the surrounding group owns the border and focus ring.\n */\n@Component({\n selector: 'tolle-input-group-input',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule, FormsModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => InputGroupInputComponent),\n multi: true,\n },\n ],\n template: `\n <input\n [id]=\"id\"\n [type]=\"type\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [readOnly]=\"readonly\"\n [value]=\"value ?? ''\"\n [attr.aria-describedby]=\"ariaDescribedby || null\"\n [class]=\"computedClass\"\n (input)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n />\n `,\n})\nexport class InputGroupInputComponent implements ControlValueAccessor {\n /** Id applied to the underlying `<input>`. */\n @Input() id = `input-group-input-${Math.random().toString(36).slice(2, 11)}`;\n /** Native input type. @default 'text' */\n @Input() type = 'text';\n /** Placeholder text. @default '' */\n @Input() placeholder = '';\n /** Disables the input. @default false */\n @Input() disabled = false;\n /** Makes the input read-only. @default false */\n @Input() readonly = false;\n /** Id of the element describing this input, for `aria-describedby`. @default '' */\n @Input() ariaDescribedby = '';\n /** Extra Tailwind classes merged onto the input via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted with the new value on every keystroke. */\n @Output() valueChange = new EventEmitter<string>();\n\n value: string | null = '';\n onChange: (value: string) => void = () => {};\n onTouched: () => void = () => {};\n\n constructor(private cdr: ChangeDetectorRef) {}\n\n get computedClass() {\n return cn(\n 'min-w-0 flex-1 border-none bg-transparent p-0 text-foreground outline-none',\n 'placeholder:text-muted-foreground',\n 'focus:outline-none focus:ring-0 focus:shadow-none',\n 'disabled:cursor-not-allowed',\n this.class\n );\n }\n\n onInputChange(event: Event): void {\n if (this.readonly || this.disabled) return;\n const next = (event.target as HTMLInputElement).value;\n this.value = next;\n this.onChange(next);\n this.valueChange.emit(next);\n }\n\n writeValue(value: string | null): void {\n this.value = value;\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: (value: string) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.cdr.markForCheck();\n }\n}\n\n/** The textarea equivalent of `tolle-input-group-input`. */\n@Component({\n selector: 'tolle-input-group-textarea',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule, FormsModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => InputGroupTextareaComponent),\n multi: true,\n },\n ],\n template: `\n <textarea\n [id]=\"id\"\n [rows]=\"rows\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [readOnly]=\"readonly\"\n [value]=\"value ?? ''\"\n [attr.aria-describedby]=\"ariaDescribedby || null\"\n [class]=\"computedClass\"\n (input)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n ></textarea>\n `,\n})\nexport class InputGroupTextareaComponent implements ControlValueAccessor {\n /** Id applied to the underlying `<textarea>`. */\n @Input() id = `input-group-textarea-${Math.random().toString(36).slice(2, 11)}`;\n /** Visible rows. @default 3 */\n @Input() rows = 3;\n /** Placeholder text. @default '' */\n @Input() placeholder = '';\n /** Disables the textarea. @default false */\n @Input() disabled = false;\n /** Makes the textarea read-only. @default false */\n @Input() readonly = false;\n /** Id of the element describing this textarea, for `aria-describedby`. @default '' */\n @Input() ariaDescribedby = '';\n /** Extra Tailwind classes merged onto the textarea via `cn()` (last-wins). */\n @Input() class = '';\n\n /** Emitted with the new value on every keystroke. */\n @Output() valueChange = new EventEmitter<string>();\n\n value: string | null = '';\n onChange: (value: string) => void = () => {};\n onTouched: () => void = () => {};\n\n constructor(private cdr: ChangeDetectorRef) {}\n\n get computedClass() {\n return cn(\n 'min-w-0 flex-1 resize-none border-none bg-transparent p-0 py-2 text-foreground outline-none',\n 'placeholder:text-muted-foreground',\n 'focus:outline-none focus:ring-0 focus:shadow-none',\n 'disabled:cursor-not-allowed',\n this.class\n );\n }\n\n onInputChange(event: Event): void {\n if (this.readonly || this.disabled) return;\n const next = (event.target as HTMLTextAreaElement).value;\n this.value = next;\n this.onChange(next);\n this.valueChange.emit(next);\n }\n\n writeValue(value: string | null): void {\n this.value = value;\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: (value: string) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.cdr.markForCheck();\n }\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
package/registry/r/input.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"path": "input.component.ts",
|
|
12
12
|
"target": "ui/input.component.ts",
|
|
13
|
-
"content": "import { Component, Input, forwardRef, ChangeDetectorRef, ElementRef, ViewChild, AfterViewInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-input',\n standalone: true,\n imports: [CommonModule, FormsModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => InputComponent),\n multi: true\n }\n ],\n template: `\n <div class=\"flex flex-col gap-1.5 w-full\">\n <label\n *ngIf=\"label\"\n [for]=\"id\"\n [class]=\"computedLabelClass\">\n {{ label }}\n </label>\n\n <div\n [class]=\"computedContainerClass\"\n (click)=\"focusInput()\"\n >\n <div class=\"h-full flex items-center text-muted-foreground group-focus-within:text-primary transition-colors duration-200\">\n <ng-content select=\"[prefix]\"></ng-content>\n </div>\n\n <input\n #inputElement\n [id]=\"id\"\n [type]=\"type\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [readOnly]=\"readonly\"\n [(ngModel)]=\"value\"\n (blur)=\"onBlur()\"\n (focus)=\"onFocus()\"\n (input)=\"onInputChange($event)\"\n [class]=\"computedInputClass\"\n [attr.aria-invalid]=\"error || null\"\n [attr.aria-describedby]=\"error && errorMessage ? id + '-error' : (hint ? id + '-hint' : null)\"\n />\n\n <div class=\"h-full flex items-center text-muted-foreground group-focus-within:text-primary transition-colors duration-200\">\n <ng-content select=\"[suffix]\"></ng-content>\n </div>\n </div>\n\n <ng-container *ngIf=\"!disabled\">\n <p\n *ngIf=\"hint && !error\"\n [id]=\"id + '-hint'\"\n class=\"text-xs text-muted-foreground px-1 transition-opacity duration-200\"\n [class.opacity-0]=\"isFocused && hideHintOnFocus\"\n >\n {{ hint }}\n </p>\n <p\n *ngIf=\"error && errorMessage\"\n [id]=\"id + '-error'\"\n class=\"text-xs text-destructive px-1\"\n >\n {{ errorMessage }}\n </p>\n </ng-container>\n </div>\n `,\n})\nexport class InputComponent implements ControlValueAccessor, AfterViewInit {\n @ViewChild('inputElement') inputElement!: ElementRef<HTMLInputElement>;\n\n @Input() id: string = `input-${Math.random().toString(36).substr(2, 9)}`;\n @Input() label: string = '';\n @Input() hint: string = '';\n @Input() errorMessage: string = '';\n @Input() type: string = 'text';\n @Input() placeholder: string = '';\n @Input() size: 'xs' | 'sm' | 'default' | 'lg' = 'default';\n @Input() containerClass: string = '';\n @Input() class: string = '';\n\n // New States\n @Input() disabled: boolean = false;\n @Input() readonly: boolean = false;\n @Input() error: boolean = false;\n\n // Focus behavior\n @Input() hideHintOnFocus: boolean = true;\n\n value: any = '';\n onChange: any = () => { };\n onTouched: any = () => { };\n isFocused: boolean = false;\n\n constructor(private cdr: ChangeDetectorRef) { }\n\n ngAfterViewInit() {\n if (this.inputElement?.nativeElement.hasAttribute('autofocus')) {\n setTimeout(() => this.inputElement.nativeElement.focus());\n }\n }\n\n writeValue(value: any): void {\n this.value = value;\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.cdr.markForCheck();\n }\n\n onInputChange(event: Event): void {\n if (this.readonly || this.disabled) return;\n const val = (event.target as HTMLInputElement).value;\n this.value = val;\n this.onChange(val);\n }\n\n onFocus(): void {\n this.isFocused = true;\n }\n\n onBlur(): void {\n this.isFocused = false;\n this.onTouched();\n }\n\n focusInput(): void {\n if (!this.disabled && this.inputElement) {\n this.inputElement.nativeElement.focus();\n }\n }\n\n protected readonly cn = cn;\n\n get computedLabelClass() {\n return cn(\n \"text-sm font-medium text-foreground leading-none transition-opacity duration-200\",\n this.disabled && \"opacity-50\"\n );\n }\n\n get computedContainerClass() {\n return cn(\n // Base styles\n \"group relative flex items-center w-full rounded-md border transition-all duration-200\",\n \"bg-background\",\n\n // Border and shadow\n \"border-input shadow-sm\",\n\n // Sizing\n this.size === 'xs' && \"h-8 px-2 gap-1.5 text-xs\",\n this.size === 'sm' && \"h-9 px-3 gap-2 text-sm\",\n this.size === 'default' && \"h-10 px-3 gap-2 text-sm\",\n this.size === 'lg' && \"h-11 px-4 gap-3 text-base\",\n\n // Focus state - SIMPLE AND ELEGANT LIKE ZARDUI\n // The magic happens in CSS: border darkens automatically on focus\n !(this.readonly || this.disabled) && [\n \"focus-within:border-primary/80\",\n \"focus-within:ring-4\",\n \"focus-within:ring-ring/30\",\n \"focus-within:ring-offset-0\",\n \"focus-within:shadow-none\",\n ],\n\n // Error state\n this.error && [\n \"border-destructive\",\n !(this.readonly || this.disabled) && [\n \"focus-within:border-destructive/80\",\n \"focus-within:ring-destructive/30\"\n ]\n ],\n\n // Disabled state\n this.disabled && [\n \"cursor-not-allowed opacity-50\",\n \"border-opacity-50\"\n ],\n\n // Readonly state\n this.readonly && [\n \"cursor-default\",\n \"border-dashed\",\n !this.disabled && \"focus-within:ring-0 focus-within:border-opacity-100\"\n ],\n\n this.containerClass\n );\n }\n\n get computedInputClass() {\n return cn(\n // Base styles\n \"flex-1 bg-transparent border-none p-0\",\n \"placeholder:text-muted-foreground\",\n\n // Remove all default focus styles\n \"focus:outline-none focus:ring-0 focus:shadow-none\",\n\n // Text sizing\n this.size === 'xs' && \"text-xs\",\n this.size === 'sm' && \"text-sm\",\n this.size === 'default' && \"text-sm\",\n this.size === 'lg' && \"text-base\",\n\n // Cursor states\n this.disabled && \"cursor-not-allowed\",\n this.readonly && \"cursor-default\",\n\n // Text color\n \"text-foreground\",\n\n // Selection color\n \"selection:bg-primary/20 selection:text-foreground\",\n\n this.class\n );\n }\n}\n"
|
|
13
|
+
"content": "import { Component, Input, forwardRef, ChangeDetectorRef, ElementRef, ViewChild, AfterViewInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-input',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule, FormsModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => InputComponent),\n multi: true\n }\n ],\n template: `\n <div class=\"flex flex-col gap-1.5 w-full\">\n <label\n *ngIf=\"label\"\n [for]=\"id\"\n [class]=\"computedLabelClass\">\n {{ label }}\n </label>\n\n <div\n [class]=\"computedContainerClass\"\n (click)=\"focusInput()\"\n >\n <div class=\"h-full flex items-center text-muted-foreground group-focus-within:text-primary transition-colors duration-200\">\n <ng-content select=\"[prefix]\"></ng-content>\n </div>\n\n <input\n #inputElement\n [id]=\"id\"\n [type]=\"type\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [readOnly]=\"readonly\"\n [(ngModel)]=\"value\"\n (blur)=\"onBlur()\"\n (focus)=\"onFocus()\"\n (input)=\"onInputChange($event)\"\n [class]=\"computedInputClass\"\n [attr.aria-invalid]=\"error || null\"\n [attr.aria-describedby]=\"error && errorMessage ? id + '-error' : (hint ? id + '-hint' : null)\"\n />\n\n <div class=\"h-full flex items-center text-muted-foreground group-focus-within:text-primary transition-colors duration-200\">\n <ng-content select=\"[suffix]\"></ng-content>\n </div>\n </div>\n\n <ng-container *ngIf=\"!disabled\">\n <p\n *ngIf=\"hint && !error\"\n [id]=\"id + '-hint'\"\n class=\"text-xs text-muted-foreground px-1 transition-opacity duration-200\"\n [class.opacity-0]=\"isFocused && hideHintOnFocus\"\n >\n {{ hint }}\n </p>\n <p\n *ngIf=\"error && errorMessage\"\n [id]=\"id + '-error'\"\n class=\"text-xs text-destructive px-1\"\n >\n {{ errorMessage }}\n </p>\n </ng-container>\n </div>\n `,\n})\nexport class InputComponent implements ControlValueAccessor, AfterViewInit {\n @ViewChild('inputElement') inputElement!: ElementRef<HTMLInputElement>;\n\n @Input() id: string = `input-${Math.random().toString(36).substr(2, 9)}`;\n @Input() label: string = '';\n @Input() hint: string = '';\n @Input() errorMessage: string = '';\n @Input() type: string = 'text';\n @Input() placeholder: string = '';\n @Input() size: 'xs' | 'sm' | 'default' | 'lg' = 'default';\n @Input() containerClass: string = '';\n @Input() class: string = '';\n\n // New States\n @Input() disabled: boolean = false;\n @Input() readonly: boolean = false;\n @Input() error: boolean = false;\n\n // Focus behavior\n @Input() hideHintOnFocus: boolean = true;\n\n value: any = '';\n onChange: any = () => { };\n onTouched: any = () => { };\n isFocused: boolean = false;\n\n constructor(private cdr: ChangeDetectorRef) { }\n\n ngAfterViewInit() {\n if (this.inputElement?.nativeElement.hasAttribute('autofocus')) {\n setTimeout(() => this.inputElement.nativeElement.focus());\n }\n }\n\n writeValue(value: any): void {\n this.value = value;\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.cdr.markForCheck();\n }\n\n onInputChange(event: Event): void {\n if (this.readonly || this.disabled) return;\n const val = (event.target as HTMLInputElement).value;\n this.value = val;\n this.onChange(val);\n }\n\n onFocus(): void {\n this.isFocused = true;\n }\n\n onBlur(): void {\n this.isFocused = false;\n this.onTouched();\n }\n\n focusInput(): void {\n if (!this.disabled && this.inputElement) {\n this.inputElement.nativeElement.focus();\n }\n }\n\n protected readonly cn = cn;\n\n get computedLabelClass() {\n return cn(\n \"text-sm font-medium text-foreground leading-none transition-opacity duration-200\",\n this.disabled && \"opacity-50\"\n );\n }\n\n get computedContainerClass() {\n return cn(\n // Base styles\n \"group relative flex items-center w-full rounded-md border transition-all duration-200\",\n \"bg-background\",\n\n // Border and shadow\n \"border-input shadow-sm\",\n\n // Sizing\n this.size === 'xs' && \"h-8 px-2 gap-1.5 text-xs\",\n this.size === 'sm' && \"h-9 px-3 gap-2 text-sm\",\n this.size === 'default' && \"h-10 px-3 gap-2 text-sm\",\n this.size === 'lg' && \"h-11 px-4 gap-3 text-base\",\n\n // Focus state - SIMPLE AND ELEGANT LIKE ZARDUI\n // The magic happens in CSS: border darkens automatically on focus\n !(this.readonly || this.disabled) && [\n \"focus-within:border-primary/80\",\n \"focus-within:ring-4\",\n \"focus-within:ring-ring/30\",\n \"focus-within:ring-offset-0\",\n \"focus-within:shadow-none\",\n ],\n\n // Error state\n this.error && [\n \"border-destructive\",\n !(this.readonly || this.disabled) && [\n \"focus-within:border-destructive/80\",\n \"focus-within:ring-destructive/30\"\n ]\n ],\n\n // Disabled state\n this.disabled && [\n \"cursor-not-allowed opacity-50\",\n \"border-opacity-50\"\n ],\n\n // Readonly state\n this.readonly && [\n \"cursor-default\",\n \"border-dashed\",\n !this.disabled && \"focus-within:ring-0 focus-within:border-opacity-100\"\n ],\n\n this.containerClass\n );\n }\n\n get computedInputClass() {\n return cn(\n // Base styles\n \"flex-1 bg-transparent border-none p-0\",\n \"placeholder:text-muted-foreground\",\n\n // Remove all default focus styles\n \"focus:outline-none focus:ring-0 focus:shadow-none\",\n\n // Text sizing\n this.size === 'xs' && \"text-xs\",\n this.size === 'sm' && \"text-sm\",\n this.size === 'default' && \"text-sm\",\n this.size === 'lg' && \"text-base\",\n\n // Cursor states\n this.disabled && \"cursor-not-allowed\",\n this.readonly && \"cursor-default\",\n\n // Text color\n \"text-foreground\",\n\n // Selection color\n \"selection:bg-primary/20 selection:text-foreground\",\n\n this.class\n );\n }\n}\n"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"path": "utils/cn.ts",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "item",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Item",
|
|
5
|
+
"description": "Item component with variant and size options.",
|
|
6
|
+
"category": "layout",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "item.component.ts",
|
|
14
|
+
"target": "ui/item.component.ts",
|
|
15
|
+
"content": "import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef, OnChanges, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst itemVariants = cva(\n 'group/item flex items-center gap-4 rounded-md border text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n {\n variants: {\n variant: {\n default: 'border-transparent bg-transparent',\n outline: 'border-border bg-transparent',\n muted: 'border-transparent bg-muted/50',\n },\n size: {\n xs: 'px-2 py-1.5 gap-2',\n sm: 'px-3 py-2 gap-3',\n default: 'px-4 py-3',\n },\n },\n defaultVariants: { variant: 'default', size: 'default' },\n }\n);\n\nexport type ItemProps = VariantProps<typeof itemVariants>;\n\n/**\n * A flex container for content rows built from media, title, description and\n * actions — profiles, notifications, settings rows, list entries. Use\n * `tolle-field` instead when wrapping a form control.\n * @new\n */\n@Component({\n selector: 'tolle-item',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"computedClass\"><ng-content></ng-content></div>`,\n})\nexport class ItemComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Visual style of the row. @default 'default' */\n @Input() variant: ItemProps['variant'] = 'default';\n /** Padding and gap density of the row. @default 'default' */\n @Input() size: ItemProps['size'] = 'default';\n /** Extra Tailwind classes merged onto the row via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass() {\n return cn(itemVariants({ variant: this.variant, size: this.size }), this.class);\n }\n}\n\n/** Stacks several `tolle-item` rows as a single list. */\n@Component({\n selector: 'tolle-item-group',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div role=\"list\" [class]=\"cn('flex flex-col', class)\"><ng-content></ng-content></div>`,\n})\nexport class ItemGroupComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the group via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Leading media slot — avatar, icon, or thumbnail. */\n@Component({\n selector: 'tolle-item-media',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"cn('flex shrink-0 items-center justify-center text-muted-foreground', class)\"><ng-content></ng-content></div>`,\n})\nexport class ItemMediaComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the media slot via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Grows to fill the row; wraps the title and description. */\n@Component({\n selector: 'tolle-item-content',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"cn('flex min-w-0 flex-1 flex-col gap-0.5', class)\"><ng-content></ng-content></div>`,\n})\nexport class ItemContentComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the content column via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Primary line of the row. */\n@Component({\n selector: 'tolle-item-title',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<p [class]=\"cn('truncate font-medium leading-none text-foreground', class)\"><ng-content></ng-content></p>`,\n})\nexport class ItemTitleComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the title via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Secondary line of the row. */\n@Component({\n selector: 'tolle-item-description',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<p [class]=\"cn('truncate text-sm text-muted-foreground', class)\"><ng-content></ng-content></p>`,\n})\nexport class ItemDescriptionComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the description via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Trailing slot for buttons, switches, or badges. */\n@Component({\n selector: 'tolle-item-actions',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"cn('flex shrink-0 items-center gap-2', class)\"><ng-content></ng-content></div>`,\n})\nexport class ItemActionsComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the actions slot via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Optional full-width header above the row's main line. */\n@Component({\n selector: 'tolle-item-header',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"cn('flex w-full items-center justify-between gap-2', class)\"><ng-content></ng-content></div>`,\n})\nexport class ItemHeaderComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the header via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n\n/** Optional full-width footer below the row's main line. */\n@Component({\n selector: 'tolle-item-footer',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<div [class]=\"cn('flex w-full items-center justify-between gap-2', class)\"><ng-content></ng-content></div>`,\n})\nexport class ItemFooterComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the footer via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kbd",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"title": "Kbd",
|
|
5
|
+
"description": "Kbd component with size and variant options.",
|
|
6
|
+
"category": "actions",
|
|
7
|
+
"registryDependencies": [],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "kbd.component.ts",
|
|
14
|
+
"target": "ui/kbd.component.ts",
|
|
15
|
+
"content": "import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef, OnChanges, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from './utils/cn';\n\nconst kbdVariants = cva(\n 'inline-flex select-none items-center justify-center gap-1 rounded border border-border font-mono font-medium text-muted-foreground',\n {\n variants: {\n size: {\n sm: 'h-4 min-w-4 px-1 text-[10px]',\n default: 'h-5 min-w-5 px-1.5 text-[11px]',\n lg: 'h-6 min-w-6 px-2 text-xs',\n },\n variant: {\n default: 'bg-muted',\n outline: 'bg-transparent',\n },\n },\n defaultVariants: { size: 'default', variant: 'default' },\n }\n);\n\nexport type KbdProps = VariantProps<typeof kbdVariants>;\n\n/**\n * Renders a keyboard key or shortcut, e.g. `<tolle-kbd>⌘</tolle-kbd>`.\n * Pairs with `tolle-command-shortcut` inside command menus.\n * @new\n */\n@Component({\n selector: 'tolle-kbd',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<kbd [class]=\"computedClass\"><ng-content></ng-content></kbd>`,\n})\nexport class KbdComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Size of the key cap. @default 'default' */\n @Input() size: KbdProps['size'] = 'default';\n /** Visual style of the key cap. @default 'default' */\n @Input() variant: KbdProps['variant'] = 'default';\n /** Extra Tailwind classes merged onto the key cap via `cn()` (last-wins). */\n @Input() class = '';\n\n get computedClass() {\n return cn(kbdVariants({ size: this.size, variant: this.variant }), this.class);\n }\n}\n\n/**\n * Groups several `tolle-kbd` keys into one shortcut, e.g. ⌘ + K.\n */\n@Component({\n selector: 'tolle-kbd-group',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `<span [class]=\"cn('inline-flex items-center gap-1', class)\"><ng-content></ng-content></span>`,\n})\nexport class KbdGroupComponent implements OnChanges{\n private readonly cdr = inject(ChangeDetectorRef);\n\n /**\n * Angular writes a bound `class` input through its styling path, which does\n * not mark an OnPush component dirty — without this hook the component keeps\n * rendering the class it was born with.\n */\n ngOnChanges(): void {\n this.cdr.markForCheck();\n }\n\n /** Extra Tailwind classes merged onto the group via `cn()` (last-wins). */\n @Input() class = '';\n protected cn = cn;\n}\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "utils/cn.ts",
|
|
19
|
+
"target": "ui/utils/cn.ts",
|
|
20
|
+
"content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
package/registry/r/label.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"path": "label.component.ts",
|
|
12
12
|
"target": "ui/label.component.ts",
|
|
13
|
-
"content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-label',\n standalone: true,\n imports: [CommonModule],\n template: `\n <label\n [attr.for]=\"for\"\n [class]=\"computedClass\"\n >\n <ng-content></ng-content>\n </label>\n `\n})\nexport class LabelComponent {\n @Input() for?: string;\n @Input() class: string = '';\n\n get computedClass() {\n return cn(\n \"inline-flex items-center gap-2 select-none text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n this.class\n );\n }\n}\n"
|
|
13
|
+
"content": "import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { cn } from './utils/cn';\n\n@Component({\n selector: 'tolle-label',\n styles: [':host { display: inline-flex; }'],\n standalone: true,\n imports: [CommonModule],\n template: `\n <label\n [attr.for]=\"for\"\n [class]=\"computedClass\"\n >\n <ng-content></ng-content>\n </label>\n `\n})\nexport class LabelComponent {\n @Input() for?: string;\n @Input() class: string = '';\n\n get computedClass() {\n return cn(\n \"inline-flex items-center gap-2 select-none text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n this.class\n );\n }\n}\n"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"path": "utils/cn.ts",
|