@ng-primitives/mcp 0.127.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.
@@ -76,7 +76,7 @@
76
76
  }
77
77
  ],
78
78
  "reusableComponent": {
79
- "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",
79
+ "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",
80
80
  "hasVariants": false,
81
81
  "hasSizes": true
82
82
  }
@@ -93,22 +93,51 @@
93
93
  "NgpPromptComposerDictation",
94
94
  "NgpThreadViewport",
95
95
  "NgpThreadSuggestion",
96
+ "NgpPromptComposerStateToken",
97
+ "ngpPromptComposer",
96
98
  "providePromptComposerState",
97
99
  "injectPromptComposerState",
100
+ "type NgpPromptComposerState",
101
+ "type NgpPromptComposerProps",
102
+ "NgpPromptComposerDictationStateToken",
103
+ "ngpPromptComposerDictation",
98
104
  "providePromptComposerDictationState",
99
105
  "injectPromptComposerDictationState",
106
+ "type NgpPromptComposerDictationState",
107
+ "type NgpPromptComposerDictationProps",
108
+ "NgpPromptComposerInputStateToken",
109
+ "ngpPromptComposerInput",
100
110
  "providePromptComposerInputState",
101
111
  "injectPromptComposerInputState",
112
+ "type NgpPromptComposerInputState",
113
+ "NgpPromptComposerSubmitStateToken",
114
+ "ngpPromptComposerSubmit",
102
115
  "providePromptComposerSubmitState",
103
116
  "injectPromptComposerSubmitState",
117
+ "type NgpPromptComposerSubmitState",
118
+ "type NgpPromptComposerSubmitProps",
119
+ "NgpThreadStateToken",
120
+ "ngpThread",
104
121
  "provideThreadState",
105
122
  "injectThreadState",
123
+ "type NgpThreadState",
124
+ "NgpThreadSuggestionStateToken",
125
+ "ngpThreadSuggestion",
106
126
  "provideThreadSuggestionState",
107
127
  "injectThreadSuggestionState",
128
+ "type NgpThreadSuggestionState",
129
+ "type NgpThreadSuggestionProps",
130
+ "NgpThreadViewportStateToken",
131
+ "ngpThreadViewport",
108
132
  "provideThreadViewportState",
109
133
  "injectThreadViewportState",
134
+ "type NgpThreadViewportState",
135
+ "type NgpThreadViewportProps",
136
+ "NgpThreadMessageStateToken",
137
+ "ngpThreadMessage",
110
138
  "provideThreadMessageState",
111
- "injectThreadMessageState"
139
+ "injectThreadMessageState",
140
+ "type NgpThreadMessageState"
112
141
  ],
113
142
  "hasSecondaryEntryPoint": true,
114
143
  "category": "utility",
@@ -281,7 +310,7 @@
281
310
  }
282
311
  ],
283
312
  "reusableComponent": {
284
- "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",
313
+ "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",
285
314
  "hasVariants": true,
286
315
  "hasSizes": true
287
316
  }
@@ -518,7 +547,7 @@
518
547
  }
519
548
  ],
520
549
  "reusableComponent": {
521
- "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",
550
+ "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",
522
551
  "hasVariants": false,
523
552
  "hasSizes": true
524
553
  }
@@ -567,7 +596,7 @@
567
596
  }
568
597
  ],
569
598
  "reusableComponent": {
570
- "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",
599
+ "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",
571
600
  "hasVariants": false,
572
601
  "hasSizes": true
573
602
  }
@@ -601,11 +630,56 @@
601
630
  "transformToFirstDayOfWeekNumber",
602
631
  "type NgpDatePickerFirstDayOfWeekNumber",
603
632
  "type NgpDatePickerFirstDayOfWeekNumberInput",
633
+ "NgpDatePickerStateToken",
634
+ "ngpDatePicker",
604
635
  "injectDatePickerState",
605
636
  "provideDatePickerState",
637
+ "type NgpDatePickerState",
638
+ "type NgpDatePickerProps",
639
+ "type NgpDateControllerState",
606
640
  "NgpDateRangePicker",
641
+ "NgpDateRangePickerStateToken",
642
+ "ngpDateRangePicker",
643
+ "injectDateRangePickerState",
607
644
  "provideDateRangePickerState",
608
- "injectDateRangePickerState"
645
+ "type NgpDateRangePickerState",
646
+ "type NgpDateRangePickerProps",
647
+ "NgpDatePickerDateButtonStateToken",
648
+ "ngpDatePickerDateButton",
649
+ "injectDatePickerDateButtonState",
650
+ "provideDatePickerDateButtonState",
651
+ "type NgpDatePickerDateButtonState",
652
+ "type NgpDatePickerDateButtonProps",
653
+ "NgpDatePickerGridStateToken",
654
+ "ngpDatePickerGrid",
655
+ "injectDatePickerGridState",
656
+ "provideDatePickerGridState",
657
+ "type NgpDatePickerGridState",
658
+ "type NgpDatePickerGridProps",
659
+ "NgpDatePickerLabelStateToken",
660
+ "ngpDatePickerLabel",
661
+ "injectDatePickerLabelState",
662
+ "provideDatePickerLabelState",
663
+ "type NgpDatePickerLabelState",
664
+ "type NgpDatePickerLabelProps",
665
+ "NgpDatePickerCellStateToken",
666
+ "ngpDatePickerCell",
667
+ "injectDatePickerCellState",
668
+ "provideDatePickerCellState",
669
+ "type NgpDatePickerCellState",
670
+ "type NgpDatePickerCellProps",
671
+ "NgpDatePickerNextMonthStateToken",
672
+ "ngpDatePickerNextMonth",
673
+ "injectDatePickerNextMonthState",
674
+ "provideDatePickerNextMonthState",
675
+ "type NgpDatePickerNextMonthState",
676
+ "type NgpDatePickerNextMonthProps",
677
+ "NgpDatePickerPreviousMonthStateToken",
678
+ "ngpDatePickerPreviousMonth",
679
+ "injectDatePickerPreviousMonthState",
680
+ "provideDatePickerPreviousMonthState",
681
+ "type NgpDatePickerPreviousMonthState",
682
+ "type NgpDatePickerPreviousMonthProps"
609
683
  ],
610
684
  "hasSecondaryEntryPoint": true,
611
685
  "category": "date-time",
@@ -631,7 +705,7 @@
631
705
  }
632
706
  ],
633
707
  "reusableComponent": {
634
- "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",
708
+ "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",
635
709
  "hasVariants": false,
636
710
  "hasSizes": true
637
711
  }
@@ -744,7 +818,7 @@
744
818
  }
745
819
  ],
746
820
  "reusableComponent": {
747
- "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",
821
+ "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",
748
822
  "hasVariants": false,
749
823
  "hasSizes": true
750
824
  }
@@ -850,7 +924,7 @@
850
924
  }
851
925
  ],
852
926
  "reusableComponent": {
853
- "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",
927
+ "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",
854
928
  "hasVariants": false,
855
929
  "hasSizes": false
856
930
  }
@@ -887,12 +961,19 @@
887
961
  "name": "input-otp",
888
962
  "entryPoint": "ng-primitives/input-otp",
889
963
  "exports": [
890
- "NgpInputOtpInput",
891
- "NgpInputOtpSlot",
892
964
  "NgpInputOtp",
893
- "type NgpInputOtpInputMode",
965
+ "NgpInputOtpInputMode",
894
966
  "injectInputOtpState",
895
- "provideInputOtpState"
967
+ "ngpInputOtp",
968
+ "NgpInputOtpProps",
969
+ "NgpInputOtpState",
970
+ "provideInputOtpState",
971
+ "NgpInputOtpInput",
972
+ "ngpInputOtpInput",
973
+ "NgpInputOtpInputStateToken",
974
+ "NgpInputOtpSlot",
975
+ "ngpInputOtpSlot",
976
+ "NgpInputOtpSlotStateToken"
896
977
  ],
897
978
  "hasSecondaryEntryPoint": true,
898
979
  "category": "form",
@@ -908,7 +989,7 @@
908
989
  }
909
990
  ],
910
991
  "reusableComponent": {
911
- "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",
992
+ "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",
912
993
  "hasVariants": false,
913
994
  "hasSizes": true
914
995
  }
@@ -946,14 +1027,43 @@
946
1027
  "NgpListboxSection",
947
1028
  "NgpListboxTrigger",
948
1029
  "NgpListbox",
1030
+ "NgpListboxTriggerStateToken",
1031
+ "ngpListboxTrigger",
1032
+ "injectListboxTriggerState",
1033
+ "provideListboxTriggerState",
1034
+ "type NgpListboxTriggerState",
1035
+ "type NgpListboxTriggerProps",
1036
+ "NgpListboxSectionStateToken",
1037
+ "ngpListboxSection",
1038
+ "injectListboxSectionState",
1039
+ "provideListboxSectionState",
1040
+ "type NgpListboxSectionState",
1041
+ "type NgpListboxSectionProps",
1042
+ "NgpListboxOptionStateToken",
1043
+ "ngpListboxOption",
1044
+ "injectListboxOptionState",
1045
+ "provideListboxOptionState",
1046
+ "type NgpListboxOptionState",
1047
+ "type NgpListboxOptionProps",
1048
+ "NgpListboxHeaderStateToken",
1049
+ "ngpListboxHeader",
1050
+ "injectListboxHeaderState",
1051
+ "provideListboxHeaderState",
1052
+ "type NgpListboxHeaderState",
1053
+ "type NgpListboxHeaderProps",
1054
+ "NgpListboxStateToken",
1055
+ "ngpListbox",
949
1056
  "injectListboxState",
950
- "provideListboxState"
1057
+ "provideListboxState",
1058
+ "type NgpListboxState",
1059
+ "type NgpListboxProps"
951
1060
  ],
952
1061
  "hasSecondaryEntryPoint": true,
953
1062
  "category": "form",
954
1063
  "description": "A listbox presents a set of choices and lets users select one or multiple options.",
955
1064
  "accessibility": [
956
- "ARIA Listbox"
1065
+ "ARIA Listbox",
1066
+ "keyboard navigation"
957
1067
  ],
958
1068
  "examples": [
959
1069
  {
@@ -975,6 +1085,7 @@
975
1085
  "injectOverlayContext as injectMenuContext",
976
1086
  "NgpMenuConfig",
977
1087
  "provideMenuConfig",
1088
+ "type NgpMenuTriggerType",
978
1089
  "NgpMenuItem",
979
1090
  "ngpMenuItem",
980
1091
  "injectMenuItemState",
@@ -1052,17 +1163,27 @@
1052
1163
  },
1053
1164
  {
1054
1165
  "name": "example-4",
1166
+ "code": "<!-- Keep the menu inside a container element -->\n<div #boundary>\n <button [ngpMenuTrigger]=\"menu\" [ngpMenuTriggerFlip]=\"{boundary: boundary}\">\n Menu constrained to a container\n </button>\n</div>\n\n<!-- Keep the menu inside whatever scrolls the trigger -->\n<div style=\"overflow: auto\">\n <button [ngpMenuTrigger]=\"menu\" [ngpMenuTriggerFlip]=\"{altBoundary: true}\">\n Menu constrained to the trigger's scroll container\n </button>\n</div>\n\n<!-- Measure against the whole document rather than the viewport -->\n<button [ngpMenuTrigger]=\"menu\" [ngpMenuTriggerFlip]=\"{rootBoundary: 'document'}\">\n Menu that does not flip while off-screen\n</button>",
1167
+ "description": "Overflow Boundary"
1168
+ },
1169
+ {
1170
+ "name": "example-5",
1171
+ "code": "<button [ngpMenuTrigger]=\"menu\" [ngpMenuTriggerFlip]=\"{crossAxis: false}\">\n Keep the alignment\n</button>\n<button [ngpMenuTrigger]=\"menu\" [ngpMenuTriggerShift]=\"{crossAxis: true}\">\n Shift on both axes\n</button>",
1172
+ "description": "Overflow Boundary"
1173
+ },
1174
+ {
1175
+ "name": "example-6",
1055
1176
  "code": "<!-- Enable Enter key to toggle menu -->\n<button [ngpMenuTrigger]=\"menu\" [ngpMenuTriggerOpenTriggers]=\"['click', 'enter']\">Menu</button>\n\n<!-- Enable arrow keys (placement-aware) -->\n<button\n [ngpMenuTrigger]=\"menu\"\n [ngpMenuTriggerOpenTriggers]=\"['arrowkey']\"\n ngpMenuTriggerPlacement=\"right-start\"\n>\n Sidebar Menu\n</button>\n\n<!-- Combine triggers for best UX -->\n<button\n [ngpMenuTrigger]=\"menu\"\n [ngpMenuTriggerOpenTriggers]=\"['hover', 'arrowkey', 'enter']\"\n ngpMenuTriggerPlacement=\"right-start\"\n>\n Navigation Item\n</button>",
1056
1177
  "description": "Keyboard Triggers"
1057
1178
  },
1058
1179
  {
1059
- "name": "example-5",
1180
+ "name": "example-7",
1060
1181
  "code": "import { provideMenuConfig } from 'ng-primitives/menu';\n\nbootstrapApplication(AppComponent, {\n providers: [\n provideMenuConfig({\n offset: 4,\n placement: 'top',\n flip: true,\n container: document.body,\n scrollBehavior: 'reposition',\n cooldown: 0,\n }),\n ],\n});",
1061
1182
  "description": "Global Configuration"
1062
1183
  }
1063
1184
  ],
1064
1185
  "reusableComponent": {
1065
- "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",
1186
+ "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",
1066
1187
  "hasVariants": false,
1067
1188
  "hasSizes": true
1068
1189
  }
@@ -1072,12 +1193,22 @@
1072
1193
  "entryPoint": "ng-primitives/meter",
1073
1194
  "exports": [
1074
1195
  "NgpMeter",
1075
- "provideMeterState",
1196
+ "NgpMeterValueTextFn",
1076
1197
  "injectMeterState",
1077
- "NgpMeterTrack",
1198
+ "ngpMeter",
1199
+ "NgpMeterProps",
1200
+ "NgpMeterState",
1201
+ "provideMeterState",
1078
1202
  "NgpMeterIndicator",
1203
+ "ngpMeterIndicator",
1204
+ "NgpMeterIndicatorStateToken",
1205
+ "NgpMeterLabel",
1206
+ "ngpMeterLabel",
1207
+ "NgpMeterLabelStateToken",
1208
+ "NgpMeterTrack",
1079
1209
  "NgpMeterValue",
1080
- "NgpMeterLabel"
1210
+ "ngpMeterValue",
1211
+ "NgpMeterValueStateToken"
1081
1212
  ],
1082
1213
  "hasSecondaryEntryPoint": true,
1083
1214
  "category": "feedback",
@@ -1096,7 +1227,7 @@
1096
1227
  }
1097
1228
  ],
1098
1229
  "reusableComponent": {
1099
- "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",
1230
+ "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",
1100
1231
  "hasVariants": false,
1101
1232
  "hasSizes": true
1102
1233
  }
@@ -1237,7 +1368,7 @@
1237
1368
  }
1238
1369
  ],
1239
1370
  "reusableComponent": {
1240
- "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",
1371
+ "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",
1241
1372
  "hasVariants": false,
1242
1373
  "hasSizes": true
1243
1374
  }
@@ -1308,7 +1439,7 @@
1308
1439
  }
1309
1440
  ],
1310
1441
  "reusableComponent": {
1311
- "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",
1442
+ "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",
1312
1443
  "hasVariants": false,
1313
1444
  "hasSizes": true
1314
1445
  }
@@ -1360,7 +1491,7 @@
1360
1491
  }
1361
1492
  ],
1362
1493
  "reusableComponent": {
1363
- "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",
1494
+ "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",
1364
1495
  "hasVariants": false,
1365
1496
  "hasSizes": true
1366
1497
  }
@@ -1417,6 +1548,16 @@
1417
1548
  },
1418
1549
  {
1419
1550
  "name": "example-3",
1551
+ "code": "<!-- Keep the popover inside a container element -->\n<div #boundary>\n <button [ngpPopoverTrigger]=\"popover\" [ngpPopoverTriggerFlip]=\"{boundary: boundary}\">\n Popover constrained to a container\n </button>\n</div>\n\n<!-- Keep the popover inside whatever scrolls the trigger -->\n<div style=\"overflow: auto\">\n <button [ngpPopoverTrigger]=\"popover\" [ngpPopoverTriggerFlip]=\"{altBoundary: true}\">\n Popover constrained to the trigger's scroll container\n </button>\n</div>\n\n<!-- Measure against the whole document rather than the viewport -->\n<button [ngpPopoverTrigger]=\"popover\" [ngpPopoverTriggerFlip]=\"{rootBoundary: 'document'}\">\n Popover that does not flip while off-screen\n</button>",
1552
+ "description": "Overflow Boundary"
1553
+ },
1554
+ {
1555
+ "name": "example-4",
1556
+ "code": "<button [ngpPopoverTrigger]=\"popover\" [ngpPopoverTriggerFlip]=\"{crossAxis: false}\">\n Keep the alignment\n</button>\n<button [ngpPopoverTrigger]=\"popover\" [ngpPopoverTriggerShift]=\"{crossAxis: true}\">\n Shift on both axes\n</button>",
1557
+ "description": "Overflow Boundary"
1558
+ },
1559
+ {
1560
+ "name": "example-5",
1420
1561
  "code": "import { providePopoverConfig } from 'ng-primitives/popover';\n\nbootstrapApplication(AppComponent, {\n providers: [\n providePopoverConfig({\n offset: 4,\n placement: 'top',\n showDelay: 0,\n hideDelay: 0,\n flip: true,\n container: document.body,\n closeOnOutsideClick: true,\n scrollBehavior: 'reposition',\n cooldown: 0,\n }),\n ],\n});",
1421
1562
  "description": "Global Configuration"
1422
1563
  }
@@ -1467,6 +1608,11 @@
1467
1608
  "NgpPortal",
1468
1609
  "NgpTemplatePortal",
1469
1610
  "NgpPosition",
1611
+ "NgpBoundary",
1612
+ "NgpMiddleware",
1613
+ "NgpPlacement",
1614
+ "NgpPositioningStrategy",
1615
+ "NgpRootBoundary",
1470
1616
  "BlockScrollStrategy",
1471
1617
  "CloseScrollStrategy",
1472
1618
  "NoopScrollStrategy",
@@ -1525,7 +1671,7 @@
1525
1671
  }
1526
1672
  ],
1527
1673
  "reusableComponent": {
1528
- "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",
1674
+ "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",
1529
1675
  "hasVariants": false,
1530
1676
  "hasSizes": true
1531
1677
  }
@@ -1608,7 +1754,7 @@
1608
1754
  }
1609
1755
  ],
1610
1756
  "reusableComponent": {
1611
- "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 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",
1757
+ "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",
1612
1758
  "hasVariants": false,
1613
1759
  "hasSizes": true
1614
1760
  }
@@ -1673,10 +1819,16 @@
1673
1819
  "name": "search",
1674
1820
  "entryPoint": "ng-primitives/search",
1675
1821
  "exports": [
1676
- "NgpSearchClear",
1677
1822
  "NgpSearch",
1678
1823
  "injectSearchState",
1679
- "provideSearchState"
1824
+ "ngpSearch",
1825
+ "NgpSearchState",
1826
+ "provideSearchState",
1827
+ "NgpSearchClear",
1828
+ "injectSearchClearState",
1829
+ "ngpSearchClear",
1830
+ "NgpSearchClearState",
1831
+ "provideSearchClearState"
1680
1832
  ],
1681
1833
  "hasSecondaryEntryPoint": true,
1682
1834
  "category": "form",
@@ -1758,7 +1910,7 @@
1758
1910
  }
1759
1911
  ],
1760
1912
  "reusableComponent": {
1761
- "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",
1913
+ "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",
1762
1914
  "hasVariants": false,
1763
1915
  "hasSizes": true
1764
1916
  }
@@ -1913,7 +2065,7 @@
1913
2065
  }
1914
2066
  ],
1915
2067
  "reusableComponent": {
1916
- "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",
2068
+ "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",
1917
2069
  "hasVariants": false,
1918
2070
  "hasSizes": false
1919
2071
  }
@@ -2041,7 +2193,7 @@
2041
2193
  }
2042
2194
  ],
2043
2195
  "reusableComponent": {
2044
- "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",
2196
+ "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",
2045
2197
  "hasVariants": false,
2046
2198
  "hasSizes": true
2047
2199
  }
@@ -2069,7 +2221,7 @@
2069
2221
  }
2070
2222
  ],
2071
2223
  "reusableComponent": {
2072
- "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",
2224
+ "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",
2073
2225
  "hasVariants": false,
2074
2226
  "hasSizes": false
2075
2227
  }
@@ -2112,7 +2264,7 @@
2112
2264
  }
2113
2265
  ],
2114
2266
  "reusableComponent": {
2115
- "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",
2267
+ "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",
2116
2268
  "hasVariants": false,
2117
2269
  "hasSizes": true
2118
2270
  }
@@ -2142,7 +2294,7 @@
2142
2294
  }
2143
2295
  ],
2144
2296
  "reusableComponent": {
2145
- "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",
2297
+ "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",
2146
2298
  "hasVariants": false,
2147
2299
  "hasSizes": false
2148
2300
  }
@@ -2207,21 +2359,31 @@
2207
2359
  },
2208
2360
  {
2209
2361
  "name": "example-3",
2362
+ "code": "<!-- Keep the tooltip inside a container element -->\n<div #boundary>\n <button [ngpTooltipTrigger]=\"tooltip\" [ngpTooltipTriggerFlip]=\"{boundary: boundary}\">\n Tooltip constrained to a container\n </button>\n</div>\n\n<!-- Keep the tooltip inside whatever scrolls the trigger -->\n<div style=\"overflow: auto\">\n <button [ngpTooltipTrigger]=\"tooltip\" [ngpTooltipTriggerFlip]=\"{altBoundary: true}\">\n Tooltip constrained to the trigger's scroll container\n </button>\n</div>\n\n<!-- Measure against the whole document rather than the viewport -->\n<button [ngpTooltipTrigger]=\"tooltip\" [ngpTooltipTriggerFlip]=\"{rootBoundary: 'document'}\">\n Tooltip that does not flip while off-screen\n</button>",
2363
+ "description": "Overflow Boundary"
2364
+ },
2365
+ {
2366
+ "name": "example-4",
2367
+ "code": "<button [ngpTooltipTrigger]=\"tooltip\" [ngpTooltipTriggerFlip]=\"{crossAxis: false}\">\n Keep the alignment\n</button>\n<button [ngpTooltipTrigger]=\"tooltip\" [ngpTooltipTriggerShift]=\"{crossAxis: true}\">\n Shift on both axes\n</button>",
2368
+ "description": "Overflow Boundary"
2369
+ },
2370
+ {
2371
+ "name": "example-5",
2210
2372
  "code": "<!-- Simple usage - uses text content automatically -->\n<div class=\"truncated-text\" ngpTooltipTrigger>\n This text might be truncated with ellipsis and show the full content in the tooltip\n</div>\n\n<!-- Passing content directly takes precedence -->\n<button [ngpTooltipTrigger]=\"myToolip\">This won't show a tooltip unless content is provided</button>",
2211
2373
  "description": "Using Text Content as Tooltip"
2212
2374
  },
2213
2375
  {
2214
- "name": "example-4",
2376
+ "name": "example-6",
2215
2377
  "code": "<div\n class=\"truncated-text\"\n appTooltipTrigger=\"This tooltip only shows when text overflows\"\n ngpTooltipTriggerShowOnOverflow\n>\n This text might be truncated\n</div>",
2216
2378
  "description": "Conditional Tooltips"
2217
2379
  },
2218
2380
  {
2219
- "name": "example-5",
2381
+ "name": "example-7",
2220
2382
  "code": "<!-- Default: closes when pointer leaves trigger -->\n<button [ngpTooltipTrigger]=\"tooltip\">Default behavior</button>\n\n<!-- Opt in: stays open while pointer moves into tooltip content -->\n<button [ngpTooltipTrigger]=\"tooltip\" ngpTooltipTriggerHoverableContent=\"true\">\n Hover bridge enabled\n</button>",
2221
2383
  "description": "Hoverable Tooltip Content"
2222
2384
  },
2223
2385
  {
2224
- "name": "example-6",
2386
+ "name": "example-8",
2225
2387
  "code": "import { provideTooltipConfig } from 'ng-primitives/tooltip';\n\nbootstrapApplication(AppComponent, {\n providers: [\n provideTooltipConfig({\n offset: 4,\n placement: 'top',\n showDelay: 0,\n hideDelay: 500,\n flip: true,\n container: document.body,\n showOnOverflow: false,\n useTextContent: true,\n scrollBehavior: 'reposition',\n cooldown: 300,\n hoverableContent: false,\n }),\n ],\n});",
2226
2388
  "description": "Global Configuration"
2227
2389
  }