@ngneers/controls 0.0.1-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/fesm2022/ngneers-controls-accordion.mjs +234 -0
  2. package/fesm2022/ngneers-controls-accordion.mjs.map +1 -0
  3. package/fesm2022/ngneers-controls-api-ng.mjs +959 -0
  4. package/fesm2022/ngneers-controls-api-ng.mjs.map +1 -0
  5. package/fesm2022/ngneers-controls-api-resize.mjs +712 -0
  6. package/fesm2022/ngneers-controls-api-resize.mjs.map +1 -0
  7. package/fesm2022/ngneers-controls-api.mjs +347 -0
  8. package/fesm2022/ngneers-controls-api.mjs.map +1 -0
  9. package/fesm2022/ngneers-controls-avatar.mjs +77 -0
  10. package/fesm2022/ngneers-controls-avatar.mjs.map +1 -0
  11. package/fesm2022/ngneers-controls-base.mjs +620 -0
  12. package/fesm2022/ngneers-controls-base.mjs.map +1 -0
  13. package/fesm2022/ngneers-controls-breadcrumb.mjs +100 -0
  14. package/fesm2022/ngneers-controls-breadcrumb.mjs.map +1 -0
  15. package/fesm2022/ngneers-controls-button-group.mjs +110 -0
  16. package/fesm2022/ngneers-controls-button-group.mjs.map +1 -0
  17. package/fesm2022/ngneers-controls-button.mjs +83 -0
  18. package/fesm2022/ngneers-controls-button.mjs.map +1 -0
  19. package/fesm2022/ngneers-controls-calendar-time-CJT5AFlA.mjs +74 -0
  20. package/fesm2022/ngneers-controls-calendar-time-CJT5AFlA.mjs.map +1 -0
  21. package/fesm2022/ngneers-controls-calendar.mjs +801 -0
  22. package/fesm2022/ngneers-controls-calendar.mjs.map +1 -0
  23. package/fesm2022/ngneers-controls-checkbox.mjs +63 -0
  24. package/fesm2022/ngneers-controls-checkbox.mjs.map +1 -0
  25. package/fesm2022/ngneers-controls-chip.mjs +55 -0
  26. package/fesm2022/ngneers-controls-chip.mjs.map +1 -0
  27. package/fesm2022/ngneers-controls-default-icons.mjs +82 -0
  28. package/fesm2022/ngneers-controls-default-icons.mjs.map +1 -0
  29. package/fesm2022/ngneers-controls-defer.mjs +72 -0
  30. package/fesm2022/ngneers-controls-defer.mjs.map +1 -0
  31. package/fesm2022/ngneers-controls-dialog.mjs +407 -0
  32. package/fesm2022/ngneers-controls-dialog.mjs.map +1 -0
  33. package/fesm2022/ngneers-controls-directives.mjs +498 -0
  34. package/fesm2022/ngneers-controls-directives.mjs.map +1 -0
  35. package/fesm2022/ngneers-controls-drawer.mjs +271 -0
  36. package/fesm2022/ngneers-controls-drawer.mjs.map +1 -0
  37. package/fesm2022/ngneers-controls-edit-inplace.mjs +127 -0
  38. package/fesm2022/ngneers-controls-edit-inplace.mjs.map +1 -0
  39. package/fesm2022/ngneers-controls-errors.mjs +329 -0
  40. package/fesm2022/ngneers-controls-errors.mjs.map +1 -0
  41. package/fesm2022/ngneers-controls-filter.mjs +708 -0
  42. package/fesm2022/ngneers-controls-filter.mjs.map +1 -0
  43. package/fesm2022/ngneers-controls-hint.mjs +153 -0
  44. package/fesm2022/ngneers-controls-hint.mjs.map +1 -0
  45. package/fesm2022/ngneers-controls-i18n-translations-de.mjs +170 -0
  46. package/fesm2022/ngneers-controls-i18n-translations-de.mjs.map +1 -0
  47. package/fesm2022/ngneers-controls-i18n-translations-en.mjs +170 -0
  48. package/fesm2022/ngneers-controls-i18n-translations-en.mjs.map +1 -0
  49. package/fesm2022/ngneers-controls-i18n.mjs +57 -0
  50. package/fesm2022/ngneers-controls-i18n.mjs.map +1 -0
  51. package/fesm2022/ngneers-controls-icon.mjs +195 -0
  52. package/fesm2022/ngneers-controls-icon.mjs.map +1 -0
  53. package/fesm2022/ngneers-controls-inplace.mjs +150 -0
  54. package/fesm2022/ngneers-controls-inplace.mjs.map +1 -0
  55. package/fesm2022/ngneers-controls-input-field.mjs +165 -0
  56. package/fesm2022/ngneers-controls-input-field.mjs.map +1 -0
  57. package/fesm2022/ngneers-controls-input.mjs +72 -0
  58. package/fesm2022/ngneers-controls-input.mjs.map +1 -0
  59. package/fesm2022/ngneers-controls-item-view.mjs +473 -0
  60. package/fesm2022/ngneers-controls-item-view.mjs.map +1 -0
  61. package/fesm2022/ngneers-controls-list-box.mjs +281 -0
  62. package/fesm2022/ngneers-controls-list-box.mjs.map +1 -0
  63. package/fesm2022/ngneers-controls-mask-input.mjs +888 -0
  64. package/fesm2022/ngneers-controls-mask-input.mjs.map +1 -0
  65. package/fesm2022/ngneers-controls-menu.mjs +371 -0
  66. package/fesm2022/ngneers-controls-menu.mjs.map +1 -0
  67. package/fesm2022/ngneers-controls-message.mjs +30 -0
  68. package/fesm2022/ngneers-controls-message.mjs.map +1 -0
  69. package/fesm2022/ngneers-controls-number-input.mjs +366 -0
  70. package/fesm2022/ngneers-controls-number-input.mjs.map +1 -0
  71. package/fesm2022/ngneers-controls-paginator.mjs +129 -0
  72. package/fesm2022/ngneers-controls-paginator.mjs.map +1 -0
  73. package/fesm2022/ngneers-controls-popover.mjs +215 -0
  74. package/fesm2022/ngneers-controls-popover.mjs.map +1 -0
  75. package/fesm2022/ngneers-controls-progress.mjs +98 -0
  76. package/fesm2022/ngneers-controls-progress.mjs.map +1 -0
  77. package/fesm2022/ngneers-controls-radio.mjs +152 -0
  78. package/fesm2022/ngneers-controls-radio.mjs.map +1 -0
  79. package/fesm2022/ngneers-controls-roving-focus.mjs +323 -0
  80. package/fesm2022/ngneers-controls-roving-focus.mjs.map +1 -0
  81. package/fesm2022/ngneers-controls-scroll-shadow.mjs +65 -0
  82. package/fesm2022/ngneers-controls-scroll-shadow.mjs.map +1 -0
  83. package/fesm2022/ngneers-controls-scroller.mjs +308 -0
  84. package/fesm2022/ngneers-controls-scroller.mjs.map +1 -0
  85. package/fesm2022/ngneers-controls-select-button.mjs +58 -0
  86. package/fesm2022/ngneers-controls-select-button.mjs.map +1 -0
  87. package/fesm2022/ngneers-controls-select.mjs +417 -0
  88. package/fesm2022/ngneers-controls-select.mjs.map +1 -0
  89. package/fesm2022/ngneers-controls-slider.mjs +167 -0
  90. package/fesm2022/ngneers-controls-slider.mjs.map +1 -0
  91. package/fesm2022/ngneers-controls-snackbar.mjs +414 -0
  92. package/fesm2022/ngneers-controls-snackbar.mjs.map +1 -0
  93. package/fesm2022/ngneers-controls-spin-buttons.mjs +126 -0
  94. package/fesm2022/ngneers-controls-spin-buttons.mjs.map +1 -0
  95. package/fesm2022/ngneers-controls-spinner.mjs +184 -0
  96. package/fesm2022/ngneers-controls-spinner.mjs.map +1 -0
  97. package/fesm2022/ngneers-controls-splitter.mjs +604 -0
  98. package/fesm2022/ngneers-controls-splitter.mjs.map +1 -0
  99. package/fesm2022/ngneers-controls-state.mjs +185 -0
  100. package/fesm2022/ngneers-controls-state.mjs.map +1 -0
  101. package/fesm2022/ngneers-controls-switch.mjs +39 -0
  102. package/fesm2022/ngneers-controls-switch.mjs.map +1 -0
  103. package/fesm2022/ngneers-controls-table.mjs +2634 -0
  104. package/fesm2022/ngneers-controls-table.mjs.map +1 -0
  105. package/fesm2022/ngneers-controls-tabs.mjs +336 -0
  106. package/fesm2022/ngneers-controls-tabs.mjs.map +1 -0
  107. package/fesm2022/ngneers-controls-tag.mjs +30 -0
  108. package/fesm2022/ngneers-controls-tag.mjs.map +1 -0
  109. package/fesm2022/ngneers-controls-toast.mjs +350 -0
  110. package/fesm2022/ngneers-controls-toast.mjs.map +1 -0
  111. package/fesm2022/ngneers-controls-toggle-button.mjs +70 -0
  112. package/fesm2022/ngneers-controls-toggle-button.mjs.map +1 -0
  113. package/fesm2022/ngneers-controls-tooltip.mjs +506 -0
  114. package/fesm2022/ngneers-controls-tooltip.mjs.map +1 -0
  115. package/fesm2022/ngneers-controls-tree.mjs +599 -0
  116. package/fesm2022/ngneers-controls-tree.mjs.map +1 -0
  117. package/fesm2022/ngneers-controls-upload.mjs +377 -0
  118. package/fesm2022/ngneers-controls-upload.mjs.map +1 -0
  119. package/fesm2022/ngneers-controls-utils-ng.mjs +411 -0
  120. package/fesm2022/ngneers-controls-utils-ng.mjs.map +1 -0
  121. package/fesm2022/ngneers-controls-utils.mjs +261 -0
  122. package/fesm2022/ngneers-controls-utils.mjs.map +1 -0
  123. package/fesm2022/ngneers-controls.mjs +6 -0
  124. package/fesm2022/ngneers-controls.mjs.map +1 -0
  125. package/package.json +302 -0
  126. package/types/ngneers-controls-accordion.d.ts +139 -0
  127. package/types/ngneers-controls-api-ng.d.ts +364 -0
  128. package/types/ngneers-controls-api-resize.d.ts +318 -0
  129. package/types/ngneers-controls-api.d.ts +145 -0
  130. package/types/ngneers-controls-avatar.d.ts +54 -0
  131. package/types/ngneers-controls-base.d.ts +277 -0
  132. package/types/ngneers-controls-breadcrumb.d.ts +272 -0
  133. package/types/ngneers-controls-button-group.d.ts +46 -0
  134. package/types/ngneers-controls-button.d.ts +48 -0
  135. package/types/ngneers-controls-calendar.d.ts +466 -0
  136. package/types/ngneers-controls-checkbox.d.ts +40 -0
  137. package/types/ngneers-controls-chip.d.ts +202 -0
  138. package/types/ngneers-controls-default-icons.d.ts +5 -0
  139. package/types/ngneers-controls-defer.d.ts +48 -0
  140. package/types/ngneers-controls-dialog.d.ts +399 -0
  141. package/types/ngneers-controls-directives.d.ts +171 -0
  142. package/types/ngneers-controls-drawer.d.ts +308 -0
  143. package/types/ngneers-controls-edit-inplace.d.ts +263 -0
  144. package/types/ngneers-controls-errors.d.ts +155 -0
  145. package/types/ngneers-controls-filter.d.ts +609 -0
  146. package/types/ngneers-controls-hint.d.ts +88 -0
  147. package/types/ngneers-controls-i18n-translations-de.d.ts +164 -0
  148. package/types/ngneers-controls-i18n-translations-en.d.ts +164 -0
  149. package/types/ngneers-controls-i18n.d.ts +186 -0
  150. package/types/ngneers-controls-icon.d.ts +68 -0
  151. package/types/ngneers-controls-inplace.d.ts +98 -0
  152. package/types/ngneers-controls-input-field.d.ts +248 -0
  153. package/types/ngneers-controls-input.d.ts +30 -0
  154. package/types/ngneers-controls-item-view.d.ts +215 -0
  155. package/types/ngneers-controls-list-box.d.ts +334 -0
  156. package/types/ngneers-controls-mask-input.d.ts +434 -0
  157. package/types/ngneers-controls-menu.d.ts +192 -0
  158. package/types/ngneers-controls-message.d.ts +21 -0
  159. package/types/ngneers-controls-number-input.d.ts +146 -0
  160. package/types/ngneers-controls-paginator.d.ts +255 -0
  161. package/types/ngneers-controls-popover.d.ts +116 -0
  162. package/types/ngneers-controls-progress.d.ts +49 -0
  163. package/types/ngneers-controls-radio.d.ts +91 -0
  164. package/types/ngneers-controls-roving-focus.d.ts +107 -0
  165. package/types/ngneers-controls-scroll-shadow.d.ts +25 -0
  166. package/types/ngneers-controls-scroller.d.ts +151 -0
  167. package/types/ngneers-controls-select-button.d.ts +51 -0
  168. package/types/ngneers-controls-select.d.ts +409 -0
  169. package/types/ngneers-controls-slider.d.ts +57 -0
  170. package/types/ngneers-controls-snackbar.d.ts +330 -0
  171. package/types/ngneers-controls-spin-buttons.d.ts +69 -0
  172. package/types/ngneers-controls-spinner.d.ts +117 -0
  173. package/types/ngneers-controls-splitter.d.ts +465 -0
  174. package/types/ngneers-controls-state.d.ts +102 -0
  175. package/types/ngneers-controls-switch.d.ts +18 -0
  176. package/types/ngneers-controls-table.d.ts +3127 -0
  177. package/types/ngneers-controls-tabs.d.ts +118 -0
  178. package/types/ngneers-controls-tag.d.ts +21 -0
  179. package/types/ngneers-controls-toast.d.ts +298 -0
  180. package/types/ngneers-controls-toggle-button.d.ts +48 -0
  181. package/types/ngneers-controls-tooltip.d.ts +189 -0
  182. package/types/ngneers-controls-tree.d.ts +409 -0
  183. package/types/ngneers-controls-upload.d.ts +347 -0
  184. package/types/ngneers-controls-utils-ng.d.ts +129 -0
  185. package/types/ngneers-controls-utils.d.ts +130 -0
  186. package/types/ngneers-controls.d.ts +2 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngneers-controls-snackbar.mjs","sources":["../../src/snackbar/defaults.ts","../../src/snackbar/snackbar-templates.ts","../../src/snackbar/snackbar.ts","../../src/snackbar/snackbar.html","../../src/snackbar/snackbar-host.ts","../../src/snackbar/snackbar-host.html","../../src/snackbar/snackbar-manager.ts","../../src/snackbar/provider.ts","../../src/snackbar/snackbar-creator.ts","../../src/snackbar/ngneers-controls-snackbar.ts"],"sourcesContent":["import type { NgnSnackbarOptions } from './types';\n\nexport const DEFAULT_SNACKBAR_OPTIONS: NgnSnackbarOptions = {\n closable: false,\n autoHide: 5000,\n showProgress: true,\n pauseOnHover: true,\n};\n","import { Directive, input, TemplateRef } from '@angular/core';\nimport { NgnBase } from '@ngneers/controls/base';\n\nimport type { ContentTemplateType, HeaderTemplateType } from './types';\n\n/**\n * Template inputs for {@link NgnSnackbar}, extracted into a dedicated base class so the\n * component itself stays focused on behavior.\n */\n@Directive()\nexport abstract class SnackbarTemplates extends NgnBase<'snackbar'> {\n /** Custom template for the snackbar body, rendered in place of the plain `content` string. */\n public readonly templateContent = input<TemplateRef<ContentTemplateType> | null>();\n /** Custom template for the snackbar header, rendered in place of the plain `header` string. */\n public readonly templateHeader = input<TemplateRef<HeaderTemplateType> | null>();\n}\n","import { NgTemplateOutlet } from '@angular/common';\nimport {\n booleanAttribute,\n Component,\n computed,\n ElementRef,\n inject,\n input,\n signal,\n output,\n type OnInit,\n} from '@angular/core';\nimport { provideSelf, NgnPt } from '@ngneers/controls/base';\nimport { NgnActionButton, NgnButton } from '@ngneers/controls/button';\nimport { I18n } from '@ngneers/controls/i18n';\nimport { NgnIcon } from '@ngneers/controls/icon';\nimport { snackbarControlTemplate } from '@ngneers/controls-themes/templates/snackbar';\n\nimport { DEFAULT_SNACKBAR_OPTIONS } from './defaults';\nimport { SnackbarTemplates } from './snackbar-templates';\n\nimport type { NgnActionButtonConfig } from '@ngneers/controls/api';\nimport type { IconType } from '@ngneers/controls-custom-types';\n\n/**\n * @category control\n */\n@Component({\n selector: 'ngn-snackbar',\n templateUrl: './snackbar.html',\n imports: [NgTemplateOutlet, NgnPt, NgnButton, NgnIcon, NgnActionButton],\n providers: [provideSelf(NgnSnackbar)],\n host: {\n '(mouseenter)': 'mouseEnter()',\n '(mouseleave)': 'mouseLeave()',\n '(focusin)': 'focusEnter($event)',\n '(focusout)': 'focusLeave($event)',\n '(keydown.escape)': 'onEscape()',\n '[animate.enter]': \"theme.class('anim-enter')\",\n '[animate.leave]': \"theme.class('anim-leave')\",\n '[attr.role]': 'role()',\n '[attr.aria-live]': 'liveMode()',\n 'aria-atomic': 'true',\n },\n})\nexport class NgnSnackbar extends SnackbarTemplates implements OnInit {\n protected readonly theme = this.injectThemeTemplate(snackbarControlTemplate, 'root');\n protected readonly i18n = inject(I18n).translations;\n private readonly _host = inject<ElementRef<HTMLElement>>(ElementRef);\n\n /** Heading text shown at the top of the snackbar. Overridden by {@link templateHeader}. */\n public readonly header = input<string>();\n /** Body text of the snackbar. Overridden by {@link templateContent}. */\n public readonly content = input<string>();\n\n /** Icon shown at the start of the snackbar. */\n public readonly icon = input<IconType>();\n /** Icon used for the close button when {@link closable} is enabled. */\n public readonly iconClose = input<IconType>();\n /**\n * Whether to render a close button that lets the user dismiss the snackbar.\n * @default false\n */\n public readonly closable = input<boolean | undefined>(DEFAULT_SNACKBAR_OPTIONS.closable);\n /**\n * Time in ms before the snackbar auto-dismisses, or `false` to keep it open\n * until dismissed manually.\n * @default 5000\n */\n public readonly autoHide = input<number | false | undefined>(DEFAULT_SNACKBAR_OPTIONS.autoHide);\n\n /** Emitted when the snackbar is dismissed (by the timer, close button, or an action). */\n public readonly closeSnackbar = output<void>();\n\n /**\n * Action buttons rendered at the end of the snackbar. Clicking any action always\n * dismisses the snackbar; its `action` callback fires first. Import the config\n * type from `@ngneers/controls/api`.\n */\n public readonly actions = input<NgnActionButtonConfig[]>();\n /**\n * Shows a thin progress bar at the bottom of the snackbar that depletes over the\n * {@link autoHide} duration.\n * @default true\n */\n public readonly showProgress = input(DEFAULT_SNACKBAR_OPTIONS.showProgress ?? true, {\n transform: booleanAttribute,\n });\n /**\n * Pauses the auto-hide timer and progress bar while the snackbar is hovered,\n * resuming from the current position on mouse leave.\n * @default true\n */\n public readonly pauseOnHover = input(DEFAULT_SNACKBAR_OPTIONS.pauseOnHover ?? true, {\n transform: booleanAttribute,\n });\n\n /**\n * Politeness of the live region used to announce the snackbar to assistive tech.\n * When omitted, it is derived from {@link color}: `error`/`warning` announce as\n * `assertive` (mapped to `role=\"alert\"`), everything else as `polite`\n * (`role=\"status\"`).\n */\n public readonly ariaLive = input<'polite' | 'assertive' | 'off'>();\n\n /** Resolved live-region politeness — explicit input, else derived from color. */\n protected readonly liveMode = computed<'polite' | 'assertive' | 'off'>(() => {\n const explicit = this.ariaLive();\n if (explicit) {\n return explicit;\n }\n const color = this.color();\n return color === 'error' || color === 'warning' ? 'assertive' : 'polite';\n });\n\n /** Landmark role paired with {@link liveMode} — `alert` when assertive, else `status`. */\n protected readonly role = computed(() => (this.liveMode() === 'assertive' ? 'alert' : 'status'));\n\n /**\n * A visually-hidden severity prefix (e.g. \"Error:\") announced before the header\n * so screen-reader users get the semantic that sighted users read from color.\n * Only emitted for the known semantic colors.\n */\n protected readonly severityLabel = computed<string | undefined>(() => {\n switch (this.color()) {\n case 'error':\n return this.i18n['snackbar_severity_error']();\n case 'warning':\n return this.i18n['snackbar_severity_warning']();\n case 'success':\n return this.i18n['snackbar_severity_success']();\n case 'info':\n return this.i18n['snackbar_severity_info']();\n default:\n return undefined;\n }\n });\n\n protected readonly paused = signal(false);\n\n /** Auto-hide duration in ms, or 0 when auto-hide is disabled. Drives the progress bar. */\n protected readonly autoHideMs = computed(() => {\n const autoHide = this.autoHide();\n return typeof autoHide === 'number' && autoHide > 0 ? autoHide : 0;\n });\n\n private _closeTimeout?: number;\n private _remaining = 0;\n private _startedAt = 0;\n private _hovered = false;\n private _focused = false;\n private _regionPaused = false;\n /** Element that had focus before the user moved into the snackbar; focus returns here on close. */\n private _returnFocus: HTMLElement | null = null;\n\n public ngOnInit(): void {\n // autoHideMs() is the single source of truth for \"is auto-hide enabled\".\n this._remaining = this.autoHideMs();\n this.startHideTimer();\n }\n\n private startHideTimer() {\n // Idempotent: a timer already running must not be restarted (would reset _remaining tracking).\n if (this._closeTimeout !== undefined) {\n return;\n }\n if (this._remaining <= 0) {\n // Distinguish \"auto-hide disabled\" (never started, _startedAt still 0) from\n // \"a pause consumed all the remaining time\" (_startedAt > 0). In the latter\n // case the snackbar is overdue, so close now rather than leave it stuck open\n // with no active timer — otherwise a non-closable snackbar can never dismiss.\n if (this._startedAt > 0) {\n this.close();\n }\n return;\n }\n this._startedAt = performance.now();\n this.paused.set(false);\n this._closeTimeout = setTimeout(() => this.close(), this._remaining) as unknown as number;\n }\n\n private pauseHideTimer() {\n if (this._closeTimeout === undefined) {\n return;\n }\n clearTimeout(this._closeTimeout);\n this._closeTimeout = undefined;\n this._remaining -= performance.now() - this._startedAt;\n this.paused.set(true);\n }\n\n /**\n * Pause the auto-hide timer while the snackbar is hovered (opt-in via\n * {@link pauseOnHover}), focused (always — a WCAG 2.2.1 requirement so keyboard\n * and screen-reader users can read it), or while the whole region holds focus,\n * resuming once none holds.\n */\n private updatePause(): void {\n const shouldPause =\n this._regionPaused || (this.pauseOnHover() && this._hovered) || this._focused;\n if (shouldPause) {\n this.pauseHideTimer();\n } else {\n this.startHideTimer();\n }\n }\n\n /** Move DOM focus to the snackbar root (used by the region's roving/hotkey navigation). */\n public focus(): void {\n this._host.nativeElement.focus();\n }\n\n /** Freeze the timer because the notification region as a whole holds focus. */\n public regionPause(): void {\n this._regionPaused = true;\n this.updatePause();\n }\n\n /** Release the region-level freeze; resumes unless still hovered/focused. */\n public regionResume(): void {\n this._regionPaused = false;\n this.updatePause();\n }\n\n // Only ever called immediately before the snackbar is removed (by the timer,\n // the close button, or an action), so it need not reset paused/_remaining —\n // the component is torn down right after. Revisit if close() ever keeps the\n // snackbar mounted.\n protected close() {\n this.restoreFocus();\n this.closeSnackbar.emit();\n if (this._closeTimeout !== undefined) {\n clearTimeout(this._closeTimeout);\n }\n }\n\n /** Return focus to the pre-snackbar element when it is dismissed while focused within. */\n private restoreFocus(): void {\n const host = this._host.nativeElement;\n if (host.contains(document.activeElement) && this._returnFocus?.isConnected) {\n this._returnFocus.focus();\n }\n }\n\n protected onEscape(): void {\n if (this.closable()) {\n this.close();\n }\n }\n\n protected mouseEnter(): void {\n this._hovered = true;\n this.updatePause();\n }\n\n protected mouseLeave(): void {\n this._hovered = false;\n this.updatePause();\n }\n\n protected focusEnter(event: FocusEvent): void {\n const from = event.relatedTarget as HTMLElement | null;\n if (from && !this._host.nativeElement.contains(from)) {\n this._returnFocus = from;\n }\n this._focused = true;\n this.updatePause();\n }\n\n protected focusLeave(event: FocusEvent): void {\n // focusout bubbles; ignore focus moving between the snackbar's own children.\n const next = event.relatedTarget as Node | null;\n if (next && this._host.nativeElement.contains(next)) {\n return;\n }\n this._focused = false;\n this.updatePause();\n }\n}\n","<div [ptInt]=\"this\" [ptClass]=\"'body'\">\n @if (severityLabel(); as severityLabel) {\n <span [ptInt]=\"this\" [ptClass]=\"'sr-only'\">{{ severityLabel }}</span>\n }\n @if (templateHeader(); as headerTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"headerTemplate\"\n [ngTemplateOutletContext]=\"{\n header: header(),\n closable: !!closable(),\n closeSnackbar: close.bind(this),\n }\"\n ></ng-container>\n } @else {\n <div [ptInt]=\"this\" [ptClass]=\"'defaultHeader'\">\n <span [ptInt]=\"this\" [ptClass]=\"'defaultHeaderText'\">\n @if (icon(); as icon) {\n <ngn-icon [icon]=\"icon\" [attr.aria-hidden]=\"true\" />\n }\n {{ header() }}\n </span>\n </div>\n }\n @if (templateContent(); as contentTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"contentTemplate\"\n [ngTemplateOutletContext]=\"{\n content: content(),\n }\"\n ></ng-container>\n } @else {\n <div [ptInt]=\"this\" [ptClass]=\"'defaultContent'\">\n {{ content() }}\n </div>\n }\n</div>\n@if (actions()?.length || (!templateHeader() && closable())) {\n <div [ptInt]=\"this\" [ptClass]=\"'actions'\">\n @for (action of actions(); track $index) {\n <ngn-action-button [config]=\"action\" (clicked)=\"close()\" />\n }\n @if (!templateHeader() && closable()) {\n <button\n ngnButton\n kind=\"icon\"\n [ptInt]=\"this\"\n [ptClass]=\"'closeButton'\"\n [attr.aria-label]=\"i18n['snackbar_close']()\"\n (click)=\"close()\"\n >\n <ngn-icon [defaultIcon]=\"'snackbar-close'\" [icon]=\"iconClose()\" [attr.aria-hidden]=\"true\" />\n </button>\n }\n </div>\n}\n@if (showProgress() && autoHideMs() > 0) {\n <div\n [ptInt]=\"this\"\n [ptClass]=\"'progressBar'\"\n aria-hidden=\"true\"\n [style.animationDuration.ms]=\"autoHideMs()\"\n [style.animationPlayState]=\"paused() ? 'paused' : 'running'\"\n ></div>\n}\n","import { afterNextRender, Component, ElementRef, inject, viewChildren } from '@angular/core';\nimport { NgnBase, provideSelf } from '@ngneers/controls/base';\nimport { I18n } from '@ngneers/controls/i18n';\nimport { NotificationRegionController } from '@ngneers/controls/utils-ng';\nimport { snackbarControlTemplate } from '@ngneers/controls-themes/templates/snackbar';\n\nimport { NgnSnackbar } from './snackbar';\nimport { NgnSnackbarManager } from './snackbar-manager';\n\n/**\n * The host component that renders snackbars.\n */\n@Component({\n selector: 'ngn-snackbar-host',\n templateUrl: 'snackbar-host.html',\n imports: [NgnSnackbar],\n providers: [provideSelf(NgnSnackbarHost)],\n host: {\n '[attr.popover]': '\"manual\"',\n role: 'region',\n '[attr.aria-label]': \"i18n['snackbar_region']()\",\n '(document:keydown)': 'region.handleGlobalKeydown($event)',\n '(keydown)': 'region.handleKeydown($event)',\n '(focusin)': 'region.handleFocusIn()',\n '(focusout)': 'region.handleFocusOut($event)',\n },\n})\nexport class NgnSnackbarHost extends NgnBase<'snackbar'> {\n protected readonly theme = this.injectThemeTemplate(snackbarControlTemplate, 'host');\n protected readonly i18n = inject(I18n).translations;\n private readonly _snackbarManager = inject(NgnSnackbarManager);\n private readonly _el = inject<ElementRef<HTMLElement>>(ElementRef);\n\n private readonly _items = viewChildren(NgnSnackbar);\n\n // The first snackbar is anchored at the bottom; each new one stacks above it (mirroring how a\n // second toast appears below the first). Paired with the host's `column-reverse`, the first item\n // in DOM order sits at the bottom anchor, so a leaving item's following siblings are exactly the\n // ones that reflow toward the anchor — keeping the `& ~ *` leave-shift animation correct.\n protected readonly snackbars = this._snackbarManager.snackbars;\n\n /**\n * Drives keyboard access to the region: `F6` jumps focus to the newest snackbar,\n * arrow keys rove between them, and focus anywhere inside pauses every timer.\n */\n protected readonly region = new NotificationRegionController(this._el.nativeElement, () =>\n this._items()\n );\n\n constructor() {\n super();\n afterNextRender(() => {\n this._el.nativeElement.showPopover();\n });\n }\n\n protected removeSnackbar(id: number): void {\n this._snackbarManager.removeSnackbar(id);\n }\n}\n","@for (snackbar of snackbars(); track snackbar.id; let i = $index) {\n <ngn-snackbar\n [attr.tabindex]=\"region.tabbableIndex() === i ? 0 : -1\"\n [header]=\"snackbar.header\"\n [content]=\"snackbar.content\"\n [color]=\"snackbar.color\"\n [icon]=\"snackbar.icon\"\n [autoHide]=\"snackbar.autoHide\"\n [actions]=\"snackbar.actions\"\n [showProgress]=\"!!snackbar.showProgress\"\n [pauseOnHover]=\"!!snackbar.pauseOnHover\"\n [ariaLive]=\"snackbar.ariaLive\"\n [templateContent]=\"snackbar.contentTemplate\"\n [templateHeader]=\"snackbar.headerTemplate\"\n [closable]=\"!!snackbar.closable\"\n (closeSnackbar)=\"removeSnackbar(snackbar.id)\"\n />\n}\n","import {\n ApplicationRef,\n ComponentRef,\n createComponent,\n inject,\n Injectable,\n type OnDestroy,\n signal,\n} from '@angular/core';\nimport { Platform } from '@ngneers/controls/api/ng';\nimport { NgnError } from '@ngneers/controls/utils';\nimport { injectOrThrow } from '@ngneers/controls/utils-ng';\n\nimport { DEFAULT_SNACKBAR_OPTIONS } from './defaults';\nimport { NGN_SNACKBAR_USER_DEFAULTS } from './provider';\nimport { NgnSnackbarHost } from './snackbar-host';\n\nimport type { NgnSnackbarOptions } from './types';\n\ntype SnackbarFull = NgnSnackbarOptions & { id: number };\n\n@Injectable()\nexport class NgnSnackbarManager implements OnDestroy {\n private readonly _snackbars = signal<SnackbarFull[]>([]);\n private readonly _appRef = inject(ApplicationRef);\n private readonly _userDefaults = injectOrThrow(\n NGN_SNACKBAR_USER_DEFAULTS,\n 'NgnSnackbarManager',\n 'Failed to inject NGN_SNACKBAR_USER_DEFAULTS, make sure to use withSnackbars() to provide ngn snackbars!'\n );\n private _nextId = 0;\n\n public readonly snackbars = this._snackbars.asReadonly();\n private _component?: ComponentRef<NgnSnackbarHost>;\n\n constructor() {\n if (!inject(Platform).isBrowser) {\n return;\n }\n // Defer creation to avoid issues with Angular's injection tree during app initialization\n queueMicrotask(() => {\n // Create and attach the snackbar host component to the application's root\n this._component = createComponent(NgnSnackbarHost, {\n environmentInjector: this._appRef.injector,\n });\n this._appRef.attachView(this._component.hostView);\n const hostEl = this._component.location.nativeElement as HTMLElement;\n const appRootEl = this._appRef.components[0]?.location.nativeElement as\n | undefined\n | HTMLElement;\n if (!appRootEl) {\n throw new NgnError(\n 'NgnSnackbarManager',\n 'Failed to find application root element to attach snackbar host!'\n );\n }\n appRootEl.appendChild(hostEl);\n });\n }\n\n public ngOnDestroy(): void {\n if (this._component) {\n this._appRef.detachView(this._component.hostView);\n this._component.destroy();\n this._component = undefined;\n }\n }\n\n public removeSnackbar(id: number): void {\n this._snackbars.update(snackbars => snackbars.filter(t => t.id !== id));\n }\n\n public addSnackbar(options: NgnSnackbarOptions): number {\n const id = this._nextId++;\n const snackbar: SnackbarFull = {\n ...DEFAULT_SNACKBAR_OPTIONS,\n ...this._userDefaults,\n ...options,\n id,\n };\n this._snackbars.update(snackbars => [...snackbars, snackbar]);\n return id;\n }\n}\n","import { InjectionToken, type Provider } from '@angular/core';\n\nimport { NgnSnackbarManager } from './snackbar-manager';\n\nimport type { NgnSnackbarOptionsMeta } from './types';\n\nexport type NgnSnackbarFeature = {\n providers: Provider[];\n};\n\nexport const NGN_SNACKBAR_USER_DEFAULTS = new InjectionToken<NgnSnackbarOptionsMeta>(\n 'NGN_SNACKBAR_USER_DEFAULTS'\n);\n\nexport function withSnackbars(defaultOptions?: NgnSnackbarOptionsMeta): NgnSnackbarFeature {\n return {\n providers: [\n NgnSnackbarManager,\n {\n provide: NGN_SNACKBAR_USER_DEFAULTS,\n useValue: defaultOptions || {},\n },\n ],\n };\n}\n","import { Injector, runInInjectionContext } from '@angular/core';\nimport { injectOrThrow } from '@ngneers/controls/utils-ng';\n\nimport { NgnSnackbarManager } from './snackbar-manager';\n\nimport type { NgnSnackbarOptions, NgnSnackbarRef } from './types';\n\nexport class SnackbarCreator {\n private readonly _manager: NgnSnackbarManager;\n\n constructor() {\n this._manager = injectOrThrow(\n NgnSnackbarManager,\n 'injectSnackbarCreator',\n 'NgnSnackbarManager not found. Make sure to use withSnackbars() to provide ngn snackbars!'\n );\n }\n\n public show(options: NgnSnackbarOptions): NgnSnackbarRef {\n const id = this._manager.addSnackbar(options);\n return {\n hide: () => this._manager.removeSnackbar(id),\n };\n }\n}\n\nexport function injectSnackbarCreator(injector?: Injector): SnackbarCreator {\n if (injector) {\n return runInInjectionContext(injector, () => injectSnackbarCreator());\n }\n return new SnackbarCreator();\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;;;;;;;AAEO,MAAM,wBAAwB,GAAuB;AAC1D,IAAA,QAAQ,EAAE,KAAK;AACf,IAAA,QAAQ,EAAE,IAAI;AACd,IAAA,YAAY,EAAE,IAAI;AAClB,IAAA,YAAY,EAAE,IAAI;CACnB;;ACFD;;;AAGG;AAEG,MAAgB,iBAAkB,SAAQ,OAAmB,CAAA;;AAEjD,IAAA,eAAe,GAAG,KAAK;mGAA2C;;AAElE,IAAA,cAAc,GAAG,KAAK;kGAA0C;uGAJ5D,iBAAiB,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAjB,iBAAiB,EAAA,YAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,eAAA,EAAA,EAAA,iBAAA,EAAA,iBAAA,EAAA,UAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,cAAA,EAAA,EAAA,iBAAA,EAAA,gBAAA,EAAA,UAAA,EAAA,gBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;2FAAjB,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBADtC;;;ACeD;;AAEG;AAmBG,MAAO,WAAY,SAAQ,iBAAiB,CAAA;IAC7B,KAAK,GAAG,IAAI,CAAC,mBAAmB,CAAC,uBAAuB,EAAE,MAAM,CAAC;AACjE,IAAA,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY;AAClC,IAAA,KAAK,GAAG,MAAM,CAA0B,UAAU,CAAC;;AAGpD,IAAA,MAAM,GAAG,KAAK;0FAAU;;AAExB,IAAA,OAAO,GAAG,KAAK;2FAAU;;AAGzB,IAAA,IAAI,GAAG,KAAK;wFAAY;;AAExB,IAAA,SAAS,GAAG,KAAK;6FAAY;AAC7C;;;AAGG;AACa,IAAA,QAAQ,GAAG,KAAK,CAAsB,wBAAwB,CAAC,QAAQ;iFAAC;AACxF;;;;AAIG;AACa,IAAA,QAAQ,GAAG,KAAK,CAA6B,wBAAwB,CAAC,QAAQ;iFAAC;;IAG/E,aAAa,GAAG,MAAM,EAAQ;AAE9C;;;;AAIG;AACa,IAAA,OAAO,GAAG,KAAK;2FAA2B;AAC1D;;;;AAIG;AACa,IAAA,YAAY,GAAG,KAAK,CAAC,wBAAwB,CAAC,YAAY,IAAI,IAAI,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,cAAA,EAAA,8BAAA,EAAA,CAAA,EAChF,SAAS,EAAE,gBAAgB,GAC3B;AACF;;;;AAIG;AACa,IAAA,YAAY,GAAG,KAAK,CAAC,wBAAwB,CAAC,YAAY,IAAI,IAAI,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,cAAA,EAAA,8BAAA,EAAA,CAAA,EAChF,SAAS,EAAE,gBAAgB,GAC3B;AAEF;;;;;AAKG;AACa,IAAA,QAAQ,GAAG,KAAK;4FAAkC;;AAG/C,IAAA,QAAQ,GAAG,QAAQ,CAAiC,MAAK;AAC1E,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE;QAChC,IAAI,QAAQ,EAAE;AACZ,YAAA,OAAO,QAAQ;QACjB;AACA,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE;AAC1B,QAAA,OAAO,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,SAAS,GAAG,WAAW,GAAG,QAAQ;IAC1E,CAAC;iFAAC;;IAGiB,IAAI,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,QAAQ,EAAE,KAAK,WAAW,GAAG,OAAO,GAAG,QAAQ,CAAC;6EAAC;AAEhG;;;;AAIG;AACgB,IAAA,aAAa,GAAG,QAAQ,CAAqB,MAAK;AACnE,QAAA,QAAQ,IAAI,CAAC,KAAK,EAAE;AAClB,YAAA,KAAK,OAAO;AACV,gBAAA,OAAO,IAAI,CAAC,IAAI,CAAC,yBAAyB,CAAC,EAAE;AAC/C,YAAA,KAAK,SAAS;AACZ,gBAAA,OAAO,IAAI,CAAC,IAAI,CAAC,2BAA2B,CAAC,EAAE;AACjD,YAAA,KAAK,SAAS;AACZ,gBAAA,OAAO,IAAI,CAAC,IAAI,CAAC,2BAA2B,CAAC,EAAE;AACjD,YAAA,KAAK,MAAM;AACT,gBAAA,OAAO,IAAI,CAAC,IAAI,CAAC,wBAAwB,CAAC,EAAE;AAC9C,YAAA;AACE,gBAAA,OAAO,SAAS;;IAEtB,CAAC;sFAAC;IAEiB,MAAM,GAAG,MAAM,CAAC,KAAK;+EAAC;;AAGtB,IAAA,UAAU,GAAG,QAAQ,CAAC,MAAK;AAC5C,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE;AAChC,QAAA,OAAO,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,GAAG,CAAC,GAAG,QAAQ,GAAG,CAAC;IACpE,CAAC;mFAAC;AAEM,IAAA,aAAa;IACb,UAAU,GAAG,CAAC;IACd,UAAU,GAAG,CAAC;IACd,QAAQ,GAAG,KAAK;IAChB,QAAQ,GAAG,KAAK;IAChB,aAAa,GAAG,KAAK;;IAErB,YAAY,GAAuB,IAAI;IAExC,QAAQ,GAAA;;AAEb,QAAA,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE;QACnC,IAAI,CAAC,cAAc,EAAE;IACvB;IAEQ,cAAc,GAAA;;AAEpB,QAAA,IAAI,IAAI,CAAC,aAAa,KAAK,SAAS,EAAE;YACpC;QACF;AACA,QAAA,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,EAAE;;;;;AAKxB,YAAA,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,EAAE;gBACvB,IAAI,CAAC,KAAK,EAAE;YACd;YACA;QACF;AACA,QAAA,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;AACnC,QAAA,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;AACtB,QAAA,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,UAAU,CAAsB;IAC3F;IAEQ,cAAc,GAAA;AACpB,QAAA,IAAI,IAAI,CAAC,aAAa,KAAK,SAAS,EAAE;YACpC;QACF;AACA,QAAA,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC;AAChC,QAAA,IAAI,CAAC,aAAa,GAAG,SAAS;QAC9B,IAAI,CAAC,UAAU,IAAI,WAAW,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU;AACtD,QAAA,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;IACvB;AAEA;;;;;AAKG;IACK,WAAW,GAAA;QACjB,MAAM,WAAW,GACf,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC,YAAY,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ;QAC/E,IAAI,WAAW,EAAE;YACf,IAAI,CAAC,cAAc,EAAE;QACvB;aAAO;YACL,IAAI,CAAC,cAAc,EAAE;QACvB;IACF;;IAGO,KAAK,GAAA;AACV,QAAA,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,EAAE;IAClC;;IAGO,WAAW,GAAA;AAChB,QAAA,IAAI,CAAC,aAAa,GAAG,IAAI;QACzB,IAAI,CAAC,WAAW,EAAE;IACpB;;IAGO,YAAY,GAAA;AACjB,QAAA,IAAI,CAAC,aAAa,GAAG,KAAK;QAC1B,IAAI,CAAC,WAAW,EAAE;IACpB;;;;;IAMU,KAAK,GAAA;QACb,IAAI,CAAC,YAAY,EAAE;AACnB,QAAA,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE;AACzB,QAAA,IAAI,IAAI,CAAC,aAAa,KAAK,SAAS,EAAE;AACpC,YAAA,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC;QAClC;IACF;;IAGQ,YAAY,GAAA;AAClB,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa;AACrC,QAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,YAAY,EAAE,WAAW,EAAE;AAC3E,YAAA,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE;QAC3B;IACF;IAEU,QAAQ,GAAA;AAChB,QAAA,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE;YACnB,IAAI,CAAC,KAAK,EAAE;QACd;IACF;IAEU,UAAU,GAAA;AAClB,QAAA,IAAI,CAAC,QAAQ,GAAG,IAAI;QACpB,IAAI,CAAC,WAAW,EAAE;IACpB;IAEU,UAAU,GAAA;AAClB,QAAA,IAAI,CAAC,QAAQ,GAAG,KAAK;QACrB,IAAI,CAAC,WAAW,EAAE;IACpB;AAEU,IAAA,UAAU,CAAC,KAAiB,EAAA;AACpC,QAAA,MAAM,IAAI,GAAG,KAAK,CAAC,aAAmC;AACtD,QAAA,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;AACpD,YAAA,IAAI,CAAC,YAAY,GAAG,IAAI;QAC1B;AACA,QAAA,IAAI,CAAC,QAAQ,GAAG,IAAI;QACpB,IAAI,CAAC,WAAW,EAAE;IACpB;AAEU,IAAA,UAAU,CAAC,KAAiB,EAAA;;AAEpC,QAAA,MAAM,IAAI,GAAG,KAAK,CAAC,aAA4B;AAC/C,QAAA,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;YACnD;QACF;AACA,QAAA,IAAI,CAAC,QAAQ,GAAG,KAAK;QACrB,IAAI,CAAC,WAAW,EAAE;IACpB;uGAxOW,WAAW,EAAA,IAAA,EAAA,IAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAX,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,WAAW,mwDAdX,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EC/BvC,s9DAgEA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EDlCY,gBAAgB,oJAAE,KAAK,EAAA,QAAA,EAAA,SAAA,EAAA,MAAA,EAAA,CAAA,OAAA,EAAA,SAAA,EAAA,OAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,SAAS,EAAA,QAAA,EAAA,iCAAA,EAAA,MAAA,EAAA,CAAA,iBAAA,CAAA,EAAA,QAAA,EAAA,CAAA,WAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,OAAO,sFAAE,eAAe,EAAA,QAAA,EAAA,mBAAA,EAAA,MAAA,EAAA,CAAA,QAAA,EAAA,QAAA,CAAA,EAAA,OAAA,EAAA,CAAA,SAAA,CAAA,EAAA,CAAA,EAAA,CAAA;;2FAe3D,WAAW,EAAA,UAAA,EAAA,CAAA;kBAlBvB,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,cAAc,WAEf,CAAC,gBAAgB,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,CAAC,EAAA,SAAA,EAC5D,CAAC,WAAW,CAAA,WAAA,CAAa,CAAC,EAAA,IAAA,EAC/B;AACJ,wBAAA,cAAc,EAAE,cAAc;AAC9B,wBAAA,cAAc,EAAE,cAAc;AAC9B,wBAAA,WAAW,EAAE,oBAAoB;AACjC,wBAAA,YAAY,EAAE,oBAAoB;AAClC,wBAAA,kBAAkB,EAAE,YAAY;AAChC,wBAAA,iBAAiB,EAAE,2BAA2B;AAC9C,wBAAA,iBAAiB,EAAE,2BAA2B;AAC9C,wBAAA,aAAa,EAAE,QAAQ;AACvB,wBAAA,kBAAkB,EAAE,YAAY;AAChC,wBAAA,aAAa,EAAE,MAAM;AACtB,qBAAA,EAAA,QAAA,EAAA,s9DAAA,EAAA;;;AElCH;;AAEG;AAgBG,MAAO,eAAgB,SAAQ,OAAmB,CAAA;IACnC,KAAK,GAAG,IAAI,CAAC,mBAAmB,CAAC,uBAAuB,EAAE,MAAM,CAAC;AACjE,IAAA,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY;AAClC,IAAA,gBAAgB,GAAG,MAAM,CAAC,kBAAkB,CAAC;AAC7C,IAAA,GAAG,GAAG,MAAM,CAA0B,UAAU,CAAC;IAEjD,MAAM,GAAG,YAAY,CAAC,WAAW;+EAAC;;;;;AAMhC,IAAA,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,SAAS;AAE9D;;;AAGG;AACgB,IAAA,MAAM,GAAG,IAAI,4BAA4B,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,MACnF,IAAI,CAAC,MAAM,EAAE,CACd;AAED,IAAA,WAAA,GAAA;AACE,QAAA,KAAK,EAAE;QACP,eAAe,CAAC,MAAK;AACnB,YAAA,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,WAAW,EAAE;AACtC,QAAA,CAAC,CAAC;IACJ;AAEU,IAAA,cAAc,CAAC,EAAU,EAAA;AACjC,QAAA,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,EAAE,CAAC;IAC1C;uGA/BW,eAAe,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAf,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,eAAe,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,mBAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,MAAA,EAAA,QAAA,EAAA,EAAA,SAAA,EAAA,EAAA,kBAAA,EAAA,oCAAA,EAAA,SAAA,EAAA,8BAAA,EAAA,SAAA,EAAA,wBAAA,EAAA,UAAA,EAAA,+BAAA,EAAA,EAAA,UAAA,EAAA,EAAA,cAAA,EAAA,YAAA,EAAA,iBAAA,EAAA,2BAAA,EAAA,EAAA,EAAA,SAAA,EAXf,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,EAAA,WAAA,EAAA,CAAA,EAAA,YAAA,EAAA,QAAA,EAAA,SAAA,EAiBF,WAAW,EAAA,WAAA,EAAA,IAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,eAAA,EAAA,IAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,ECjCpD,urBAkBA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EDHY,WAAW,EAAA,QAAA,EAAA,cAAA,EAAA,MAAA,EAAA,CAAA,QAAA,EAAA,SAAA,EAAA,MAAA,EAAA,WAAA,EAAA,UAAA,EAAA,UAAA,EAAA,SAAA,EAAA,cAAA,EAAA,cAAA,EAAA,UAAA,CAAA,EAAA,OAAA,EAAA,CAAA,eAAA,CAAA,EAAA,CAAA,EAAA,CAAA;;2FAYV,eAAe,EAAA,UAAA,EAAA,CAAA;kBAf3B,SAAS;+BACE,mBAAmB,EAAA,OAAA,EAEpB,CAAC,WAAW,CAAC,aACX,CAAC,WAAW,CAAA,eAAA,CAAiB,CAAC,EAAA,IAAA,EACnC;AACJ,wBAAA,gBAAgB,EAAE,UAAU;AAC5B,wBAAA,IAAI,EAAE,QAAQ;AACd,wBAAA,mBAAmB,EAAE,2BAA2B;AAChD,wBAAA,oBAAoB,EAAE,oCAAoC;AAC1D,wBAAA,WAAW,EAAE,8BAA8B;AAC3C,wBAAA,WAAW,EAAE,wBAAwB;AACrC,wBAAA,YAAY,EAAE,+BAA+B;AAC9C,qBAAA,EAAA,QAAA,EAAA,urBAAA,EAAA;qHAQsC,WAAW,CAAA,EAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,EAAA,CAAA;;MEXvC,kBAAkB,CAAA;IACZ,UAAU,GAAG,MAAM,CAAiB,EAAE;mFAAC;AACvC,IAAA,OAAO,GAAG,MAAM,CAAC,cAAc,CAAC;IAChC,aAAa,GAAG,aAAa,CAC5C,0BAA0B,EAC1B,oBAAoB,EACpB,yGAAyG,CAC1G;IACO,OAAO,GAAG,CAAC;AAEH,IAAA,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE;AAChD,IAAA,UAAU;AAElB,IAAA,WAAA,GAAA;QACE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,EAAE;YAC/B;QACF;;QAEA,cAAc,CAAC,MAAK;;AAElB,YAAA,IAAI,CAAC,UAAU,GAAG,eAAe,CAAC,eAAe,EAAE;AACjD,gBAAA,mBAAmB,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;AAC3C,aAAA,CAAC;YACF,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YACjD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,aAA4B;AACpE,YAAA,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,aAExC;YACf,IAAI,CAAC,SAAS,EAAE;AACd,gBAAA,MAAM,IAAI,QAAQ,CAChB,oBAAoB,EACpB,kEAAkE,CACnE;YACH;AACA,YAAA,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC;AAC/B,QAAA,CAAC,CAAC;IACJ;IAEO,WAAW,GAAA;AAChB,QAAA,IAAI,IAAI,CAAC,UAAU,EAAE;YACnB,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;AACjD,YAAA,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE;AACzB,YAAA,IAAI,CAAC,UAAU,GAAG,SAAS;QAC7B;IACF;AAEO,IAAA,cAAc,CAAC,EAAU,EAAA;QAC9B,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,SAAS,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACzE;AAEO,IAAA,WAAW,CAAC,OAA2B,EAAA;AAC5C,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE;AACzB,QAAA,MAAM,QAAQ,GAAiB;AAC7B,YAAA,GAAG,wBAAwB;YAC3B,GAAG,IAAI,CAAC,aAAa;AACrB,YAAA,GAAG,OAAO;YACV,EAAE;SACH;AACD,QAAA,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,GAAG,SAAS,EAAE,QAAQ,CAAC,CAAC;AAC7D,QAAA,OAAO,EAAE;IACX;uGA5DW,kBAAkB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA;2GAAlB,kBAAkB,EAAA,CAAA;;2FAAlB,kBAAkB,EAAA,UAAA,EAAA,CAAA;kBAD9B;;;MCXY,0BAA0B,GAAG,IAAI,cAAc,CAC1D,4BAA4B;AAGxB,SAAU,aAAa,CAAC,cAAuC,EAAA;IACnE,OAAO;AACL,QAAA,SAAS,EAAE;YACT,kBAAkB;AAClB,YAAA;AACE,gBAAA,OAAO,EAAE,0BAA0B;gBACnC,QAAQ,EAAE,cAAc,IAAI,EAAE;AAC/B,aAAA;AACF,SAAA;KACF;AACH;;MCjBa,eAAe,CAAA;AACT,IAAA,QAAQ;AAEzB,IAAA,WAAA,GAAA;QACE,IAAI,CAAC,QAAQ,GAAG,aAAa,CAC3B,kBAAkB,EAClB,uBAAuB,EACvB,0FAA0F,CAC3F;IACH;AAEO,IAAA,IAAI,CAAC,OAA2B,EAAA;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC;QAC7C,OAAO;YACL,IAAI,EAAE,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;SAC7C;IACH;AACD;AAEK,SAAU,qBAAqB,CAAC,QAAmB,EAAA;IACvD,IAAI,QAAQ,EAAE;QACZ,OAAO,qBAAqB,CAAC,QAAQ,EAAE,MAAM,qBAAqB,EAAE,CAAC;IACvE;IACA,OAAO,IAAI,eAAe,EAAE;AAC9B;;AC/BA;;AAEG;;;;"}
@@ -0,0 +1,126 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, inject, computed, DestroyRef, Component } from '@angular/core';
3
+ import { NgnBase, NgnPt, provideSelf } from '@ngneers/controls/base';
4
+ import { NgnButton } from '@ngneers/controls/button';
5
+ import { NgnIcon } from '@ngneers/controls/icon';
6
+ import { NgnInputField } from '@ngneers/controls/input-field';
7
+ import { spinButtonsControlTemplate } from '@ngneers/controls-themes/templates/spin-buttons';
8
+
9
+ /** Delay before press-and-hold starts auto-repeating (ms). */
10
+ const HOLD_DELAY = 400;
11
+ /** Interval between auto-repeated steps while holding (ms). */
12
+ const HOLD_INTERVAL = 60;
13
+ /**
14
+ * Increment/decrement buttons for a steppable control (e.g. `ngnNumberInput`).
15
+ *
16
+ * Inside an `ngn-input-field` the buttons find the field's primary control
17
+ * automatically; outside (or to target a specific control) bind `[for]` to a
18
+ * control instance (e.g. a `#num="ngnNumberInput"` template reference).
19
+ *
20
+ * The buttons are a pointer affordance only (`tabindex="-1"`, `aria-hidden`):
21
+ * keyboard users step via `ArrowUp`/`ArrowDown` on the control itself, which
22
+ * carries the `spinbutton` role. Press-and-hold auto-repeats; the buttons
23
+ * disable at the control's min/max bounds.
24
+ *
25
+ * Use `buttons="increment"`/`buttons="decrement"` on two instances to flank
26
+ * an input on both sides; the stacked/inline arrangement of the `both` pair
27
+ * is a theme kind (`kind="stacked" | "inline"`).
28
+ *
29
+ * @category control
30
+ */
31
+ class NgnSpinButtons extends NgnBase {
32
+ theme = this.injectThemeTemplate(spinButtonsControlTemplate, {
33
+ root: true,
34
+ // A lone decrement button flanks the input's leading edge; everything else
35
+ // (increment, the stacked/inline pair) sits at the trailing edge.
36
+ leading: () => this.buttons() === 'decrement',
37
+ trailing: () => this.buttons() !== 'decrement',
38
+ pair: () => this.buttons() === 'both',
39
+ });
40
+ /**
41
+ * Which buttons to render. Use two instances (`decrement` + `increment`)
42
+ * to place one button on each side of the input.
43
+ * @default 'both'
44
+ */
45
+ buttons = input('both', /* @ts-ignore */
46
+ ...(ngDevMode ? [{ debugName: "buttons" }] : /* istanbul ignore next */ []));
47
+ /**
48
+ * The control to step. Falls back to the primary control of a surrounding
49
+ * `ngn-input-field`.
50
+ */
51
+ for = input(undefined, /* @ts-ignore */
52
+ ...(ngDevMode ? [{ debugName: "for" }] : /* istanbul ignore next */ []));
53
+ /** Custom icon for the increment button. */
54
+ iconIncrement = input(/* @ts-ignore */
55
+ ...(ngDevMode ? [undefined, { debugName: "iconIncrement" }] : /* istanbul ignore next */ []));
56
+ /** Custom icon for the decrement button. */
57
+ iconDecrement = input(/* @ts-ignore */
58
+ ...(ngDevMode ? [undefined, { debugName: "iconDecrement" }] : /* istanbul ignore next */ []));
59
+ _field = inject(NgnInputField, { optional: true });
60
+ /** The resolved step target: explicit `for` wins over the surrounding field. */
61
+ target = computed(() => this.for() ?? this._field?.control(), /* @ts-ignore */
62
+ ...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
63
+ _holdTimeout;
64
+ _holdInterval;
65
+ constructor() {
66
+ super();
67
+ inject(DestroyRef).onDestroy(() => this._stopRepeat());
68
+ }
69
+ canStep(direction) {
70
+ return this.target()?.canStepValue(direction) ?? false;
71
+ }
72
+ /**
73
+ * First step on pointer down (like native spinners), then auto-repeat while
74
+ * held. `preventDefault` keeps focus (and caret) where it is.
75
+ */
76
+ pressStart(event, direction) {
77
+ if (event.pointerType === 'mouse' && event.button !== 0) {
78
+ return;
79
+ }
80
+ event.preventDefault();
81
+ this._stopRepeat();
82
+ if (!this.target()?.stepValue(direction)) {
83
+ return;
84
+ }
85
+ this._holdTimeout = setTimeout(() => {
86
+ this._holdInterval = setInterval(() => {
87
+ // Stop at the bound: a disabled button may no longer emit pointerup.
88
+ const target = this.target();
89
+ if (!target?.canStepValue(direction) || !target.stepValue(direction)) {
90
+ this._stopRepeat();
91
+ }
92
+ }, HOLD_INTERVAL);
93
+ }, HOLD_DELAY);
94
+ }
95
+ pressEnd() {
96
+ this._stopRepeat();
97
+ }
98
+ /** Stepping already happened on pointer down — just keep the click local. */
99
+ clicked(event) {
100
+ event.stopPropagation();
101
+ event.preventDefault();
102
+ }
103
+ _stopRepeat() {
104
+ if (this._holdTimeout !== undefined) {
105
+ clearTimeout(this._holdTimeout);
106
+ this._holdTimeout = undefined;
107
+ }
108
+ if (this._holdInterval !== undefined) {
109
+ clearInterval(this._holdInterval);
110
+ this._holdInterval = undefined;
111
+ }
112
+ }
113
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnSpinButtons, deps: [], target: i0.ɵɵFactoryTarget.Component });
114
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: NgnSpinButtons, isStandalone: true, selector: "ngn-spin-buttons", inputs: { buttons: { classPropertyName: "buttons", publicName: "buttons", isSignal: true, isRequired: false, transformFunction: null }, for: { classPropertyName: "for", publicName: "for", isSignal: true, isRequired: false, transformFunction: null }, iconIncrement: { classPropertyName: "iconIncrement", publicName: "iconIncrement", isSignal: true, isRequired: false, transformFunction: null }, iconDecrement: { classPropertyName: "iconDecrement", publicName: "iconDecrement", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideSelf(NgnSpinButtons)], usesInheritance: true, ngImport: i0, template: "@if (buttons() !== 'increment') {\n <button\n ngnButton\n type=\"button\"\n [kind]=\"'icon'\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [ptInt]=\"this\"\n [ptDep]=\"'decrement'\"\n [disabled]=\"!canStep(-1)\"\n (pointerdown)=\"pressStart($event, -1)\"\n (pointerup)=\"pressEnd()\"\n (pointerleave)=\"pressEnd()\"\n (pointercancel)=\"pressEnd()\"\n (click)=\"clicked($event)\"\n >\n <ngn-icon defaultIcon=\"spin-decrement\" [icon]=\"iconDecrement()\" />\n </button>\n}\n@if (buttons() !== 'decrement') {\n <button\n ngnButton\n type=\"button\"\n [kind]=\"'icon'\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [ptInt]=\"this\"\n [ptDep]=\"'increment'\"\n [disabled]=\"!canStep(1)\"\n (pointerdown)=\"pressStart($event, 1)\"\n (pointerup)=\"pressEnd()\"\n (pointerleave)=\"pressEnd()\"\n (pointercancel)=\"pressEnd()\"\n (click)=\"clicked($event)\"\n >\n <ngn-icon defaultIcon=\"spin-increment\" [icon]=\"iconIncrement()\" />\n </button>\n}\n", dependencies: [{ kind: "directive", type: NgnPt, selector: "[ptInt]", inputs: ["ptInt", "ptClass", "ptDep"] }, { kind: "directive", type: NgnButton, selector: "button[ngnButton], a[ngnButton]", inputs: ["ngnButtonInline"], exportAs: ["ngnButton"] }, { kind: "component", type: NgnIcon, selector: "ngn-icon", inputs: ["defaultIcon", "icon"] }] });
115
+ }
116
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnSpinButtons, decorators: [{
117
+ type: Component,
118
+ args: [{ selector: 'ngn-spin-buttons', imports: [NgnPt, NgnButton, NgnIcon], providers: [provideSelf(NgnSpinButtons)], template: "@if (buttons() !== 'increment') {\n <button\n ngnButton\n type=\"button\"\n [kind]=\"'icon'\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [ptInt]=\"this\"\n [ptDep]=\"'decrement'\"\n [disabled]=\"!canStep(-1)\"\n (pointerdown)=\"pressStart($event, -1)\"\n (pointerup)=\"pressEnd()\"\n (pointerleave)=\"pressEnd()\"\n (pointercancel)=\"pressEnd()\"\n (click)=\"clicked($event)\"\n >\n <ngn-icon defaultIcon=\"spin-decrement\" [icon]=\"iconDecrement()\" />\n </button>\n}\n@if (buttons() !== 'decrement') {\n <button\n ngnButton\n type=\"button\"\n [kind]=\"'icon'\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [ptInt]=\"this\"\n [ptDep]=\"'increment'\"\n [disabled]=\"!canStep(1)\"\n (pointerdown)=\"pressStart($event, 1)\"\n (pointerup)=\"pressEnd()\"\n (pointerleave)=\"pressEnd()\"\n (pointercancel)=\"pressEnd()\"\n (click)=\"clicked($event)\"\n >\n <ngn-icon defaultIcon=\"spin-increment\" [icon]=\"iconIncrement()\" />\n </button>\n}\n" }]
119
+ }], ctorParameters: () => [], propDecorators: { buttons: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttons", required: false }] }], for: [{ type: i0.Input, args: [{ isSignal: true, alias: "for", required: false }] }], iconIncrement: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconIncrement", required: false }] }], iconDecrement: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconDecrement", required: false }] }] } });
120
+
121
+ /**
122
+ * Generated bundle index. Do not edit.
123
+ */
124
+
125
+ export { NgnSpinButtons };
126
+ //# sourceMappingURL=ngneers-controls-spin-buttons.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngneers-controls-spin-buttons.mjs","sources":["../../src/spin-buttons/spin-buttons.ts","../../src/spin-buttons/spin-buttons.html","../../src/spin-buttons/ngneers-controls-spin-buttons.ts"],"sourcesContent":["import { Component, computed, DestroyRef, inject, input } from '@angular/core';\nimport { type AnyNgnBase, NgnBase, NgnPt, provideSelf } from '@ngneers/controls/base';\nimport { NgnButton } from '@ngneers/controls/button';\nimport { NgnIcon } from '@ngneers/controls/icon';\nimport { NgnInputField } from '@ngneers/controls/input-field';\nimport { spinButtonsControlTemplate } from '@ngneers/controls-themes/templates/spin-buttons';\n\nimport type { IconType } from '@ngneers/controls-custom-types';\n\n/** Delay before press-and-hold starts auto-repeating (ms). */\nconst HOLD_DELAY = 400;\n/** Interval between auto-repeated steps while holding (ms). */\nconst HOLD_INTERVAL = 60;\n\n/**\n * Increment/decrement buttons for a steppable control (e.g. `ngnNumberInput`).\n *\n * Inside an `ngn-input-field` the buttons find the field's primary control\n * automatically; outside (or to target a specific control) bind `[for]` to a\n * control instance (e.g. a `#num=\"ngnNumberInput\"` template reference).\n *\n * The buttons are a pointer affordance only (`tabindex=\"-1\"`, `aria-hidden`):\n * keyboard users step via `ArrowUp`/`ArrowDown` on the control itself, which\n * carries the `spinbutton` role. Press-and-hold auto-repeats; the buttons\n * disable at the control's min/max bounds.\n *\n * Use `buttons=\"increment\"`/`buttons=\"decrement\"` on two instances to flank\n * an input on both sides; the stacked/inline arrangement of the `both` pair\n * is a theme kind (`kind=\"stacked\" | \"inline\"`).\n *\n * @category control\n */\n@Component({\n selector: 'ngn-spin-buttons',\n templateUrl: './spin-buttons.html',\n imports: [NgnPt, NgnButton, NgnIcon],\n providers: [provideSelf(NgnSpinButtons)],\n})\nexport class NgnSpinButtons extends NgnBase<'spinButtons'> {\n protected readonly theme = this.injectThemeTemplate(spinButtonsControlTemplate, {\n root: true,\n // A lone decrement button flanks the input's leading edge; everything else\n // (increment, the stacked/inline pair) sits at the trailing edge.\n leading: () => this.buttons() === 'decrement',\n trailing: () => this.buttons() !== 'decrement',\n pair: () => this.buttons() === 'both',\n });\n\n /**\n * Which buttons to render. Use two instances (`decrement` + `increment`)\n * to place one button on each side of the input.\n * @default 'both'\n */\n public readonly buttons = input<'both' | 'increment' | 'decrement'>('both');\n /**\n * The control to step. Falls back to the primary control of a surrounding\n * `ngn-input-field`.\n */\n public readonly for = input<AnyNgnBase | undefined>(undefined);\n /** Custom icon for the increment button. */\n public readonly iconIncrement = input<IconType>();\n /** Custom icon for the decrement button. */\n public readonly iconDecrement = input<IconType>();\n\n private readonly _field = inject(NgnInputField, { optional: true });\n\n /** The resolved step target: explicit `for` wins over the surrounding field. */\n protected readonly target = computed(() => this.for() ?? this._field?.control());\n\n private _holdTimeout: ReturnType<typeof setTimeout> | undefined;\n private _holdInterval: ReturnType<typeof setInterval> | undefined;\n\n constructor() {\n super();\n inject(DestroyRef).onDestroy(() => this._stopRepeat());\n }\n\n protected canStep(direction: 1 | -1): boolean {\n return this.target()?.canStepValue(direction) ?? false;\n }\n\n /**\n * First step on pointer down (like native spinners), then auto-repeat while\n * held. `preventDefault` keeps focus (and caret) where it is.\n */\n protected pressStart(event: PointerEvent, direction: 1 | -1): void {\n if (event.pointerType === 'mouse' && event.button !== 0) {\n return;\n }\n event.preventDefault();\n this._stopRepeat();\n if (!this.target()?.stepValue(direction)) {\n return;\n }\n this._holdTimeout = setTimeout(() => {\n this._holdInterval = setInterval(() => {\n // Stop at the bound: a disabled button may no longer emit pointerup.\n const target = this.target();\n if (!target?.canStepValue(direction) || !target.stepValue(direction)) {\n this._stopRepeat();\n }\n }, HOLD_INTERVAL);\n }, HOLD_DELAY);\n }\n\n protected pressEnd(): void {\n this._stopRepeat();\n }\n\n /** Stepping already happened on pointer down — just keep the click local. */\n protected clicked(event: MouseEvent): void {\n event.stopPropagation();\n event.preventDefault();\n }\n\n private _stopRepeat(): void {\n if (this._holdTimeout !== undefined) {\n clearTimeout(this._holdTimeout);\n this._holdTimeout = undefined;\n }\n if (this._holdInterval !== undefined) {\n clearInterval(this._holdInterval);\n this._holdInterval = undefined;\n }\n }\n}\n","@if (buttons() !== 'increment') {\n <button\n ngnButton\n type=\"button\"\n [kind]=\"'icon'\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [ptInt]=\"this\"\n [ptDep]=\"'decrement'\"\n [disabled]=\"!canStep(-1)\"\n (pointerdown)=\"pressStart($event, -1)\"\n (pointerup)=\"pressEnd()\"\n (pointerleave)=\"pressEnd()\"\n (pointercancel)=\"pressEnd()\"\n (click)=\"clicked($event)\"\n >\n <ngn-icon defaultIcon=\"spin-decrement\" [icon]=\"iconDecrement()\" />\n </button>\n}\n@if (buttons() !== 'decrement') {\n <button\n ngnButton\n type=\"button\"\n [kind]=\"'icon'\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [ptInt]=\"this\"\n [ptDep]=\"'increment'\"\n [disabled]=\"!canStep(1)\"\n (pointerdown)=\"pressStart($event, 1)\"\n (pointerup)=\"pressEnd()\"\n (pointerleave)=\"pressEnd()\"\n (pointercancel)=\"pressEnd()\"\n (click)=\"clicked($event)\"\n >\n <ngn-icon defaultIcon=\"spin-increment\" [icon]=\"iconIncrement()\" />\n </button>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,184 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, booleanAttribute, computed, Component, runInInjectionContext, inject, ElementRef, effect, ViewContainerRef, DestroyRef } from '@angular/core';
3
+ import { setComponentInput } from '@ngneers/controls/api/ng';
4
+ import { debounceSignal } from '@ngneers/controls/utils-ng';
5
+ import { NgnBase, NgnPt, provideSelf } from '@ngneers/controls/base';
6
+ import { spinnerControlTemplate } from '@ngneers/controls-themes/templates/spinner';
7
+
8
+ /**
9
+ * @category control
10
+ */
11
+ class NgnSpinner extends NgnBase {
12
+ theme = this.injectThemeTemplate(spinnerControlTemplate, {
13
+ root: true,
14
+ centered: () => !!this.centered(),
15
+ });
16
+ /**
17
+ * Diameter of the spinner in pixels.
18
+ * @default 64
19
+ */
20
+ size = input(64, /* @ts-ignore */
21
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
22
+ /**
23
+ * Stroke width of the spinner ring as a CSS length (e.g. `'4px'`).
24
+ * Falls back to the theme's default thickness when unset.
25
+ */
26
+ thickness = input(/* @ts-ignore */
27
+ ...(ngDevMode ? [undefined, { debugName: "thickness" }] : /* istanbul ignore next */ []));
28
+ /**
29
+ * Whether to center the spinner within its containing block.
30
+ * @default false
31
+ */
32
+ centered = input(false, { ...(ngDevMode ? { debugName: "centered" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
33
+ viewBox = computed(() => `0 0 ${this.size()} ${this.size()}`, /* @ts-ignore */
34
+ ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
35
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnSpinner, deps: null, target: i0.ɵɵFactoryTarget.Component });
36
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", type: NgnSpinner, isStandalone: true, selector: "ngn-spinner", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, thickness: { classPropertyName: "thickness", publicName: "thickness", isSignal: true, isRequired: false, transformFunction: null }, centered: { classPropertyName: "centered", publicName: "centered", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "status" }, properties: { "style.--size": "size()", "style.--thickness": "thickness() || null" } }, providers: [provideSelf(NgnSpinner)], usesInheritance: true, ngImport: i0, template: "<svg [attr.viewBox]=\"viewBox()\" xmlns=\"http://www.w3.org/2000/svg\" [ptInt]=\"this\" [ptClass]=\"'svg'\">\n <circle\n [attr.cx]=\"size() / 2\"\n [attr.cy]=\"size() / 2\"\n fill=\"none\"\n [ptInt]=\"this\"\n [ptClass]=\"'circle'\"\n />\n</svg>\n", dependencies: [{ kind: "directive", type: NgnPt, selector: "[ptInt]", inputs: ["ptInt", "ptClass", "ptDep"] }] });
37
+ }
38
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnSpinner, decorators: [{
39
+ type: Component,
40
+ args: [{ selector: 'ngn-spinner', imports: [NgnPt], providers: [provideSelf(NgnSpinner)], host: {
41
+ role: 'status',
42
+ '[style.--size]': 'size()',
43
+ '[style.--thickness]': 'thickness() || null',
44
+ }, template: "<svg [attr.viewBox]=\"viewBox()\" xmlns=\"http://www.w3.org/2000/svg\" [ptInt]=\"this\" [ptClass]=\"'svg'\">\n <circle\n [attr.cx]=\"size() / 2\"\n [attr.cy]=\"size() / 2\"\n fill=\"none\"\n [ptInt]=\"this\"\n [ptClass]=\"'circle'\"\n />\n</svg>\n" }]
45
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], thickness: [{ type: i0.Input, args: [{ isSignal: true, alias: "thickness", required: false }] }], centered: [{ type: i0.Input, args: [{ isSignal: true, alias: "centered", required: false }] }] } });
46
+
47
+ /**
48
+ * Creates an instance of the spinner creator.
49
+ *
50
+ * @param injector The injector to use. Defaults to the current injector.
51
+ * @returns The spinner creator.
52
+ */
53
+ function injectSpinnerCreator(injector) {
54
+ if (injector) {
55
+ return runInInjectionContext(injector, () => injectSpinnerCreator());
56
+ }
57
+ return new NgnSpinnerCreator();
58
+ }
59
+ /**
60
+ * Creates and manages a conditional spinner based on a visibility signal.
61
+ * @param isVisible Signal that determines whether the spinner is visible.
62
+ * @param options Optional configuration for the spinner.
63
+ */
64
+ function createConditionalSpinner(isVisible, options) {
65
+ if (options?.injector) {
66
+ runInInjectionContext(options.injector, () => createConditionalSpinner(isVisible, {
67
+ ...options,
68
+ injector: undefined,
69
+ }));
70
+ return;
71
+ }
72
+ const debounceMs = typeof options?.debounce === 'number'
73
+ ? options.debounce
74
+ : (options?.debounce ?? true)
75
+ ? 200
76
+ : 0;
77
+ const debouncedVisible = debounceMs ? debounceSignal(isVisible, debounceMs) : isVisible;
78
+ const spinnerCreator = injectSpinnerCreator();
79
+ const element = options?.element ?? inject(ElementRef).nativeElement;
80
+ let ref;
81
+ effect(() => {
82
+ const visible = debouncedVisible();
83
+ if (visible && !ref) {
84
+ ref = spinnerCreator.show(element, options?.spinnerOptions);
85
+ }
86
+ else if (!visible && ref) {
87
+ ref.hide();
88
+ ref = undefined;
89
+ }
90
+ });
91
+ }
92
+ function getElement(target) {
93
+ if (typeof target === 'string') {
94
+ return document.querySelector(target);
95
+ }
96
+ else if (target instanceof ElementRef) {
97
+ return target.nativeElement;
98
+ }
99
+ else {
100
+ return target;
101
+ }
102
+ }
103
+ const DEFAULT_OPTIONS = {
104
+ centered: true,
105
+ ariaBusy: true,
106
+ };
107
+ class NgnSpinnerCreator {
108
+ _vcr = inject(ViewContainerRef);
109
+ _destroyRef = inject(DestroyRef);
110
+ _destroyed = false;
111
+ constructor() {
112
+ this._destroyRef.onDestroy(() => {
113
+ this._destroyed = true;
114
+ });
115
+ }
116
+ /**
117
+ * Shows a spinner on the specified target element.
118
+ * @param target The target element or selector where the spinner will be attached.
119
+ * If a string is provided, it will be used as a selector to find the element.
120
+ * If the element is not found, the spinner will be retried until it is found.
121
+ * @param options Optional configuration for the spinner.
122
+ * @returns A reference to the spinner instance.
123
+ */
124
+ show(target, options) {
125
+ options = { ...DEFAULT_OPTIONS, ...options };
126
+ const hideCb = {
127
+ hide: () => { },
128
+ };
129
+ this._tryAttach(target, options, hideCb);
130
+ return {
131
+ hide: () => hideCb.hide(),
132
+ };
133
+ }
134
+ _tryAttach(target, options, hideCb) {
135
+ if (this._destroyed) {
136
+ return;
137
+ }
138
+ const element = getElement(target);
139
+ if (!element) {
140
+ setTimeout(() => {
141
+ this._tryAttach(target, options, hideCb);
142
+ }, 10);
143
+ return;
144
+ }
145
+ const componentRef = this._vcr.createComponent(NgnSpinner);
146
+ const doHide = () => {
147
+ componentRef.destroy();
148
+ if (options?.ariaBusy) {
149
+ element.removeAttribute('aria-busy');
150
+ }
151
+ delete hideCb.hide;
152
+ };
153
+ hideCb.hide = () => {
154
+ doHide();
155
+ unregisterOnDestroy();
156
+ };
157
+ element.appendChild(componentRef.location.nativeElement);
158
+ if (options.ariaBusy) {
159
+ element.setAttribute('aria-busy', 'true');
160
+ }
161
+ const unregisterOnDestroy = this._destroyRef.onDestroy(() => {
162
+ doHide();
163
+ });
164
+ if (options.color) {
165
+ setComponentInput(componentRef, 'color', options.color);
166
+ }
167
+ if (options.size) {
168
+ setComponentInput(componentRef, 'size', options.size);
169
+ }
170
+ if (options.thickness) {
171
+ setComponentInput(componentRef, 'thickness', options.thickness);
172
+ }
173
+ if (options.centered) {
174
+ setComponentInput(componentRef, 'centered', options.centered);
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Generated bundle index. Do not edit.
181
+ */
182
+
183
+ export { NgnSpinner, createConditionalSpinner, injectSpinnerCreator };
184
+ //# sourceMappingURL=ngneers-controls-spinner.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngneers-controls-spinner.mjs","sources":["../../src/spinner/spinner.ts","../../src/spinner/spinner.html","../../src/spinner/spinner-creator.ts","../../src/spinner/ngneers-controls-spinner.ts"],"sourcesContent":["import { booleanAttribute, Component, input, computed } from '@angular/core';\nimport { NgnBase, NgnPt, provideSelf } from '@ngneers/controls/base';\nimport { spinnerControlTemplate } from '@ngneers/controls-themes/templates/spinner';\n\n/**\n * @category control\n */\n@Component({\n selector: 'ngn-spinner',\n templateUrl: './spinner.html',\n imports: [NgnPt],\n providers: [provideSelf(NgnSpinner)],\n host: {\n role: 'status',\n '[style.--size]': 'size()',\n '[style.--thickness]': 'thickness() || null',\n },\n})\nexport class NgnSpinner extends NgnBase<'spinner'> {\n protected readonly theme = this.injectThemeTemplate(spinnerControlTemplate, {\n root: true,\n centered: () => !!this.centered(),\n });\n\n /**\n * Diameter of the spinner in pixels.\n * @default 64\n */\n public readonly size = input<number>(64);\n /**\n * Stroke width of the spinner ring as a CSS length (e.g. `'4px'`).\n * Falls back to the theme's default thickness when unset.\n */\n public readonly thickness = input<string>();\n /**\n * Whether to center the spinner within its containing block.\n * @default false\n */\n public readonly centered = input(false, { transform: booleanAttribute });\n\n protected readonly viewBox = computed(() => `0 0 ${this.size()} ${this.size()}`);\n}\n","<svg [attr.viewBox]=\"viewBox()\" xmlns=\"http://www.w3.org/2000/svg\" [ptInt]=\"this\" [ptClass]=\"'svg'\">\n <circle\n [attr.cx]=\"size() / 2\"\n [attr.cy]=\"size() / 2\"\n fill=\"none\"\n [ptInt]=\"this\"\n [ptClass]=\"'circle'\"\n />\n</svg>\n","import {\n DestroyRef,\n effect,\n ElementRef,\n inject,\n Injector,\n runInInjectionContext,\n type Signal,\n ViewContainerRef,\n} from '@angular/core';\nimport { setComponentInput } from '@ngneers/controls/api/ng';\nimport { debounceSignal } from '@ngneers/controls/utils-ng';\n\nimport { NgnSpinner } from './spinner';\n\nimport type { CustomColor } from '@ngneers/controls-custom-types';\n\n/**\n * Creates an instance of the spinner creator.\n *\n * @param injector The injector to use. Defaults to the current injector.\n * @returns The spinner creator.\n */\nexport function injectSpinnerCreator(injector?: Injector): NgnSpinnerCreator {\n if (injector) {\n return runInInjectionContext(injector, () => injectSpinnerCreator());\n }\n return new NgnSpinnerCreator();\n}\n\n/**\n * Creates and manages a conditional spinner based on a visibility signal.\n * @param isVisible Signal that determines whether the spinner is visible.\n * @param options Optional configuration for the spinner.\n */\nexport function createConditionalSpinner(\n isVisible: Signal<boolean>,\n options?: {\n /**\n * Options for the spinner instance.\n */\n spinnerOptions?: SpinnerOptions;\n /**\n * Debounce time in milliseconds or a boolean indicating whether to debounce (default is 200ms).\n */\n debounce?: number | boolean;\n /**\n * The target element where the spinner will be attached.\n * If a string is provided, it will be used as a selector to find the element.\n * If omitted, the spinner will be attached to the host element of the current injector.\n */\n element?: SpinnerTarget;\n /**\n * The injector to use. If omitted, the current injector will be used.\n */\n injector?: Injector;\n }\n) {\n if (options?.injector) {\n runInInjectionContext(options.injector, () =>\n createConditionalSpinner(isVisible, {\n ...options,\n injector: undefined,\n })\n );\n return;\n }\n\n const debounceMs =\n typeof options?.debounce === 'number'\n ? options.debounce\n : (options?.debounce ?? true)\n ? 200\n : 0;\n\n const debouncedVisible = debounceMs ? debounceSignal(isVisible, debounceMs) : isVisible;\n\n const spinnerCreator = injectSpinnerCreator();\n const element = options?.element ?? inject(ElementRef).nativeElement;\n let ref: SpinnerRef | undefined;\n effect(() => {\n const visible = debouncedVisible();\n if (visible && !ref) {\n ref = spinnerCreator.show(element, options?.spinnerOptions);\n } else if (!visible && ref) {\n ref.hide();\n ref = undefined;\n }\n });\n}\n\nexport type SpinnerRef = {\n /**\n * Hides the spinner.\n */\n hide: () => void;\n};\n\nexport type SpinnerTarget = HTMLElement | ElementRef<HTMLElement> | string;\nexport type SpinnerOptions = {\n /**\n * The color of the spinner.\n */\n color?: CustomColor;\n /**\n * The diameter of the spinner in pixels.\n * @default 64\n */\n size?: number;\n /**\n * The thickness of the spinner's stroke. If not provided,\n * a default thickness derived from the size will be used.\n * @default undefined\n */\n thickness?: string;\n /**\n * Whether the spinner should be centered within its container.\n * @default true\n */\n centered?: boolean;\n /**\n * Set aria busy attribute on the reference element while the spinner is visible.\n * @default true\n */\n ariaBusy?: boolean;\n};\n\nfunction getElement(target: SpinnerTarget): HTMLElement | null {\n if (typeof target === 'string') {\n return document.querySelector(target);\n } else if (target instanceof ElementRef) {\n return target.nativeElement;\n } else {\n return target;\n }\n}\n\nconst DEFAULT_OPTIONS: SpinnerOptions = {\n centered: true,\n ariaBusy: true,\n};\n\nclass NgnSpinnerCreator {\n private readonly _vcr = inject(ViewContainerRef);\n private readonly _destroyRef = inject(DestroyRef);\n private _destroyed = false;\n\n constructor() {\n this._destroyRef.onDestroy(() => {\n this._destroyed = true;\n });\n }\n\n /**\n * Shows a spinner on the specified target element.\n * @param target The target element or selector where the spinner will be attached.\n * If a string is provided, it will be used as a selector to find the element.\n * If the element is not found, the spinner will be retried until it is found.\n * @param options Optional configuration for the spinner.\n * @returns A reference to the spinner instance.\n */\n public show(target: SpinnerTarget, options?: SpinnerOptions): SpinnerRef {\n options = { ...DEFAULT_OPTIONS, ...options };\n const hideCb = {\n hide: () => {},\n };\n this._tryAttach(target, options, hideCb);\n return {\n hide: () => hideCb.hide(),\n };\n }\n\n private _tryAttach(\n target: SpinnerTarget,\n options: SpinnerOptions,\n hideCb: { hide?: () => void }\n ) {\n if (this._destroyed) {\n return;\n }\n const element = getElement(target);\n if (!element) {\n setTimeout(() => {\n this._tryAttach(target, options, hideCb);\n }, 10);\n return;\n }\n const componentRef = this._vcr.createComponent(NgnSpinner);\n\n const doHide = () => {\n componentRef.destroy();\n if (options?.ariaBusy) {\n element.removeAttribute('aria-busy');\n }\n delete hideCb.hide;\n };\n\n hideCb.hide = () => {\n doHide();\n unregisterOnDestroy();\n };\n element.appendChild(componentRef.location.nativeElement);\n if (options.ariaBusy) {\n element.setAttribute('aria-busy', 'true');\n }\n const unregisterOnDestroy = this._destroyRef.onDestroy(() => {\n doHide();\n });\n if (options.color) {\n setComponentInput(componentRef, 'color', options.color);\n }\n if (options.size) {\n setComponentInput(componentRef, 'size', options.size);\n }\n if (options.thickness) {\n setComponentInput(componentRef, 'thickness', options.thickness);\n }\n if (options.centered) {\n setComponentInput(componentRef, 'centered', options.centered);\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}