@ng-primitives/mcp 0.126.0 → 0.128.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.
@@ -1,7 +1,7 @@
1
1
  [
2
2
  {
3
3
  "name": "accordion",
4
- "code": "import { Component, input } from '@angular/core';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroChevronDownMini } from '@ng-icons/heroicons/mini';\nimport {\n NgpAccordionContent,\n NgpAccordionItem,\n NgpAccordionTrigger,\n} from 'ng-primitives/accordion';\nimport { NgpButton } from 'ng-primitives/button';\n\n@Component({\n selector: 'app-accordion-item',\n hostDirectives: [\n {\n directive: NgpAccordionItem,\n inputs: ['ngpAccordionItemValue:value', 'ngpAccordionItemDisabled:disabled'],\n },\n ],\n imports: [NgpAccordionContent, NgpAccordionTrigger, NgpButton, NgIcon],\n providers: [provideIcons({ heroChevronDownMini })],\n template: `\n <button ngpAccordionTrigger ngpButton>\n {{ heading() }}\n\n <ng-icon name=\"heroChevronDownMini\" />\n </button>\n <div ngpAccordionContent>\n <ng-content />\n </div>\n `,\n styles: `\n :host {\n display: block;\n }\n\n :host:has(+ :host) {\n border-bottom: 1px solid var(--ngp-border);\n }\n\n [ngpAccordionTrigger] {\n display: flex;\n padding-left: 1rem;\n padding-right: 1rem;\n font-size: 0.875rem;\n line-height: 1.25rem;\n font-weight: 500;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n height: 2.75rem;\n border-radius: 0.75rem;\n outline: none;\n color: var(--ngp-text-primary);\n background-color: var(--ngp-background);\n border: none;\n }\n\n [ngpAccordionTrigger][data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n [ngpAccordionContent] {\n font-size: 0.875rem;\n color: var(--ngp-text-secondary);\n overflow: hidden;\n padding: 0 16px;\n box-sizing: border-box;\n }\n\n [ngpAccordionContent][data-open] {\n animation: slideDown 0.2s ease-in-out forwards;\n }\n\n [ngpAccordionContent][data-closed] {\n animation: slideUp 0.2s ease-in-out forwards;\n }\n\n ng-icon {\n font-size: 1.25rem;\n color: var(--ngp-text-secondary);\n }\n\n [ngpAccordionTrigger][data-open] ng-icon {\n transform: rotate(180deg);\n }\n\n @keyframes slideDown {\n from {\n height: 0;\n }\n to {\n height: var(--ngp-accordion-content-height);\n }\n }\n\n @keyframes slideUp {\n from {\n height: var(--ngp-accordion-content-height);\n }\n to {\n height: 0;\n }\n }\n `,\n})\nexport class AccordionItem {\n /** The accordion item heading */\n readonly heading = input.required<string>();\n}\n",
4
+ "code": "import { Component, input } from '@angular/core';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroChevronDownMini } from '@ng-icons/heroicons/mini';\nimport {\n NgpAccordionContent,\n NgpAccordionItem,\n NgpAccordionTrigger,\n} from 'ng-primitives/accordion';\nimport { NgpButton } from 'ng-primitives/button';\n\n@Component({\n selector: 'app-accordion-item',\n hostDirectives: [\n {\n directive: NgpAccordionItem,\n inputs: ['ngpAccordionItemValue:value', 'ngpAccordionItemDisabled:disabled'],\n },\n ],\n imports: [NgpAccordionContent, NgpAccordionTrigger, NgpButton, NgIcon],\n providers: [provideIcons({ heroChevronDownMini })],\n template: `\n <button ngpAccordionTrigger ngpButton>\n {{ heading() }}\n\n <ng-icon name=\"heroChevronDownMini\" />\n </button>\n <div ngpAccordionContent>\n <ng-content />\n </div>\n `,\n styles: `\n :host {\n display: block;\n }\n\n :host:has(+ :host) {\n border-bottom: 1px solid var(--ngp-border);\n }\n\n [ngpAccordionTrigger] {\n display: flex;\n padding-left: 1rem;\n padding-right: 1rem;\n font-size: 0.875rem;\n line-height: 1.25rem;\n font-weight: 500;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n height: 2.75rem;\n border-radius: 0.75rem;\n outline: none;\n color: var(--ngp-text-primary);\n background-color: var(--ngp-background);\n border: none;\n }\n\n [ngpAccordionTrigger][data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n [ngpAccordionContent] {\n font-size: 0.875rem;\n color: var(--ngp-text-secondary);\n overflow: hidden;\n padding: 0 16px;\n box-sizing: border-box;\n }\n\n [ngpAccordionContent][data-open] {\n animation: slideDown 0.2s ease-in-out forwards;\n }\n\n [ngpAccordionContent][data-closed] {\n animation: slideUp 0.2s ease-in-out forwards;\n }\n\n ng-icon {\n font-size: 1.25rem;\n color: var(--ngp-text-secondary);\n }\n\n [ngpAccordionTrigger][data-open] ng-icon {\n transform: rotate(180deg);\n }\n\n @keyframes slideDown {\n from {\n height: 0;\n }\n to {\n height: var(--ngp-accordion-content-height);\n }\n }\n\n @keyframes slideUp {\n from {\n height: var(--ngp-accordion-content-height);\n }\n to {\n height: 0;\n }\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n [ngpAccordionContent][data-open],\n [ngpAccordionContent][data-closed] {\n animation-duration: 0s;\n }\n }\n `,\n})\nexport class AccordionItem {\n /** The accordion item heading */\n readonly heading = input.required<string>();\n}\n",
5
5
  "primitive": "accordion",
6
6
  "hasVariants": false,
7
7
  "hasSizes": true
@@ -15,7 +15,7 @@
15
15
  },
16
16
  {
17
17
  "name": "button",
18
- "code": "import { Component, input } from '@angular/core';\nimport { NgpButton } from 'ng-primitives/button';\n\n/**\n * The size of the button.\n */\nexport type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/**\n * The variant of the button.\n */\nexport type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link';\n\n@Component({\n selector: 'button[app-button]',\n hostDirectives: [{ directive: NgpButton, inputs: ['disabled'] }],\n template: `\n <ng-content select=\"[slot=leading]\" />\n <ng-content />\n <ng-content select=\"[slot=trailing]\" />\n `,\n host: {\n '[attr.data-size]': 'size()',\n '[attr.data-variant]': 'variant()',\n },\n styles: `\n :host {\n padding-left: 1rem;\n padding-right: 1rem;\n border-radius: 0.5rem;\n color: var(--ngp-text-primary);\n border: none;\n height: 2.5rem;\n font-weight: 500;\n background-color: var(--ngp-background);\n transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: var(--ngp-button-shadow);\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n gap: 0.5rem;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n :host[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n /* Size variants */\n :host[data-size='sm'] {\n height: 2rem;\n padding-left: 0.75rem;\n padding-right: 0.75rem;\n font-size: 0.875rem;\n --ng-icon__size: 0.875rem;\n }\n\n :host[data-size='md'],\n :host:not([data-size]) {\n height: 2.5rem;\n padding-left: 1rem;\n padding-right: 1rem;\n font-size: 0.875rem;\n --ng-icon__size: 0.875rem;\n }\n\n :host[data-size='lg'] {\n height: 3rem;\n padding-left: 1.25rem;\n padding-right: 1.25rem;\n font-size: 1rem;\n --ng-icon__size: 1rem;\n }\n\n :host[data-size='xl'] {\n height: 3.5rem;\n padding-left: 1.5rem;\n padding-right: 1.5rem;\n font-size: 1.125rem;\n --ng-icon__size: 1.125rem;\n }\n\n /* Variant styles */\n :host[data-variant='primary'],\n :host:not([data-variant]) {\n background-color: var(--ngp-background);\n color: var(--ngp-text-primary);\n border: none;\n }\n\n :host[data-variant='primary'][data-hover],\n :host:not([data-variant])[data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n :host[data-variant='primary'][data-press],\n :host:not([data-variant])[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-variant='secondary'] {\n background-color: var(--ngp-secondary-background, #f1f5f9);\n color: var(--ngp-secondary-text, #0f172a);\n border: none;\n }\n\n :host[data-variant='secondary'][data-hover] {\n background-color: var(--ngp-secondary-background-hover, #e2e8f0);\n }\n\n :host[data-variant='secondary'][data-press] {\n background-color: var(--ngp-secondary-background-active, #cbd5e1);\n }\n\n :host[data-variant='destructive'] {\n background-color: var(--ngp-destructive-background, #ef4444);\n color: var(--ngp-destructive-text, #ffffff);\n border: none;\n }\n\n :host[data-variant='destructive'][data-hover] {\n background-color: var(--ngp-destructive-background-hover, #dc2626);\n }\n\n :host[data-variant='destructive'][data-press] {\n background-color: var(--ngp-destructive-background-active, #b91c1c);\n }\n\n :host[data-variant='outline'] {\n background-color: transparent;\n color: var(--ngp-text-primary);\n border: 1px solid var(--ngp-outline-border, #e2e8f0);\n box-shadow: none;\n }\n\n :host[data-variant='outline'][data-hover] {\n background-color: var(--ngp-background-hover);\n border-color: var(--ngp-outline-border-hover, #cbd5e1);\n }\n\n :host[data-variant='outline'][data-press] {\n background-color: var(--ngp-outline-background-active, rgba(15, 23, 42, 0.1));\n }\n\n :host[data-variant='ghost'] {\n background-color: transparent;\n color: var(--ngp-text-primary);\n border: none;\n box-shadow: none;\n }\n\n :host[data-variant='ghost'][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n :host[data-variant='ghost'][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-variant='link'] {\n background-color: transparent;\n color: var(--ngp-link-color, #3b82f6);\n border: none;\n box-shadow: none;\n text-decoration: underline;\n text-underline-offset: 4px;\n }\n\n :host[data-variant='link'][data-hover] {\n text-decoration-thickness: 2px;\n }\n\n :host[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n }\n `,\n})\nexport class Button {\n /**\n * The size of the button.\n */\n readonly size = input<ButtonSize>('md');\n\n /**\n * The variant of the button.\n */\n readonly variant = input<ButtonVariant>('primary');\n}\n",
18
+ "code": "import { Component, input } from '@angular/core';\nimport { NgpButton } from 'ng-primitives/button';\n\n/**\n * The size of the button.\n */\nexport type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/**\n * The variant of the button.\n */\nexport type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link';\n\n@Component({\n selector: 'button[app-button]',\n hostDirectives: [{ directive: NgpButton, inputs: ['disabled'] }],\n template: `\n <ng-content select=\"[slot=leading]\" />\n <ng-content />\n <ng-content select=\"[slot=trailing]\" />\n `,\n host: {\n '[attr.data-size]': 'size()',\n '[attr.data-variant]': 'variant()',\n },\n styles: `\n :host {\n padding-left: 1rem;\n padding-right: 1rem;\n border-radius: 0.5rem;\n color: var(--ngp-text-primary);\n border: none;\n height: 2.5rem;\n font-weight: 500;\n background-color: var(--ngp-background);\n transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: var(--ngp-button-shadow);\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n gap: 0.5rem;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n :host[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n /* Size variants */\n :host[data-size='sm'] {\n height: 2rem;\n padding-left: 0.75rem;\n padding-right: 0.75rem;\n font-size: 0.875rem;\n --ng-icon__size: 0.875rem;\n }\n\n :host[data-size='md'],\n :host:not([data-size]) {\n height: 2.5rem;\n padding-left: 1rem;\n padding-right: 1rem;\n font-size: 0.875rem;\n --ng-icon__size: 0.875rem;\n }\n\n :host[data-size='lg'] {\n height: 3rem;\n padding-left: 1.25rem;\n padding-right: 1.25rem;\n font-size: 1rem;\n --ng-icon__size: 1rem;\n }\n\n :host[data-size='xl'] {\n height: 3.5rem;\n padding-left: 1.5rem;\n padding-right: 1.5rem;\n font-size: 1.125rem;\n --ng-icon__size: 1.125rem;\n }\n\n /* Variant styles */\n :host[data-variant='primary'],\n :host:not([data-variant]) {\n background-color: var(--ngp-background);\n color: var(--ngp-text-primary);\n border: none;\n }\n\n :host[data-variant='primary'][data-hover],\n :host:not([data-variant])[data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n :host[data-variant='primary'][data-press],\n :host:not([data-variant])[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-variant='secondary'] {\n background-color: var(--ngp-secondary-background, #f1f5f9);\n color: var(--ngp-secondary-text, #0f172a);\n border: none;\n }\n\n :host[data-variant='secondary'][data-hover] {\n background-color: var(--ngp-secondary-background-hover, #e2e8f0);\n }\n\n :host[data-variant='secondary'][data-press] {\n background-color: var(--ngp-secondary-background-active, #cbd5e1);\n }\n\n :host[data-variant='destructive'] {\n background-color: var(--ngp-destructive-background, #ef4444);\n color: var(--ngp-destructive-text, #ffffff);\n border: none;\n }\n\n :host[data-variant='destructive'][data-hover] {\n background-color: var(--ngp-destructive-background-hover, #dc2626);\n }\n\n :host[data-variant='destructive'][data-press] {\n background-color: var(--ngp-destructive-background-active, #b91c1c);\n }\n\n :host[data-variant='outline'] {\n background-color: transparent;\n color: var(--ngp-text-primary);\n border: 1px solid var(--ngp-outline-border, #e2e8f0);\n box-shadow: none;\n }\n\n :host[data-variant='outline'][data-hover] {\n background-color: var(--ngp-background-hover);\n border-color: var(--ngp-outline-border-hover, #cbd5e1);\n }\n\n :host[data-variant='outline'][data-press] {\n background-color: var(--ngp-outline-background-active, rgba(15, 23, 42, 0.1));\n }\n\n :host[data-variant='ghost'] {\n background-color: transparent;\n color: var(--ngp-text-primary);\n border: none;\n box-shadow: none;\n }\n\n :host[data-variant='ghost'][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n :host[data-variant='ghost'][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-variant='link'] {\n background-color: transparent;\n color: var(--ngp-link-color, #3b82f6);\n border: none;\n box-shadow: none;\n text-decoration: underline;\n text-underline-offset: 4px;\n }\n\n :host[data-variant='link'][data-hover] {\n text-decoration-thickness: 2px;\n }\n\n :host[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class Button {\n /**\n * The size of the button.\n */\n readonly size = input<ButtonSize>('md');\n\n /**\n * The variant of the button.\n */\n readonly variant = input<ButtonVariant>('primary');\n}\n",
19
19
  "primitive": "button",
20
20
  "hasVariants": true,
21
21
  "hasSizes": true
@@ -29,28 +29,28 @@
29
29
  },
30
30
  {
31
31
  "name": "combobox",
32
- "code": "import { BooleanInput } from '@angular/cdk/coercion';\nimport { booleanAttribute, Component, computed, input, model, signal } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroChevronDown } from '@ng-icons/heroicons/outline';\nimport {\n NgpCombobox,\n NgpComboboxButton,\n NgpComboboxDropdown,\n NgpComboboxInput,\n NgpComboboxOption,\n NgpComboboxPortal,\n} from 'ng-primitives/combobox';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-combobox',\n imports: [\n NgpCombobox,\n NgpComboboxDropdown,\n NgpComboboxOption,\n NgpComboboxInput,\n NgpComboboxPortal,\n NgpComboboxButton,\n NgIcon,\n ],\n providers: [provideIcons({ heroChevronDown }), provideValueAccessor(Combobox)],\n template: `\n <div\n [(ngpComboboxValue)]=\"value\"\n [ngpComboboxDisabled]=\"disabled() || formDisabled()\"\n (ngpComboboxOpenChange)=\"resetOnClose($event)\"\n (ngpComboboxValueChange)=\"onValueChange($event)\"\n ngpCombobox\n >\n <input\n [value]=\"filter()\"\n [placeholder]=\"placeholder()\"\n (input)=\"onFilterChange($event)\"\n (blur)=\"onTouched?.()\"\n ngpComboboxInput\n />\n\n <button ngpComboboxButton aria-label=\"Toggle dropdown\">\n <ng-icon name=\"heroChevronDown\" />\n </button>\n\n <div *ngpComboboxPortal ngpComboboxDropdown>\n @for (option of filteredOptions(); track option) {\n <div [ngpComboboxOptionValue]=\"option\" ngpComboboxOption>\n {{ option }}\n </div>\n } @empty {\n <div class=\"empty-message\">No options found</div>\n }\n </div>\n </div>\n `,\n styles: `\n [ngpCombobox] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n height: 36px;\n width: 300px;\n border-radius: 8px;\n border: none;\n background-color: var(--ngp-background);\n box-shadow: var(--ngp-input-shadow);\n box-sizing: border-box;\n }\n\n [ngpCombobox][data-focus] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n [ngpComboboxInput] {\n flex: 1;\n padding: 0 16px;\n border: none;\n background-color: transparent;\n color: var(--ngp-text);\n font-family: inherit;\n font-size: 14px;\n padding: 0 16px;\n outline: none;\n height: 100%;\n }\n\n [ngpComboboxButton] {\n display: inline-flex;\n justify-content: center;\n align-items: center;\n height: 100%;\n width: 36px;\n background-color: transparent;\n border: none;\n color: var(--ngp-text);\n cursor: pointer;\n box-sizing: border-box;\n }\n\n [ngpComboboxDropdown] {\n background-color: var(--ngp-background);\n border: 1px solid var(--ngp-border);\n padding: 0.25rem;\n border-radius: 0.75rem;\n outline: none;\n position: absolute;\n animation: popover-show 0.1s ease-out;\n width: var(--ngp-combobox-width);\n box-shadow: var(--ngp-shadow-lg);\n box-sizing: border-box;\n margin-top: 4px;\n max-height: 240px;\n overflow-y: auto;\n transform-origin: var(--ngp-combobox-transform-origin);\n }\n\n [ngpComboboxDropdown][data-enter] {\n animation: combobox-show 0.1s ease-out;\n }\n\n [ngpComboboxDropdown][data-exit] {\n animation: combobox-hide 0.1s ease-out;\n }\n\n [ngpComboboxOption] {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.375rem 0.75rem;\n cursor: pointer;\n border-radius: 0.5rem;\n width: 100%;\n height: 36px;\n font-size: 14px;\n color: var(--ngp-text-primary);\n box-sizing: border-box;\n }\n\n [ngpComboboxOption][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n [ngpComboboxOption][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpComboboxOption][data-active] {\n background-color: var(--ngp-background-active);\n }\n\n .empty-message {\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 0.5rem;\n color: var(--ngp-text-secondary);\n font-size: 14px;\n font-weight: 500;\n text-align: center;\n }\n\n @keyframes combobox-show {\n 0% {\n opacity: 0;\n transform: translateY(-10px) scale(0.9);\n }\n 100% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n }\n\n @keyframes combobox-hide {\n 0% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n 100% {\n opacity: 0;\n transform: translateY(-10px) scale(0.9);\n }\n }\n `,\n})\nexport class Combobox implements ControlValueAccessor {\n /** The options for the combobox. */\n readonly options = input<string[]>([]);\n\n /** The selected value. */\n readonly value = model<string | undefined>();\n\n /** The placeholder for the input. */\n readonly placeholder = input<string>('');\n\n /** The disabled state of the combobox. */\n readonly disabled = input<boolean, BooleanInput>(false, {\n transform: booleanAttribute,\n });\n\n /** The filter value. */\n protected readonly filter = signal<string>('');\n\n /** Get the filtered options. */\n protected readonly filteredOptions = computed(() =>\n this.options().filter(option => option.toLowerCase().includes(this.filter().toLowerCase())),\n );\n\n /** Store the form disabled state */\n protected readonly formDisabled = signal(false);\n\n /** The on change callback */\n private onChange?: ChangeFn<string | undefined>;\n\n /** The on touch callback */\n protected onTouched?: TouchedFn;\n\n onFilterChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n this.filter.set(input.value);\n }\n\n writeValue(value: string | undefined): void {\n this.value.set(value);\n this.filter.set(value ?? '');\n }\n\n registerOnChange(fn: ChangeFn<string | undefined>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.formDisabled.set(isDisabled);\n }\n\n protected onValueChange(value: string): void {\n this.onChange?.(value);\n // update the filter value\n this.filter.set(value);\n }\n\n protected resetOnClose(open: boolean): void {\n // if the dropdown is closed, reset the filter value\n if (open) {\n return;\n }\n\n // if the filter value is empty, clear the value and notify the form control\n if (this.filter() === '') {\n this.value.set(undefined);\n this.onChange?.(undefined);\n } else {\n // otherwise set the filter value to the selected value\n this.filter.set(this.value() ?? '');\n }\n }\n}\n",
32
+ "code": "import { BooleanInput } from '@angular/cdk/coercion';\nimport { booleanAttribute, Component, computed, input, model, signal } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroChevronDown } from '@ng-icons/heroicons/outline';\nimport {\n NgpCombobox,\n NgpComboboxButton,\n NgpComboboxDropdown,\n NgpComboboxInput,\n NgpComboboxOption,\n NgpComboboxPortal,\n} from 'ng-primitives/combobox';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-combobox',\n imports: [\n NgpCombobox,\n NgpComboboxDropdown,\n NgpComboboxOption,\n NgpComboboxInput,\n NgpComboboxPortal,\n NgpComboboxButton,\n NgIcon,\n ],\n providers: [provideIcons({ heroChevronDown }), provideValueAccessor(Combobox)],\n template: `\n <div\n [(ngpComboboxValue)]=\"value\"\n [ngpComboboxDisabled]=\"disabled() || formDisabled()\"\n (ngpComboboxOpenChange)=\"resetOnClose($event)\"\n (ngpComboboxValueChange)=\"onValueChange($event)\"\n ngpCombobox\n >\n <input\n [value]=\"filter()\"\n [placeholder]=\"placeholder()\"\n (input)=\"onFilterChange($event)\"\n (blur)=\"onTouched?.()\"\n ngpComboboxInput\n />\n\n <button ngpComboboxButton aria-label=\"Toggle dropdown\">\n <ng-icon name=\"heroChevronDown\" />\n </button>\n\n <div *ngpComboboxPortal ngpComboboxDropdown>\n @for (option of filteredOptions(); track option) {\n <div [ngpComboboxOptionValue]=\"option\" ngpComboboxOption>\n {{ option }}\n </div>\n } @empty {\n <div class=\"empty-message\">No options found</div>\n }\n </div>\n </div>\n `,\n styles: `\n [ngpCombobox] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n height: 36px;\n width: 300px;\n border-radius: 8px;\n border: none;\n background-color: var(--ngp-background);\n box-shadow: var(--ngp-input-shadow);\n box-sizing: border-box;\n }\n\n [ngpCombobox][data-focus] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n [ngpComboboxInput] {\n flex: 1;\n padding: 0 16px;\n border: none;\n background-color: transparent;\n color: var(--ngp-text);\n font-family: inherit;\n font-size: 14px;\n padding: 0 16px;\n outline: none;\n height: 100%;\n }\n\n [ngpComboboxButton] {\n display: inline-flex;\n justify-content: center;\n align-items: center;\n height: 100%;\n width: 36px;\n background-color: transparent;\n border: none;\n color: var(--ngp-text);\n cursor: pointer;\n box-sizing: border-box;\n }\n\n [ngpComboboxDropdown] {\n background-color: var(--ngp-background);\n border: 1px solid var(--ngp-border);\n padding: 0.25rem;\n border-radius: 0.75rem;\n outline: none;\n position: absolute;\n animation: popover-show 0.1s ease-out;\n width: var(--ngp-combobox-width);\n box-shadow: var(--ngp-shadow-lg);\n box-sizing: border-box;\n margin-top: 4px;\n max-height: 240px;\n overflow-y: auto;\n transform-origin: var(--ngp-combobox-transform-origin);\n }\n\n [ngpComboboxDropdown][data-enter] {\n animation: combobox-show 0.1s ease-out;\n }\n\n [ngpComboboxDropdown][data-exit] {\n animation: combobox-hide 0.1s ease-out;\n }\n\n [ngpComboboxOption] {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.375rem 0.75rem;\n cursor: pointer;\n border-radius: 0.5rem;\n width: 100%;\n height: 36px;\n font-size: 14px;\n color: var(--ngp-text-primary);\n box-sizing: border-box;\n }\n\n [ngpComboboxOption][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n [ngpComboboxOption][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpComboboxOption][data-active] {\n background-color: var(--ngp-background-active);\n }\n\n .empty-message {\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 0.5rem;\n color: var(--ngp-text-secondary);\n font-size: 14px;\n font-weight: 500;\n text-align: center;\n }\n\n @keyframes combobox-show {\n 0% {\n opacity: 0;\n transform: translateY(-10px) scale(0.9);\n }\n 100% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n }\n\n @keyframes combobox-hide {\n 0% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n 100% {\n opacity: 0;\n transform: translateY(-10px) scale(0.9);\n }\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n [ngpComboboxDropdown],\n [ngpComboboxDropdown][data-enter],\n [ngpComboboxDropdown][data-exit] {\n animation-duration: 0s;\n }\n }\n `,\n})\nexport class Combobox implements ControlValueAccessor {\n /** The options for the combobox. */\n readonly options = input<string[]>([]);\n\n /** The selected value. */\n readonly value = model<string | undefined>();\n\n /** The placeholder for the input. */\n readonly placeholder = input<string>('');\n\n /** The disabled state of the combobox. */\n readonly disabled = input<boolean, BooleanInput>(false, {\n transform: booleanAttribute,\n });\n\n /** The filter value. */\n protected readonly filter = signal<string>('');\n\n /** Get the filtered options. */\n protected readonly filteredOptions = computed(() =>\n this.options().filter(option => option.toLowerCase().includes(this.filter().toLowerCase())),\n );\n\n /** Store the form disabled state */\n protected readonly formDisabled = signal(false);\n\n /** The on change callback */\n private onChange?: ChangeFn<string | undefined>;\n\n /** The on touch callback */\n protected onTouched?: TouchedFn;\n\n onFilterChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n this.filter.set(input.value);\n }\n\n writeValue(value: string | undefined): void {\n this.value.set(value);\n this.filter.set(value ?? '');\n }\n\n registerOnChange(fn: ChangeFn<string | undefined>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.formDisabled.set(isDisabled);\n }\n\n protected onValueChange(value: string): void {\n this.onChange?.(value);\n // update the filter value\n this.filter.set(value);\n }\n\n protected resetOnClose(open: boolean): void {\n // if the dropdown is closed, reset the filter value\n if (open) {\n return;\n }\n\n // if the filter value is empty, clear the value and notify the form control\n if (this.filter() === '') {\n this.value.set(undefined);\n this.onChange?.(undefined);\n } else {\n // otherwise set the filter value to the selected value\n this.filter.set(this.value() ?? '');\n }\n }\n}\n",
33
33
  "primitive": "combobox",
34
34
  "hasVariants": false,
35
35
  "hasSizes": true
36
36
  },
37
37
  {
38
38
  "name": "context-menu",
39
- "code": "import { Component } from '@angular/core';\nimport { NgpContextMenuItem } from 'ng-primitives/context-menu';\n\n@Component({\n selector: 'button[app-context-menu-item]',\n hostDirectives: [NgpContextMenuItem],\n host: { type: 'button' },\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 8px 6px 14px;\n border: none;\n background: none;\n cursor: pointer;\n transition: background-color 0.2s;\n border-radius: 4px;\n min-width: 120px;\n text-align: start;\n outline: none;\n font-size: 14px;\n font-weight: 400;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n z-index: 1;\n }\n `,\n})\nexport class ContextMenuItem {}\n",
39
+ "code": "import { Component } from '@angular/core';\nimport { NgpContextMenuItem } from 'ng-primitives/context-menu';\n\n@Component({\n selector: 'button[app-context-menu-item]',\n hostDirectives: [NgpContextMenuItem],\n host: { type: 'button' },\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 8px 6px 14px;\n border: none;\n background: none;\n cursor: pointer;\n transition: background-color 0.2s;\n border-radius: 4px;\n min-width: 120px;\n text-align: start;\n outline: none;\n font-size: 14px;\n font-weight: 400;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n z-index: 1;\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class ContextMenuItem {}\n",
40
40
  "primitive": "context-menu",
41
41
  "hasVariants": false,
42
42
  "hasSizes": true
43
43
  },
44
44
  {
45
45
  "name": "date-picker",
46
- "code": "import { Component, computed } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroChevronLeftMini, heroChevronRightMini } from '@ng-icons/heroicons/mini';\nimport {\n injectDatePickerState,\n NgpDatePicker,\n NgpDatePickerCell,\n NgpDatePickerCellRender,\n NgpDatePickerDateButton,\n NgpDatePickerGrid,\n NgpDatePickerLabel,\n NgpDatePickerNextMonth,\n NgpDatePickerPreviousMonth,\n NgpDatePickerRowRender,\n} from 'ng-primitives/date-picker';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-date-picker',\n hostDirectives: [\n {\n directive: NgpDatePicker,\n inputs: [\n 'ngpDatePickerDate: date',\n 'ngpDatePickerMin: min',\n 'ngpDatePickerMax: max',\n 'ngpDatePickerDisabled: disabled',\n ],\n outputs: ['ngpDatePickerDateChange: dateChange'],\n },\n ],\n imports: [\n NgIcon,\n NgpDatePickerLabel,\n NgpDatePickerNextMonth,\n NgpDatePickerPreviousMonth,\n NgpDatePickerGrid,\n NgpDatePickerCell,\n NgpDatePickerRowRender,\n NgpDatePickerCellRender,\n NgpDatePickerDateButton,\n ],\n providers: [\n provideIcons({ heroChevronRightMini, heroChevronLeftMini }),\n provideValueAccessor(DatePicker),\n ],\n template: `\n <div class=\"date-picker-header\">\n <button ngpDatePickerPreviousMonth aria-label=\"previous month\">\n <ng-icon name=\"heroChevronLeftMini\" />\n </button>\n <h2 ngpDatePickerLabel>{{ label() }}</h2>\n <button ngpDatePickerNextMonth aria-label=\"next month\">\n <ng-icon name=\"heroChevronRightMini\" />\n </button>\n </div>\n <table ngpDatePickerGrid>\n <thead>\n <tr>\n <th scope=\"col\" abbr=\"Sunday\">S</th>\n <th scope=\"col\" abbr=\"Monday\">M</th>\n <th scope=\"col\" abbr=\"Tuesday\">T</th>\n <th scope=\"col\" abbr=\"Wednesday\">W</th>\n <th scope=\"col\" abbr=\"Thursday\">T</th>\n <th scope=\"col\" abbr=\"Friday\">F</th>\n <th scope=\"col\" abbr=\"Saturday\">S</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngpDatePickerRowRender>\n <td *ngpDatePickerCellRender=\"let date\" ngpDatePickerCell>\n <button ngpDatePickerDateButton>\n {{ date.getDate() }}\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n `,\n styles: `\n :host {\n display: inline-block;\n background-color: var(--ngp-background);\n border-radius: 12px;\n padding: 16px;\n box-shadow: var(--ngp-shadow);\n border: 1px solid var(--ngp-border);\n }\n\n .date-picker-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n height: 36px;\n margin-bottom: 16px;\n }\n\n th {\n font-size: 14px;\n font-weight: 500;\n width: 40px;\n height: 40px;\n text-align: center;\n color: var(--ngp-text-secondary);\n }\n\n [ngpDatePickerLabel] {\n font-size: 14px;\n font-weight: 500;\n color: var(--ngp-text-primary);\n }\n\n [ngpDatePickerPreviousMonth],\n [ngpDatePickerNextMonth] {\n all: unset;\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n font-size: 20px;\n border: 1px solid var(--ngp-border);\n cursor: pointer;\n }\n\n [ngpDatePickerPreviousMonth][data-hover],\n [ngpDatePickerNextMonth][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n [ngpDatePickerPreviousMonth][data-focus-visible],\n [ngpDatePickerNextMonth][data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n [ngpDatePickerPreviousMonth][data-press],\n [ngpDatePickerNextMonth][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpDatePickerPreviousMonth][data-disabled],\n [ngpDatePickerNextMonth][data-disabled] {\n cursor: not-allowed;\n color: var(--ngp-text-disabled);\n }\n\n [ngpDatePickerDateButton] {\n all: unset;\n width: 40px;\n height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n cursor: pointer;\n }\n\n [ngpDatePickerDateButton][data-today] {\n color: var(--ngp-text-blue);\n }\n\n [ngpDatePickerDateButton][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n [ngpDatePickerDateButton][data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n [ngpDatePickerDateButton][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpDatePickerDateButton][data-outside-month] {\n color: var(--ngp-text-disabled);\n }\n\n [ngpDatePickerDateButton][data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n }\n\n [ngpDatePickerDateButton][data-selected][data-outside-month] {\n background-color: var(--ngp-background-disabled);\n color: var(--ngp-text-disabled);\n }\n\n [ngpDatePickerDateButton][data-disabled] {\n cursor: not-allowed;\n color: var(--ngp-text-disabled);\n }\n `,\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class DatePicker implements ControlValueAccessor {\n /** Access the date picker host directive */\n private readonly state = injectDatePickerState<Date>();\n\n /**\n * Get the current focused date in string format.\n * @returns The focused date in \"February 2024\" format.\n */\n readonly label = computed(\n () =>\n `${this.state().focusedDate().toLocaleString('default', { month: 'long' })} ${this.state().focusedDate().getFullYear()}`,\n );\n\n /**\n * The onChange callback function for the date picker.\n */\n protected onChange?: ChangeFn<Date | undefined>;\n\n /**\n * The onTouched callback function for the date picker.\n */\n protected onTouched?: TouchedFn;\n\n constructor() {\n // Whenever the user interacts with the date picker, call the onChange function with the new value.\n this.state().dateChange.subscribe(date => this.onChange?.(date));\n }\n\n writeValue(date: Date): void {\n // writing a value from the model must not re-emit through onChange\n this.state().select(date, false, { emit: false });\n }\n\n registerOnChange(fn: ChangeFn<Date | undefined>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.state().disabled.set(isDisabled);\n }\n}\n",
46
+ "code": "import { Component, computed } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroChevronLeftMini, heroChevronRightMini } from '@ng-icons/heroicons/mini';\nimport {\n injectDatePickerState,\n NgpDatePicker,\n NgpDatePickerCell,\n NgpDatePickerCellRender,\n NgpDatePickerDateButton,\n NgpDatePickerGrid,\n NgpDatePickerLabel,\n NgpDatePickerNextMonth,\n NgpDatePickerPreviousMonth,\n NgpDatePickerRowRender,\n} from 'ng-primitives/date-picker';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-date-picker',\n hostDirectives: [\n {\n directive: NgpDatePicker,\n inputs: [\n 'ngpDatePickerDate: date',\n 'ngpDatePickerMin: min',\n 'ngpDatePickerMax: max',\n 'ngpDatePickerDisabled: disabled',\n ],\n outputs: ['ngpDatePickerDateChange: dateChange'],\n },\n ],\n imports: [\n NgIcon,\n NgpDatePickerLabel,\n NgpDatePickerNextMonth,\n NgpDatePickerPreviousMonth,\n NgpDatePickerGrid,\n NgpDatePickerCell,\n NgpDatePickerRowRender,\n NgpDatePickerCellRender,\n NgpDatePickerDateButton,\n ],\n providers: [\n provideIcons({ heroChevronRightMini, heroChevronLeftMini }),\n provideValueAccessor(DatePicker),\n ],\n template: `\n <div class=\"date-picker-header\">\n <button ngpDatePickerPreviousMonth aria-label=\"previous month\">\n <ng-icon name=\"heroChevronLeftMini\" />\n </button>\n <h2 ngpDatePickerLabel>{{ label() }}</h2>\n <button ngpDatePickerNextMonth aria-label=\"next month\">\n <ng-icon name=\"heroChevronRightMini\" />\n </button>\n </div>\n <table ngpDatePickerGrid>\n <thead>\n <tr>\n <th scope=\"col\" abbr=\"Sunday\">S</th>\n <th scope=\"col\" abbr=\"Monday\">M</th>\n <th scope=\"col\" abbr=\"Tuesday\">T</th>\n <th scope=\"col\" abbr=\"Wednesday\">W</th>\n <th scope=\"col\" abbr=\"Thursday\">T</th>\n <th scope=\"col\" abbr=\"Friday\">F</th>\n <th scope=\"col\" abbr=\"Saturday\">S</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngpDatePickerRowRender>\n <td *ngpDatePickerCellRender=\"let date\" ngpDatePickerCell>\n <button ngpDatePickerDateButton>\n {{ date.getDate() }}\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n `,\n styles: `\n :host {\n display: inline-block;\n background-color: var(--ngp-background);\n border-radius: 12px;\n padding: 16px;\n box-shadow: var(--ngp-shadow);\n border: 1px solid var(--ngp-border);\n }\n\n .date-picker-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n height: 36px;\n margin-bottom: 16px;\n }\n\n th {\n font-size: 14px;\n font-weight: 500;\n width: 40px;\n height: 40px;\n text-align: center;\n color: var(--ngp-text-secondary);\n }\n\n [ngpDatePickerLabel] {\n font-size: 14px;\n font-weight: 500;\n color: var(--ngp-text-primary);\n }\n\n [ngpDatePickerPreviousMonth],\n [ngpDatePickerNextMonth] {\n all: unset;\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n font-size: 20px;\n border: 1px solid var(--ngp-border);\n cursor: pointer;\n }\n\n [ngpDatePickerPreviousMonth][data-hover],\n [ngpDatePickerNextMonth][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n [ngpDatePickerPreviousMonth][data-focus-visible],\n [ngpDatePickerNextMonth][data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n [ngpDatePickerPreviousMonth][data-press],\n [ngpDatePickerNextMonth][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpDatePickerPreviousMonth][data-disabled],\n [ngpDatePickerNextMonth][data-disabled] {\n cursor: not-allowed;\n color: var(--ngp-text-disabled);\n }\n\n [ngpDatePickerDateButton] {\n all: unset;\n width: 40px;\n height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n cursor: pointer;\n }\n\n [ngpDatePickerDateButton][data-today] {\n color: var(--ngp-text-blue);\n }\n\n [ngpDatePickerDateButton][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n [ngpDatePickerDateButton][data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n [ngpDatePickerDateButton][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpDatePickerDateButton][data-outside-month] {\n color: var(--ngp-text-disabled);\n }\n\n [ngpDatePickerDateButton][data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n }\n\n [ngpDatePickerDateButton][data-selected][data-outside-month] {\n background-color: var(--ngp-background-disabled);\n color: var(--ngp-text-disabled);\n }\n\n [ngpDatePickerDateButton][data-disabled] {\n cursor: not-allowed;\n color: var(--ngp-text-disabled);\n }\n `,\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class DatePicker implements ControlValueAccessor {\n /** Access the date picker host directive */\n private readonly state = injectDatePickerState<Date>();\n\n /**\n * Get the current focused date in string format.\n * @returns The focused date in \"February 2024\" format.\n */\n readonly label = computed(\n () =>\n `${this.state().focusedDate().toLocaleString('default', { month: 'long' })} ${this.state().focusedDate().getFullYear()}`,\n );\n\n /**\n * The onChange callback function for the date picker.\n */\n protected onChange?: ChangeFn<Date | undefined>;\n\n /**\n * The onTouched callback function for the date picker.\n */\n protected onTouched?: TouchedFn;\n\n constructor() {\n // Whenever the user interacts with the date picker, call the onChange function with the new value.\n this.state().dateChange.subscribe(date => this.onChange?.(date));\n }\n\n writeValue(date: Date): void {\n // writing a value from the model must not re-emit through onChange\n this.state().select(date, false, { emit: false });\n }\n\n registerOnChange(fn: ChangeFn<Date | undefined>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.state().setDisabled(isDisabled);\n }\n}\n",
47
47
  "primitive": "date-picker",
48
48
  "hasVariants": false,
49
49
  "hasSizes": true
50
50
  },
51
51
  {
52
52
  "name": "dialog",
53
- "code": "import { Component, input } from '@angular/core';\nimport {\n NgpDialog,\n NgpDialogDescription,\n NgpDialogOverlay,\n NgpDialogTitle,\n provideDialogState,\n} from 'ng-primitives/dialog';\n\n@Component({\n selector: 'app-dialog',\n hostDirectives: [NgpDialogOverlay],\n imports: [NgpDialog, NgpDialogTitle, NgpDialogDescription],\n providers: [\n // We need to hoist the dialog state to the host component so that it can be used\n // within ng-content\n provideDialogState(),\n ],\n template: `\n <div ngpDialog>\n <h2 ngpDialogTitle>{{ header() }}</h2>\n <p ngpDialogDescription>\n <ng-content />\n </p>\n </div>\n `,\n styles: `\n :host {\n background-color: rgba(0, 0, 0, 0.5);\n backdrop-filter: blur(4px);\n position: fixed;\n inset: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n animation: fadeIn 300ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n :host[data-exit] {\n animation: fadeOut 300ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n [ngpDialog] {\n background-color: var(--ngp-background);\n padding: 24px;\n border-radius: 8px;\n box-shadow: var(--ngp-shadow);\n animation: slideIn 300ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n [ngpDialog][data-exit] {\n animation: slideOut 300ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n [ngpDialogTitle] {\n font-size: 18px;\n line-height: 28px;\n font-weight: 600;\n color: var(--ngp-text-primary);\n margin: 0 0 4px;\n }\n\n [ngpDialogDescription] {\n font-size: 14px;\n line-height: 20px;\n color: var(--ngp-text-secondary);\n margin: 0;\n }\n\n @keyframes fadeIn {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n }\n\n @keyframes fadeOut {\n 0% {\n opacity: 1;\n }\n 100% {\n opacity: 0;\n }\n }\n\n @keyframes slideIn {\n 0% {\n transform: translateY(-20px);\n opacity: 0;\n }\n 100% {\n transform: translateY(0);\n opacity: 1;\n }\n }\n @keyframes slideOut {\n 0% {\n transform: translateY(0);\n opacity: 1;\n }\n\n 100% {\n transform: translateY(-20px);\n opacity: 0;\n }\n }\n `,\n})\nexport class Dialog {\n /** The dialog title */\n readonly header = input.required<string>();\n}\n",
53
+ "code": "import { Component, input } from '@angular/core';\nimport {\n NgpDialog,\n NgpDialogDescription,\n NgpDialogOverlay,\n NgpDialogTitle,\n provideDialogState,\n} from 'ng-primitives/dialog';\n\n@Component({\n selector: 'app-dialog',\n hostDirectives: [NgpDialogOverlay],\n imports: [NgpDialog, NgpDialogTitle, NgpDialogDescription],\n providers: [\n // We need to hoist the dialog state to the host component so that it can be used\n // within ng-content\n provideDialogState(),\n ],\n template: `\n <div ngpDialog>\n <h2 ngpDialogTitle>{{ header() }}</h2>\n <p ngpDialogDescription>\n <ng-content />\n </p>\n </div>\n `,\n styles: `\n :host {\n background-color: rgba(0, 0, 0, 0.5);\n backdrop-filter: blur(4px);\n position: fixed;\n inset: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n animation: fadeIn 300ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n :host[data-exit] {\n animation: fadeOut 300ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n [ngpDialog] {\n background-color: var(--ngp-background);\n padding: 24px;\n border-radius: 8px;\n box-shadow: var(--ngp-shadow);\n animation: slideIn 300ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n [ngpDialog][data-exit] {\n animation: slideOut 300ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n [ngpDialogTitle] {\n font-size: 18px;\n line-height: 28px;\n font-weight: 600;\n color: var(--ngp-text-primary);\n margin: 0 0 4px;\n }\n\n [ngpDialogDescription] {\n font-size: 14px;\n line-height: 20px;\n color: var(--ngp-text-secondary);\n margin: 0;\n }\n\n @keyframes fadeIn {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n }\n\n @keyframes fadeOut {\n 0% {\n opacity: 1;\n }\n 100% {\n opacity: 0;\n }\n }\n\n @keyframes slideIn {\n 0% {\n transform: translateY(-20px);\n opacity: 0;\n }\n 100% {\n transform: translateY(0);\n opacity: 1;\n }\n }\n @keyframes slideOut {\n 0% {\n transform: translateY(0);\n opacity: 1;\n }\n\n 100% {\n transform: translateY(-20px);\n opacity: 0;\n }\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n :host,\n :host[data-exit],\n [ngpDialog],\n [ngpDialog][data-exit] {\n animation-duration: 0s;\n }\n }\n `,\n})\nexport class Dialog {\n /** The dialog title */\n readonly header = input.required<string>();\n}\n",
54
54
  "primitive": "dialog",
55
55
  "hasVariants": false,
56
56
  "hasSizes": true
@@ -64,7 +64,7 @@
64
64
  },
65
65
  {
66
66
  "name": "form-field",
67
- "code": "import { Component } from '@angular/core';\nimport { NgpFormField } from 'ng-primitives/form-field';\n\n@Component({\n selector: 'app-form-field',\n hostDirectives: [NgpFormField],\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 90%;\n }\n `,\n})\nexport class FormField {}\n",
67
+ "code": "import { Component } from '@angular/core';\nimport { NgpFormField } from 'ng-primitives/form-field';\n\n@Component({\n selector: 'app-field',\n hostDirectives: [NgpFormField],\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 90%;\n }\n `,\n})\nexport class Field {}\n",
68
68
  "primitive": "form-field",
69
69
  "hasVariants": false,
70
70
  "hasSizes": false
@@ -78,7 +78,7 @@
78
78
  },
79
79
  {
80
80
  "name": "input-otp",
81
- "code": "import { BooleanInput, NumberInput } from '@angular/cdk/coercion';\nimport {\n booleanAttribute,\n Component,\n computed,\n forwardRef,\n input,\n model,\n numberAttribute,\n signal,\n} from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { NgpInputOtp, NgpInputOtpInput, NgpInputOtpSlot } from 'ng-primitives/input-otp';\nimport { ChangeFn, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-input-otp',\n imports: [NgpInputOtp, NgpInputOtpInput, NgpInputOtpSlot],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => InputOtp),\n multi: true,\n },\n ],\n template: `\n <div\n [ngpInputOtpValue]=\"value()\"\n [ngpInputOtpDisabled]=\"disabled() || formDisabled()\"\n [ngpInputOtpPattern]=\"pattern()\"\n [ngpInputOtpPlaceholder]=\"placeholder()\"\n [ngpInputOtpInputMode]=\"inputMode()\"\n (ngpInputOtpValueChange)=\"onValueChange($event)\"\n (ngpInputOtpComplete)=\"onComplete()\"\n ngpInputOtp\n >\n <input [attr.aria-label]=\"ariaLabel()\" ngpInputOtpInput />\n <div class=\"slots\">\n @for (_ of slots(); track $index) {\n <div class=\"slot\" ngpInputOtpSlot></div>\n }\n </div>\n </div>\n `,\n styles: `\n :host {\n display: inline-flex;\n flex-direction: column;\n gap: 12px;\n max-width: 100%;\n }\n\n .slots {\n display: flex;\n gap: 8px;\n }\n\n .slot {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n border: 2px solid var(--ngp-border);\n border-radius: 8px;\n background: var(--ngp-background);\n font-size: 18px;\n font-weight: 600;\n color: var(--ngp-text-primary);\n transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);\n cursor: pointer;\n position: relative;\n }\n\n .slot[data-filled] {\n border-color: var(--ngp-border-strong);\n background: var(--ngp-background-accent);\n }\n\n .slot[data-active] {\n border-color: var(--ngp-focus-ring);\n box-shadow: 0 0 0 1px var(--ngp-focus-ring);\n }\n\n .slot[data-placeholder] {\n color: var(--ngp-text-placeholder);\n }\n\n .slot[data-caret]::after {\n content: '';\n position: absolute;\n width: 2px;\n height: 20px;\n background: var(--ngp-focus-ring);\n animation: blink 1s infinite;\n }\n\n @keyframes blink {\n 0%,\n 50% {\n opacity: 1;\n }\n 51%,\n 100% {\n opacity: 0;\n }\n }\n\n :host([data-disabled]) .slot {\n background: var(--ngp-background-disabled);\n color: var(--ngp-text-disabled);\n border-color: var(--ngp-border-disabled);\n cursor: not-allowed;\n }\n `,\n})\nexport class InputOtp implements ControlValueAccessor {\n /**\n * The number of slots to display.\n */\n readonly length = input<number, NumberInput>(6, {\n transform: numberAttribute,\n });\n\n /**\n * Whether the input is disabled.\n */\n readonly disabled = input<boolean, BooleanInput>(false, {\n transform: booleanAttribute,\n });\n\n /**\n * The pattern for allowed characters.\n */\n readonly pattern = input('[0-9]');\n\n /**\n * The placeholder character for empty slots.\n */\n readonly placeholder = input('');\n\n /**\n * The accessible label for the hidden input.\n */\n readonly ariaLabel = input('');\n\n /**\n * The input mode for the hidden input.\n */\n readonly inputMode = input<'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url'>(\n 'numeric',\n );\n\n /**\n * Create an array for tracking slots.\n */\n protected readonly slots = computed(() => Array.from({ length: this.length() }, (_, i) => i));\n\n /**\n * The current value.\n */\n readonly value = model<string>('');\n\n private onChange: ChangeFn<string> = () => {};\n private onTouched: TouchedFn = () => {};\n\n protected readonly formDisabled = signal(false);\n\n /**\n * Handle value changes from the input-otp directive.\n */\n onValueChange(value: string): void {\n this.value.set(value);\n this.onChange(value);\n }\n\n /**\n * Handle completion events from the input-otp directive.\n */\n onComplete(): void {\n this.onTouched();\n }\n\n // ControlValueAccessor implementation\n writeValue(value: string): void {\n this.value.set(value);\n }\n\n registerOnChange(fn: ChangeFn<string>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.formDisabled.set(isDisabled);\n }\n}\n",
81
+ "code": "import { BooleanInput, NumberInput } from '@angular/cdk/coercion';\nimport {\n booleanAttribute,\n Component,\n computed,\n forwardRef,\n input,\n model,\n numberAttribute,\n signal,\n} from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { NgpInputOtp, NgpInputOtpInput, NgpInputOtpSlot } from 'ng-primitives/input-otp';\nimport { ChangeFn, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-input-otp',\n imports: [NgpInputOtp, NgpInputOtpInput, NgpInputOtpSlot],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => InputOtp),\n multi: true,\n },\n ],\n template: `\n <div\n [ngpInputOtpValue]=\"value()\"\n [ngpInputOtpDisabled]=\"disabled() || formDisabled()\"\n [ngpInputOtpPattern]=\"pattern()\"\n [ngpInputOtpPlaceholder]=\"placeholder()\"\n [ngpInputOtpInputMode]=\"inputMode()\"\n (ngpInputOtpValueChange)=\"onValueChange($event)\"\n (ngpInputOtpComplete)=\"onComplete()\"\n ngpInputOtp\n >\n <input [attr.aria-label]=\"ariaLabel()\" ngpInputOtpInput />\n <div class=\"slots\">\n @for (_ of slots(); track $index) {\n <div class=\"slot\" ngpInputOtpSlot></div>\n }\n </div>\n </div>\n `,\n styles: `\n :host {\n display: inline-flex;\n flex-direction: column;\n gap: 12px;\n max-width: 100%;\n }\n\n .slots {\n display: flex;\n gap: 8px;\n }\n\n .slot {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n border: 2px solid var(--ngp-border);\n border-radius: 8px;\n background: var(--ngp-background);\n font-size: 18px;\n font-weight: 600;\n color: var(--ngp-text-primary);\n transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);\n cursor: pointer;\n position: relative;\n }\n\n .slot[data-filled] {\n border-color: var(--ngp-border-strong);\n background: var(--ngp-background-accent);\n }\n\n .slot[data-active] {\n border-color: var(--ngp-focus-ring);\n box-shadow: 0 0 0 1px var(--ngp-focus-ring);\n }\n\n .slot[data-placeholder] {\n color: var(--ngp-text-placeholder);\n }\n\n .slot[data-caret]::after {\n content: '';\n position: absolute;\n width: 2px;\n height: 20px;\n background: var(--ngp-focus-ring);\n animation: blink 1s infinite;\n }\n\n @keyframes blink {\n 0%,\n 50% {\n opacity: 1;\n }\n 51%,\n 100% {\n opacity: 0;\n }\n }\n\n :host([data-disabled]) .slot {\n background: var(--ngp-background-disabled);\n color: var(--ngp-text-disabled);\n border-color: var(--ngp-border-disabled);\n cursor: not-allowed;\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n .slot[data-caret]::after {\n animation: none;\n }\n\n .slot {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class InputOtp implements ControlValueAccessor {\n /**\n * The number of slots to display.\n */\n readonly length = input<number, NumberInput>(6, {\n transform: numberAttribute,\n });\n\n /**\n * Whether the input is disabled.\n */\n readonly disabled = input<boolean, BooleanInput>(false, {\n transform: booleanAttribute,\n });\n\n /**\n * The pattern for allowed characters.\n */\n readonly pattern = input('[0-9]');\n\n /**\n * The placeholder character for empty slots.\n */\n readonly placeholder = input('');\n\n /**\n * The accessible label for the hidden input.\n */\n readonly ariaLabel = input('');\n\n /**\n * The input mode for the hidden input.\n */\n readonly inputMode = input<'numeric' | 'text' | 'decimal' | 'tel' | 'search' | 'email' | 'url'>(\n 'numeric',\n );\n\n /**\n * Create an array for tracking slots.\n */\n protected readonly slots = computed(() => Array.from({ length: this.length() }, (_, i) => i));\n\n /**\n * The current value.\n */\n readonly value = model<string>('');\n\n private onChange: ChangeFn<string> = () => {};\n private onTouched: TouchedFn = () => {};\n\n protected readonly formDisabled = signal(false);\n\n /**\n * Handle value changes from the input-otp directive.\n */\n onValueChange(value: string): void {\n this.value.set(value);\n this.onChange(value);\n }\n\n /**\n * Handle completion events from the input-otp directive.\n */\n onComplete(): void {\n this.onTouched();\n }\n\n // ControlValueAccessor implementation\n writeValue(value: string): void {\n this.value.set(value);\n }\n\n registerOnChange(fn: ChangeFn<string>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.formDisabled.set(isDisabled);\n }\n}\n",
82
82
  "primitive": "input-otp",
83
83
  "hasVariants": false,
84
84
  "hasSizes": true
@@ -92,14 +92,14 @@
92
92
  },
93
93
  {
94
94
  "name": "menu",
95
- "code": "import { Component } from '@angular/core';\nimport { NgpMenuItem } from 'ng-primitives/menu';\n\n@Component({\n selector: 'button[app-menu-item]',\n hostDirectives: [NgpMenuItem],\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 8px 6px 14px;\n border: none;\n background: none;\n cursor: pointer;\n transition: background-color 0.2s;\n border-radius: 4px;\n min-width: 120px;\n text-align: start;\n outline: none;\n font-size: 14px;\n font-weight: 400;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n z-index: 1;\n }\n `,\n})\nexport class MenuItem {}\n",
95
+ "code": "import { Component } from '@angular/core';\nimport { NgpMenuItem } from 'ng-primitives/menu';\n\n@Component({\n selector: 'button[app-menu-item]',\n hostDirectives: [NgpMenuItem],\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 8px 6px 14px;\n border: none;\n background: none;\n cursor: pointer;\n transition: background-color 0.2s;\n border-radius: 4px;\n min-width: 120px;\n text-align: start;\n outline: none;\n font-size: 14px;\n font-weight: 400;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n z-index: 1;\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class MenuItem {}\n",
96
96
  "primitive": "menu",
97
97
  "hasVariants": false,
98
98
  "hasSizes": true
99
99
  },
100
100
  {
101
101
  "name": "meter",
102
- "code": "import { NumberInput } from '@angular/cdk/coercion';\nimport { Component, input, numberAttribute } from '@angular/core';\nimport {\n NgpMeter,\n NgpMeterIndicator,\n NgpMeterLabel,\n NgpMeterTrack,\n NgpMeterValue,\n} from 'ng-primitives/meter';\n\n@Component({\n selector: 'app-meter',\n hostDirectives: [\n { directive: NgpMeter, inputs: ['ngpMeterValue:value', 'ngpMeterMin:min', 'ngpMeterMax:max'] },\n ],\n imports: [NgpMeterIndicator, NgpMeterLabel, NgpMeterValue, NgpMeterTrack],\n template: `\n <span ngpMeterLabel>{{ label() }}</span>\n <span ngpMeterValue>{{ value() }}%</span>\n\n <div ngpMeterTrack>\n <div ngpMeterIndicator></div>\n </div>\n `,\n styles: `\n :host {\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-row-gap: 0.5rem;\n width: 200px;\n box-sizing: border-box;\n padding: 0.5rem;\n }\n\n [ngpMeterLabel] {\n color: var(--ngp-text-emphasis);\n font-size: 14px;\n font-weight: 600;\n }\n\n [ngpMeterValue] {\n color: var(--ngp-text-secondary);\n font-size: 14px;\n font-weight: 500;\n text-align: right;\n grid-column-start: 2;\n text-align: end;\n }\n\n [ngpMeterTrack] {\n grid-column: 1 / 3;\n overflow: hidden;\n background-color: var(--ngp-background);\n box-shadow: var(--ngp-shadow-border);\n border-radius: 4px;\n height: 8px;\n }\n\n [ngpMeterIndicator] {\n background-color: var(--ngp-background-success);\n height: 100%;\n transition: width 0.2s ease-in-out;\n inset-inline-start: 0px;\n border-radius: 4px;\n }\n `,\n})\nexport class Meter {\n /** The value of the meter. */\n readonly value = input<number, NumberInput>(0, {\n transform: numberAttribute,\n });\n\n /** The label of the meter. */\n readonly label = input.required<string>();\n}\n",
102
+ "code": "import { NumberInput } from '@angular/cdk/coercion';\nimport { Component, input, numberAttribute } from '@angular/core';\nimport {\n NgpMeter,\n NgpMeterIndicator,\n NgpMeterLabel,\n NgpMeterTrack,\n NgpMeterValue,\n} from 'ng-primitives/meter';\n\n@Component({\n selector: 'app-meter',\n hostDirectives: [\n { directive: NgpMeter, inputs: ['ngpMeterValue:value', 'ngpMeterMin:min', 'ngpMeterMax:max'] },\n ],\n imports: [NgpMeterIndicator, NgpMeterLabel, NgpMeterValue, NgpMeterTrack],\n template: `\n <span ngpMeterLabel>{{ label() }}</span>\n <span ngpMeterValue>{{ value() }}%</span>\n\n <div ngpMeterTrack>\n <div ngpMeterIndicator></div>\n </div>\n `,\n styles: `\n :host {\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-row-gap: 0.5rem;\n width: 200px;\n box-sizing: border-box;\n padding: 0.5rem;\n }\n\n [ngpMeterLabel] {\n color: var(--ngp-text-emphasis);\n font-size: 14px;\n font-weight: 600;\n }\n\n [ngpMeterValue] {\n color: var(--ngp-text-secondary);\n font-size: 14px;\n font-weight: 500;\n text-align: right;\n grid-column-start: 2;\n text-align: end;\n }\n\n [ngpMeterTrack] {\n grid-column: 1 / 3;\n overflow: hidden;\n background-color: var(--ngp-background);\n box-shadow: var(--ngp-shadow-border);\n border-radius: 4px;\n height: 8px;\n }\n\n [ngpMeterIndicator] {\n background-color: var(--ngp-background-success);\n height: 100%;\n transition: width 0.2s ease-in-out;\n inset-inline-start: 0px;\n border-radius: 4px;\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n [ngpMeterIndicator] {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class Meter {\n /** The value of the meter. */\n readonly value = input<number, NumberInput>(0, {\n transform: numberAttribute,\n });\n\n /** The label of the meter. */\n readonly label = input.required<string>();\n}\n",
103
103
  "primitive": "meter",
104
104
  "hasVariants": false,
105
105
  "hasSizes": true
@@ -113,21 +113,21 @@
113
113
  },
114
114
  {
115
115
  "name": "number-field",
116
- "code": "import { Component } from '@angular/core';\nimport {\n NgpNumberField,\n NgpNumberFieldDecrement,\n NgpNumberFieldIncrement,\n NgpNumberFieldInput,\n} from 'ng-primitives/number-field';\n\n@Component({\n selector: 'app-number-field',\n hostDirectives: [\n {\n directive: NgpNumberField,\n inputs: [\n 'ngpNumberFieldValue:value',\n 'ngpNumberFieldMin:min',\n 'ngpNumberFieldMax:max',\n 'ngpNumberFieldStep:step',\n 'ngpNumberFieldLargeStep:largeStep',\n 'ngpNumberFieldDisabled:disabled',\n 'ngpNumberFieldReadonly:readonly',\n ],\n outputs: ['ngpNumberFieldValueChange:valueChange'],\n },\n ],\n imports: [NgpNumberFieldInput, NgpNumberFieldIncrement, NgpNumberFieldDecrement],\n template: `\n <button ngpNumberFieldDecrement aria-label=\"Decrement\">−</button>\n <input ngpNumberFieldInput />\n <button ngpNumberFieldIncrement aria-label=\"Increment\">+</button>\n `,\n styles: `\n :host {\n display: inline-flex;\n align-items: center;\n border-radius: 8px;\n box-shadow: var(--ngp-input-shadow);\n background-color: var(--ngp-background);\n overflow: hidden;\n }\n\n :host:focus-within {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n [ngpNumberFieldInput] {\n width: 64px;\n height: 36px;\n border: none;\n outline: none;\n text-align: center;\n font-size: 0.875rem;\n color: var(--ngp-text-primary);\n background: transparent;\n box-sizing: border-box;\n padding: 0;\n }\n\n [ngpNumberFieldInput]::placeholder {\n color: var(--ngp-text-placeholder);\n }\n\n [ngpNumberFieldIncrement],\n [ngpNumberFieldDecrement] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border: none;\n background: transparent;\n color: var(--ngp-text-secondary);\n cursor: pointer;\n font-size: 1rem;\n user-select: none;\n -webkit-touch-callout: none;\n transition:\n color 150ms ease,\n background-color 150ms ease;\n }\n\n [ngpNumberFieldIncrement][data-hover],\n [ngpNumberFieldDecrement][data-hover] {\n background-color: var(--ngp-background-hover);\n color: var(--ngp-text-primary);\n }\n\n [ngpNumberFieldIncrement][data-press],\n [ngpNumberFieldDecrement][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpNumberFieldIncrement][data-disabled],\n [ngpNumberFieldDecrement][data-disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n [ngpNumberFieldIncrement][data-disabled][data-hover],\n [ngpNumberFieldDecrement][data-disabled][data-hover] {\n background: transparent;\n color: var(--ngp-text-secondary);\n }\n `,\n})\nexport class NumberField {}\n",
116
+ "code": "import { Component } from '@angular/core';\nimport {\n NgpNumberField,\n NgpNumberFieldDecrement,\n NgpNumberFieldIncrement,\n NgpNumberFieldInput,\n} from 'ng-primitives/number-field';\n\n@Component({\n selector: 'app-number-field',\n hostDirectives: [\n {\n directive: NgpNumberField,\n inputs: [\n 'ngpNumberFieldValue:value',\n 'ngpNumberFieldMin:min',\n 'ngpNumberFieldMax:max',\n 'ngpNumberFieldStep:step',\n 'ngpNumberFieldLargeStep:largeStep',\n 'ngpNumberFieldDisabled:disabled',\n 'ngpNumberFieldReadonly:readonly',\n ],\n outputs: ['ngpNumberFieldValueChange:valueChange'],\n },\n ],\n imports: [NgpNumberFieldInput, NgpNumberFieldIncrement, NgpNumberFieldDecrement],\n template: `\n <button ngpNumberFieldDecrement aria-label=\"Decrement\">−</button>\n <input ngpNumberFieldInput />\n <button ngpNumberFieldIncrement aria-label=\"Increment\">+</button>\n `,\n styles: `\n :host {\n display: inline-flex;\n align-items: center;\n border-radius: 8px;\n box-shadow: var(--ngp-input-shadow);\n background-color: var(--ngp-background);\n overflow: hidden;\n }\n\n :host:focus-within {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n [ngpNumberFieldInput] {\n width: 64px;\n height: 36px;\n border: none;\n outline: none;\n text-align: center;\n font-size: 0.875rem;\n color: var(--ngp-text-primary);\n background: transparent;\n box-sizing: border-box;\n padding: 0;\n }\n\n [ngpNumberFieldInput]::placeholder {\n color: var(--ngp-text-placeholder);\n }\n\n [ngpNumberFieldIncrement],\n [ngpNumberFieldDecrement] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border: none;\n background: transparent;\n color: var(--ngp-text-secondary);\n cursor: pointer;\n font-size: 1rem;\n user-select: none;\n -webkit-touch-callout: none;\n transition:\n color 150ms ease,\n background-color 150ms ease;\n }\n\n [ngpNumberFieldIncrement][data-hover],\n [ngpNumberFieldDecrement][data-hover] {\n background-color: var(--ngp-background-hover);\n color: var(--ngp-text-primary);\n }\n\n [ngpNumberFieldIncrement][data-press],\n [ngpNumberFieldDecrement][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpNumberFieldIncrement][data-disabled],\n [ngpNumberFieldDecrement][data-disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n [ngpNumberFieldIncrement][data-disabled][data-hover],\n [ngpNumberFieldDecrement][data-disabled][data-hover] {\n background: transparent;\n color: var(--ngp-text-secondary);\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n [ngpNumberFieldIncrement],\n [ngpNumberFieldDecrement] {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class NumberField {}\n",
117
117
  "primitive": "number-field",
118
118
  "hasVariants": false,
119
119
  "hasSizes": true
120
120
  },
121
121
  {
122
122
  "name": "pagination",
123
- "code": "import { Component, computed } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport {\n heroChevronDoubleLeft,\n heroChevronDoubleRight,\n heroChevronLeft,\n heroChevronRight,\n} from '@ng-icons/heroicons/outline';\nimport {\n injectPaginationState,\n NgpPagination,\n NgpPaginationButton,\n NgpPaginationFirst,\n NgpPaginationLast,\n NgpPaginationNext,\n NgpPaginationPrevious,\n} from 'ng-primitives/pagination';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-pagination',\n hostDirectives: [\n {\n directive: NgpPagination,\n inputs: [\n 'ngpPaginationPage:page',\n 'ngpPaginationPageCount:pageCount',\n 'ngpPaginationDisabled:disabled',\n ],\n outputs: ['ngpPaginationPageChange:pageChange'],\n },\n ],\n imports: [\n NgpPaginationButton,\n NgpPaginationFirst,\n NgpPaginationLast,\n NgpPaginationNext,\n NgpPaginationPrevious,\n NgIcon,\n ],\n providers: [\n provideValueAccessor(NgpPagination),\n provideIcons({\n heroChevronDoubleLeft,\n heroChevronDoubleRight,\n heroChevronLeft,\n heroChevronRight,\n }),\n ],\n template: `\n <ul>\n <li>\n <button ngpPaginationFirst aria-label=\"First Page\">\n <ng-icon name=\"heroChevronDoubleLeft\" />\n </button>\n </li>\n\n <li>\n <button ngpPaginationPrevious aria-label=\"Previous Page\">\n <ng-icon name=\"heroChevronLeft\" />\n </button>\n </li>\n\n @for (page of pages(); track page) {\n <li>\n <button\n [ngpPaginationButtonPage]=\"page\"\n [attr.aria-label]=\"'Page ' + page\"\n ngpPaginationButton\n >\n {{ page }}\n </button>\n </li>\n }\n\n <li>\n <button ngpPaginationNext aria-label=\"Next Page\">\n <ng-icon name=\"heroChevronRight\" />\n </button>\n </li>\n\n <li>\n <button ngpPaginationLast aria-label=\"Last Page\">\n <ng-icon name=\"heroChevronDoubleRight\" />\n </button>\n </li>\n </ul>\n `,\n styles: `\n ul {\n display: flex;\n align-items: center;\n column-gap: 0.5rem;\n list-style: none;\n }\n\n [ngpPaginationFirst],\n [ngpPaginationPrevious],\n [ngpPaginationButton],\n [ngpPaginationNext],\n [ngpPaginationLast] {\n all: unset;\n display: flex;\n justify-content: center;\n align-items: center;\n width: 2rem;\n height: 2rem;\n border-radius: 0.5rem;\n color: var(--ngp-text-primary);\n outline: none;\n font-size: 14px;\n font-weight: 500;\n background-color: var(--ngp-background);\n box-shadow: var(--ngp-button-shadow);\n cursor: pointer;\n transition: background-color 0.2s;\n\n &[data-hover]:not([data-disabled]):not([data-selected]) {\n background-color: var(--ngp-background-hover);\n }\n\n &[data-focus-visible]:not([data-disabled]) {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n &[data-press]:not([data-disabled]):not([data-selected]) {\n background-color: var(--ngp-background-active);\n }\n\n &[data-disabled] {\n color: rgb(210 210 210);\n background-color: var(--ngp-background-disabled);\n cursor: not-allowed;\n box-shadow: none;\n }\n\n &[data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n }\n }\n `,\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Pagination implements ControlValueAccessor {\n /** Access the pagination state */\n protected readonly state = injectPaginationState();\n\n /** Get the pages as an array we can iterate over */\n protected readonly pages = computed(() =>\n Array.from({ length: this.state().pageCount() }).map((_, i) => i + 1),\n );\n\n /** The onChange callback */\n private onChange?: ChangeFn<number>;\n\n /** The onTouched callback */\n protected onTouched?: TouchedFn;\n\n constructor() {\n this.state().pageChange.subscribe(value => this.onChange?.(value));\n }\n\n /** Write a new value to the control */\n writeValue(value: number): void {\n // writing a value from the model must not re-emit through onChange\n this.state().setPage(value, { emit: false });\n }\n\n /** Register a callback to be called when the value changes */\n registerOnChange(fn: ChangeFn<number>): void {\n this.onChange = fn;\n }\n\n /** Register a callback to be called when the control is touched */\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n /** Reflect the form control's disabled state onto the pagination. */\n setDisabledState(isDisabled: boolean): void {\n this.state().setDisabled(isDisabled);\n }\n}\n",
123
+ "code": "import { Component, computed } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport {\n heroChevronDoubleLeft,\n heroChevronDoubleRight,\n heroChevronLeft,\n heroChevronRight,\n} from '@ng-icons/heroicons/outline';\nimport {\n injectPaginationState,\n NgpPagination,\n NgpPaginationButton,\n NgpPaginationFirst,\n NgpPaginationLast,\n NgpPaginationNext,\n NgpPaginationPrevious,\n} from 'ng-primitives/pagination';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-pagination',\n hostDirectives: [\n {\n directive: NgpPagination,\n inputs: [\n 'ngpPaginationPage:page',\n 'ngpPaginationPageCount:pageCount',\n 'ngpPaginationDisabled:disabled',\n ],\n outputs: ['ngpPaginationPageChange:pageChange'],\n },\n ],\n imports: [\n NgpPaginationButton,\n NgpPaginationFirst,\n NgpPaginationLast,\n NgpPaginationNext,\n NgpPaginationPrevious,\n NgIcon,\n ],\n providers: [\n provideValueAccessor(Pagination),\n provideIcons({\n heroChevronDoubleLeft,\n heroChevronDoubleRight,\n heroChevronLeft,\n heroChevronRight,\n }),\n ],\n template: `\n <ul>\n <li>\n <button ngpPaginationFirst aria-label=\"First Page\">\n <ng-icon name=\"heroChevronDoubleLeft\" />\n </button>\n </li>\n\n <li>\n <button ngpPaginationPrevious aria-label=\"Previous Page\">\n <ng-icon name=\"heroChevronLeft\" />\n </button>\n </li>\n\n @for (page of pages(); track page) {\n <li>\n <button\n [ngpPaginationButtonPage]=\"page\"\n [attr.aria-label]=\"'Page ' + page\"\n ngpPaginationButton\n >\n {{ page }}\n </button>\n </li>\n }\n\n <li>\n <button ngpPaginationNext aria-label=\"Next Page\">\n <ng-icon name=\"heroChevronRight\" />\n </button>\n </li>\n\n <li>\n <button ngpPaginationLast aria-label=\"Last Page\">\n <ng-icon name=\"heroChevronDoubleRight\" />\n </button>\n </li>\n </ul>\n `,\n styles: `\n ul {\n display: flex;\n align-items: center;\n column-gap: 0.5rem;\n list-style: none;\n }\n\n [ngpPaginationFirst],\n [ngpPaginationPrevious],\n [ngpPaginationButton],\n [ngpPaginationNext],\n [ngpPaginationLast] {\n all: unset;\n display: flex;\n justify-content: center;\n align-items: center;\n width: 2rem;\n height: 2rem;\n border-radius: 0.5rem;\n color: var(--ngp-text-primary);\n outline: none;\n font-size: 14px;\n font-weight: 500;\n background-color: var(--ngp-background);\n box-shadow: var(--ngp-button-shadow);\n cursor: pointer;\n transition: background-color 0.2s;\n\n &[data-hover]:not([data-disabled]):not([data-selected]) {\n background-color: var(--ngp-background-hover);\n }\n\n &[data-focus-visible]:not([data-disabled]) {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n &[data-press]:not([data-disabled]):not([data-selected]) {\n background-color: var(--ngp-background-active);\n }\n\n &[data-disabled] {\n color: rgb(210 210 210);\n background-color: var(--ngp-background-disabled);\n cursor: not-allowed;\n box-shadow: none;\n }\n\n &[data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n }\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n [ngpPaginationFirst],\n [ngpPaginationPrevious],\n [ngpPaginationButton],\n [ngpPaginationNext],\n [ngpPaginationLast] {\n transition-duration: 0s;\n }\n }\n `,\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Pagination implements ControlValueAccessor {\n /** Access the pagination state */\n protected readonly state = injectPaginationState();\n\n /** Get the pages as an array we can iterate over */\n protected readonly pages = computed(() =>\n Array.from({ length: this.state().pageCount() }).map((_, i) => i + 1),\n );\n\n /** The onChange callback */\n private onChange?: ChangeFn<number>;\n\n /** The onTouched callback */\n protected onTouched?: TouchedFn;\n\n constructor() {\n this.state().pageChange.subscribe(value => this.onChange?.(value));\n }\n\n /** Write a new value to the control */\n writeValue(value: number): void {\n // writing a value from the model must not re-emit through onChange\n this.state().setPage(value, { emit: false });\n }\n\n /** Register a callback to be called when the value changes */\n registerOnChange(fn: ChangeFn<number>): void {\n this.onChange = fn;\n }\n\n /** Register a callback to be called when the control is touched */\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n /** Reflect the form control's disabled state onto the pagination. */\n setDisabledState(isDisabled: boolean): void {\n this.state().setDisabled(isDisabled);\n }\n}\n",
124
124
  "primitive": "pagination",
125
125
  "hasVariants": false,
126
126
  "hasSizes": true
127
127
  },
128
128
  {
129
129
  "name": "password",
130
- "code": "import { Component, input, signal } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { lucideEye, lucideEyeOff } from '@ng-icons/lucide';\nimport { NgpButton } from 'ng-primitives/button';\nimport {\n injectPasswordState,\n NgpPassword,\n NgpPasswordInput,\n NgpPasswordToggle,\n} from 'ng-primitives/password';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-password',\n hostDirectives: [NgpPassword],\n imports: [NgIcon, NgpPasswordInput, NgpPasswordToggle, NgpButton],\n providers: [provideValueAccessor(Password), provideIcons({ lucideEye, lucideEyeOff })],\n template: `\n <input\n [value]=\"value()\"\n [placeholder]=\"placeholder()\"\n (input)=\"onValueChange($event)\"\n ngpPasswordInput\n type=\"password\"\n autocomplete=\"current-password\"\n />\n <button ngpButton ngpPasswordToggle>\n <ng-icon [name]=\"state().visible() ? 'lucideEyeOff' : 'lucideEye'\" />\n </button>\n `,\n styles: `\n :host {\n display: block;\n position: relative;\n }\n\n [ngpPasswordInput] {\n height: 2.125rem;\n width: 100%;\n border-radius: 0.5rem;\n padding: 0 40px 0 12px;\n border: none;\n background: var(--ngp-background);\n color: var(--ngp-text-primary);\n font-size: 0.875rem;\n letter-spacing: -0.006em;\n box-shadow: var(--ngp-input-shadow);\n outline: none;\n }\n\n [ngpPasswordInput][data-focus] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n [ngpPasswordInput]::placeholder {\n color: var(--ngp-text-placeholder);\n }\n\n [ngpPasswordToggle] {\n position: absolute;\n top: 0;\n right: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 2.125rem;\n width: 2.125rem;\n border: none;\n border-radius: 0.5rem;\n background-color: transparent;\n color: var(--ngp-text-tertiary);\n font-size: 1.125rem;\n cursor: pointer;\n outline: none;\n transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n [ngpPasswordToggle][data-hover] {\n color: var(--ngp-text-secondary);\n }\n\n [ngpPasswordToggle][data-visible] {\n color: var(--ngp-primary);\n }\n\n [ngpPasswordToggle][data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: -2px;\n }\n `,\n host: {\n '(focusout)': 'onFocusOut($event)',\n },\n})\nexport class Password implements ControlValueAccessor {\n /** Access the password state to read the visibility. */\n protected readonly state = injectPasswordState();\n\n /** The placeholder text */\n readonly placeholder = input<string>('');\n\n /** The current value */\n protected value = signal<string>('');\n\n /** The function to call when the value changes */\n private onChange?: ChangeFn<string>;\n\n /** The function to call when the control is touched */\n protected onTouched?: TouchedFn;\n\n writeValue(value: string): void {\n this.value.set(value);\n }\n\n registerOnChange(fn: ChangeFn<string>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n protected onValueChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n this.value.set(input.value);\n this.onChange?.(input.value);\n }\n\n protected onFocusOut(event: FocusEvent): void {\n // only mark as touched when focus leaves the component, not when moving\n // between the input and the toggle button\n const host = event.currentTarget as HTMLElement;\n if (!host.contains(event.relatedTarget as Node | null)) {\n this.onTouched?.();\n }\n }\n}\n",
130
+ "code": "import { Component, input, signal } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { lucideEye, lucideEyeOff } from '@ng-icons/lucide';\nimport { NgpButton } from 'ng-primitives/button';\nimport {\n injectPasswordState,\n NgpPassword,\n NgpPasswordInput,\n NgpPasswordToggle,\n} from 'ng-primitives/password';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-password',\n hostDirectives: [NgpPassword],\n imports: [NgIcon, NgpPasswordInput, NgpPasswordToggle, NgpButton],\n providers: [provideValueAccessor(Password), provideIcons({ lucideEye, lucideEyeOff })],\n template: `\n <input\n [value]=\"value()\"\n [placeholder]=\"placeholder()\"\n (input)=\"onValueChange($event)\"\n ngpPasswordInput\n type=\"password\"\n autocomplete=\"current-password\"\n />\n <button ngpButton ngpPasswordToggle>\n <ng-icon [name]=\"state().visible() ? 'lucideEyeOff' : 'lucideEye'\" />\n </button>\n `,\n styles: `\n :host {\n display: block;\n position: relative;\n }\n\n [ngpPasswordInput] {\n height: 2.125rem;\n width: 100%;\n border-radius: 0.5rem;\n padding: 0 40px 0 12px;\n border: none;\n background: var(--ngp-background);\n color: var(--ngp-text-primary);\n font-size: 0.875rem;\n letter-spacing: -0.006em;\n box-shadow: var(--ngp-input-shadow);\n outline: none;\n }\n\n [ngpPasswordInput][data-focus] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n [ngpPasswordInput]::placeholder {\n color: var(--ngp-text-placeholder);\n }\n\n [ngpPasswordToggle] {\n position: absolute;\n top: 0;\n right: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 2.125rem;\n width: 2.125rem;\n border: none;\n border-radius: 0.5rem;\n background-color: transparent;\n color: var(--ngp-text-tertiary);\n font-size: 1.125rem;\n cursor: pointer;\n outline: none;\n transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n [ngpPasswordToggle][data-hover] {\n color: var(--ngp-text-secondary);\n }\n\n [ngpPasswordToggle][data-visible] {\n color: var(--ngp-primary);\n }\n\n [ngpPasswordToggle][data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: -2px;\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n [ngpPasswordToggle] {\n transition-duration: 0s;\n }\n }\n `,\n host: {\n '(focusout)': 'onFocusOut($event)',\n },\n})\nexport class Password implements ControlValueAccessor {\n /** Access the password state to read the visibility. */\n protected readonly state = injectPasswordState();\n\n /** The placeholder text */\n readonly placeholder = input<string>('');\n\n /** The current value */\n protected value = signal<string>('');\n\n /** The function to call when the value changes */\n private onChange?: ChangeFn<string>;\n\n /** The function to call when the control is touched */\n protected onTouched?: TouchedFn;\n\n writeValue(value: string): void {\n this.value.set(value);\n }\n\n registerOnChange(fn: ChangeFn<string>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n protected onValueChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n this.value.set(input.value);\n this.onChange?.(input.value);\n }\n\n protected onFocusOut(event: FocusEvent): void {\n // only mark as touched when focus leaves the component, not when moving\n // between the input and the toggle button\n const host = event.currentTarget as HTMLElement;\n if (!host.contains(event.relatedTarget as Node | null)) {\n this.onTouched?.();\n }\n }\n}\n",
131
131
  "primitive": "password",
132
132
  "hasVariants": false,
133
133
  "hasSizes": true
@@ -141,7 +141,7 @@
141
141
  },
142
142
  {
143
143
  "name": "progress",
144
- "code": "import { NumberInput } from '@angular/cdk/coercion';\nimport { Component, input, numberAttribute } from '@angular/core';\nimport {\n NgpProgress,\n NgpProgressIndicator,\n NgpProgressLabel,\n NgpProgressTrack,\n NgpProgressValue,\n} from 'ng-primitives/progress';\n\n@Component({\n selector: 'app-progress',\n hostDirectives: [\n {\n directive: NgpProgress,\n inputs: ['ngpProgressValue:value', 'ngpProgressMax:max', 'ngpProgressValueLabel:valueLabel'],\n },\n ],\n imports: [NgpProgressIndicator, NgpProgressTrack, NgpProgressLabel, NgpProgressValue],\n template: `\n <label ngpProgressLabel>{{ label() }}</label>\n <span ngpProgressValue>{{ value() }}%</span>\n\n <div ngpProgressTrack>\n <div ngpProgressIndicator></div>\n </div>\n `,\n styles: `\n :host {\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-row-gap: 0.5rem;\n width: 200px;\n box-sizing: border-box;\n padding: 0.5rem;\n }\n\n [ngpProgressLabel] {\n color: var(--ngp-text-emphasis);\n font-size: 14px;\n font-weight: 600;\n }\n\n [ngpProgressValue] {\n color: var(--ngp-text-secondary);\n font-size: 14px;\n font-weight: 500;\n text-align: right;\n grid-column-start: 2;\n text-align: end;\n }\n\n [ngpProgressTrack] {\n grid-column: 1 / 3;\n position: relative;\n height: 12px;\n width: 100%;\n max-width: 320px;\n overflow: hidden;\n border-radius: 0.5rem;\n border: 1px solid var(--ngp-border);\n background-color: var(--ngp-background);\n }\n\n [ngpProgressIndicator] {\n height: 100%;\n border-radius: 0.5rem;\n background-color: var(--ngp-background-inverse);\n transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n `,\n})\nexport class Progress {\n /** The value of the progress. */\n readonly value = input<number, NumberInput>(0, {\n transform: numberAttribute,\n });\n\n /** The label of the progress. */\n readonly label = input.required<string>();\n}\n",
144
+ "code": "import { NumberInput } from '@angular/cdk/coercion';\nimport { Component, input, numberAttribute } from '@angular/core';\nimport {\n NgpProgress,\n NgpProgressIndicator,\n NgpProgressLabel,\n NgpProgressTrack,\n NgpProgressValue,\n} from 'ng-primitives/progress';\n\n@Component({\n selector: 'app-progress',\n hostDirectives: [\n {\n directive: NgpProgress,\n inputs: ['ngpProgressValue:value', 'ngpProgressMax:max', 'ngpProgressValueLabel:valueLabel'],\n },\n ],\n imports: [NgpProgressIndicator, NgpProgressTrack, NgpProgressLabel, NgpProgressValue],\n template: `\n <label ngpProgressLabel>{{ label() }}</label>\n <span ngpProgressValue>{{ value() }}%</span>\n\n <div ngpProgressTrack>\n <div ngpProgressIndicator></div>\n </div>\n `,\n styles: `\n :host {\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-row-gap: 0.5rem;\n width: 200px;\n box-sizing: border-box;\n padding: 0.5rem;\n }\n\n [ngpProgressLabel] {\n color: var(--ngp-text-emphasis);\n font-size: 14px;\n font-weight: 600;\n }\n\n [ngpProgressValue] {\n color: var(--ngp-text-secondary);\n font-size: 14px;\n font-weight: 500;\n text-align: right;\n grid-column-start: 2;\n text-align: end;\n }\n\n [ngpProgressTrack] {\n grid-column: 1 / 3;\n position: relative;\n height: 12px;\n width: 100%;\n max-width: 320px;\n overflow: hidden;\n border-radius: 0.5rem;\n border: 1px solid var(--ngp-border);\n background-color: var(--ngp-background);\n }\n\n [ngpProgressIndicator] {\n height: 100%;\n border-radius: 0.5rem;\n background-color: var(--ngp-background-inverse);\n transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n [ngpProgressIndicator] {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class Progress {\n /** The value of the progress. */\n readonly value = input<number, NumberInput>(0, {\n transform: numberAttribute,\n });\n\n /** The label of the progress. */\n readonly label = input.required<string>();\n}\n",
145
145
  "primitive": "progress",
146
146
  "hasVariants": false,
147
147
  "hasSizes": true
@@ -160,6 +160,13 @@
160
160
  "hasVariants": false,
161
161
  "hasSizes": false
162
162
  },
163
+ {
164
+ "name": "rating",
165
+ "code": "import { Component } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroStarSolid } from '@ng-icons/heroicons/solid';\nimport { injectRatingState, NgpRating, NgpRatingItem } from 'ng-primitives/rating';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-rating',\n hostDirectives: [\n {\n directive: NgpRating,\n inputs: [\n 'ngpRatingValue:value',\n 'ngpRatingCount:count',\n 'ngpRatingAllowHalf:allowHalf',\n 'ngpRatingDisabled:disabled',\n 'ngpRatingReadonly:readonly',\n 'ngpRatingClearable:clearable',\n ],\n outputs: ['ngpRatingValueChange:valueChange'],\n },\n ],\n imports: [NgIcon, NgpRatingItem],\n providers: [provideIcons({ heroStarSolid }), provideValueAccessor(Rating)],\n template: `\n <span class=\"star\" *ngpRatingItem=\"let star\">\n <ng-icon class=\"star-background\" name=\"heroStarSolid\" />\n <span class=\"star-fill\" [style.width.%]=\"star.fraction * 100\">\n <ng-icon name=\"heroStarSolid\" />\n </span>\n </span>\n `,\n styles: `\n :host {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n outline: none;\n border-radius: 0.5rem;\n cursor: pointer;\n }\n\n :host[data-disabled] {\n cursor: not-allowed;\n opacity: 0.5;\n }\n\n :host[data-readonly] {\n cursor: default;\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n .star {\n position: relative;\n display: inline-flex;\n font-size: 1.5rem;\n line-height: 1;\n color: var(--ngp-background-secondary);\n transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n .star-fill {\n position: absolute;\n inset-block-start: 0;\n inset-inline-start: 0;\n display: inline-flex;\n overflow: hidden;\n color: var(--ngp-primary);\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n .star {\n transition-duration: 0s;\n }\n }\n `,\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Rating implements ControlValueAccessor {\n /** Access the rating state. */\n private readonly state = injectRatingState();\n\n /** The onChange callback function. */\n private onChange?: ChangeFn<number>;\n\n /** The onTouched callback function. */\n protected onTouched?: TouchedFn;\n\n constructor() {\n // Whenever the user interacts with the rating, call onChange with the new value.\n this.state()\n .valueChange.pipe(takeUntilDestroyed())\n .subscribe(value => this.onChange?.(value));\n }\n\n writeValue(value: number): void {\n // writing a value from the model must not re-emit through onChange\n this.state().setValue(value, { emit: false });\n }\n\n registerOnChange(fn: ChangeFn<number>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.state().setDisabled(isDisabled);\n }\n}\n",
166
+ "primitive": "rating",
167
+ "hasVariants": false,
168
+ "hasSizes": true
169
+ },
163
170
  {
164
171
  "name": "search",
165
172
  "code": "import { Component, input, model } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroMagnifyingGlass } from '@ng-icons/heroicons/outline';\nimport { NgpButton } from 'ng-primitives/button';\nimport { NgpInput } from 'ng-primitives/input';\nimport { NgpSearch, NgpSearchClear } from 'ng-primitives/search';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-search',\n hostDirectives: [NgpSearch],\n imports: [NgIcon, NgpSearchClear, NgpInput, NgpButton],\n providers: [provideValueAccessor(Search), provideIcons({ heroMagnifyingGlass })],\n template: `\n <ng-icon name=\"heroMagnifyingGlass\" />\n <input\n [value]=\"query()\"\n [placeholder]=\"placeholder()\"\n (input)=\"onQueryChange($event)\"\n ngpInput\n type=\"search\"\n />\n <button ngpSearchClear ngpButton aria-label=\"Clear search\">Clear</button>\n `,\n styles: `\n :host {\n display: block;\n position: relative;\n }\n\n [ngpInput] {\n height: 36px;\n width: 100%;\n border-radius: 8px;\n padding: 0 16px 0 40px;\n border: none;\n background: var(--ngp-background);\n box-shadow: var(--ngp-input-shadow);\n outline: none;\n }\n\n /* clears the 'X' from Chrome */\n [ngpInput]::-webkit-search-decoration,\n [ngpInput]::-webkit-search-cancel-button,\n [ngpInput]::-webkit-search-results-button,\n [ngpInput]::-webkit-search-results-decoration {\n display: none;\n }\n\n [ngpInput][data-focus] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 0px;\n }\n\n [ngpInput]::placeholder {\n color: var(--ngp-text-placeholder);\n }\n\n [ngpButton] {\n position: absolute;\n top: 0;\n right: 0;\n height: 36px;\n padding: 0 16px;\n border: none;\n border-radius: 0 8px 8px 0;\n background-color: transparent;\n color: var(--ngp-text-blue);\n font-size: 0.875rem;\n line-height: 1.25rem;\n cursor: pointer;\n outline: none;\n display: none;\n }\n\n [ngpButton]:not([data-empty]) {\n display: block;\n }\n\n ng-icon {\n position: absolute;\n font-size: 1.25rem;\n top: 18px;\n left: 12px;\n transform: translateY(-50%);\n color: var(--ngp-text-tertiary);\n }\n `,\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Search implements ControlValueAccessor {\n /** The search query */\n readonly query = model<string>('');\n\n /** The placeholder text */\n readonly placeholder = input<string>('');\n\n /** The function to call when the value changes */\n private onChange?: ChangeFn<string>;\n\n /** The function to call when the control is touched */\n protected onTouched?: TouchedFn;\n\n writeValue(value: string): void {\n this.query.set(value);\n }\n\n registerOnChange(fn: ChangeFn<string>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n protected onQueryChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n this.query.set(input.value);\n this.onChange?.(input.value);\n }\n}\n",
@@ -169,7 +176,7 @@
169
176
  },
170
177
  {
171
178
  "name": "select",
172
- "code": "import { Component, input } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroChevronDown } from '@ng-icons/heroicons/outline';\nimport {\n injectSelectState,\n NgpSelect,\n NgpSelectDropdown,\n NgpSelectOption,\n NgpSelectPortal,\n} from 'ng-primitives/select';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-select',\n hostDirectives: [\n {\n directive: NgpSelect,\n inputs: ['ngpSelectValue:value', 'ngpSelectMultiple:multiple', 'ngpSelectDisabled:disabled'],\n outputs: ['ngpSelectValueChange:valueChange'],\n },\n ],\n providers: [provideIcons({ heroChevronDown }), provideValueAccessor(Select)],\n imports: [NgpSelectDropdown, NgpSelectOption, NgpSelectPortal, NgIcon],\n template: `\n @if (state().value(); as value) {\n <span class=\"select-value\">{{ value }}</span>\n } @else {\n <span class=\"select-placeholder\">{{ placeholder() }}</span>\n }\n\n <ng-icon name=\"heroChevronDown\" />\n\n <div *ngpSelectPortal ngpSelectDropdown>\n @for (option of options(); track option) {\n <div [ngpSelectOptionValue]=\"option\" ngpSelectOption>\n {{ option }}\n </div>\n } @empty {\n <div class=\"empty-message\">No options found</div>\n }\n </div>\n `,\n styles: `\n :host {\n display: flex;\n justify-content: space-between;\n align-items: center;\n height: 36px;\n width: 300px;\n border-radius: 8px;\n border: none;\n background-color: var(--ngp-background);\n box-shadow: var(--ngp-input-shadow);\n box-sizing: border-box;\n }\n\n :host[data-focus] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n .select-value,\n .select-placeholder {\n display: flex;\n align-items: center;\n flex: 1;\n padding: 0 16px;\n background-color: transparent;\n color: var(--ngp-text-primary);\n font-family: inherit;\n font-size: 14px;\n padding: 0 16px;\n height: 100%;\n }\n\n .select-placeholder {\n color: var(--ngp-text-secondary);\n }\n\n ng-icon {\n display: inline-flex;\n justify-content: center;\n align-items: center;\n height: 100%;\n margin-inline: 8px;\n font-size: 14px;\n }\n\n [ngpSelectDropdown] {\n background-color: var(--ngp-background);\n border: 1px solid var(--ngp-border);\n padding: 0.25rem;\n border-radius: 0.75rem;\n outline: none;\n position: absolute;\n animation: popover-show 0.1s ease-out;\n width: var(--ngp-select-width);\n box-shadow: var(--ngp-shadow-lg);\n box-sizing: border-box;\n margin-top: 4px;\n max-height: 240px;\n overflow-y: auto;\n transform-origin: var(--ngp-select-transform-origin);\n }\n\n [ngpSelectDropdown][data-enter] {\n animation: select-show 0.1s ease-out;\n }\n\n [ngpSelectDropdown][data-exit] {\n animation: select-hide 0.1s ease-out;\n }\n\n [ngpSelectOption] {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.375rem 0.75rem;\n cursor: pointer;\n border-radius: 0.5rem;\n width: 100%;\n height: 36px;\n font-size: 14px;\n color: var(--ngp-text-primary);\n box-sizing: border-box;\n }\n\n [ngpSelectOption][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n [ngpSelectOption][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpSelectOption][data-active] {\n background-color: var(--ngp-background-active);\n }\n\n .empty-message {\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 0.5rem;\n color: var(--ngp-text-secondary);\n font-size: 14px;\n font-weight: 500;\n text-align: center;\n }\n\n @keyframes select-show {\n 0% {\n opacity: 0;\n transform: translateY(-10px) scale(0.9);\n }\n 100% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n }\n\n @keyframes select-hide {\n 0% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n 100% {\n opacity: 0;\n transform: translateY(-10px) scale(0.9);\n }\n }\n `,\n host: {\n '[attr.aria-label]': 'ariaLabel() || null',\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Select implements ControlValueAccessor {\n /** Access the underlying select primitive state. */\n protected readonly state = injectSelectState<string>();\n\n /** The options for the select. */\n readonly options = input<string[]>([]);\n\n /** The placeholder for the input. */\n readonly placeholder = input<string>('');\n\n /** The accessible label for the select trigger. */\n readonly ariaLabel = input<string>('');\n\n /** Form change callback. */\n private onChange?: ChangeFn<string | undefined>;\n\n /** Form touched callback. */\n protected onTouched?: TouchedFn;\n\n constructor() {\n // Forward primitive value changes (user-driven selections) to the form.\n this.state()\n .valueChange.pipe(takeUntilDestroyed())\n .subscribe(value => this.onChange?.(value as string | undefined));\n }\n\n writeValue(value: string | undefined): void {\n // Pass { emit: false } so writeValue doesn't bounce back through onChange.\n this.state().setValue(value, { emit: false });\n }\n\n registerOnChange(fn: ChangeFn<string | undefined>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.state().setDisabled(isDisabled);\n }\n}\n",
179
+ "code": "import { Component, input } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { heroChevronDown } from '@ng-icons/heroicons/outline';\nimport {\n injectSelectState,\n NgpSelect,\n NgpSelectDropdown,\n NgpSelectOption,\n NgpSelectPortal,\n} from 'ng-primitives/select';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-select',\n hostDirectives: [\n {\n directive: NgpSelect,\n inputs: ['ngpSelectValue:value', 'ngpSelectMultiple:multiple', 'ngpSelectDisabled:disabled'],\n outputs: ['ngpSelectValueChange:valueChange'],\n },\n ],\n providers: [provideIcons({ heroChevronDown }), provideValueAccessor(Select)],\n imports: [NgpSelectDropdown, NgpSelectOption, NgpSelectPortal, NgIcon],\n template: `\n @if (state().value(); as value) {\n <span class=\"select-value\">{{ value }}</span>\n } @else {\n <span class=\"select-placeholder\">{{ placeholder() }}</span>\n }\n\n <ng-icon name=\"heroChevronDown\" />\n\n <div *ngpSelectPortal ngpSelectDropdown>\n @for (option of options(); track option) {\n <div [ngpSelectOptionValue]=\"option\" ngpSelectOption>\n {{ option }}\n </div>\n } @empty {\n <div class=\"empty-message\">No options found</div>\n }\n </div>\n `,\n styles: `\n :host {\n display: flex;\n justify-content: space-between;\n align-items: center;\n height: 36px;\n width: 300px;\n border-radius: 8px;\n border: none;\n background-color: var(--ngp-background);\n box-shadow: var(--ngp-input-shadow);\n box-sizing: border-box;\n }\n\n :host[data-focus] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n .select-value,\n .select-placeholder {\n display: flex;\n align-items: center;\n flex: 1;\n padding: 0 16px;\n background-color: transparent;\n color: var(--ngp-text-primary);\n font-family: inherit;\n font-size: 14px;\n padding: 0 16px;\n height: 100%;\n }\n\n .select-placeholder {\n color: var(--ngp-text-secondary);\n }\n\n ng-icon {\n display: inline-flex;\n justify-content: center;\n align-items: center;\n height: 100%;\n margin-inline: 8px;\n font-size: 14px;\n }\n\n [ngpSelectDropdown] {\n background-color: var(--ngp-background);\n border: 1px solid var(--ngp-border);\n padding: 0.25rem;\n border-radius: 0.75rem;\n outline: none;\n position: absolute;\n animation: popover-show 0.1s ease-out;\n width: var(--ngp-select-width);\n box-shadow: var(--ngp-shadow-lg);\n box-sizing: border-box;\n margin-top: 4px;\n max-height: 240px;\n overflow-y: auto;\n transform-origin: var(--ngp-select-transform-origin);\n }\n\n [ngpSelectDropdown][data-enter] {\n animation: select-show 0.1s ease-out;\n }\n\n [ngpSelectDropdown][data-exit] {\n animation: select-hide 0.1s ease-out;\n }\n\n [ngpSelectOption] {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.375rem 0.75rem;\n cursor: pointer;\n border-radius: 0.5rem;\n width: 100%;\n height: 36px;\n font-size: 14px;\n color: var(--ngp-text-primary);\n box-sizing: border-box;\n }\n\n [ngpSelectOption][data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n [ngpSelectOption][data-press] {\n background-color: var(--ngp-background-active);\n }\n\n [ngpSelectOption][data-active] {\n background-color: var(--ngp-background-active);\n }\n\n .empty-message {\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 0.5rem;\n color: var(--ngp-text-secondary);\n font-size: 14px;\n font-weight: 500;\n text-align: center;\n }\n\n @keyframes select-show {\n 0% {\n opacity: 0;\n transform: translateY(-10px) scale(0.9);\n }\n 100% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n }\n\n @keyframes select-hide {\n 0% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n 100% {\n opacity: 0;\n transform: translateY(-10px) scale(0.9);\n }\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n [ngpSelectDropdown],\n [ngpSelectDropdown][data-enter],\n [ngpSelectDropdown][data-exit] {\n animation-duration: 0s;\n }\n }\n `,\n host: {\n '[attr.aria-label]': 'ariaLabel() || null',\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Select implements ControlValueAccessor {\n /** Access the underlying select primitive state. */\n protected readonly state = injectSelectState<string>();\n\n /** The options for the select. */\n readonly options = input<string[]>([]);\n\n /** The placeholder for the input. */\n readonly placeholder = input<string>('');\n\n /** The accessible label for the select trigger. */\n readonly ariaLabel = input<string>('');\n\n /** Form change callback. */\n private onChange?: ChangeFn<string | undefined>;\n\n /** Form touched callback. */\n protected onTouched?: TouchedFn;\n\n constructor() {\n // Forward primitive value changes (user-driven selections) to the form.\n this.state()\n .valueChange.pipe(takeUntilDestroyed())\n .subscribe(value => this.onChange?.(value as string | undefined));\n }\n\n writeValue(value: string | undefined): void {\n // Pass { emit: false } so writeValue doesn't bounce back through onChange.\n this.state().setValue(value, { emit: false });\n }\n\n registerOnChange(fn: ChangeFn<string | undefined>): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.state().setDisabled(isDisabled);\n }\n}\n",
173
180
  "primitive": "select",
174
181
  "hasVariants": false,
175
182
  "hasSizes": true
@@ -190,7 +197,7 @@
190
197
  },
191
198
  {
192
199
  "name": "switch",
193
- "code": "import { Component } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { injectSwitchState, NgpSwitch, NgpSwitchThumb } from 'ng-primitives/switch';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-switch',\n hostDirectives: [\n {\n directive: NgpSwitch,\n inputs: ['ngpSwitchChecked:checked', 'ngpSwitchDisabled:disabled'],\n outputs: ['ngpSwitchCheckedChange:checkedChange'],\n },\n ],\n imports: [NgpSwitchThumb],\n template: `\n <span ngpSwitchThumb></span>\n `,\n styles: `\n :host {\n display: inline-flex;\n align-items: center;\n position: relative;\n width: 2.5rem;\n height: 1.5rem;\n border-radius: 999px;\n background-color: var(--ngp-background-secondary);\n border: 1px solid var(--ngp-border);\n padding: 0;\n outline: none;\n transition-property:\n color, background-color, border-color, text-decoration-color, fill, stroke;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n box-sizing: border-box;\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n :host[data-checked] {\n background-color: var(--ngp-background-blue);\n border-color: var(--ngp-border-blue);\n }\n\n [ngpSwitchThumb] {\n display: block;\n height: 1.25rem;\n width: 1.25rem;\n border-radius: 999px;\n background-color: white;\n box-shadow: var(--ngp-button-shadow);\n outline: none;\n transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);\n transform: translateX(1px);\n box-sizing: border-box;\n }\n\n [ngpSwitchThumb][data-checked] {\n transform: translateX(17px);\n }\n `,\n providers: [provideValueAccessor(Switch)],\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Switch implements ControlValueAccessor {\n /** Access the switch state. */\n private readonly switch = injectSwitchState();\n\n /** The on change callback */\n private onChange?: ChangeFn<boolean>;\n\n /** The on touched callback */\n protected onTouched?: TouchedFn;\n\n constructor() {\n // Any time the switch changes, update the form value.\n this.switch()\n .checkedChange.pipe(takeUntilDestroyed())\n .subscribe(value => this.onChange?.(value));\n }\n\n /** Write a new value to the switch. */\n writeValue(value: boolean): void {\n // writing a value from the model must not re-emit through onChange\n this.switch().setChecked(value, { emit: false });\n }\n\n /** Register a callback function to be called when the value changes. */\n registerOnChange(fn: ChangeFn<boolean>): void {\n this.onChange = fn;\n }\n\n /** Register a callback function to be called when the switch is touched. */\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n /** Set the disabled state of the switch. */\n setDisabledState(isDisabled: boolean): void {\n this.switch().setDisabled(isDisabled);\n }\n}\n",
200
+ "code": "import { Component } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { injectSwitchState, NgpSwitch, NgpSwitchThumb } from 'ng-primitives/switch';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'app-switch',\n hostDirectives: [\n {\n directive: NgpSwitch,\n inputs: ['ngpSwitchChecked:checked', 'ngpSwitchDisabled:disabled'],\n outputs: ['ngpSwitchCheckedChange:checkedChange'],\n },\n ],\n imports: [NgpSwitchThumb],\n template: `\n <span ngpSwitchThumb></span>\n `,\n styles: `\n :host {\n display: inline-flex;\n align-items: center;\n position: relative;\n width: 2.5rem;\n height: 1.5rem;\n border-radius: 999px;\n background-color: var(--ngp-background-secondary);\n border: 1px solid var(--ngp-border);\n padding: 0;\n outline: none;\n transition-property:\n color, background-color, border-color, text-decoration-color, fill, stroke;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n box-sizing: border-box;\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n :host[data-checked] {\n background-color: var(--ngp-background-blue);\n border-color: var(--ngp-border-blue);\n }\n\n [ngpSwitchThumb] {\n display: block;\n height: 1.25rem;\n width: 1.25rem;\n border-radius: 999px;\n background-color: white;\n box-shadow: var(--ngp-button-shadow);\n outline: none;\n transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);\n transform: translateX(1px);\n box-sizing: border-box;\n }\n\n [ngpSwitchThumb][data-checked] {\n transform: translateX(17px);\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n :host,\n [ngpSwitchThumb] {\n transition-duration: 0s;\n }\n }\n `,\n providers: [provideValueAccessor(Switch)],\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Switch implements ControlValueAccessor {\n /** Access the switch state. */\n private readonly switch = injectSwitchState();\n\n /** The on change callback */\n private onChange?: ChangeFn<boolean>;\n\n /** The on touched callback */\n protected onTouched?: TouchedFn;\n\n constructor() {\n // Any time the switch changes, update the form value.\n this.switch()\n .checkedChange.pipe(takeUntilDestroyed())\n .subscribe(value => this.onChange?.(value));\n }\n\n /** Write a new value to the switch. */\n writeValue(value: boolean): void {\n // writing a value from the model must not re-emit through onChange\n this.switch().setChecked(value, { emit: false });\n }\n\n /** Register a callback function to be called when the value changes. */\n registerOnChange(fn: ChangeFn<boolean>): void {\n this.onChange = fn;\n }\n\n /** Register a callback function to be called when the switch is touched. */\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n /** Set the disabled state of the switch. */\n setDisabledState(isDisabled: boolean): void {\n this.switch().setDisabled(isDisabled);\n }\n}\n",
194
201
  "primitive": "switch",
195
202
  "hasVariants": false,
196
203
  "hasSizes": false
@@ -211,28 +218,28 @@
211
218
  },
212
219
  {
213
220
  "name": "toast",
214
- "code": "import { Component, inject } from '@angular/core';\nimport { NgpButton } from 'ng-primitives/button';\nimport { injectToastContext, NgpToast, NgpToastManager } from 'ng-primitives/toast';\n\n@Component({\n selector: 'app-toast',\n imports: [NgpButton],\n hostDirectives: [NgpToast],\n host: {\n 'animate.enter': 'toast-enter',\n 'animate.leave': 'toast-leave',\n },\n template: `\n <p class=\"toast-title\">{{ context.header }}</p>\n <p class=\"toast-description\">{{ context.description }}</p>\n <button class=\"toast-dismiss\" (click)=\"dismiss()\" ngpButton>Dismiss</button>\n `,\n styles: `\n :host {\n position: absolute;\n touch-action: none;\n box-sizing: border-box;\n align-items: center;\n gap: 6px;\n display: inline-grid;\n background: var(--ngp-background);\n box-shadow: var(--ngp-shadow);\n border: 1px solid var(--ngp-border);\n padding: 12px 16px;\n opacity: 0;\n border-radius: 8px;\n z-index: var(--ngp-toast-z-index);\n grid-template-columns: 1fr auto;\n grid-template-rows: min-content min-content;\n column-gap: 12px;\n align-items: center;\n width: 350px;\n height: fit-content;\n transform: var(--y);\n transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n .toast-title {\n color: var(--ngp-text-primary);\n font-size: 0.75rem;\n font-weight: 600;\n margin: 0;\n grid-column: 1 / 2;\n grid-row: 1;\n line-height: 16px;\n user-select: none;\n }\n\n .toast-description {\n font-size: 0.75rem;\n margin: 0;\n color: var(--ngp-text-secondary);\n grid-column: 1 / 2;\n grid-row: 2;\n line-height: 16px;\n user-select: none;\n }\n\n .toast-dismiss {\n background: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n border: none;\n border-radius: 8px;\n padding: 4px 8px;\n font-weight: 600;\n font-size: 12px;\n cursor: pointer;\n grid-column: 2 / 3;\n grid-row: 1 / 3;\n max-height: 27px;\n }\n\n :host[data-position-x='end'] {\n right: 0;\n }\n\n :host[data-position-x='start'] {\n left: 0;\n }\n\n :host[data-position-y='top'] {\n top: 0;\n --lift: 1;\n --lift-amount: calc(var(--lift) * var(--ngp-toast-gap));\n --y: translateY(-100%);\n }\n\n :host[data-position-y='bottom'] {\n bottom: 0;\n --lift: -1;\n --lift-amount: calc(var(--lift) * var(--ngp-toast-gap));\n --y: translateY(100%);\n }\n\n :host[data-enter] {\n opacity: 1;\n --y: translateY(0);\n }\n\n :host[data-exit] {\n opacity: 0;\n --y: translateY(calc(calc(var(--lift) * var(--ngp-toast-gap)) * -1));\n }\n\n :host[data-visible='false'] {\n opacity: 0;\n pointer-events: none;\n }\n\n :host[data-expanded='true']::after {\n content: '';\n position: absolute;\n left: 0;\n height: calc(var(--ngp-toast-gap) + 1px);\n bottom: 100%;\n width: 100%;\n }\n\n :host[data-expanded='false'][data-front='false'] {\n --scale: var(--ngp-toasts-before) * 0.05 + 1;\n --y: translateY(calc(var(--lift-amount) * var(--ngp-toasts-before)))\n scale(calc(-1 * var(--scale)));\n height: var(--ngp-toast-front-height);\n }\n\n :host[data-expanded='true'] {\n --y: translateY(calc(var(--lift) * var(--ngp-toast-offset)));\n height: auto;\n }\n\n :host[data-swiping='true'] {\n transform: var(--y) translateY(var(--ngp-toast-swipe-amount-y, 0))\n translateX(var(--ngp-toast-swipe-amount-x, 0));\n transition: none;\n }\n\n :host[data-swiping='true'][data-swipe-direction='left'] {\n /* Fade out from -45px to -100px swipe */\n opacity: calc(1 - clamp(0, ((-1 * var(--ngp-toast-swipe-x, 0px)) - 45) / 55, 1));\n }\n\n :host[data-swiping='true'][data-swipe-direction='right'] {\n /* Fade out from 45px to 100px swipe */\n opacity: calc(1 - clamp(0, (var(--ngp-toast-swipe-x, 0px) - 45) / 55, 1));\n }\n\n :host[data-swiping='true'][data-swipe-direction='top'] {\n /* Fade out from -45px to -100px swipe */\n opacity: calc(1 - clamp(0, ((-1 * var(--ngp-toast-swipe-y, 0px)) - 45) / 55, 1));\n }\n\n :host[data-swiping='true'][data-swipe-direction='bottom'] {\n /* Fade out from 45px to 100px swipe */\n opacity: calc(1 - clamp(0, (var(--ngp-toast-swipe-y, 0px) - 45) / 55, 1));\n }\n\n /* Truncate text only when toast is not front AND not expanded */\n :host[data-front='false'][data-expanded='false'] .toast-title {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n :host[data-front='false'][data-expanded='false'] .toast-description {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* Angular animations based on position */\n\n /* Bottom position animations */\n :host[data-position-y='bottom'].toast-enter {\n animation: toast-slide-in-bottom 400ms cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n :host[data-position-y='bottom'].toast-leave {\n opacity: 0;\n transform: translateY(100%);\n transition:\n opacity 400ms cubic-bezier(0.215, 0.61, 0.355, 1),\n transform 400ms cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n /* Top position animations */\n :host[data-position-y='top'].toast-enter {\n animation: toast-slide-in-top 400ms cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n :host[data-position-y='top'].toast-leave {\n opacity: 0;\n transform: translateY(-100%);\n transition:\n opacity 400ms cubic-bezier(0.215, 0.61, 0.355, 1),\n transform 400ms cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n /* Keyframes for bottom position */\n @keyframes toast-slide-in-bottom {\n from {\n opacity: 0;\n transform: translateY(100%);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n /* Keyframes for top position */\n @keyframes toast-slide-in-top {\n from {\n opacity: 0;\n transform: translateY(-100%);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n `,\n})\nexport class Toast {\n private readonly toastManager = inject(NgpToastManager);\n private readonly toast = inject(NgpToast);\n protected readonly context = injectToastContext<ToastContext>();\n\n dismiss(): void {\n this.toastManager.dismiss(this.toast);\n }\n}\n\ninterface ToastContext {\n header: string;\n description: string;\n}\n",
221
+ "code": "import { Component, inject } from '@angular/core';\nimport { NgpButton } from 'ng-primitives/button';\nimport { injectToastContext, NgpToast, NgpToastManager } from 'ng-primitives/toast';\n\n@Component({\n selector: 'app-toast',\n imports: [NgpButton],\n hostDirectives: [NgpToast],\n host: {\n 'animate.enter': 'toast-enter',\n 'animate.leave': 'toast-leave',\n },\n template: `\n <p class=\"toast-title\">{{ context.header }}</p>\n <p class=\"toast-description\">{{ context.description }}</p>\n <button class=\"toast-dismiss\" (click)=\"dismiss()\" ngpButton>Dismiss</button>\n `,\n styles: `\n :host {\n position: absolute;\n touch-action: none;\n box-sizing: border-box;\n align-items: center;\n gap: 6px;\n display: inline-grid;\n background: var(--ngp-background);\n box-shadow: var(--ngp-shadow);\n border: 1px solid var(--ngp-border);\n padding: 12px 16px;\n opacity: 0;\n border-radius: 8px;\n z-index: var(--ngp-toast-z-index);\n grid-template-columns: 1fr auto;\n grid-template-rows: min-content min-content;\n column-gap: 12px;\n align-items: center;\n width: 350px;\n height: fit-content;\n transform: var(--y);\n transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n .toast-title {\n color: var(--ngp-text-primary);\n font-size: 0.75rem;\n font-weight: 600;\n margin: 0;\n grid-column: 1 / 2;\n grid-row: 1;\n line-height: 16px;\n user-select: none;\n }\n\n .toast-description {\n font-size: 0.75rem;\n margin: 0;\n color: var(--ngp-text-secondary);\n grid-column: 1 / 2;\n grid-row: 2;\n line-height: 16px;\n user-select: none;\n }\n\n .toast-dismiss {\n background: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n border: none;\n border-radius: 8px;\n padding: 4px 8px;\n font-weight: 600;\n font-size: 12px;\n cursor: pointer;\n grid-column: 2 / 3;\n grid-row: 1 / 3;\n max-height: 27px;\n }\n\n :host[data-position-x='end'] {\n right: 0;\n }\n\n :host[data-position-x='start'] {\n left: 0;\n }\n\n :host[data-position-y='top'] {\n top: 0;\n --lift: 1;\n --lift-amount: calc(var(--lift) * var(--ngp-toast-gap));\n --y: translateY(-100%);\n }\n\n :host[data-position-y='bottom'] {\n bottom: 0;\n --lift: -1;\n --lift-amount: calc(var(--lift) * var(--ngp-toast-gap));\n --y: translateY(100%);\n }\n\n :host[data-enter] {\n opacity: 1;\n --y: translateY(0);\n }\n\n :host[data-exit] {\n opacity: 0;\n --y: translateY(calc(calc(var(--lift) * var(--ngp-toast-gap)) * -1));\n }\n\n :host[data-visible='false'] {\n opacity: 0;\n pointer-events: none;\n }\n\n :host[data-expanded='true']::after {\n content: '';\n position: absolute;\n left: 0;\n height: calc(var(--ngp-toast-gap) + 1px);\n bottom: 100%;\n width: 100%;\n }\n\n :host[data-expanded='false'][data-front='false'] {\n --scale: var(--ngp-toasts-before) * 0.05 + 1;\n --y: translateY(calc(var(--lift-amount) * var(--ngp-toasts-before)))\n scale(calc(-1 * var(--scale)));\n height: var(--ngp-toast-front-height);\n }\n\n :host[data-expanded='true'] {\n --y: translateY(calc(var(--lift) * var(--ngp-toast-offset)));\n height: auto;\n }\n\n :host[data-swiping='true'] {\n transform: var(--y) translateY(var(--ngp-toast-swipe-amount-y, 0))\n translateX(var(--ngp-toast-swipe-amount-x, 0));\n transition: none;\n }\n\n :host[data-swiping='true'][data-swipe-direction='left'] {\n /* Fade out from -45px to -100px swipe */\n opacity: calc(1 - clamp(0, ((-1 * var(--ngp-toast-swipe-x, 0px)) - 45) / 55, 1));\n }\n\n :host[data-swiping='true'][data-swipe-direction='right'] {\n /* Fade out from 45px to 100px swipe */\n opacity: calc(1 - clamp(0, (var(--ngp-toast-swipe-x, 0px) - 45) / 55, 1));\n }\n\n :host[data-swiping='true'][data-swipe-direction='top'] {\n /* Fade out from -45px to -100px swipe */\n opacity: calc(1 - clamp(0, ((-1 * var(--ngp-toast-swipe-y, 0px)) - 45) / 55, 1));\n }\n\n :host[data-swiping='true'][data-swipe-direction='bottom'] {\n /* Fade out from 45px to 100px swipe */\n opacity: calc(1 - clamp(0, (var(--ngp-toast-swipe-y, 0px) - 45) / 55, 1));\n }\n\n /* Truncate text only when toast is not front AND not expanded */\n :host[data-front='false'][data-expanded='false'] .toast-title {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n :host[data-front='false'][data-expanded='false'] .toast-description {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* Angular animations based on position */\n\n /* Bottom position animations */\n :host[data-position-y='bottom'].toast-enter {\n animation: toast-slide-in-bottom 400ms cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n :host[data-position-y='bottom'].toast-leave {\n opacity: 0;\n transform: translateY(100%);\n transition:\n opacity 400ms cubic-bezier(0.215, 0.61, 0.355, 1),\n transform 400ms cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n /* Top position animations */\n :host[data-position-y='top'].toast-enter {\n animation: toast-slide-in-top 400ms cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n :host[data-position-y='top'].toast-leave {\n opacity: 0;\n transform: translateY(-100%);\n transition:\n opacity 400ms cubic-bezier(0.215, 0.61, 0.355, 1),\n transform 400ms cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n\n /* Keyframes for bottom position */\n @keyframes toast-slide-in-bottom {\n from {\n opacity: 0;\n transform: translateY(100%);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n /* Keyframes for top position */\n @keyframes toast-slide-in-top {\n from {\n opacity: 0;\n transform: translateY(-100%);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n :host[data-position-y='bottom'].toast-enter,\n :host[data-position-y='top'].toast-enter {\n animation-duration: 0s;\n }\n\n :host,\n :host[data-position-y='bottom'].toast-leave,\n :host[data-position-y='top'].toast-leave {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class Toast {\n private readonly toastManager = inject(NgpToastManager);\n private readonly toast = inject(NgpToast);\n protected readonly context = injectToastContext<ToastContext>();\n\n dismiss(): void {\n this.toastManager.dismiss(this.toast);\n }\n}\n\ninterface ToastContext {\n header: string;\n description: string;\n}\n",
215
222
  "primitive": "toast",
216
223
  "hasVariants": false,
217
224
  "hasSizes": true
218
225
  },
219
226
  {
220
227
  "name": "toggle",
221
- "code": "import { Component } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgpButton } from 'ng-primitives/button';\nimport { injectToggleState, NgpToggle } from 'ng-primitives/toggle';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'button[app-toggle]',\n hostDirectives: [\n {\n directive: NgpToggle,\n inputs: ['ngpToggleSelected:selected', 'ngpToggleDisabled:disabled'],\n outputs: ['ngpToggleSelectedChange:selectedChange'],\n },\n { directive: NgpButton, inputs: ['disabled'] },\n ],\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n padding-left: 1rem;\n padding-right: 1rem;\n border-radius: 0.5rem;\n color: var(--ngp-text-primary);\n border: none;\n outline: none;\n height: 2.5rem;\n font-weight: 500;\n background-color: var(--ngp-background);\n transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: var(--ngp-button-shadow);\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n :host[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n }\n `,\n providers: [provideValueAccessor(Toggle)],\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Toggle implements ControlValueAccessor {\n /** Access the toggle state. */\n private readonly toggle = injectToggleState();\n\n /** The on change callback */\n private onChange?: ChangeFn<boolean>;\n\n /** The on touched callback */\n protected onTouched?: TouchedFn;\n\n constructor() {\n // Any time the toggle changes, update the form value.\n this.toggle()\n .selectedChange.pipe(takeUntilDestroyed())\n .subscribe(value => this.onChange?.(value));\n }\n\n /** Write a new value to the toggle. */\n writeValue(value: boolean): void {\n this.toggle().setSelected(value, { emit: false });\n }\n\n /** Register a callback function to be called when the value changes. */\n registerOnChange(fn: ChangeFn<boolean>): void {\n this.onChange = fn;\n }\n\n /** Register a callback function to be called when the toggle is touched. */\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n /** Set the disabled state of the toggle. */\n setDisabledState(isDisabled: boolean): void {\n this.toggle().setDisabled(isDisabled);\n }\n}\n",
228
+ "code": "import { Component } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { ControlValueAccessor } from '@angular/forms';\nimport { NgpButton } from 'ng-primitives/button';\nimport { injectToggleState, NgpToggle } from 'ng-primitives/toggle';\nimport { ChangeFn, provideValueAccessor, TouchedFn } from 'ng-primitives/utils';\n\n@Component({\n selector: 'button[app-toggle]',\n hostDirectives: [\n {\n directive: NgpToggle,\n inputs: ['ngpToggleSelected:selected', 'ngpToggleDisabled:disabled'],\n outputs: ['ngpToggleSelectedChange:selectedChange'],\n },\n { directive: NgpButton, inputs: ['disabled'] },\n ],\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n padding-left: 1rem;\n padding-right: 1rem;\n border-radius: 0.5rem;\n color: var(--ngp-text-primary);\n border: none;\n outline: none;\n height: 2.5rem;\n font-weight: 500;\n background-color: var(--ngp-background);\n transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: var(--ngp-button-shadow);\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-hover);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n outline-offset: 2px;\n }\n\n :host[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0s;\n }\n }\n `,\n providers: [provideValueAccessor(Toggle)],\n host: {\n '(focusout)': 'onTouched?.()',\n },\n})\nexport class Toggle implements ControlValueAccessor {\n /** Access the toggle state. */\n private readonly toggle = injectToggleState();\n\n /** The on change callback */\n private onChange?: ChangeFn<boolean>;\n\n /** The on touched callback */\n protected onTouched?: TouchedFn;\n\n constructor() {\n // Any time the toggle changes, update the form value.\n this.toggle()\n .selectedChange.pipe(takeUntilDestroyed())\n .subscribe(value => this.onChange?.(value));\n }\n\n /** Write a new value to the toggle. */\n writeValue(value: boolean): void {\n this.toggle().setSelected(value, { emit: false });\n }\n\n /** Register a callback function to be called when the value changes. */\n registerOnChange(fn: ChangeFn<boolean>): void {\n this.onChange = fn;\n }\n\n /** Register a callback function to be called when the toggle is touched. */\n registerOnTouched(fn: TouchedFn): void {\n this.onTouched = fn;\n }\n\n /** Set the disabled state of the toggle. */\n setDisabledState(isDisabled: boolean): void {\n this.toggle().setDisabled(isDisabled);\n }\n}\n",
222
229
  "primitive": "toggle",
223
230
  "hasVariants": false,
224
231
  "hasSizes": false
225
232
  },
226
233
  {
227
234
  "name": "toggle-group",
228
- "code": "import { Component } from '@angular/core';\nimport { NgpButton } from 'ng-primitives/button';\nimport { NgpToggleGroupItem } from 'ng-primitives/toggle-group';\n\n@Component({\n selector: 'button[app-toggle-group-item]',\n hostDirectives: [\n {\n directive: NgpToggleGroupItem,\n inputs: ['ngpToggleGroupItemValue:value', 'ngpToggleGroupItemDisabled:disabled'],\n },\n {\n directive: NgpButton,\n inputs: ['disabled'],\n },\n ],\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n width: 2rem;\n height: 2rem;\n align-items: center;\n justify-content: center;\n border-radius: 0.25rem;\n border: 1px solid transparent;\n background: transparent;\n outline: none;\n transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);\n box-sizing: border-box;\n color: var(--ngp-text-primary);\n font-size: 1.125rem;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-hover);\n border-color: var(--ngp-border);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n :host[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n border-color: transparent;\n }\n `,\n})\nexport class ToggleGroupItem {}\n",
235
+ "code": "import { Component } from '@angular/core';\nimport { NgpButton } from 'ng-primitives/button';\nimport { NgpToggleGroupItem } from 'ng-primitives/toggle-group';\n\n@Component({\n selector: 'button[app-toggle-group-item]',\n hostDirectives: [\n {\n directive: NgpToggleGroupItem,\n inputs: ['ngpToggleGroupItemValue:value', 'ngpToggleGroupItemDisabled:disabled'],\n },\n {\n directive: NgpButton,\n inputs: ['disabled'],\n },\n ],\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n width: 2rem;\n height: 2rem;\n align-items: center;\n justify-content: center;\n border-radius: 0.25rem;\n border: 1px solid transparent;\n background: transparent;\n outline: none;\n transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);\n box-sizing: border-box;\n color: var(--ngp-text-primary);\n font-size: 1.125rem;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-hover);\n border-color: var(--ngp-border);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n :host[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n border-color: transparent;\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class ToggleGroupItem {}\n",
229
236
  "primitive": "toggle-group",
230
237
  "hasVariants": false,
231
238
  "hasSizes": true
232
239
  },
233
240
  {
234
241
  "name": "toolbar",
235
- "code": "import { Component } from '@angular/core';\nimport { NgpButton } from 'ng-primitives/button';\nimport { NgpRovingFocusItem } from 'ng-primitives/roving-focus';\n\n@Component({\n selector: 'button[app-toolbar-button]',\n hostDirectives: [\n { directive: NgpButton, inputs: ['disabled'] },\n {\n directive: NgpRovingFocusItem,\n inputs: ['ngpRovingFocusItemDisabled:disabled'],\n },\n ],\n host: {\n type: 'button',\n },\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n width: 2rem;\n height: 2rem;\n align-items: center;\n justify-content: center;\n border-radius: 0.25rem;\n border: 1px solid transparent;\n background: transparent;\n outline: none;\n transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);\n box-sizing: border-box;\n color: var(--ngp-text-primary);\n cursor: pointer;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-hover);\n border-color: var(--ngp-border);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n :host[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n }\n `,\n})\nexport class ToolbarButton {}\n",
242
+ "code": "import { Component } from '@angular/core';\nimport { NgpButton } from 'ng-primitives/button';\nimport { NgpRovingFocusItem } from 'ng-primitives/roving-focus';\n\n@Component({\n selector: 'button[app-toolbar-button]',\n hostDirectives: [\n { directive: NgpButton, inputs: ['disabled'] },\n {\n directive: NgpRovingFocusItem,\n inputs: ['ngpRovingFocusItemDisabled:disabled'],\n },\n ],\n host: {\n type: 'button',\n },\n template: `\n <ng-content />\n `,\n styles: `\n :host {\n display: flex;\n width: 2rem;\n height: 2rem;\n align-items: center;\n justify-content: center;\n border-radius: 0.25rem;\n border: 1px solid transparent;\n background: transparent;\n outline: none;\n transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);\n box-sizing: border-box;\n color: var(--ngp-text-primary);\n cursor: pointer;\n }\n\n :host[data-hover] {\n background-color: var(--ngp-background-hover);\n border-color: var(--ngp-border);\n }\n\n :host[data-focus-visible] {\n outline: 2px solid var(--ngp-focus-ring);\n }\n\n :host[data-press] {\n background-color: var(--ngp-background-active);\n }\n\n :host[data-selected] {\n background-color: var(--ngp-background-inverse);\n color: var(--ngp-text-inverse);\n }\n\n /* Reduce motion for users who prefer reduced motion. */\n @media (prefers-reduced-motion: reduce) {\n :host {\n transition-duration: 0s;\n }\n }\n `,\n})\nexport class ToolbarButton {}\n",
236
243
  "primitive": "toolbar",
237
244
  "hasVariants": false,
238
245
  "hasSizes": false