@cdevhub/ngx-tw 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.json ADDED
@@ -0,0 +1 @@
1
+ {"schemaVersion":1,"libraryVersion":"0.5.0","packageName":"@cdevhub/ngx-tw","entryPoints":[{"name":"button","importPath":"@cdevhub/ngx-tw/button","symbols":[{"name":"ButtonDirective","kind":"directive","description":"Enhances a native `<button>` or `<a>` element with library styling, semantic color/size variants, and disabled/loading states. The directive does not expose a `clicked` output — bind `(click)` directly on the host element. When `disabled` or `loading` is true, the directive intercepts clicks with `preventDefault()` + `stopImmediatePropagation()` so the host handler does not run. The directive owns no template, so visual loading affordances (spinner, status text) are composed by the consumer — see the `loading` input.","selector":"[twButton]","usage":[{"form":"attribute","selector":"[twButton]","name":"twButton"}],"exportAs":"twButton","inputs":[{"name":"variant","type":"ButtonVariant","default":"'solid'","description":"Controls the visual style. Defaults to `'solid'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Sets the semantic color palette. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls the size (padding, font size, icon size). Defaults to `'md'`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, prevents interaction and applies muted styling. Defaults to `false`."},{"name":"loading","type":"boolean","default":"false","description":"When true, blocks clicks and sets `aria-busy=\"true\"`. Defaults to `false`. The directive does not render a spinner or status text — compose them in the projected content: ```html <button twButton [loading]=\"saving()\">"}]},{"name":"ButtonIconDirective","kind":"directive","description":"","selector":"[twButtonIcon]","usage":[{"form":"attribute","selector":"[twButtonIcon]","name":"twButtonIcon"}],"inputs":[{"name":"twButtonIcon","type":"'' | 'leading' | 'trailing'","default":"'leading'","description":"Position of the icon relative to the button label. Defaults to `'leading'`. The empty-string member of the union is load-bearing: Angular binds `''` to the input when the selector is used as a bare attribute (`<svg twButtonIcon>`), which is the canonical \"leading\" usage. Templates that omit a value still have the directive attached and resolve to leading placement at runtime via the `=== 'trailing'` check below."}]},{"name":"ButtonVariant","kind":"type","description":"Visual style of the button.","definition":"'solid' | 'outline' | 'ghost' | 'soft' | 'link'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'soft' | 'link';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <button twButton [variant]=\"v\">{{ v }}</button>\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (v of variants; track v) {\n <div>\n <p class=\"uppercase\">{{ v }}</p>\n @for (c of colors; track c) {\n <button twButton [variant]=\"v\" [color]=\"c\">{{ c }}</button>\n }\n </div>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <button twButton [size]=\"s\">{{ s }}</button>\n}"},{"id":"iconsSnippet","title":"With Icons","language":"html","code":"<!-- Leading icon (default) -->\n<button twButton>\n <svg twButtonIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z\"/>\n </svg>\n Add item\n</button>\n\n<!-- Trailing icon -->\n<button twButton variant=\"ghost\" color=\"neutral\">\n Settings\n <svg twButtonIcon=\"trailing\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M7.3 14.7a1 1 0 010-1.4L10.6 10 7.3 6.7a1 1 0 011.4-1.4l4 4a1 1 0 010 1.4l-4 4a1 1 0 01-1.4 0z\"/>\n </svg>\n</button>"},{"id":"anchorSnippet","title":"Anchor Elements","language":"html","code":"<a twButton href=\"/settings\">Default link</a>\n<a twButton variant=\"outline\" color=\"secondary\" href=\"/docs\">Outline link</a>\n<a twButton variant=\"link\" href=\"/pricing\">Link variant</a>\n<a twButton variant=\"link\" color=\"error\" href=\"/logout\">Error link</a>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled -->\n@for (v of variants; track v) {\n <button twButton [variant]=\"v\" [disabled]=\"true\">{{ v }}</button>\n}\n\n<!-- Loading: spinner + sr-only status pair with aria-busy -->\n<button twButton [loading]=\"isLoading()\">\n @if (isLoading()) {\n <tw-spinner twButtonIcon size=\"sm\" />\n <span class=\"sr-only\">Saving</span>\n Saving...\n } @else {\n Save\n }\n</button>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<button twButton>Save changes</button>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { ButtonDirective, ButtonIconDirective } from '@cdevhub/ngx-tw/button';"}],"summary":"Attribute directive applied to a native <button> or <a> element — there is no wrapper component — giving it variant, color, size, loading, and icon styling while the native element keeps its role and keyboard behavior.","whenToUse":["Any clickable action in a form, dialog, toolbar, or card footer: write <button twButton>Save</button>","A link that should look like a button: put twButton on the <a>, so routing and middle-click still work","A submit control that must show progress and block re-entry, via the loading state and aria-busy","Ranking actions on a surface by weight — solid for the primary action, outline or ghost for secondary, link for tertiary","A destructive confirm button, using the error color","Leading or trailing icons alongside the label, via twButtonIcon, or an icon-only button with an aria-label"],"whenNotToUse":[{"instead":"menu","because":"the control opens a list of actions rather than performing one"},{"instead":"segmented-control","because":"the buttons represent mutually exclusive states the user selects between, not one-shot actions"},{"instead":"switch","because":"the control toggles a persistent setting on or off rather than firing an action"}],"related":["icon","spinner","menu","segmented-control","tooltip","dialog"],"aliases":["cta","action","submit","link button","icon button","primary button","danger button","loading button","twButton"],"hasMeta":true,"metaPath":"projects/ngx-tw/button/button.meta.ts"},{"name":"core","importPath":"@cdevhub/ngx-tw/core","symbols":[{"name":"TwColor","kind":"type","description":"Semantic color palette used across all ngx-tw components.","definition":"| 'primary' | 'secondary' | 'accent' | 'neutral' | 'info' | 'success' | 'warning' | 'error'"},{"name":"TwSize","kind":"type","description":"Size scale used across all ngx-tw components.","definition":"'xs' | 'sm' | 'md' | 'lg' | 'xl'"},{"name":"TwOrientation","kind":"type","description":"Layout axis used by oriented components (tabs, segmented-control, separator, etc.).","definition":"'horizontal' | 'vertical'"},{"name":"TwBreakpoint","kind":"type","description":"Tailwind-aligned breakpoint. Used by responsive component inputs.","definition":"'sm' | 'md' | 'lg' | 'xl'"},{"name":"RangeBehaviorConfig","kind":"interface","description":"Range-mode behavior knobs shared by `tw-calendar` (when `mode=\"range\"`) and by overlay-bearing range pickers that forward to it. Consumer inputs accept `Partial<RangeBehaviorConfig>`; unset fields fall back to the documented defaults — `allowSingleDayRange` and `persistPartialRange` default to `true` because dropping either would surprise consumers (clicking the same cell twice and committing one day; an in-flight draft surviving navigation are the expected gestures). `allowBackwardRange` and `disableRangesCrossingDisabledDates` default to `false`.","members":[{"name":"allowBackwardRange","type":"boolean","optional":false,"description":"When `true`, the calendar accepts ranges with `start > end` and skips the auto-swap path (§21.5). Default `false` — backward clicks normalize."},{"name":"allowSingleDayRange","type":"boolean","optional":false,"description":"When `true`, clicking the same cell as `draft.start` commits a single-day range `{ start, end: start }`. When `false`, the click is rejected with `data-state-invalid-flash`. Default `true` — selecting one day twice in range mode is the expected user gesture."},{"name":"persistPartialRange","type":"boolean","optional":false,"description":"When `true`, the in-flight range draft survives across view navigation (next/prev month, drill-up/down). When `false`, navigation during SELECTING discards the draft and emits `selectionCleared`. Default `true` — losing a half-finished range on navigation is unexpected."},{"name":"disableRangesCrossingDisabledDates","type":"boolean","optional":false,"description":"When `true`, a range commit that would span any disabled date in its interior is rejected with `data-state-invalid-flash` and the committed value is left unchanged. Default `false`."}]},{"name":"TW_ERROR_STATE_MATCHER","kind":"token","description":"Injection token carrying the ErrorStateMatcher used by every ngx-tw form control. Defaults to defaultErrorStateMatcher; provide a different matcher at root to change global error-display policy, or at any descendant injector to scope the change."},{"name":"defaultErrorStateMatcher","kind":"const","description":"Default error-state strategy: errored when the control is `invalid` and either has been interacted with (`dirty` or `touched`) or its parent form has been submitted. Matches Material's default behavior.","type":"ErrorStateMatcher"},{"name":"ErrorStateMatcher","kind":"interface","description":"Strategy that decides when a form control should be rendered in the error state. Controls read the injected matcher (or accept a per-instance override) and combine the result with their own signals. Override at any injector level via TW_ERROR_STATE_MATCHER.","members":[{"name":"isErrorState","type":"boolean","optional":false,"description":"Returns true when the control should display an error."}]},{"name":"TwFormSubmitted","kind":"interface","description":"Minimal contract for a parent form's \"submitted\" state. Both `NgForm` and `FormGroupDirective` satisfy this, so the matcher can work with either.","members":[{"name":"submitted","type":"boolean","optional":false,"description":""}]},{"name":"TW_SORT_HANDLE","kind":"token","description":"DI token through which sort containers expose their state to consumers. Provided by `SortDirective` (`[twSort]`). Inject with `{ optional: true }` — components that need aria-sort plumbing should degrade gracefully when no sort directive is present."},{"name":"TwSortHandle","kind":"interface","description":"Read-only view of a sortable region's state. Components that render column headers (e.g., `<tw-table>`) consume this handle to project `aria-sort` onto the active column without taking a hard dependency on the sort implementation. The canonical provider is `SortDirective` (`[twSort]`).","members":[{"name":"active","type":"Signal<string | null>","optional":false,"description":"Signal of the id of the currently active sort header, or `null` when no sort is active."},{"name":"direction","type":"Signal<'asc' | 'desc' | null>","optional":false,"description":"Signal of the active sort direction (`'asc'` / `'desc'`), or `null` when cleared."}]},{"name":"buildSelectLikePositions","kind":"function","description":"Connected-overlay position list for \"select-like\" overlays — overlays whose panel attaches directly under (or above) a trigger element and falls back to the opposite vertical side when there is not enough room. Returns four fallback positions: below-start, below-end, above-start, above-end. Used by `SelectComponent`, `ComboboxComponent`, `DatePickerComponent`, and `DateRangePickerComponent` — any overlay-bearing form control that anchors a listbox / menu / calendar panel to its trigger. The shape is identical for all four; the historical \"select-like\" name refers to the original consumer.","signature":"buildSelectLikePositions(offset = 0): ConnectedPosition[]"},{"name":"resolveSelectScrollStrategy","kind":"function","description":"Maps a named scroll-strategy variant to the corresponding CDK `ScrollStrategy` instance. `'reposition'` is the default for select-like overlays; `'close'` dismisses the panel on scroll; `'block'` locks page scrolling while the panel is open. Used by `SelectComponent`, `ComboboxComponent`, `DatePickerComponent`, and `DateRangePickerComponent` — any overlay-bearing form control that exposes the same three-option scroll-strategy input.","signature":"resolveSelectScrollStrategy(name: SelectScrollStrategyName, overlay: Overlay): ScrollStrategy"},{"name":"SelectScrollStrategyName","kind":"type","description":"Named scroll-strategy variants supported by select-like overlays.","definition":"'reposition' | 'close' | 'block'"},{"name":"consumeOverlayEscape","kind":"function","description":"Subscribes to overlay-level `Escape` keydown events and invokes `onEscape` for each one. Returns an unsubscribe function so callers can scope teardown to the open lifecycle of the overlay (typically tied into a `Subscription` aggregate or a `takeUntilDestroyed` flow). Listens via `OverlayRef.keydownEvents()` so the handler fires for any keystroke originating inside the overlay, regardless of which element holds DOM focus — useful for select-style overlays whose search/listbox children may receive focus apart from the trigger. Callers remain responsible for calling `event.preventDefault()` / `stopPropagation()` inside `onEscape` if they want to short-circuit further key handling.","signature":"consumeOverlayEscape(overlayRef: OverlayRef, onEscape: (event: KeyboardEvent) => void): () => void"},{"name":"PickerOverlayCoordinator","kind":"service","description":"Coordinator that owns the CDK `OverlayRef`, focus trap, panel-id, and animation timing for an overlay-bearing form control. Consumed by `DatePickerComponent` and `DateRangePickerComponent`; both pickers register the coordinator at the component level (`providers: [PickerOverlayCoordinator]`) so each picker instance owns its own coordinator state. Per the library \"no `providedIn: 'root'` for services\" rule (see `.claude/CLAUDE.md` → \"What NOT To Do\"): the coordinator holds per-overlay `OverlayRef` / `FocusTrap` state and MUST be component-scoped. Scope of responsibility: - Create / dispose the CDK `OverlayRef`. - Attach a `ComponentPortal` and return the instance. - Set up / tear down a `FocusTrap` around the overlay element. - Emit `opened$` after the enter animation completes (closes the synchronous-emit bug previously present in both pickers). - Expose `backdropClick$` / `overlayKeydown$` / `escape$` streams scoped to the current open lifecycle. - Run a leave-animation timer with an `isAttached` guard so close races never touch a detached `OverlayRef` (mirrors the S14 command-palette pattern). Out of scope (stays in the consuming picker): - \"Restore previous value on close\" — semantics differ per picker. - Per-picker portal callbacks (calendar selection, action-bar buttons). - View-mode change-detection nudges (the range-picker calls `changeDetectorRef.detectChanges()` after pushing its initial overlay config; the date-picker does not).","methods":[{"name":"open","signature":"open(config: PickerOpenConfig<TOverlay>): PickerOpenResult<TOverlay> | null","description":"Creates and attaches the overlay. Returns the live overlay metadata synchronously; callers should subscribe to opened$ to observe the moment the enter animation completes. Returns `null` if an overlay is already attached."},{"name":"close","signature":"close(onAfterClose: () => void = () => {}): void","description":"Starts the close sequence — destroys the focus trap immediately so focus can return to the trigger, then detaches the overlay after the leave animation runs (PICKER_LEAVE_DURATION). Invokes `onAfterClose` once the overlay is fully detached. No-op if no overlay is open or a close is already in flight. Mirrors the S14 command-palette `isAttached`-guarded close pattern: after the timer fires we re-check `attachedSignal` before touching the overlay so a race (programmatic dispose, double-close) cannot touch a destroyed instance."},{"name":"backdropClick$","signature":"backdropClick$(): Observable<MouseEvent>","description":"Stream of overlay-level backdrop clicks for the current open lifecycle. Completes when the overlay closes."},{"name":"overlayKeydown$","signature":"overlayKeydown$(): Observable<KeyboardEvent>","description":"Stream of overlay-level keydown events for the current open lifecycle. Completes when the overlay closes."},{"name":"escape$","signature":"escape$(): Observable<KeyboardEvent>","description":"Stream filtered to `Escape` keydowns inside the overlay for the current open lifecycle. Equivalent to `overlayKeydown$().pipe(filter(e => e.key === 'Escape'))` but spelled out so consumers don't import RxJS operators just for the common case."},{"name":"opened$","signature":"opened$(): Observable<void>","description":"Emits exactly once after the enter animation completes (PICKER_ENTER_DURATIONms after `open()`). Used by consumers to fire their `opened` output at the moment the overlay panel is actually visible — closes the synchronous-emit bug the pickers carried before this coordinator existed. Completes when the overlay closes."},{"name":"ref","signature":"ref(): OverlayRef | null","description":"Exposes the live `OverlayRef` for advanced consumers (e.g. width sync)."},{"name":"panelId","signature":"panelId(): string | null","description":"Exposes the current panel id (auto-generated via CDK `_IdGenerator`, stable for the open lifecycle, reset to `null` on close). Neither consuming picker uses this today — both keep their own `${hostId}-dialog` id for `aria-controls` wiring — but the helper is exposed for future consumers (e.g. a picker wrapper that wants its panel id auto-managed)."}]},{"name":"PICKER_ENTER_DURATION","kind":"const","description":"Enter-animation duration for date-picker / date-range-picker overlays. Matches `theme/_base.css` `.scale-in 140ms` — the keyframe the picker overlays apply via `animate.enter=\"scale-in\"`. The coordinator delays `opened$` emission by this duration so consumers can hook the moment the panel has fully appeared instead of the moment `open()` was called."},{"name":"PICKER_LEAVE_DURATION","kind":"const","description":"Leave-animation duration for date-picker / date-range-picker overlays. Matches `theme/_base.css` `.scale-out 120ms` — the keyframe the picker overlays apply via `animate.leave=\"scale-out\"`. The coordinator delays overlay detach by this duration so the leave animation can play through."},{"name":"PickerOpenConfig","kind":"interface","description":"Configuration passed to PickerOverlayCoordinator.open.","members":[{"name":"origin","type":"ElementRef<HTMLElement>","optional":false,"description":"Element used as the connected-overlay origin (typically the picker trigger)."},{"name":"portalComponent","type":"ComponentType<TOverlay>","optional":false,"description":"Component type to render inside the overlay (e.g. `DatePickerOverlayComponent`)."},{"name":"viewContainerRef","type":"ViewContainerRef","optional":false,"description":"View-container that hosts the embedded view; usually the picker's `ViewContainerRef`."},{"name":"injector","type":"Injector","optional":true,"description":"Optional injector forwarded to the portal so DI tokens resolve from the picker's tree."},{"name":"positions","type":"ConnectedPosition[]","optional":false,"description":"Connected-position list — typically the result of `buildSelectLikePositions(offset)`."},{"name":"scrollStrategy","type":"ScrollStrategy","optional":false,"description":"CDK scroll-strategy instance for the overlay."},{"name":"panelClass","type":"string","optional":false,"description":"CSS class applied to the CDK overlay-pane element (NOT the panel root)."},{"name":"viewportMargin","type":"number","optional":true,"description":"Viewport margin forwarded to the CDK position-strategy. Defaults to `8`."}]},{"name":"PickerOpenResult","kind":"interface","description":"Synchronous return shape of PickerOverlayCoordinator.open.","members":[{"name":"overlayRef","type":"OverlayRef","optional":false,"description":"The CDK `OverlayRef` driving the overlay — exposed for advanced consumers."},{"name":"componentRef","type":"ComponentRef<TOverlay>","optional":false,"description":"The Angular `ComponentRef` for the attached portal — exposed so consumers may run `detectChanges()` to flush an initial-config push synchronously."},{"name":"instance","type":"TOverlay","optional":false,"description":"The instance of the attached portal component. Equivalent to `componentRef.instance`."},{"name":"panelId","type":"string","optional":false,"description":"Auto-generated id consumers may wire to `aria-controls` / dialog `id`."}]},{"name":"AriaIdQueue","kind":"class","description":"Append-only id list with idempotent insertion, used for the `aria-describedby` queue both `DialogContainer` and `SheetContainer` maintain (the matching `aria-labelledby` queue lives in CDK's `CdkDialogContainer`). Pure data structure — no DOM, no Angular signals. Consumers wrap an instance in their own change-detection mechanism (the OverlayContainerCoordinator keeps the live snapshot in a signal so the container's `[attr.aria-describedby]` binding refreshes via OnPush without a manual `markForCheck()`). First-registered-wins semantics for `first()` mirror CDK's `_ariaLabelledByQueue[0]` host binding — describing the dialog by the earliest registered description prevents a late-mounted nested directive from silently re-aiming the description target.","methods":[{"name":"add","signature":"add(id: string): void","description":"Inserts an id at the tail. No-op if the id is already present."},{"name":"remove","signature":"remove(id: string): void","description":"Removes the given id. No-op if the id is not present."},{"name":"first","signature":"first(): string | null","description":"First registered id (or `null` if empty). Matches CDK's `_ariaLabelledByQueue[0]` semantics."},{"name":"snapshot","signature":"snapshot(): readonly string[]","description":"Returns a fresh snapshot of all registered ids in insertion order."}]},{"name":"OVERLAY_ANIMATION_FALLBACK_PADDING","kind":"const","description":"Fallback padding (ms) added on top of an enter/exit animation duration when scheduling the `transitionend` fallback timer. The browser SHOULD fire `transitionend` at the configured duration, but transitions can be swallowed (focus changes during the animation, interrupted transitions, etc.) — the padding gives the browser a small grace period before our fallback runs. Both dialog and sheet containers used the same constant — extracted here so a future tweak applies to both at once."},{"name":"coerceOverlayDuration","kind":"function","description":"Coerces a user-supplied animation duration to a safe positive integer, or falls back to a default if the input is `null`, `undefined`, negative, or non-finite (`NaN`, `Infinity`). Dialog and sheet containers both used this same standalone function; centralised here.","signature":"coerceOverlayDuration(value: number | undefined, fallback: number): number"},{"name":"mergeOverlayPanelClass","kind":"function","description":"Merges a consumer-supplied `panelClass` (single class, list, or `undefined`) with the container's internal class string. Returns a single space-separated class string suitable for `[class]` host binding. `consumer` always wins ordering (appended after `internal`) so consumer overrides resolve correctly through `tailwind-merge` upstream.","signature":"mergeOverlayPanelClass(internal: string, consumer: string | readonly string[] | undefined): string"},{"name":"OverlayContainerCoordinator","kind":"service","description":"Component-scoped coordinator that owns the enter/exit animation state machine, ARIA-describedby id queue, and panel-class merge for an overlay container that subclasses `@angular/cdk/dialog`'s `CdkDialogContainer`. Consumed by `DialogContainer` and `SheetContainer`; both register the coordinator at the component level (`providers: [OverlayContainerCoordinator]`) so each container instance owns its own animation state and queue. Per the library \"no `providedIn: 'root'` for services\" rule (see `.claude/CLAUDE.md` → \"What NOT To Do\"): the coordinator holds per-overlay state (the current animation timer, the describedby queue, the lifecycle signal) and MUST be component-scoped. Scope of responsibility: - Animation state signal (`state`) and `transitionDuration` computed. - Enter/exit animation timing (`startEnterAnimation`, `startExitAnimation`) with a `transitionend`-fallback timer. - `animationStateChanged` EventEmitter forwarded to the consuming `Ref`. - ARIA-describedby id queue (the matching labelledby queue is already owned by CDK's `CdkDialogContainer._ariaLabelledByQueue`). Out of scope (stays on the container subclass): - The `CdkDialogContainer` contract — focus trap, escape key, backdrop click, overlay attach/detach. The container subclass keeps inheriting `CdkDialogContainer` directly; this coordinator layers on top. - Tailwind class resolution (`tv()` variant slots) — that's per-container. - Host bindings on the container element — that's per-container.","methods":[{"name":"setDurations","signature":"setDurations(enter: number, exit: number): void","description":"Records the resolved enter/exit durations for the open lifecycle. Called once from the container constructor with values coerced via `coerceOverlayDuration`. Stored on the coordinator so `transitionDuration()` and the timer-driven state transitions read from a single source."},{"name":"startEnterAnimation","signature":"startEnterAnimation(): void","description":"Drives the enter animation: emits `opening` immediately, then defers the state flip to `open` by one frame so the browser applies the initial (hidden / off-screen) styles before transitioning. If `enter` is `0` everything resolves synchronously (no animation)."},{"name":"startExitAnimation","signature":"startExitAnimation(): void","description":"Drives the exit animation: emits `closing` synchronously and schedules the `closed` emission for after the exit duration (+ a small fallback padding) elapses. The consuming `Ref` listens for the `closed` emission to detach the CDK overlay."},{"name":"addAriaDescribedBy","signature":"addAriaDescribedBy(id: string): void","description":"Registers a description id with the `aria-describedby` queue."},{"name":"removeAriaDescribedBy","signature":"removeAriaDescribedBy(id: string): void","description":"Removes a previously registered description id."},{"name":"firstDescribedBy","signature":"firstDescribedBy(): string | null","description":"First-registered-wins resolution for the `aria-describedby` attribute."}]},{"name":"OverlayContainerState","kind":"type","description":"Lifecycle states an overlay container passes through. Shared by dialog and sheet.","definition":"'opening' | 'open' | 'closing' | 'closed'"},{"name":"OverlayContainerAnimationEvent","kind":"interface","description":"Event emitted on every overlay-container animation-state transition.","members":[{"name":"state","type":"OverlayContainerState","optional":false,"description":"State that the container just transitioned into."},{"name":"totalTime","type":"number","optional":false,"description":"Duration, in ms, of the transition that triggered the event."}]},{"name":"tabTriggerVariants","kind":"const","description":"Trigger-only tailwind-variants config shared by tabs and tab-nav. Both components own additional component-local slots (tablist/list/panel/nav, etc.) — only the trigger shape is canonical enough to share here. The resulting class string is merged with each component's local trigger additions (e.g. tab-nav prepends `no-underline` because anchor elements need to override the default underline; tabs adds nothing extra at the base). Active and inactive trigger state is applied separately via getActiveTriggerClasses / getInactiveTriggerClasses so the `Record<TwColor, string>` lookups stay statically scannable by the Tailwind v4 content scanner."},{"name":"getActiveTriggerClasses","kind":"function","description":"Returns the active-state trigger class string for the given variant, color, and orientation. Tab-nav callers pass `'horizontal'` since it is horizontal-only.","signature":"getActiveTriggerClasses(variant: TabTriggerVariant, color: TwColor, orientation: 'horizontal' | 'vertical' = 'horizontal'): string"},{"name":"getInactiveTriggerClasses","kind":"function","description":"Returns the inactive-state trigger class string for the given variant.","signature":"getInactiveTriggerClasses(variant: TabTriggerVariant): string"},{"name":"UNDERLINE_ACTIVE_HORIZONTAL","kind":"const","description":"","type":"Record<TwColor, string>"},{"name":"UNDERLINE_ACTIVE_VERTICAL","kind":"const","description":"","type":"Record<TwColor, string>"},{"name":"ENCLOSED_ACTIVE_HORIZONTAL","kind":"const","description":"","type":"Record<TwColor, string>"},{"name":"ENCLOSED_ACTIVE_VERTICAL","kind":"const","description":"","type":"Record<TwColor, string>"},{"name":"PILL_ACTIVE","kind":"const","description":"","type":"Record<TwColor, string>"},{"name":"INACTIVE_TRIGGER_CLASSES","kind":"const","description":"","type":"Record<TabTriggerVariant, string>"},{"name":"TabTriggerVariant","kind":"type","description":"Visual style shared by `tw-tabs` (`TabsVariant`) and `nav[twTabNav]` (`TabNavVariant`). Kept as a string-literal union so downstream consumers can narrow when needed.","definition":"'underline' | 'enclosed' | 'pill'"},{"name":"padTwo","kind":"function","description":"Zero-pads a non-negative integer to exactly two digits.","signature":"padTwo(value: number): string"},{"name":"to12h","kind":"function","description":"Converts a 24h hour (0–23) to its 12h display value (1–12).","signature":"to12h(hour24: number): number"},{"name":"from12h","kind":"function","description":"Builds a canonical 0–23 hour from a 12h display hour and meridiem.","signature":"from12h(hour12: number, meridiem: TimePickerMeridiem): number"},{"name":"fieldMax","kind":"function","description":"Maximum allowed value for a field given the picker format.","signature":"fieldMax(field: 'hour' | 'minute' | 'second', format: TimePickerFormat): number"},{"name":"fieldMin","kind":"function","description":"Minimum allowed value for a field given the picker format.","signature":"fieldMin(field: 'hour' | 'minute' | 'second', format: TimePickerFormat): number"},{"name":"appendDigit","kind":"function","description":"Buffers a typed digit onto the current field text, matching the standard two-digit time-field behaviour: - empty + 'x' → 'x' - 'x' + 'y' → 'xy' (if value stays in range) - 'xy' + 'z' → 'z' (overflow → reset) - any combo that would exceed `max` resets to the new digit alone.","signature":"appendDigit(current: string, digit: string, max: number): string"},{"name":"isTerminalDigit","kind":"function","description":"Reports whether `current` + `digit` unambiguously fills the field — either because the buffer reaches two chars or because the first digit alone already excludes a valid second digit (e.g., `'6'` for minutes, `'3'` for 24h hour). Used to auto-advance focus to the next field.","signature":"isTerminalDigit(current: string, digit: string, max: number): boolean"},{"name":"stepWithWrap","kind":"function","description":"Steps a numeric value by `step`, wrapping inside `[min, max]`. Works for arbitrary step sizes; a step of 0 behaves as 1 to protect against mis-configs.","signature":"stepWithWrap(value: number, step: number, direction: 1 | -1, min: number, max: number): number"},{"name":"clamp","kind":"function","description":"Clamps a number into `[min, max]` without wrapping.","signature":"clamp(value: number, min: number, max: number): number"},{"name":"parseField","kind":"function","description":"Parses a 1- or 2-digit text field; returns `null` if empty or non-numeric.","signature":"parseField(text: string): number | null"},{"name":"timeOfDaySeconds","kind":"function","description":"Total seconds since midnight for a (h, m, s) tuple — useful for min/max compare.","signature":"timeOfDaySeconds(hour: number, minute: number, second: number): number"},{"name":"TimePickerFormat","kind":"type","description":"Supported time-picker formats.","definition":"'12h' | '24h'"},{"name":"TimePickerMeridiem","kind":"type","description":"Meridiem used by the 12h format.","definition":"'AM' | 'PM'"}],"snippets":[],"summary":"Shared primitives entry point — the TwColor / TwSize / TwOrientation / TwBreakpoint variant types, the TW_ERROR_STATE_MATCHER policy token, and cross-component overlay, sort, and time helpers — renders no UI.","whenToUse":["Typing a color, size, or orientation input on your own wrapper component so it accepts exactly the same values as the library (import type { TwColor, TwSize } from \"@cdevhub/ngx-tw/core\")","Iterating over the eight semantic colors or the xs–xl size scale to build a demo, a theme picker, or a Storybook-style matrix","Changing when form controls show their error state — provide TW_ERROR_STATE_MATCHER with a custom ErrorStateMatcher to show errors on submit only, or on dirty rather than touched","Configuring range-picker behavior with Partial<RangeBehaviorConfig> (backward ranges, single-day ranges, partial-range persistence)","Implementing a custom sort handle that the table sort header can discover, via TW_SORT_HANDLE","Building a custom overlay-bearing control that should match library positioning, scroll-strategy, escape handling, and enter/leave timing","Reusing the tab-trigger variant classes so a bespoke tab-like strip matches the built-in tabs"],"related":["theme","form-field","input","checkbox","select","table","sort","time-picker","calendar"],"aliases":["types","shared types","TwColor","TwSize","TwOrientation","TwBreakpoint","error state matcher","TW_ERROR_STATE_MATCHER","variant types","tokens","primitives","utilities","validation display"],"hasMeta":true,"metaPath":"projects/ngx-tw/core/core.meta.ts"},{"name":"badge","importPath":"@cdevhub/ngx-tw/badge","symbols":[{"name":"BadgeComponent","kind":"component","description":"Compact status label, tag, or count attached to any host element. Uses an attribute selector (`[twBadge]`) rather than the library's canonical element selector so consumers can apply the badge styling to any inline element — `<span>`, `<a>`, `<div>` — without an extra wrapper. The trade-off is intentional: badges most often live inside an existing text flow or list item where wrapping in a `<tw-badge>` element would add structural noise. For dot-only presence indicators (no text, no padding), use the sibling `[twBadgeDot]` directive — its rendering shape (no children, no dismiss, no leading slot) is structurally distinct from the labelled badge.","selector":"[twBadge]","usage":[{"form":"attribute","selector":"[twBadge]","name":"twBadge"}],"exportAs":"twBadge","contentSlots":[{"select":"tw-avatar"},{"select":"tw-icon"},{"select":null}],"inputs":[{"name":"color","type":"TwColor","default":"'neutral'","description":"Sets the semantic color palette. Defaults to `'neutral'`."},{"name":"variant","type":"BadgeVariant","default":"'soft'","description":"Controls the visual style. Defaults to `'soft'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls badge size (padding, font, icon size). Defaults to `'md'`."},{"name":"pill","type":"boolean","default":"false","description":"When true, uses fully rounded corners instead of default `rounded-md`. Defaults to `false`."},{"name":"dismissible","type":"boolean","default":"false","description":"When true, renders a dismiss button inside the badge. Defaults to `false`."},{"name":"live","type":"boolean","default":"false","description":"When true, exposes the badge as an ARIA live region (`role=\"status\"`) so assistive technology announces content changes. Defaults to `false` because most badges are decorative tags or labels — opt in only when the badge represents a value that actually updates in place."},{"name":"dismissLabel","type":"string","default":"'Dismiss'","description":"Accessible label for the dismiss button. Override for localization. Defaults to `'Dismiss'`."}],"outputs":[{"name":"dismissed","payloadType":"void","description":"Fires when the dismiss button is clicked."}]},{"name":"BadgeVariant","kind":"type","description":"Visual style of the badge.","definition":"'solid' | 'outline' | 'soft'"},{"name":"BadgeDotDirective","kind":"directive","description":"Compact colored dot used as a presence indicator, unread marker, or status pip. Applies as an attribute selector so the host element controls the structure — wrap in a `<span>`, `<div>`, list item, or any inline element. The dot has no text content; pair it with a visible label adjacent in the DOM, or set `aria-label` on the host so assistive technology can announce the state. Opt in to `live` to expose the dot as an ARIA live region when the indicator state actually changes (e.g. \"new messages received\").","selector":"[twBadgeDot]","usage":[{"form":"attribute","selector":"[twBadgeDot]","name":"twBadgeDot"}],"exportAs":"twBadgeDot","inputs":[{"name":"color","type":"TwColor","default":"'neutral'","description":"Sets the semantic color palette. Defaults to `'neutral'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls dot dimensions. Defaults to `'md'`."},{"name":"live","type":"boolean","default":"false","description":"When true, exposes the dot as an ARIA live region (`role=\"status\"`) so assistive technology announces state changes (e.g. an unread indicator appearing). Defaults to `false` because most dots are decorative pips paired with a visible label."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type BadgeVariant = 'solid' | 'outline' | 'soft';\n\ntype TwColor =\n | 'primary' | 'secondary' | 'accent' | 'neutral'\n | 'info' | 'success' | 'warning' | 'error';\n\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <span twBadge [variant]=\"v\">{{ v }}</span>\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (v of variants; track v) {\n <div>\n <p>{{ v }}</p>\n @for (c of colors; track c) {\n <span twBadge [variant]=\"v\" [color]=\"c\">{{ c }}</span>\n }\n </div>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <span twBadge [size]=\"s\">{{ s }}</span>\n}"},{"id":"iconsSnippet","title":"With Icons","language":"html","code":"<span twBadge variant=\"soft\" color=\"success\">\n <tw-icon name=\"check-circle\" />Verified\n</span>\n<span twBadge variant=\"solid\" color=\"primary\">\n <tw-icon name=\"star\" />Featured\n</span>\n<span twBadge variant=\"outline\" color=\"neutral\">\n <tw-icon name=\"lock\" />Locked\n</span>"},{"id":"avatarsSnippet","title":"With Avatars","language":"html","code":"<span twBadge variant=\"soft\" color=\"primary\">\n <tw-avatar src=\"/anna.jpg\" alt=\"Anna\" />Anna Smith\n</span>\n<span twBadge variant=\"soft\" color=\"info\">\n <tw-avatar initials=\"BC\" color=\"info\" />Bob Chen\n</span>"},{"id":"pillSnippet","title":"Pill Shape","language":"html","code":"@for (c of colors; track c) {\n <span twBadge [pill]=\"true\" [color]=\"c\">{{ c }}</span>\n}"},{"id":"dotSnippet","title":"Dot Indicator","language":"html","code":"@for (c of colors; track c) {\n <div class=\"flex items-center gap-2\">\n <span twBadgeDot [color]=\"c\"></span>\n <span class=\"text-xs text-fg-muted\">{{ c }}</span>\n </div>\n}"},{"id":"dismissibleTsSnippet","title":"Dismissible","language":"ts","code":"protected readonly allTags = ['Angular', 'Tailwind', 'TypeScript'];\nprotected readonly tags = signal([...this.allTags]);\n\nremoveTag(tag: string): void {\n this.tags.update(t => t.filter(item => item !== tag));\n}"},{"id":"dismissibleHtmlSnippet","title":"Dismissible","language":"html","code":"@for (tag of tags(); track tag) {\n <span\n twBadge\n color=\"primary\"\n [dismissible]=\"true\"\n (dismissed)=\"removeTag(tag)\"\n >{{ tag }}</span>\n}"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<span twBadge>Default</span>\n<span twBadge variant=\"solid\" color=\"primary\">Solid</span>\n<span twBadge variant=\"outline\" color=\"info\">Outline</span>\n<span twBadge variant=\"soft\" color=\"success\">Soft</span>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { BadgeComponent } from '@cdevhub/ngx-tw/badge';"}],"summary":"Compact status label, count, or tag that turns any element into an annotation on nearby content.","whenToUse":["Status on a table row or list item (\"Active\", \"Pending\", \"Failed\")","Unread or item counts on a nav item or tab","Categorical tags that may be dismissible","A dot-only presence or unread indicator with no label, via [twBadgeDot]"],"whenNotToUse":[{"instead":"alert","because":"the message needs a full-width, dismissible banner with its own body text"},{"instead":"stat","because":"the number is the primary content of the block, not an annotation on something else"},{"instead":"tags-input","because":"the user needs to add and remove the tags themselves as a form value"}],"related":["alert","stat","tags-input","avatar","icon"],"aliases":["chip","pill","tag","label","counter","status","dot","indicator"],"hasMeta":true,"metaPath":"projects/ngx-tw/badge/badge.meta.ts"},{"name":"card","importPath":"@cdevhub/ngx-tw/card","symbols":[{"name":"CardComponent","kind":"component","description":"","selector":"tw-card","usage":[{"form":"element","selector":"tw-card","name":"tw-card"}],"contentSlots":[{"select":null}],"inputs":[{"name":"variant","type":"CardVariant","default":"'elevated'","description":"Controls the visual elevation style. Defaults to `'elevated'`."},{"name":"color","type":"TwColor","default":"'neutral'","description":"Sets the semantic color for bordered regions. Only applies to `outlined` variant borders. Defaults to `'neutral'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls padding of header, body, and footer sections. Defaults to `'md'`."}]},{"name":"CardHeaderDirective","kind":"directive","description":"","selector":"[twCardHeader]","usage":[{"form":"attribute","selector":"[twCardHeader]","name":"twCardHeader"}]},{"name":"CardBodyDirective","kind":"directive","description":"","selector":"[twCardBody]","usage":[{"form":"attribute","selector":"[twCardBody]","name":"twCardBody"}]},{"name":"CardFooterDirective","kind":"directive","description":"","selector":"[twCardFooter]","usage":[{"form":"attribute","selector":"[twCardFooter]","name":"twCardFooter"}]},{"name":"CardMediaDirective","kind":"directive","description":"","selector":"[twCardMedia]","usage":[{"form":"attribute","selector":"[twCardMedia]","name":"twCardMedia"}]},{"name":"CardVariant","kind":"type","description":"Visual style of the card container.","definition":"'elevated' | 'outlined' | 'ghost'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type CardVariant = 'elevated' | 'outlined' | 'ghost';\n\n// Shared library types used above\ntype TwColor =\n | 'primary' | 'secondary' | 'accent' | 'neutral'\n | 'info' | 'success' | 'warning' | 'error';\n\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-card [variant]=\"v\">\n <div twCardHeader>Starter plan</div>\n <div twCardBody>\n <p>$29/mo</p>\n <p>Up to 10 team members, unlimited projects, and community support.</p>\n </div>\n <div twCardFooter>{{ v }}</div>\n </tw-card>\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (sample of colorSamples; track sample.color) {\n <tw-card variant=\"outlined\" [color]=\"sample.color\" size=\"sm\">\n <div twCardHeader>{{ sample.title }}</div>\n <div twCardBody>{{ sample.body }}</div>\n </tw-card>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-card variant=\"outlined\" [size]=\"s\">\n <div twCardHeader>Size: {{ s }}</div>\n <div twCardBody>\n The padding scales with the size input.\n </div>\n </tw-card>\n}"},{"id":"sectionsSnippet","title":"Sections","language":"html","code":"<tw-card>\n <div twCardHeader>\n <div class=\"flex items-center justify-between\">\n <span>Delete workspace</span>\n <span class=\"text-xs font-normal text-error-600\">Destructive</span>\n </div>\n </div>\n <div twCardBody>\n <p>This permanently deletes the Growth workspace, including 12 projects and 4,103 assets.</p>\n <p>Team members will lose access immediately. This action cannot be undone.</p>\n </div>\n <div twCardFooter>\n <div class=\"flex items-center justify-end gap-2\">\n <button twButton variant=\"ghost\" color=\"neutral\" size=\"xs\">Cancel</button>\n <button twButton variant=\"solid\" color=\"error\" size=\"xs\">Delete workspace</button>\n </div>\n </div>\n</tw-card>"},{"id":"mediaSnippet","title":"Media placement","language":"html","code":"<!-- Top media: blog preview -->\n<tw-card>\n <div twCardMedia class=\"h-32 bg-gradient-to-br from-primary-400 via-primary-500 to-accent-500\">\n <!-- hero image or cover art -->\n </div>\n <div twCardBody>\n <p class=\"font-semibold\">Shipping the new command palette</p>\n <p>How we rebuilt the search bar on top of CDK overlays.</p>\n </div>\n</tw-card>\n\n<!-- Bottom media: stat card with trend strip -->\n<tw-card>\n <div twCardHeader>Quarterly active users</div>\n <div twCardBody>\n <p>28,401</p>\n <p>Up 12.4% from last quarter.</p>\n </div>\n <div twCardMedia class=\"h-16 bg-gradient-to-r from-success-100 via-success-200 to-success-400\"></div>\n</tw-card>"},{"id":"bodyOnlySnippet","title":"Body only","language":"html","code":"<tw-card variant=\"outlined\">\n <div twCardBody>\n <div class=\"flex items-start gap-3\">\n <div class=\"avatar\">AM</div>\n <div>\n <p class=\"font-medium\">Alice Morgan</p>\n <p>Invited 3 teammates to the Growth workspace.</p>\n <p class=\"text-xs\">Just now</p>\n </div>\n </div>\n </div>\n</tw-card>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-card>\n <div twCardHeader>Q1 revenue report</div>\n <div twCardBody>\n <p>Total revenue climbed to <strong>$2.4M</strong>, up 18% year-over-year.</p>\n <p>Enterprise contracts drove the majority of the increase.</p>\n </div>\n <div twCardFooter>Updated April 22, 2026 · Finance team</div>\n</tw-card>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n CardComponent,\n CardHeaderDirective,\n CardBodyDirective,\n CardFooterDirective,\n CardMediaDirective,\n} from '@cdevhub/ngx-tw/card';"},{"id":"clickableCardSnippet","title":"Composition patterns","language":"html","code":"<a routerLink=\"/reports/q1\" class=\"block focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 rounded-lg\">\n <tw-card>\n <div twCardHeader>Q1 revenue report</div>\n <div twCardBody>\n <p>Total revenue climbed to $2.4M, up 18% year-over-year.</p>\n </div>\n <div twCardFooter>Updated April 22, 2026</div>\n </tw-card>\n</a>"},{"id":"interactiveRowSnippet","title":"Composition patterns","language":"html","code":"<tw-card variant=\"outlined\" size=\"xs\">\n @for (member of teamMembers; track member.id) {\n <tw-item\n twCardBody\n interactive\n (selected)=\"openProfile(member)\"\n >\n <span twItemTitle>{{ member.name }}</span>\n <span twItemDescription>{{ member.role }}</span>\n </tw-item>\n }\n</tw-card>"},{"id":"landmarkSnippet","title":"Composition patterns","language":"html","code":"<tw-card role=\"region\" aria-labelledby=\"q1-report-title\">\n <div twCardHeader id=\"q1-report-title\">Q1 revenue report</div>\n <div twCardBody>\n <p>Total revenue climbed to $2.4M, up 18% year-over-year.</p>\n </div>\n</tw-card>"}],"summary":"Purely presentational surface that groups related content behind a visual boundary, with directive-driven header, body, footer, and full-bleed media slots.","whenToUse":["A block of related information needs its own surface, consistent padding, and a clear edge against the page","Content splits into a title row, a body, and a metadata footer that should be divided automatically","A cover image or media strip sits above or below the text, ordered wherever the consumer places it","A dashboard or list of summary panels rendered as a grid of equally framed blocks","Wrapping the whole surface in a native anchor or button to make one big clickable tile — the card itself stays non-interactive and adds no ARIA"],"whenNotToUse":[{"instead":"alert","because":"the block is an informational or status message rather than a general content container"},{"instead":"dialog","because":"the content must interrupt the user and float above the page as a modal surface"},{"instead":"collapsible","because":"the grouped content needs to expand and collapse rather than always stay visible"},{"instead":"accordion","because":"several such groups sit in a stack where only one should be open at a time"},{"instead":"item","because":"each row needs its own focus ring, tab stop, and selection output rather than a framed surface"}],"related":["alert","dialog","accordion","collapsible","item","flip-card","aspect-ratio","skeleton"],"aliases":["panel","tile","surface","container","box","content card","media card"],"hasMeta":true,"metaPath":"projects/ngx-tw/card/card.meta.ts"},{"name":"flip-card","importPath":"@cdevhub/ngx-tw/flip-card","symbols":[{"name":"FlipCardComponent","kind":"component","description":"Two-faced card with a CSS 3D-perspective flip animation.","selector":"tw-flip-card","usage":[{"form":"element","selector":"tw-flip-card","name":"tw-flip-card"}],"contentSlots":[{"select":"[slot="},{"select":"[slot="}],"inputs":[{"name":"variant","type":"FlipCardVariant","default":"'outlined'","description":"Visual style of the card chrome. Mirrors `tw-card`'s variant vocabulary. Defaults to `'outlined'` (vs `tw-card`'s `'elevated'` default) so the flip animation reads more clearly without a baseline shadow underneath."},{"name":"direction","type":"FlipCardDirection","default":"'horizontal'","description":"Axis of rotation. `'horizontal'` rotates around the Y axis (left/right flip); `'vertical'` rotates around the X axis (top/bottom flip). Defaults to `'horizontal'`."},{"name":"trigger","type":"FlipCardTrigger","default":"'both'","description":"Which user action flips the card. `'both'` enables click and hover. `'manual'` disables all triggers and defers control to the `flipped` model. Defaults to `'both'`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, all triggers and keyboard handling are disabled; the current face stays visible. Defaults to `false`.","transform":"booleanAttribute"},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the host element. Required when `trigger` is `'manual'` (the host renders as `role=\"region\"`, which AXE requires to have an accessible name); a default of `'Flip card'` is used in that mode if no value is provided. In interactive modes the host's accessible name is normally derived from the visible face's content — set this input to override."}],"models":[{"name":"flipped","type":"boolean","default":"false","description":"Whether the back face is currently visible. Two-way bindable via `[(flipped)]`. Defaults to `false`. The `flippedChange` event fires on every toggle."}]},{"name":"FlipCardVariant","kind":"type","description":"Visual style of the flip card chrome. Mirrors `tw-card`.","definition":"'outlined' | 'elevated' | 'ghost'"},{"name":"FlipCardDirection","kind":"type","description":"Axis of rotation for the flip animation.","definition":"'horizontal' | 'vertical'"},{"name":"FlipCardTrigger","kind":"type","description":"Which user interaction flips the card.","definition":"'hover' | 'click' | 'manual' | 'both'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type FlipCardVariant = 'outlined' | 'elevated' | 'ghost';\n\ntype FlipCardDirection = 'horizontal' | 'vertical';\n\ntype FlipCardTrigger = 'hover' | 'click' | 'manual' | 'both';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-flip-card [variant]=\"v\" class=\"h-48 w-full\">\n <div slot=\"front\">Variant: {{ v }}</div>\n <div slot=\"back\">The chrome is {{ v }}; the flip is the same.</div>\n </tw-flip-card>\n}"},{"id":"directionSnippet","title":"Direction","language":"html","code":"@for (d of directions; track d) {\n <tw-flip-card [direction]=\"d\" class=\"h-48 w-full\">\n <div slot=\"front\">Direction: {{ d }}</div>\n <div slot=\"back\">Rotates around the {{ d === 'horizontal' ? 'Y' : 'X' }} axis.</div>\n </tw-flip-card>\n}"},{"id":"triggersSnippet","title":"Triggers","language":"html","code":"@for (t of triggers; track t) {\n <tw-flip-card [trigger]=\"t\" variant=\"elevated\" class=\"h-40 w-full\">\n <div slot=\"front\">Trigger: {{ t }}</div>\n <div slot=\"back\">Back face for {{ t }}</div>\n </tw-flip-card>\n}"},{"id":"manualTsSnippet","title":"Manual control","language":"ts","code":"readonly manualFlipped = signal(false);"},{"id":"manualHtmlSnippet","title":"Manual control","language":"html","code":"<tw-flip-card\n trigger=\"manual\"\n variant=\"elevated\"\n ariaLabel=\"Invoice #00412 summary\"\n [(flipped)]=\"manualFlipped\"\n class=\"h-48 w-72\"\n>\n <div slot=\"front\">Invoice #00412</div>\n <div slot=\"back\">\n <ul>\n <li>Annual plan — $1,188</li>\n <li>Team seats × 4 — $960</li>\n <li>Support add-on — $240</li>\n </ul>\n </div>\n</tw-flip-card>\n\n<button twButton (click)=\"manualFlipped.update(v => !v)\">\n {{ manualFlipped() ? 'Show summary' : 'Show line items' }}\n</button>"},{"id":"disabledSnippet","title":"Disabled","language":"html","code":"<!-- Locked showing the front face -->\n<tw-flip-card disabled variant=\"elevated\" class=\"h-44 w-full\">\n <div slot=\"front\">Disabled · front locked</div>\n <div slot=\"back\">This face cannot be reached.</div>\n</tw-flip-card>\n\n<!-- Locked showing the back face -->\n<tw-flip-card disabled [flipped]=\"true\" variant=\"elevated\" class=\"h-44 w-full\">\n <div slot=\"front\">Hidden behind.</div>\n <div slot=\"back\">Disabled · back locked</div>\n</tw-flip-card>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-flip-card class=\"h-48 w-72\">\n <div slot=\"front\">Starter plan</div>\n <div slot=\"back\">\n <ul>\n <li>Up to 10 seats</li>\n <li>Unlimited projects</li>\n <li>Community support</li>\n </ul>\n </div>\n</tw-flip-card>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { FlipCardComponent } from '@cdevhub/ngx-tw/flip-card';"}],"summary":"Two faces stacked in 3D that rotate between each other on click, hover, or programmatic control, keeping the hidden face out of the tab order.","whenToUse":["A pricing tier or plan shows its name on the front and the included features on the back","A team member photo on the front reveals a bio on the back","A KPI summary flips to its breakdown without changing the size of the block","Marketing copy on the front, product detail on the back, in a grid of equally sized tiles","The reveal must not move surrounding layout — the second face occupies exactly the same footprint","Driving the visible face from parent state with two-way [(flipped)] in manual mode"],"whenNotToUse":[{"instead":"collapsible","because":"the hidden content should push the surrounding layout open rather than replace the surface"},{"instead":"popover","because":"the extra content should float above the page instead of replacing what the user was looking at"},{"instead":"card","because":"there is only one face and the surface never needs to reveal anything"},{"instead":"tooltip","because":"the revealed content is a short hint on hover rather than a second panel of content"}],"related":["card","collapsible","popover","tooltip","aspect-ratio"],"aliases":["flip","flipper","front and back","two-sided card","reveal card","3d card","rotate card"],"hasMeta":true,"metaPath":"projects/ngx-tw/flip-card/flip-card.meta.ts"},{"name":"alert","importPath":"@cdevhub/ngx-tw/alert","symbols":[{"name":"AlertComponent","kind":"component","description":"","selector":"tw-alert","usage":[{"form":"element","selector":"tw-alert","name":"tw-alert"}],"contentSlots":[{"select":"[twAlertIcon]"},{"select":"[twAlertTitle]"},{"select":"[twAlertContent]"},{"select":null},{"select":"[twAlertActions]"}],"inputs":[{"name":"variant","type":"AlertVariant","default":"'soft'","description":"Controls the visual style of the alert. Defaults to `'soft'`."},{"name":"color","type":"TwColor","default":"'info'","description":"Sets the semantic color palette. Defaults to `'info'`."},{"name":"dismissible","type":"boolean","default":"false","description":"When true, renders a dismiss button. Defaults to `false`.","transform":"booleanAttribute"},{"name":"politeness","type":"AlertPoliteness","default":"'polite'","description":"Sets the ARIA live-region politeness. Maps to the host `role`: `'assertive'` → `role=\"alert\"`, `'polite'` → `role=\"status\"`, `'off'` → no role. Use `'off'` to suppress re-announcement when the alert content updates after initial render — assistive tech treats the alert as a static region rather than a live region. Defaults to `'polite'`."},{"name":"dismissLabel","type":"string","default":"'Dismiss'","description":"Accessible label for the dismiss button. Override for localization. Defaults to `'Dismiss'`."}],"outputs":[{"name":"dismissed","payloadType":"void","description":"Fires when the dismiss button is clicked."}]},{"name":"AlertIconDirective","kind":"directive","description":"","selector":"[twAlertIcon]","usage":[{"form":"attribute","selector":"[twAlertIcon]","name":"twAlertIcon"}]},{"name":"AlertTitleDirective","kind":"directive","description":"","selector":"[twAlertTitle]","usage":[{"form":"attribute","selector":"[twAlertTitle]","name":"twAlertTitle"}]},{"name":"AlertContentDirective","kind":"directive","description":"","selector":"[twAlertContent]","usage":[{"form":"attribute","selector":"[twAlertContent]","name":"twAlertContent"}]},{"name":"AlertActionsDirective","kind":"directive","description":"","selector":"[twAlertActions]","usage":[{"form":"attribute","selector":"[twAlertActions]","name":"twAlertActions"}]},{"name":"AlertVariant","kind":"type","description":"Visual style of the alert container.","definition":"'solid' | 'outline' | 'soft'"},{"name":"AlertPoliteness","kind":"type","description":"ARIA live-region politeness for the alert. Maps to the host `role`.","definition":"'polite' | 'assertive' | 'off'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type AlertVariant = 'solid' | 'outline' | 'soft';\n\ntype AlertPoliteness = 'polite' | 'assertive' | 'off';\n\n// Shared library types (re-exported from '@cdevhub/ngx-tw/core'):\ntype TwColor = 'primary' | 'secondary' | 'accent' | 'neutral'\n | 'info' | 'success' | 'warning' | 'error';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-alert [variant]=\"v\" color=\"info\">\n <svg twAlertIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n <span twAlertTitle>New version available</span>\n <span twAlertContent>\n v2.4.0 adds keyboard shortcuts for navigation — see the release notes for details.\n </span>\n </tw-alert>\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (s of colorSamples; track s.color) {\n <tw-alert [color]=\"s.color\">\n <svg twAlertIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n <span twAlertTitle>{{ s.title }}</span>\n <span twAlertContent>{{ s.message }}</span>\n </tw-alert>\n}"},{"id":"iconTitleSnippet","title":"With Icon Title","language":"html","code":"<tw-alert color=\"info\">\n <svg twAlertIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n <span twAlertTitle>Heads up</span>\n <span twAlertContent>Scheduled maintenance starts in 30 minutes — save your work.</span>\n</tw-alert>\n\n<tw-alert color=\"success\">\n <svg twAlertIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n <span twAlertTitle>Deployment complete</span>\n <span twAlertContent>Your application is live at acme.com.</span>\n</tw-alert>\n\n<tw-alert color=\"warning\">\n <svg twAlertIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n <span twAlertTitle>Trial ends in 3 days</span>\n <span twAlertContent>Add a payment method to keep advanced reporting.</span>\n</tw-alert>\n\n<tw-alert color=\"error\">\n <svg twAlertIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n <span twAlertTitle>Couldn't reach the API</span>\n <span twAlertContent>The last sync failed with a 502. We'll retry in the background.</span>\n</tw-alert>"},{"id":"actionsSnippet","title":"With Actions","language":"html","code":"<tw-alert color=\"error\" variant=\"outline\" [dismissible]=\"true\">\n <svg twAlertIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n <span twAlertTitle>Deployment failed on web-3</span>\n <span twAlertContent>Build #4812 couldn't start — exit code 137 suggests the container ran out of memory.</span>\n <div twAlertActions>\n <button twButton color=\"error\" variant=\"soft\" size=\"sm\">View build log</button>\n <button twButton color=\"neutral\" variant=\"ghost\" size=\"sm\">Retry</button>\n </div>\n</tw-alert>"},{"id":"dismissibleTsSnippet","title":"Dismissible","language":"ts","code":"protected readonly dismissibleAlerts = signal<readonly DismissibleAlert[]>(\n DISMISSIBLE_INITIAL,\n);\n\nprotected dismissAlert(id: string): void {\n this.dismissibleAlerts.update((alerts) => alerts.filter((a) => a.id !== id));\n}"},{"id":"dismissibleHtmlSnippet","title":"Dismissible","language":"html","code":"@for (alert of dismissibleAlerts(); track alert.id) {\n <tw-alert\n [color]=\"alert.color\"\n [dismissible]=\"true\"\n (dismissed)=\"dismissAlert(alert.id)\"\n >\n <svg twAlertIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n <span twAlertTitle>{{ alert.title }}</span>\n <span twAlertContent>{{ alert.message }}</span>\n </tw-alert>\n}"},{"id":"politenessSnippet","title":"Politeness","language":"html","code":"<!-- polite (default) — announced when the screen reader next pauses -->\n<tw-alert color=\"info\" politeness=\"polite\">…</tw-alert>\n\n<!-- assertive — interrupts the current utterance; use sparingly -->\n<tw-alert color=\"error\" politeness=\"assertive\">…</tw-alert>\n\n<!-- off — don't announce (e.g., inside a notification center) -->\n<tw-alert color=\"info\" politeness=\"off\">…</tw-alert>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-alert color=\"success\">\n <svg twAlertIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"…\" />\n </svg>\n <span twAlertTitle>Changes saved</span>\n <span twAlertContent>Your profile details have been updated.</span>\n</tw-alert>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n AlertComponent,\n AlertIconDirective,\n AlertTitleDirective,\n AlertContentDirective,\n AlertActionsDirective,\n} from '@cdevhub/ngx-tw/alert';"}],"summary":"Inline feedback banner anchored in the page flow, with an icon, title, body, and optional actions, announced through a politeness-driven live region.","whenToUse":["A status banner at the top of a form summarising what went wrong","A success confirmation that stays visible after a save","A warning the user must be able to re-read at any time — expiring trial, quota nearly full","A message with a call to action attached — upgrade, retry, update payment method","An informational note explaining a section of the page"],"whenNotToUse":[{"instead":"toast","because":"the feedback is transient, global, and several messages should stack and auto-dismiss"},{"instead":"dialog","because":"the message must block the rest of the page until the user responds"},{"instead":"form-field","because":"the feedback is validation for one specific field and belongs in its error region"},{"instead":"badge","because":"the status is a tiny inline chip on another element, not a message with a body"},{"instead":"empty-state","because":"the region has no data to show and needs a full placeholder rather than a message strip"}],"related":["toast","dialog","form-field","badge","button","empty-state"],"aliases":["banner","callout","notice","message","inline message","flash","admonition","warning box","error message"],"hasMeta":true,"metaPath":"projects/ngx-tw/alert/alert.meta.ts"},{"name":"tabs","importPath":"@cdevhub/ngx-tw/tabs","symbols":[{"name":"TabsComponent","kind":"component","description":"","selector":"tw-tabs","usage":[{"form":"element","selector":"tw-tabs","name":"tw-tabs"}],"contentSlots":[{"select":null}],"inputs":[{"name":"variant","type":"TabsVariant","default":"'underline'","description":"Controls the visual style of the tab strip. Defaults to 'underline'."},{"name":"color","type":"TwColor","default":"'primary'","description":"Sets the semantic color for active tab indicators and highlights. Defaults to 'primary'."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls padding, font size, and icon size of tab triggers. Defaults to 'md'."},{"name":"orientation","type":"'horizontal' | 'vertical'","default":"'horizontal'","description":"Layout direction of the tab strip. Defaults to 'horizontal'."},{"name":"fitted","type":"boolean","default":"false","description":"When true, tab triggers stretch to fill the available width equally. Defaults to false."}],"outputs":[{"name":"closed","payloadType":"string","description":"Fires when a closable tab's close button is clicked. Payload is the tab's value."}],"models":[{"name":"value","type":"string","default":"''","description":"The value of the currently active tab. Two-way bound. Updates when the user selects a tab."}]},{"name":"TabComponent","kind":"component","description":"","selector":"tw-tab","usage":[{"form":"element","selector":"tw-tab","name":"tw-tab"}],"contentSlots":[{"select":null}],"inputs":[{"name":"value","type":"string","required":true,"description":"Unique identifier for this tab. Used to match the active tab value."},{"name":"label","type":"string","default":"''","description":"Plain text label shown in the trigger. Ignored when a custom trigger template is provided."},{"name":"disabled","type":"boolean","default":"false","description":"When true, the tab cannot be selected and is skipped by keyboard navigation. Defaults to false."},{"name":"closable","type":"boolean","default":"false","description":"When true, a close button is rendered in the tab trigger. Defaults to false."},{"name":"lazy","type":"boolean","default":"false","description":"When true, the tab panel content is only instantiated when the tab becomes active for the first time. Defaults to false."}]},{"name":"TabTriggerDirective","kind":"directive","description":"","selector":"ng-template[twTabTrigger]","usage":[{"form":"element-with-attribute","selector":"ng-template[twTabTrigger]","name":"ng-template"}]},{"name":"TabContentDirective","kind":"directive","description":"","selector":"ng-template[twTabContent]","usage":[{"form":"element-with-attribute","selector":"ng-template[twTabContent]","name":"ng-template"}]},{"name":"TabsVariant","kind":"type","description":"Visual style of the tab strip.","definition":"TabTriggerVariant"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TabsVariant = 'underline' | 'enclosed' | 'pill';\n\ntype TwColor =\n | 'primary' | 'secondary' | 'accent' | 'neutral'\n | 'info' | 'success' | 'warning' | 'error';\n\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-tabs [variant]=\"v\" [(value)]=\"variantTabs[v]\">\n <tw-tab value=\"account\" label=\"Account\">…</tw-tab>\n <tw-tab value=\"notifications\" label=\"Notifications\">…</tw-tab>\n <tw-tab value=\"security\" label=\"Security\">…</tw-tab>\n </tw-tabs>\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-tabs [color]=\"c\" variant=\"underline\" [(value)]=\"colorTabs[c]\">\n @for (m of mailboxes; track m.value) {\n <tw-tab [value]=\"m.value\" [label]=\"m.label\">…</tw-tab>\n }\n </tw-tabs>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-tabs [size]=\"s\" variant=\"pill\" [(value)]=\"sizeTabs[s]\">\n <tw-tab value=\"details\" label=\"Details\">…</tw-tab>\n <tw-tab value=\"shipping\" label=\"Shipping\">…</tw-tab>\n <tw-tab value=\"reviews\" label=\"Reviews\">…</tw-tab>\n </tw-tabs>\n}"},{"id":"verticalSnippet","title":"Vertical Orientation","language":"html","code":"<tw-tabs orientation=\"vertical\" [variant]=\"v\" [(value)]=\"tab\">\n <tw-tab value=\"general\" label=\"General\">…</tw-tab>\n <tw-tab value=\"privacy\" label=\"Privacy\">…</tw-tab>\n <tw-tab value=\"billing\" label=\"Billing\">…</tw-tab>\n</tw-tabs>"},{"id":"fittedSnippet","title":"Fitted (Equal Width)","language":"html","code":"<tw-tabs [fitted]=\"true\" variant=\"enclosed\" [(value)]=\"tab\">\n <tw-tab value=\"code\" label=\"Code\">…</tw-tab>\n <tw-tab value=\"preview\" label=\"Preview\">…</tw-tab>\n</tw-tabs>"},{"id":"disabledSnippet","title":"Disabled Tab","language":"html","code":"<tw-tabs [(value)]=\"tab\">\n <tw-tab value=\"draft\" label=\"Draft\">…</tw-tab>\n <tw-tab value=\"scheduled\" label=\"Scheduled\" [disabled]=\"true\">…</tw-tab>\n <tw-tab value=\"published\" label=\"Published\">…</tw-tab>\n</tw-tabs>"},{"id":"closableTsSnippet","title":"Closable Tabs","language":"ts","code":"protected readonly allTabs = ['draft', 'review', 'published'];\nprotected readonly openTabs = signal([...this.allTabs]);\nprotected readonly active = signal('home');\n\ncloseTab(value: string): void {\n this.openTabs.update(tabs => tabs.filter(t => t !== value));\n if (this.active() === value) this.active.set('home');\n}"},{"id":"closableHtmlSnippet","title":"Closable Tabs","language":"html","code":"<tw-tabs [(value)]=\"active\" (closed)=\"closeTab($event)\">\n <tw-tab value=\"home\" label=\"Home\">…</tw-tab>\n @for (tab of openTabs(); track tab) {\n <tw-tab [value]=\"tab\" [label]=\"tab | titlecase\" [closable]=\"true\">…</tw-tab>\n }\n</tw-tabs>"},{"id":"lazySnippet","title":"Lazy Content","language":"html","code":"<tw-tabs [(value)]=\"tab\">\n <tw-tab value=\"overview\" label=\"Overview\">Rendered eagerly</tw-tab>\n\n <tw-tab value=\"analytics\" label=\"Analytics\" [lazy]=\"true\">\n <ng-template twTabContent>\n <!-- Mounted only on first activation -->\n <app-analytics-chart />\n </ng-template>\n </tw-tab>\n\n <tw-tab value=\"activity\" label=\"Activity Log\" [lazy]=\"true\">\n <ng-template twTabContent>\n <app-activity-feed />\n </ng-template>\n </tw-tab>\n</tw-tabs>"},{"id":"customTriggerSnippet","title":"Custom Triggers with Icons","language":"html","code":"<tw-tabs variant=\"pill\" color=\"accent\" [(value)]=\"tab\">\n <tw-tab value=\"dashboard\">\n <ng-template twTabTrigger>\n <svg class=\"size-4 shrink-0\" aria-hidden=\"true\">…</svg>\n Dashboard\n </ng-template>\n …\n </tw-tab>\n\n <tw-tab value=\"reports\">\n <ng-template twTabTrigger let-ctx>\n <svg class=\"size-4 shrink-0\" aria-hidden=\"true\">…</svg>\n Reports\n <span class=\"badge\">2</span>\n @if (ctx.active) { <span class=\"sr-only\">(selected)</span> }\n </ng-template>\n …\n </tw-tab>\n</tw-tabs>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-tabs [(value)]=\"activeTab\">\n <tw-tab value=\"overview\" label=\"Overview\">Overview panel content</tw-tab>\n <tw-tab value=\"features\" label=\"Features\">Features panel content</tw-tab>\n <tw-tab value=\"specs\" label=\"Specifications\">Specs panel content</tw-tab>\n</tw-tabs>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n TabsComponent,\n TabComponent,\n TabTriggerDirective,\n TabContentDirective,\n} from '@cdevhub/ngx-tw/tabs';"}],"summary":"Groups related content into one region where only a single panel is visible at a time, implementing the WAI-ARIA Tabs pattern with roving tabindex.","whenToUse":["Splitting one page section into parallel views the component itself owns and swaps (\"Overview\", \"Features\", \"Specs\")","The selected panel is local UI state bound with [(value)], not something the URL should reflect","Panel content is expensive and should only render once its tab is first selected, via [lazy]=\"true\"","User-closable tabs (editor-style) that emit a (closed) event","A vertical tab strip beside its panels, or a scrollable strip with overflow navigation buttons"],"whenNotToUse":[{"instead":"tab-nav","because":"each tab is a route and the Angular Router — not the component — owns which content is shown"},{"instead":"accordion","because":"more than one section may be open at once, or the sections should stack vertically rather than sit behind a horizontal strip"},{"instead":"segmented-control","because":"the control only picks a value from a small set and there is no panel content to switch"},{"instead":"stepper","because":"the views are a sequence the user advances through, not parallel alternatives"}],"related":["tab-nav","segmented-control","accordion","stepper","card"],"aliases":["tab bar","tabbed panels","tablist","tab group","tabview","panel switcher","tabpanel"],"hasMeta":true,"metaPath":"projects/ngx-tw/tabs/tabs.meta.ts"},{"name":"tab-nav","importPath":"@cdevhub/ngx-tw/tab-nav","symbols":[{"name":"TabNavComponent","kind":"component","description":"","selector":"nav[twTabNav]","usage":[{"form":"element-with-attribute","selector":"nav[twTabNav]","name":"nav"}],"contentSlots":[{"select":null}],"inputs":[{"name":"variant","type":"TabNavVariant","default":"'underline'","description":"Visual style of the tab strip. Defaults to `'underline'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color used for the active link indicator and text. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls padding and font size of tab links. Defaults to `'md'`."},{"name":"fitted","type":"boolean","default":"false","description":"When true, tab links stretch to fill available width equally. Defaults to `false`."},{"name":"tabPanel","type":"TabNavPanel | undefined","default":"undefined","description":"Associated panel element. When provided, the nav follows the ARIA tabs pattern (role=tablist). When omitted, it uses the standard navigation landmark pattern with `aria-current=\"page\"` on the active link. A panel projected as a child of the `<nav>` is auto-discovered."},{"name":"navClass","type":"string","default":"''","description":"Additional classes merged onto the `<nav>` host. Useful for layout tweaks or borders that the default tv() config does not cover."},{"name":"linkClass","type":"string","default":"''","description":"Additional classes merged onto every tab link. Applied after base/active/disabled classes so consumer styles always win the cascade."},{"name":"labels","type":"TabNavLabels","default":"{}","description":"Optional label overrides for screen-reader announcements. Only used in the ARIA tabs pattern (i.e. when a panel is associated)."}]},{"name":"TabLinkDirective","kind":"directive","description":"","selector":"a[twTabLink]","usage":[{"form":"element-with-attribute","selector":"a[twTabLink]","name":"a"}],"inputs":[{"name":"active","type":"boolean","default":"false","description":"Whether the link represents the current active page/tab. Typically bound to router state. Defaults to `false`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, the link is visually disabled and cannot be activated by click or keyboard. Defaults to `false`."},{"name":"linkId","type":"string","default":"`tw-tab-link-${nextLinkId++}`","description":"Unique id for this link. Referenced by the panel's `aria-labelledby` when the link is active. Auto-generated when not provided."}],"methods":[{"name":"focus","signature":"focus(): void","description":"Programmatically focus this link."}]},{"name":"TabNavPanel","kind":"component","description":"","selector":"tw-tab-nav-panel","usage":[{"form":"element","selector":"tw-tab-nav-panel","name":"tw-tab-nav-panel"}],"contentSlots":[{"select":null}],"inputs":[{"name":"id","type":"string","default":"`tw-tab-nav-panel-${nextPanelId++}`","description":"Unique id for this panel. Referenced by the active link's `aria-controls`. Auto-generated when not provided."}]},{"name":"TabNavVariant","kind":"type","description":"Visual style of the tab navigation strip.","definition":"TabTriggerVariant"},{"name":"TabNavLabels","kind":"interface","description":"Optional label overrides for screen-reader announcements emitted by `TabNavComponent`.","members":[{"name":"activeTabAnnouncement","type":"(label: string, index: number, total: number) => string","optional":true,"description":"Formatter for the LiveAnnouncer message emitted when the active link changes in the ARIA tabs pattern. Receives the link's visible text, its 1-based index, and the total link count."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TabNavVariant = 'underline' | 'enclosed' | 'pill';\n\ninterface TabNavLabels {\n /** Formatter for the LiveAnnouncer message emitted when the active link changes. */\n activeTabAnnouncement?: (label: string, index: number, total: number) => string;\n}"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <nav twTabNav [variant]=\"v\" [attr.aria-label]=\"v + ' variant demo'\">\n @for (link of links; track link) {\n <a\n twTabLink\n href=\"#\"\n [active]=\"activeByVariant()[v] === link\"\n (click)=\"selectByVariant(v, link, $event)\"\n >\n {{ link }}\n </a>\n }\n </nav>\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <nav twTabNav [color]=\"c\" [attr.aria-label]=\"c + ' color demo'\">\n <a twTabLink href=\"#\" [active]=\"activeByColor()[c] === 'a'\"\n (click)=\"selectByColor(c, 'a', $event)\">Overview</a>\n <a twTabLink href=\"#\" [active]=\"activeByColor()[c] === 'b'\"\n (click)=\"selectByColor(c, 'b', $event)\">Details</a>\n <a twTabLink href=\"#\" [active]=\"activeByColor()[c] === 'c'\"\n (click)=\"selectByColor(c, 'c', $event)\">Activity</a>\n </nav>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <nav twTabNav [size]=\"s\" [attr.aria-label]=\"s + ' size demo'\">\n @for (link of links; track link) {\n <a\n twTabLink\n href=\"#\"\n [active]=\"activeBySize()[s] === link\"\n (click)=\"selectBySize(s, link, $event)\"\n >\n {{ link }}\n </a>\n }\n </nav>\n}"},{"id":"fittedSnippet","title":"Fitted (equal-width links)","language":"html","code":"<nav twTabNav variant=\"pill\" [fitted]=\"true\" aria-label=\"Fitted demo\">\n @for (range of ranges; track range) {\n <a\n twTabLink\n href=\"#\"\n [active]=\"fittedActive() === range\"\n (click)=\"selectFitted(range, $event)\"\n >\n {{ range }}\n </a>\n }\n</nav>"},{"id":"panelSnippet","title":"With Panel (ARIA tabs pattern)","language":"html","code":"<nav twTabNav [tabPanel]=\"panel\" aria-label=\"Settings tabs\">\n <a twTabLink href=\"#\" linkId=\"panel-link-account\"\n [active]=\"panelActive() === 'a'\" (click)=\"selectPanel('a', $event)\">Account</a>\n <a twTabLink href=\"#\" linkId=\"panel-link-billing\"\n [active]=\"panelActive() === 'b'\" (click)=\"selectPanel('b', $event)\">Billing</a>\n <a twTabLink href=\"#\" linkId=\"panel-link-team\"\n [active]=\"panelActive() === 'c'\" (click)=\"selectPanel('c', $event)\">Team</a>\n</nav>\n<tw-tab-nav-panel #panel>\n @switch (panelActive()) {\n @case ('a') { <p>Manage your account details and preferences.</p> }\n @case ('b') { <p>Review invoices, payment methods, and plan options.</p> }\n @case ('c') { <p>Invite teammates and manage seats.</p> }\n }\n</tw-tab-nav-panel>"},{"id":"disabledSnippet","title":"Disabled link","language":"html","code":"<nav twTabNav aria-label=\"Disabled demo\">\n <a twTabLink href=\"#\" [active]=\"active() === 'a'\" (click)=\"select('a', $event)\">Enabled</a>\n <a twTabLink href=\"#\" [active]=\"active() === 'b'\" (click)=\"select('b', $event)\">Also Enabled</a>\n <a twTabLink href=\"#\" [disabled]=\"true\">Coming Soon</a>\n</nav>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<nav twTabNav aria-label=\"Basic tab nav demo\">\n @for (link of links; track link) {\n <a\n twTabLink\n href=\"#\"\n [active]=\"activeLink() === link\"\n (click)=\"selectLink(link, $event)\"\n >\n {{ link }}\n </a>\n }\n</nav>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n TabNavComponent,\n TabLinkDirective,\n TabNavPanel,\n} from '@cdevhub/ngx-tw/tab-nav';"},{"id":"routerRecipeSnippet","title":"Router Integration (canonical recipe)","language":"html","code":"<nav twTabNav aria-label=\"Docs\">\n <a\n twTabLink\n routerLink=\"overview\"\n routerLinkActive\n #overviewActive=\"routerLinkActive\"\n [active]=\"overviewActive.isActive\"\n >Overview</a>\n <a\n twTabLink\n routerLink=\"examples\"\n routerLinkActive\n #examplesActive=\"routerLinkActive\"\n [active]=\"examplesActive.isActive\"\n >Examples</a>\n <a\n twTabLink\n routerLink=\"api\"\n routerLinkActive\n #apiActive=\"routerLinkActive\"\n [active]=\"apiActive.isActive\"\n >API</a>\n</nav>\n<router-outlet />"}],"summary":"Renders the look of a tab bar on top of real anchor elements, so page-level navigation between routed sections gets tab styling without owning any panel state.","whenToUse":["A strip of sibling routes (\"Overview\", \"Examples\", \"API\") where the URL decides which one is active","Driving the active state from routerLinkActive.isActive, a signal, or any custom history wrapper — the directive has no routerLink coupling","Navigation targets that must remain real links: middle-click, open-in-new-tab, and copy-link all have to work","A <nav> landmark strip whose active entry carries aria-current=\"page\"","Upgrading the same strip to the full tabs ARIA pattern by associating a <tw-tab-nav-panel>"],"whenNotToUse":[{"instead":"tabs","because":"the panels are owned by the component and swapped in place, with no route or URL change involved"},{"instead":"breadcrumbs","because":"the links ascend a hierarchy toward the current page rather than switching between siblings"},{"instead":"menu","because":"there are too many navigation targets to fit in a horizontal strip"},{"instead":"segmented-control","because":"the strip toggles a value in local state instead of navigating"}],"related":["tabs","segmented-control","menu","breadcrumbs"],"aliases":["routed tabs","router tabs","nav tabs","navigation tabs","tab links","page tabs","section navigation","link tabs"],"hasMeta":true,"metaPath":"projects/ngx-tw/tab-nav/tab-nav.meta.ts"},{"name":"separator","importPath":"@cdevhub/ngx-tw/separator","symbols":[{"name":"SeparatorComponent","kind":"component","description":"","selector":"tw-separator","usage":[{"form":"element","selector":"tw-separator","name":"tw-separator"}],"contentSlots":[{"select":null}],"inputs":[{"name":"orientation","type":"'horizontal' | 'vertical'","default":"'horizontal'","description":"Controls layout direction. Defaults to `'horizontal'`."},{"name":"variant","type":"SeparatorVariant","default":"'solid'","description":"Controls the line style. Defaults to `'solid'`."},{"name":"weight","type":"SeparatorWeight","default":"'thin'","description":"Controls line thickness. Defaults to `'thin'`."},{"name":"color","type":"TwColor","default":"'neutral'","description":"Sets the semantic color of the line. Defaults to `'neutral'`."},{"name":"decorative","type":"boolean","default":"false","description":"When true, hides the separator from assistive technology. Defaults to `false`."}]},{"name":"SeparatorVariant","kind":"type","description":"Line style of the separator.","definition":"'solid' | 'dashed' | 'dotted'"},{"name":"SeparatorWeight","kind":"type","description":"Line thickness of the separator.","definition":"'thin' | 'medium' | 'thick'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type SeparatorVariant = 'solid' | 'dashed' | 'dotted';\n\ntype SeparatorWeight = 'thin' | 'medium' | 'thick';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-separator [variant]=\"v\" />\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-separator [color]=\"c\" weight=\"medium\" />\n}"},{"id":"weightsSnippet","title":"Weights","language":"html","code":"@for (w of weights; track w) {\n <tw-separator [weight]=\"w\" />\n}"},{"id":"orientationSnippet","title":"Orientation","language":"html","code":"<div class=\"flex items-center gap-3 h-10\">\n <span>Profile</span>\n <tw-separator orientation=\"vertical\" />\n <span>Billing</span>\n <tw-separator orientation=\"vertical\" color=\"primary\" weight=\"medium\" />\n <span>Team</span>\n</div>"},{"id":"labelsSnippet","title":"With Labels","language":"html","code":"<tw-separator>OR</tw-separator>\n\n<tw-separator color=\"primary\" variant=\"dashed\">Continue with email</tw-separator>\n\n<tw-separator color=\"accent\" weight=\"medium\">\n <svg class=\"size-4 shrink-0\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n</tw-separator>"},{"id":"decorativeSnippet","title":"Decorative","language":"html","code":"<p>Featured articles</p>\n<tw-separator [decorative]=\"true\" color=\"accent\" variant=\"dotted\" weight=\"medium\" />\n<p>The decorative divider above is ignored by assistive tech.</p>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<p>Profile settings</p>\n<tw-separator />\n<p>Account preferences, email notifications, and connected services.</p>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { SeparatorComponent } from '@cdevhub/ngx-tw/separator';"}],"summary":"Thin horizontal or vertical rule that marks content as distinct but related, optionally carrying a centered label between two lines.","whenToUse":["Splitting stacked sections of a settings page or form so the groups read as separate","A vertical rule between inline items such as a toolbar, a byline, or a row of actions","A labelled divider such as \"OR\" between a credentials form and social sign-in buttons","Marking a semantic boundary that assistive technology should hear, via role=\"separator\" with aria-orientation","A purely visual rule that should be skipped by screen readers, via the decorative input"],"whenNotToUse":[{"instead":"card","because":"the sections are header/body/footer of a card, which already draws its own dividers"},{"instead":"menu","because":"the rule sits between groups of menu entries, where the menu emits its own separator items"},{"instead":"tabs","because":"each side of the divide is a whole screen of content that should be shown one at a time"},{"instead":"split","because":"the divider must be draggable so the user controls the size of the regions on either side"}],"related":["card","menu","tabs","split","item"],"aliases":["divider","hr","rule","horizontal rule","line","spacer","divider with label"],"hasMeta":true,"metaPath":"projects/ngx-tw/separator/separator.meta.ts"},{"name":"tooltip","importPath":"@cdevhub/ngx-tw/tooltip","symbols":[{"name":"TooltipDirective","kind":"directive","description":"","selector":"[twTooltip]","usage":[{"form":"attribute","selector":"[twTooltip]","name":"twTooltip"}],"exportAs":"twTooltip","inputs":[{"name":"twTooltip","type":"string | TemplateRef<void>","required":true,"description":"The tooltip content. Strings render as text; TemplateRef renders via ngTemplateOutlet."},{"name":"twTooltipPosition","type":"TooltipPosition","default":"'top'","description":"Preferred placement relative to the trigger. CDK handles fallback. Defaults to `'top'`."},{"name":"twTooltipColor","type":"TwColor","default":"'neutral'","description":"Semantic color palette for the tooltip. Defaults to `'neutral'`."},{"name":"twTooltipSize","type":"TwSize","default":"'md'","description":"Controls padding, font size, and maximum width. Defaults to `'md'`."},{"name":"twTooltipShowDelay","type":"number","default":"200","description":"Milliseconds to wait before showing after trigger. Defaults to `200` — an intent threshold: the trigger must be held long enough that the user reads as \"wants the tooltip\" rather than a passing graze with the pointer."},{"name":"twTooltipHideDelay","type":"number","default":"0","description":"Milliseconds to wait before hiding after trigger ends. Defaults to `0` (immediate dismiss). The asymmetry with `twTooltipShowDelay` is intentional — show is gated to filter noise, hide is instant so the tooltip never lingers over content the user has moved on from."},{"name":"twTooltipDisabled","type":"boolean","default":"false","description":"When true, tooltip never shows. Defaults to `false`."},{"name":"twTooltipArrow","type":"boolean","default":"true","description":"When true, renders an arrow pointing to the trigger. Defaults to `true`."},{"name":"twTooltipPanelClass","type":"string","default":"''","description":"Optional class string (space-separated) merged into the panel slot for consumer customization."}],"outputs":[{"name":"twTooltipShown","payloadType":"void","description":"Fires when the tooltip becomes visible."},{"name":"twTooltipHidden","payloadType":"void","description":"Fires when the tooltip is fully hidden."}],"methods":[{"name":"show","signature":"show(): void","description":"Programmatically show the tooltip."},{"name":"hide","signature":"hide(): void","description":"Programmatically hide the tooltip."},{"name":"toggle","signature":"toggle(): void","description":"Toggle tooltip visibility."}]},{"name":"TooltipPosition","kind":"type","description":"Placement position of the tooltip relative to its trigger element.","definition":"| 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TooltipPosition =\n | 'top' | 'top-start' | 'top-end'\n | 'bottom' | 'bottom-start' | 'bottom-end'\n | 'left' | 'left-start' | 'left-end'\n | 'right' | 'right-start' | 'right-end';\n\ntype TooltipSize = 'sm' | 'md' | 'lg';"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <button\n twButton\n variant=\"soft\"\n [color]=\"c\"\n [twTooltip]=\"c + ' tooltip'\"\n [twTooltipColor]=\"c\"\n >{{ c }}</button>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <button\n twButton\n [twTooltip]=\"'This is a ' + s + ' tooltip'\"\n [twTooltipSize]=\"s\"\n >{{ s }}</button>\n}"},{"id":"positionsSnippet","title":"Positions","language":"html","code":"@for (pos of positions; track pos) {\n <button\n twButton\n [twTooltip]=\"pos\"\n [twTooltipPosition]=\"pos\"\n >{{ pos }}</button>\n}"},{"id":"arrowSnippet","title":"Arrow","language":"html","code":"<button twButton twTooltip=\"With arrow (default)\">With arrow</button>\n<button twButton twTooltip=\"No arrow\" [twTooltipArrow]=\"false\">No arrow</button>"},{"id":"richContentSnippet","title":"Rich Content","language":"html","code":"<button\n twButton\n [twTooltip]=\"shortcutTip\"\n aria-label=\"Save — keyboard shortcut ctrl S\"\n>Save</button>\n\n<ng-template #shortcutTip>\n <div class=\"flex items-center gap-2\">\n <span>Save</span>\n <span class=\"rounded bg-white/20 px-1.5 py-0.5 text-2xs font-mono\">Ctrl+S</span>\n </div>\n</ng-template>"},{"id":"programmaticSnippet","title":"Programmatic Control","language":"html","code":"<button\n twButton\n twTooltip=\"Controlled tooltip\"\n #tip=\"twTooltip\"\n>Target</button>\n\n<button twButton (click)=\"tip.show()\">Show</button>\n<button twButton (click)=\"tip.hide()\">Hide</button>\n<button twButton (click)=\"tip.toggle()\">Toggle</button>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled: no triggers fire, overlay tears down on toggle -->\n<button twButton twTooltip=\"You won't see this\" [twTooltipDisabled]=\"true\">\n Disabled tooltip\n</button>\n\n<button twButton twTooltip=\"This one works\">Enabled tooltip</button>"},{"id":"delaysSnippet","title":"Custom Delays","language":"html","code":"<button twButton twTooltip=\"500ms show delay\" [twTooltipShowDelay]=\"500\">\n Slow show\n</button>\n\n<button\n twButton\n twTooltip=\"300ms hide delay\"\n [twTooltipShowDelay]=\"0\"\n [twTooltipHideDelay]=\"300\"\n>Slow hide</button>\n\n<button\n twButton\n twTooltip=\"Instant\"\n [twTooltipShowDelay]=\"0\"\n [twTooltipHideDelay]=\"0\"\n>No delay</button>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<button twButton twTooltip=\"Save your changes\">Hover me</button>\n<button twButton variant=\"outline\" twTooltip=\"Delete this item\" twTooltipColor=\"error\">Delete</button>\n<button twButton variant=\"soft\" twTooltip=\"View more information\" twTooltipPosition=\"bottom\">Bottom tip</button>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { TooltipDirective } from '@cdevhub/ngx-tw/tooltip';"}],"summary":"Short floating label shown on hover, focus, or long-press that supplements an element with a non-interactive hint.","whenToUse":["Naming an icon-only button or a truncated/abbreviated label","Surfacing a keyboard shortcut or a one-line explanation of a control","Adding supplemental detail that must never take focus or hold a link","Any element that needs an `aria-describedby` hint wired for assistive tech"],"whenNotToUse":[{"instead":"popover","because":"the floating content contains links, buttons, or a form the user must interact with"},{"instead":"menu","because":"the floating content is a list of actions triggered from a button"},{"instead":"dialog","because":"the content demands a decision and should block the rest of the page"},{"instead":"alert","because":"the message is page state the user should be able to re-read without hovering"}],"related":["popover","menu","dialog","button","icon"],"aliases":["hint","title","hover text","tip","label overlay","infotip","describedby"],"hasMeta":true,"metaPath":"projects/ngx-tw/tooltip/tooltip.meta.ts"},{"name":"code-block","importPath":"@cdevhub/ngx-tw/code-block","symbols":[{"name":"CodeBlockComponent","kind":"component","description":"Code-display surface with copy-to-clipboard, optional language label, and a projection slot for filename / extra actions.","selector":"tw-code-block","usage":[{"form":"element","selector":"tw-code-block","name":"tw-code-block"}],"contentSlots":[{"select":"[twCodeBlockHeader]"}],"inputs":[{"name":"code","type":"string","required":true,"description":"The code string to display and copy to clipboard."},{"name":"language","type":"string","description":"Optional language label displayed in the header (e.g. `'TypeScript'`, `'HTML'`)."},{"name":"variant","type":"CodeBlockVariant","default":"'filled'","description":"Visual style of the container. Defaults to `'filled'`."},{"name":"wrap","type":"boolean","default":"false","description":"When true, wraps long lines instead of horizontal scrolling. Defaults to `false`.","transform":"booleanAttribute"},{"name":"labels","type":"CodeBlockLabels","default":"{}","description":"Localizable strings for the copy button's aria-labels and the screen-reader announcement. Provide a partial override; missing fields fall back to English defaults."}],"outputs":[{"name":"copied","payloadType":"void","description":"Fires when code is successfully copied to clipboard."},{"name":"copyFailed","payloadType":"Error","description":"Fires when the clipboard copy fails (e.g. user denied permission, no clipboard API available). Payload is an `Error` describing the failure."}],"models":[{"name":"isCopied","type":"boolean","default":"false","description":"Whether the copy-to-clipboard button is currently in its \"copied\" confirmation state. Two-way bindable via `[(isCopied)]`. Set to `true` for ~2s after a successful copy, then auto-resets to `false`. Defaults to `false`."}]},{"name":"CodeBlockHeaderDirective","kind":"directive","description":"Marker directive for projected header content (e.g. filename, secondary actions). Sits alongside the language label inside the code block's header row.","selector":"[twCodeBlockHeader]","usage":[{"form":"attribute","selector":"[twCodeBlockHeader]","name":"twCodeBlockHeader"}]},{"name":"CodeBlockVariant","kind":"type","description":"Visual style of the code block container.","definition":"'filled' | 'outlined'"},{"name":"CodeBlockLabels","kind":"interface","description":"Localizable strings used by the code block. Provide a partial override — any missing fields fall back to English defaults.","members":[{"name":"copy","type":"string","optional":true,"description":"aria-label for the copy button in its resting state. Defaults to `'Copy code'`."},{"name":"copied","type":"string","optional":true,"description":"aria-label for the copy button after a successful copy. Defaults to `'Copied'`."},{"name":"announcement","type":"string","optional":true,"description":"Text passed to LiveAnnouncer after a successful copy. Defaults to `'Copied to clipboard'`."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type CodeBlockVariant = 'filled' | 'outlined';\n\ninterface CodeBlockLabels {\n /** aria-label for the copy button in its resting state. Default: 'Copy code'. */\n copy?: string;\n /** aria-label for the copy button after a successful copy. Default: 'Copied'. */\n copied?: string;\n /** Text passed to LiveAnnouncer after a successful copy. Default: 'Copied to clipboard'. */\n announcement?: string;\n}"},{"id":"tsSnippet","title":"Language Labels","language":"ts","code":"import { Component, input, output, computed } from '@angular/core';\nimport { tv } from 'tailwind-variants';\n\nconst buttonVariants = tv({\n base: 'inline-flex items-center justify-center rounded-md font-medium',\n variants: {\n size: {\n sm: 'px-3 py-1.5 text-sm',\n md: 'px-4 py-2 text-sm',\n lg: 'px-5 py-2.5 text-base',\n },\n },\n defaultVariants: { size: 'md' },\n}, { twMerge: true });"},{"id":"htmlSnippet","title":"Language Labels","language":"html","code":"<tw-code-block [code]=\"snippet\" wrap (copied)=\"onCopied()\" />"},{"id":"cssSnippet","title":"Language Labels","language":"ts","code":"@theme {\n --color-primary-500: oklch(0.55 0.2 260);\n --color-surface-sunken: oklch(0.95 0 0);\n}"},{"id":"jsonSnippet","title":"Language Labels","language":"ts","code":"{\n \"name\": \"ngx-tw\",\n \"version\": \"0.0.1\",\n \"peerDependencies\": {\n \"@angular/core\": \"^21.0.0\",\n \"tailwindcss\": \"^4.0.0\"\n }\n}"},{"id":"shortSnippet","title":"Variants","language":"ts","code":"npm install ngx-tw"},{"id":"playgroundSnippet","title":"Playground","language":"ts","code":"export class AppComponent {\n readonly snippet = 'const greeting = \"Hello, world!\"; console.log(greeting); // This is a long line that demonstrates horizontal scrolling vs word wrapping behavior in the code block component';\n}"},{"id":"basicSnippet","title":"Basic Usage","language":"ts","code":"import { CodeBlockComponent } from '@cdevhub/ngx-tw/code-block';\n\n@Component({\n imports: [CodeBlockComponent],\n template: `<tw-code-block [code]=\"snippet\" language=\"TypeScript\" />`,\n})\nexport class MyComponent {}"},{"id":"htmlSnippet","title":"Outlined Variant","language":"html","code":"<tw-code-block [code]=\"snippet\" language=\"HTML\" variant=\"outlined\" />"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-code-block [code]=\"snippet\" language=\"TypeScript\" />"},{"id":"outlinedSnippet","title":"Outlined Variant","language":"html","code":"<tw-code-block\n [code]=\"snippet\"\n language=\"HTML\"\n variant=\"outlined\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n CodeBlockComponent,\n CodeBlockHeaderDirective,\n} from '@cdevhub/ngx-tw/code-block';"}],"summary":"Preformatted code display with a header bar carrying a language label and a copy-to-clipboard button that gives both visual and screen-reader feedback.","whenToUse":["Showing a usage snippet or install command in documentation, with one-click copy","Rendering an API key, a webhook URL, or a config block the user is meant to copy verbatim","A log or stack trace that should scroll horizontally, or wrap, inside a keyboard-reachable region","A filename or extra actions belong in the header bar, projected via [twCodeBlockHeader]","The copy button label and copied announcement must be localized, via the labels input"],"related":["card","button","tooltip","alert"],"aliases":["code","snippet","pre","preformatted","syntax","copy to clipboard","terminal","command","source code"],"hasMeta":true,"metaPath":"projects/ngx-tw/code-block/code-block.meta.ts"},{"name":"segmented-control","importPath":"@cdevhub/ngx-tw/segmented-control","symbols":[{"name":"SegmentedControlComponent","kind":"component","description":"","selector":"tw-segmented-control","usage":[{"form":"element","selector":"tw-segmented-control","name":"tw-segmented-control"}],"contentSlots":[{"select":null}],"inputs":[{"name":"variant","type":"SegmentedControlVariant","default":"'surface'","description":"Controls the active indicator style. `'surface'` shows a raised white pill; `'filled'` shows a solid colored background; `'outline'` shows a colored ring border. Defaults to `'surface'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Sets the semantic color for the active option indicator. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls padding, font size, and gap of options. Defaults to `'md'`."},{"name":"orientation","type":"TwOrientation","default":"'horizontal'","description":"Layout direction of the control. Defaults to `'horizontal'`."},{"name":"rounded","type":"SegmentedControlRounded","default":"'pill'","description":"Controls the border-radius shape of the container and options. `'pill'` uses fully rounded corners; `'md'` uses standard radius. Vertical orientation forces `'md'`. Defaults to `'pill'`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, prevents all interaction and applies muted styling. Defaults to `false`."}],"models":[{"name":"value","type":"string | null","default":"null","description":"The value of the currently selected option. Two-way bound. Updates on user selection."}],"formControl":true},{"name":"SegmentedControlOptionComponent","kind":"component","description":"","selector":"tw-segmented-option","usage":[{"form":"element","selector":"tw-segmented-option","name":"tw-segmented-option"}],"contentSlots":[{"select":null}],"inputs":[{"name":"value","type":"string","required":true,"description":"Unique value identifying this option. Required."},{"name":"disabled","type":"boolean","default":"false","description":"When true, this option cannot be selected and is skipped by keyboard navigation. Defaults to `false`."}]},{"name":"SegmentedControlVariant","kind":"type","description":"Visual style of the active indicator.","definition":"'surface' | 'filled' | 'outline'"},{"name":"SegmentedControlRounded","kind":"type","description":"Border-radius shape of the container and options.","definition":"'pill' | 'md'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type SegmentedControlVariant = 'surface' | 'filled' | 'outline';\n\ntype SegmentedControlRounded = 'pill' | 'md';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-segmented-control [variant]=\"v\" [(value)]=\"variantValues[v]\" [attr.aria-label]=\"'Variant ' + v\">\n <tw-segmented-option value=\"daily\">Daily</tw-segmented-option>\n <tw-segmented-option value=\"weekly\">Weekly</tw-segmented-option>\n <tw-segmented-option value=\"monthly\">Monthly</tw-segmented-option>\n </tw-segmented-control>\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-segmented-control [color]=\"c\" [(value)]=\"colorValues[c]\" [attr.aria-label]=\"'Color ' + c\">\n <tw-segmented-option value=\"day\">Day</tw-segmented-option>\n <tw-segmented-option value=\"week\">Week</tw-segmented-option>\n <tw-segmented-option value=\"month\">Month</tw-segmented-option>\n </tw-segmented-control>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-segmented-control [size]=\"s\" [(value)]=\"sizeValues[s]\" [attr.aria-label]=\"'Size ' + s\">\n <tw-segmented-option value=\"left\">Left</tw-segmented-option>\n <tw-segmented-option value=\"center\">Center</tw-segmented-option>\n <tw-segmented-option value=\"right\">Right</tw-segmented-option>\n </tw-segmented-control>\n}"},{"id":"roundedSnippet","title":"Rounded","language":"html","code":"@for (r of rounded; track r) {\n <tw-segmented-control [rounded]=\"r\" [(value)]=\"roundedValues[r]\" [attr.aria-label]=\"'Rounded ' + r\">\n <tw-segmented-option value=\"a\">Alpha</tw-segmented-option>\n <tw-segmented-option value=\"b\">Beta</tw-segmented-option>\n <tw-segmented-option value=\"c\">Gamma</tw-segmented-option>\n </tw-segmented-control>\n}"},{"id":"orientationSnippet","title":"Orientation","language":"html","code":"<tw-segmented-control\n orientation=\"vertical\"\n [(value)]=\"view\"\n aria-label=\"Alignment\"\n>\n <tw-segmented-option value=\"top\">Top</tw-segmented-option>\n <tw-segmented-option value=\"middle\">Middle</tw-segmented-option>\n <tw-segmented-option value=\"bottom\">Bottom</tw-segmented-option>\n</tw-segmented-control>"},{"id":"iconsSnippet","title":"With Icons","language":"html","code":"<tw-segmented-control [(value)]=\"layout\" aria-label=\"Layout\">\n <tw-segmented-option value=\"grid\">\n <svg class=\"size-4 shrink-0\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n Grid\n </tw-segmented-option>\n <tw-segmented-option value=\"list\">\n <svg class=\"size-4 shrink-0\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n List\n </tw-segmented-option>\n <tw-segmented-option value=\"kanban\">\n <svg class=\"size-4 shrink-0\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n Kanban\n </tw-segmented-option>\n</tw-segmented-control>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled option -->\n<tw-segmented-control [(value)]=\"plan\" aria-label=\"Plan\">\n <tw-segmented-option value=\"free\">Free</tw-segmented-option>\n <tw-segmented-option value=\"pro\">Pro</tw-segmented-option>\n <tw-segmented-option value=\"enterprise\" [disabled]=\"true\">Enterprise</tw-segmented-option>\n</tw-segmented-control>\n\n<!-- Disabled group -->\n<tw-segmented-control [(value)]=\"theme\" [disabled]=\"true\" aria-label=\"Theme\">\n <tw-segmented-option value=\"light\">Light</tw-segmented-option>\n <tw-segmented-option value=\"dark\">Dark</tw-segmented-option>\n <tw-segmented-option value=\"system\">System</tw-segmented-option>\n</tw-segmented-control>"},{"id":"customizationSnippet","title":"Customization","language":"html","code":"<tw-segmented-control\n [(value)]=\"view\"\n class=\"shadow-md ring-1 ring-primary-200 dark:ring-primary-800\"\n aria-label=\"Customized\"\n>\n <tw-segmented-option value=\"day\" class=\"uppercase tracking-wide\">Day</tw-segmented-option>\n <tw-segmented-option value=\"week\" class=\"uppercase tracking-wide\">Week</tw-segmented-option>\n <tw-segmented-option value=\"month\" class=\"uppercase tracking-wide\">Month</tw-segmented-option>\n</tw-segmented-control>"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly view = signal<string | null>('list');"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-segmented-control\n name=\"view\"\n [(ngModel)]=\"view\"\n aria-label=\"View\"\n>\n <tw-segmented-option value=\"list\">List</tw-segmented-option>\n <tw-segmented-option value=\"grid\">Grid</tw-segmented-option>\n <tw-segmented-option value=\"table\">Table</tw-segmented-option>\n</tw-segmented-control>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly viewCtrl = new FormControl<string | null>('grid');"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-segmented-control [formControl]=\"viewCtrl\" aria-label=\"View\">\n <tw-segmented-option value=\"list\">List</tw-segmented-option>\n <tw-segmented-option value=\"grid\">Grid</tw-segmented-option>\n <tw-segmented-option value=\"table\">Table</tw-segmented-option>\n</tw-segmented-control>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly model = signal<{ view: string | null }>({ view: 'list' });\nprotected readonly viewForm = form(this.model, (p) => {\n required(p.view);\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-segmented-control [formField]=\"viewForm.view\" aria-label=\"View\">\n <tw-segmented-option value=\"list\">List</tw-segmented-option>\n <tw-segmented-option value=\"grid\">Grid</tw-segmented-option>\n <tw-segmented-option value=\"table\">Table</tw-segmented-option>\n</tw-segmented-control>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-segmented-control [(value)]=\"view\" aria-label=\"View mode\">\n <tw-segmented-option value=\"list\">List</tw-segmented-option>\n <tw-segmented-option value=\"grid\">Grid</tw-segmented-option>\n <tw-segmented-option value=\"table\">Table</tw-segmented-option>\n</tw-segmented-control>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n SegmentedControlComponent,\n SegmentedControlOptionComponent,\n} from '@cdevhub/ngx-tw/segmented-control';"}],"summary":"Row of mutually exclusive toggle buttons where exactly one is always selected, implementing the ARIA radiogroup pattern with roving tabindex.","whenToUse":["View switches such as list / grid / table where all choices should stay visible","Short time-range or scope filters like Day / Week / Month","A choice from three or four options where a dropdown would be overkill but a plain button group would not communicate selection","A compact inline form value that still needs to work with any Angular form strategy"],"whenNotToUse":[{"instead":"select","because":"the option set is larger than four or five, or needs search"},{"instead":"radio","because":"the options need their own labels and descriptions, or the list is long enough to stack vertically as a traditional radio group"},{"instead":"switch","because":"the value is a single binary on/off rather than a choice from a set"},{"instead":"tabs","because":"activating an option swaps a panel of content instead of setting a value"}],"related":["radio","switch","tabs","select","button"],"aliases":["segmented buttons","toggle group","button group","switcher","view toggle","radio group","pill toggle"],"hasMeta":true,"metaPath":"projects/ngx-tw/segmented-control/segmented-control.meta.ts"},{"name":"avatar","importPath":"@cdevhub/ngx-tw/avatar","symbols":[{"name":"AvatarComponent","kind":"component","description":"","selector":"tw-avatar","usage":[{"form":"element","selector":"tw-avatar","name":"tw-avatar"}],"contentSlots":[{"select":null}],"inputs":[{"name":"src","type":"string | null","default":"null","description":"URL of the avatar image. When set, renders an `<img>`. Falls back to initials or projected content on load error. Defaults to `null`."},{"name":"alt","type":"string","default":"''","description":"Alt text for the avatar image. Also used as `aria-label` for non-image avatars. Defaults to `''`."},{"name":"initials","type":"string | null","default":"null","description":"Text initials displayed when no image is available (1-2 characters). Rendered uppercase regardless of input casing. Defaults to `null`."},{"name":"color","type":"TwColor","default":"'neutral'","description":"Semantic color for the initials/icon background. Only applies when no image is shown. Defaults to `'neutral'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls the avatar dimensions. Defaults to `'md'`."},{"name":"appearance","type":"AvatarAppearance","default":"{}","description":"Bundles decorative axes: `rounded` (shape) and `status` (indicator dot). Defaults to `{ rounded: 'full', status: null }`."}],"outputs":[{"name":"imageError","payloadType":"Event","description":"Fires when the avatar image fails to load. Payload is the native `Event` from the `<img>` `error` event."}]},{"name":"AvatarGroupComponent","kind":"component","description":"","selector":"tw-avatar-group","usage":[{"form":"element","selector":"tw-avatar-group","name":"tw-avatar-group"}],"contentSlots":[{"select":null}],"inputs":[{"name":"size","type":"TwSize","default":"'md'","description":"Sets the size for all child avatars. Individual avatar size inputs are ignored when inside a group. Defaults to `'md'`."},{"name":"max","type":"number | null","default":"null","description":"Maximum number of avatars to display. Remaining count is shown as a \"+N\" overflow indicator. Defaults to `null` (show all)."},{"name":"ariaLabel","type":"string","default":"'Avatar group'","description":"Accessible label for the avatar group. Defaults to `'Avatar group'` (English) — override for localisation."}]},{"name":"AVATAR_GROUP_SIZE","kind":"token","description":"Injection token used by `AvatarGroupComponent` to propagate its size to child avatars. When present, overrides the individual avatar's `size` input."},{"name":"AvatarStatus","kind":"type","description":"Status indicator for the avatar.","definition":"'online' | 'busy' | 'away' | 'offline'"},{"name":"AvatarRounded","kind":"type","description":"Border radius shape for the avatar.","definition":"'full' | 'lg' | 'none'"},{"name":"AvatarAppearance","kind":"interface","description":"Decorative axes bundled into a single avatar input.","members":[{"name":"rounded","type":"AvatarRounded","optional":true,"description":"Border-radius shape. Defaults to `'full'`."},{"name":"status","type":"AvatarStatus | null","optional":true,"description":"Status indicator dot. Defaults to `null` (no indicator)."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type AvatarStatus = 'online' | 'busy' | 'away' | 'offline';\n\ntype AvatarRounded = 'full' | 'lg' | 'none';\n\ninterface AvatarAppearance {\n rounded?: AvatarRounded; // default: 'full'\n status?: AvatarStatus | null; // default: null\n}\n\n// Shared library types, re-exported from '@cdevhub/ngx-tw/core':\ntype TwColor = 'primary' | 'secondary' | 'accent' | 'neutral'\n | 'info' | 'success' | 'warning' | 'error';\n\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-avatar [initials]=\"initialMap[c]\" [color]=\"c\" [alt]=\"c\" />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-avatar initials=\"JD\" color=\"primary\" [size]=\"s\" alt=\"Jane Doe\" />\n}"},{"id":"roundedSnippet","title":"Rounded Shapes","language":"html","code":"@for (r of roundedOptions; track r) {\n <tw-avatar initials=\"AC\" color=\"accent\" [appearance]=\"{ rounded: r }\" alt=\"Acme Corp\" />\n}"},{"id":"statusSnippet","title":"Status Indicators","language":"html","code":"@for (st of statuses; track st) {\n <tw-avatar\n initials=\"JD\"\n color=\"primary\"\n [appearance]=\"{ status: st }\"\n [alt]=\"'Jane Doe, ' + st\"\n />\n}"},{"id":"fallbackSnippet","title":"Fallback Cascade","language":"html","code":"<!-- Image loads -->\n<tw-avatar\n src=\"https://i.pravatar.cc/80?img=12\"\n initials=\"JD\"\n color=\"primary\"\n alt=\"Jane Doe\"\n/>\n\n<!-- Image fails, initials take over -->\n<tw-avatar src=\"/broken-link.png\" initials=\"JD\" color=\"primary\" alt=\"Jane Doe\" />\n\n<!-- No image, no initials — default silhouette -->\n<tw-avatar alt=\"Anonymous user\" />"},{"id":"projectedSnippet","title":"Custom Projected Content","language":"html","code":"<tw-avatar color=\"accent\" alt=\"Team channel\" [appearance]=\"{ rounded: 'lg' }\">\n <svg class=\"size-[60%] text-accent-700\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"…users icon path…\" />\n </svg>\n</tw-avatar>\n\n<tw-avatar color=\"warning\" alt=\"Build bot\" [appearance]=\"{ rounded: 'lg' }\">\n <svg class=\"size-[60%] text-warning-700\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"…bot icon path…\" />\n </svg>\n</tw-avatar>"},{"id":"groupSnippet","title":"Avatar Group","language":"html","code":"<!-- Basic group -->\n<tw-avatar-group ariaLabel=\"Project members\">\n <tw-avatar initials=\"JD\" color=\"primary\" alt=\"Jane Doe\" />\n <tw-avatar initials=\"AB\" color=\"success\" alt=\"Alice Brown\" />\n <tw-avatar initials=\"MK\" color=\"accent\" alt=\"Mike Keller\" />\n <tw-avatar initials=\"SL\" color=\"info\" alt=\"Sarah Lee\" />\n</tw-avatar-group>\n\n<!-- With max overflow -->\n<tw-avatar-group [max]=\"3\" ariaLabel=\"Project members\">\n <tw-avatar initials=\"JD\" color=\"primary\" alt=\"Jane Doe\" />\n <tw-avatar initials=\"AB\" color=\"success\" alt=\"Alice Brown\" />\n <tw-avatar initials=\"MK\" color=\"accent\" alt=\"Mike Keller\" />\n <tw-avatar initials=\"SL\" color=\"info\" alt=\"Sarah Lee\" />\n <tw-avatar initials=\"RW\" color=\"warning\" alt=\"Rob Ward\" />\n</tw-avatar-group>\n\n<!-- Sizes propagate -->\n@for (s of sizes; track s) {\n <tw-avatar-group [size]=\"s\" ariaLabel=\"Team\">\n <tw-avatar initials=\"A\" color=\"primary\" alt=\"Alice\" />\n <tw-avatar initials=\"B\" color=\"success\" alt=\"Ben\" />\n <tw-avatar initials=\"C\" color=\"accent\" alt=\"Chen\" />\n </tw-avatar-group>\n}"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-avatar initials=\"JD\" color=\"primary\" alt=\"Jane Doe\" />\n<tw-avatar initials=\"AB\" color=\"success\" alt=\"Alice Brown\" />\n<tw-avatar initials=\"MK\" color=\"accent\" alt=\"Mike Keller\" />\n<tw-avatar alt=\"Anonymous user\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { AvatarComponent, AvatarGroupComponent } from '@cdevhub/ngx-tw/avatar';"}],"summary":"Compact visual identity for a user or entity, cascading automatically from an image to initials to a projected or default silhouette fallback.","whenToUse":["Member lists, comment threads, assignee cells, and profile summaries where a portrait identifies the person","Identity display where the photo URL may be missing or fail to load and initials should take over","Showing presence with an online / busy / away / offline status dot positioned to match the shape","Stacking several participants with overlap and a \"+N\" overflow indicator via `tw-avatar-group`","The leading slot of a list row or the marker of a timeline event"],"related":["badge","icon","card","item","timeline"],"aliases":["profile picture","profile photo","user picture","initials","gravatar","user image","monogram","presence","avatar group","face pile"],"hasMeta":true,"metaPath":"projects/ngx-tw/avatar/avatar.meta.ts"},{"name":"menu","importPath":"@cdevhub/ngx-tw/menu","symbols":[{"name":"MenuComponent","kind":"component","description":"","selector":"tw-menu","usage":[{"form":"element","selector":"tw-menu","name":"tw-menu"}],"contentSlots":[{"select":null}],"inputs":[{"name":"size","type":"TwSize","default":"'md'","description":"Controls item density and padding. Defaults to `'md'`."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible label for the menu panel. Use when no visible heading describes the menu (e.g. a kebab-icon trigger).","alias":"aria-label"},{"name":"ariaLabelledBy","type":"string | undefined","default":"undefined","description":"ID of an element that labels the menu panel. Ignored when `ariaLabel` is set.","alias":"aria-labelledby"}],"outputs":[{"name":"closed","payloadType":"unknown","description":"Re-exposed from the `CdkMenu` host directive.","from":"CdkMenu"}]},{"name":"MenuTriggerDirective","kind":"directive","description":"","selector":"[twMenuTrigger]","usage":[{"form":"attribute","selector":"[twMenuTrigger]","name":"twMenuTrigger"}],"inputs":[{"name":"twMenuTrigger","type":"unknown","description":"Re-exposed from the `CdkMenuTrigger` host directive.","from":"CdkMenuTrigger"},{"name":"position","type":"unknown","description":"Re-exposed from the `CdkMenuTrigger` host directive.","from":"CdkMenuTrigger"},{"name":"data","type":"unknown","description":"Re-exposed from the `CdkMenuTrigger` host directive.","from":"CdkMenuTrigger"}],"outputs":[{"name":"opened","payloadType":"unknown","description":"Re-exposed from the `CdkMenuTrigger` host directive.","from":"CdkMenuTrigger"},{"name":"closed","payloadType":"unknown","description":"Re-exposed from the `CdkMenuTrigger` host directive.","from":"CdkMenuTrigger"}]},{"name":"ContextMenuTriggerDirective","kind":"directive","description":"","selector":"[twContextMenuTrigger]","usage":[{"form":"attribute","selector":"[twContextMenuTrigger]","name":"twContextMenuTrigger"}],"inputs":[{"name":"twContextMenuTrigger","type":"unknown","description":"Re-exposed from the `CdkContextMenuTrigger` host directive.","from":"CdkContextMenuTrigger"},{"name":"disabled","type":"unknown","description":"Re-exposed from the `CdkContextMenuTrigger` host directive.","from":"CdkContextMenuTrigger"},{"name":"position","type":"unknown","description":"Re-exposed from the `CdkContextMenuTrigger` host directive.","from":"CdkContextMenuTrigger"},{"name":"data","type":"unknown","description":"Re-exposed from the `CdkContextMenuTrigger` host directive.","from":"CdkContextMenuTrigger"}],"outputs":[{"name":"opened","payloadType":"unknown","description":"Re-exposed from the `CdkContextMenuTrigger` host directive.","from":"CdkContextMenuTrigger"},{"name":"closed","payloadType":"unknown","description":"Re-exposed from the `CdkContextMenuTrigger` host directive.","from":"CdkContextMenuTrigger"}]},{"name":"MenuItemDirective","kind":"directive","description":"","selector":"[twMenuItem]","usage":[{"form":"attribute","selector":"[twMenuItem]","name":"twMenuItem"}],"inputs":[{"name":"color","type":"TwColor | undefined","default":"undefined","description":"Semantic role tint applied to the item. Use `'error'` for destructive actions. Defaults to `undefined` — no role tint is applied and the item inherits the base `text-fg` styling at full prominence; `'neutral'` (the explicit muted variant) is a distinct value with `text-fg-muted` + `bg-surface-muted` hovers."},{"name":"disabled","type":"boolean","default":"false","description":"Whether this item is disabled. Defaults to `false`."}],"outputs":[{"name":"triggered","payloadType":"unknown","description":"Re-exposed from the `CdkMenuItem` host directive.","from":"CdkMenuItem"}]},{"name":"MenuItemCheckboxComponent","kind":"component","description":"","selector":"[twMenuItemCheckbox]","usage":[{"form":"attribute","selector":"[twMenuItemCheckbox]","name":"twMenuItemCheckbox"}],"contentSlots":[{"select":null}],"inputs":[{"name":"checked","type":"unknown","description":"Re-exposed from the `CdkMenuItemCheckbox` host directive.","from":"CdkMenuItemCheckbox"},{"name":"disabled","type":"boolean","default":"false","description":"Whether this item is disabled. Defaults to `false`."}],"outputs":[{"name":"triggered","payloadType":"unknown","description":"Re-exposed from the `CdkMenuItemCheckbox` host directive.","from":"CdkMenuItemCheckbox"},{"name":"checkedChange","payloadType":"boolean","description":"Fires when the item is activated, carrying the new `checked` value after CDK toggles it."}]},{"name":"MenuItemRadioComponent","kind":"component","description":"","selector":"[twMenuItemRadio]","usage":[{"form":"attribute","selector":"[twMenuItemRadio]","name":"twMenuItemRadio"}],"contentSlots":[{"select":null}],"inputs":[{"name":"checked","type":"unknown","description":"Re-exposed from the `CdkMenuItemRadio` host directive.","from":"CdkMenuItemRadio"},{"name":"disabled","type":"boolean","default":"false","description":"Whether this item is disabled. Defaults to `false`."}],"outputs":[{"name":"triggered","payloadType":"unknown","description":"Re-exposed from the `CdkMenuItemRadio` host directive.","from":"CdkMenuItemRadio"},{"name":"checkedChange","payloadType":"boolean","description":"Fires when the item is activated, carrying the new `checked` value after CDK selects it."}]},{"name":"MenuGroupDirective","kind":"directive","description":"","selector":"[twMenuGroup]","usage":[{"form":"attribute","selector":"[twMenuGroup]","name":"twMenuGroup"}]},{"name":"MenuItemIconDirective","kind":"directive","description":"Styles a leading icon inside a menu item.","selector":"[twMenuItemIcon]","usage":[{"form":"attribute","selector":"[twMenuItemIcon]","name":"twMenuItemIcon"}]},{"name":"MenuItemDescriptionDirective","kind":"directive","description":"Styles secondary description text inside a menu item.","selector":"[twMenuItemDescription]","usage":[{"form":"attribute","selector":"[twMenuItemDescription]","name":"twMenuItemDescription"}]},{"name":"MenuItemShortcutDirective","kind":"directive","description":"Right-aligns and mutes keyboard shortcut hint text.","selector":"[twMenuItemShortcut]","usage":[{"form":"attribute","selector":"[twMenuItemShortcut]","name":"twMenuItemShortcut"}]},{"name":"MenuItemSubmenuIndicatorDirective","kind":"directive","description":"Styles the trailing chevron for submenu triggers.","selector":"[twMenuItemSubmenuIcon]","usage":[{"form":"attribute","selector":"[twMenuItemSubmenuIcon]","name":"twMenuItemSubmenuIcon"}]}],"snippets":[{"id":"basicSnippet","title":"Basic Menu","language":"html","code":"<button twButton [twMenuTrigger]=\"menu\">Project actions</button>\n\n<ng-template #menu>\n <tw-menu>\n <button twMenuItem>Rename</button>\n <button twMenuItem>Duplicate</button>\n <button twMenuItem>Move to folder…</button>\n <tw-separator />\n <button twMenuItem [disabled]=\"true\">Archive</button>\n <button twMenuItem color=\"error\">Delete project</button>\n </tw-menu>\n</ng-template>"},{"id":"iconsShortcutsSnippet","title":"Icons Shortcuts","language":"html","code":"<button twButton variant=\"outline\" [twMenuTrigger]=\"fileMenu\">File</button>\n\n<ng-template #fileMenu>\n <tw-menu>\n <button twMenuItem>\n <svg twMenuItemIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n New file\n <span twMenuItemShortcut>⌘N</span>\n </button>\n <button twMenuItem>\n <svg twMenuItemIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n Open project…\n <span twMenuItemShortcut>⌘O</span>\n </button>\n <tw-separator />\n <button twMenuItem color=\"error\">\n <svg twMenuItemIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n Close project\n <span twMenuItemShortcut>⌘W</span>\n </button>\n </tw-menu>\n</ng-template>"},{"id":"descriptionsSnippet","title":"Descriptions","language":"html","code":"<ng-template #menu>\n <tw-menu>\n <button twMenuItem>\n Share\n <span twMenuItemDescription>Send a link to teammates inside your organization</span>\n </button>\n <!-- … -->\n <button twMenuItem color=\"error\">\n Revoke all access\n <span twMenuItemDescription>Removes every collaborator — this cannot be undone</span>\n </button>\n </tw-menu>\n</ng-template>"},{"id":"submenuSnippet","title":"Nested Submenus","language":"html","code":"<ng-template #editMenu>\n <tw-menu>\n <button twMenuItem>Undo<span twMenuItemShortcut>⌘Z</span></button>\n <!-- … more items -->\n <button twMenuItem [twMenuTrigger]=\"shareMenu\">\n Share\n <svg twMenuItemSubmenuIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n </button>\n </tw-menu>\n</ng-template>\n\n<ng-template #shareMenu>\n <tw-menu>\n <button twMenuItem>Email link</button>\n <button twMenuItem>Slack</button>\n <button twMenuItem>Copy URL<span twMenuItemShortcut>⌘⇧C</span></button>\n </tw-menu>\n</ng-template>"},{"id":"toggleTsSnippet","title":"Checkbox Radio Items","language":"ts","code":"protected readonly showToolbar = signal(true);\nprotected readonly showSidebar = signal(true);\nprotected readonly showStatusBar = signal(false);\nprotected readonly viewMode = signal<'grid' | 'list' | 'board'>('grid');\n\nprotected toggleToolbar(): void { this.showToolbar.update((v) => !v); }\n// …same for sidebar / status bar"},{"id":"toggleHtmlSnippet","title":"Checkbox Radio Items","language":"html","code":"<tw-menu>\n <div twMenuGroup>\n <button twMenuItemCheckbox [checked]=\"showToolbar()\" (triggered)=\"toggleToolbar()\">Toolbar</button>\n <button twMenuItemCheckbox [checked]=\"showSidebar()\" (triggered)=\"toggleSidebar()\">Sidebar</button>\n <button twMenuItemCheckbox [checked]=\"showStatusBar()\" (triggered)=\"toggleStatusBar()\">Status bar</button>\n </div>\n <tw-separator />\n <div twMenuGroup>\n <button twMenuItemRadio [checked]=\"viewMode() === 'grid'\" (triggered)=\"viewMode.set('grid')\">Grid view</button>\n <button twMenuItemRadio [checked]=\"viewMode() === 'list'\" (triggered)=\"viewMode.set('list')\">List view</button>\n <button twMenuItemRadio [checked]=\"viewMode() === 'board'\" (triggered)=\"viewMode.set('board')\">Board view</button>\n </div>\n</tw-menu>"},{"id":"contextSnippet","title":"Context Menu","language":"html","code":"<div\n [twContextMenuTrigger]=\"ctxMenu\"\n class=\"rounded-lg border border-dashed border-border p-10 text-center\"\n>\n Right-click anywhere inside this area.\n</div>\n\n<ng-template #ctxMenu>\n <tw-menu size=\"sm\">\n <button twMenuItem>Copy<span twMenuItemShortcut>⌘C</span></button>\n <button twMenuItem>Duplicate<span twMenuItemShortcut>⌘D</span></button>\n <tw-separator />\n <button twMenuItem color=\"error\">Delete<span twMenuItemShortcut>⌫</span></button>\n </tw-menu>\n</ng-template>"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <button twButton variant=\"outline\" size=\"sm\" [twMenuTrigger]=\"sizeMenu\">{{ s }}</button>\n\n <ng-template #sizeMenu>\n <tw-menu [size]=\"s\">\n <button twMenuItem>Edit</button>\n <button twMenuItem>Duplicate</button>\n <button twMenuItem>Archive</button>\n </tw-menu>\n </ng-template>\n}"},{"id":"colorsSnippet","title":"Item Colors","language":"html","code":"<ng-template #colorMenu>\n <tw-menu>\n @for (c of colors; track c) {\n <button twMenuItem [color]=\"c\">{{ c }}</button>\n }\n </tw-menu>\n</ng-template>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<button twButton [twMenuTrigger]=\"menu\">Options</button>\n\n<ng-template #menu>\n <tw-menu>\n <button twMenuItem>Edit</button>\n <button twMenuItem>Duplicate</button>\n <tw-separator />\n <button twMenuItem [disabled]=\"true\">Archive</button>\n <button twMenuItem color=\"error\">Delete</button>\n </tw-menu>\n</ng-template>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n MenuComponent,\n MenuTriggerDirective,\n ContextMenuTriggerDirective,\n MenuItemDirective,\n MenuItemCheckboxComponent,\n MenuItemRadioComponent,\n MenuGroupDirective,\n MenuItemIconDirective,\n MenuItemDescriptionDirective,\n MenuItemShortcutDirective,\n MenuItemSubmenuIndicatorDirective,\n} from '@cdevhub/ngx-tw/menu';"}],"summary":"List of actions surfaced from a trigger, built on CDK Menu so the full WAI-ARIA menu keyboard and focus contract comes for free.","whenToUse":["An overflow or \"more actions\" button on a row, card, or toolbar","A grouped set of commands with separators, checkbox items, or radio items","A context menu for a selected object","Nested submenus of related commands"],"whenNotToUse":[{"instead":"select","because":"the user is choosing a value for a form field rather than running an action"},{"instead":"combobox","because":"the option list is long enough to need type-ahead filtering"},{"instead":"command-palette","because":"the actions are global and reached by keyboard search rather than from an anchor"},{"instead":"popover","because":"the panel holds arbitrary interactive content instead of a list of commands"}],"related":["popover","select","command-palette","button","separator"],"aliases":["dropdown","context menu","actions","overflow menu","kebab menu","more menu"],"hasMeta":true,"metaPath":"projects/ngx-tw/menu/menu.meta.ts"},{"name":"icon","importPath":"@cdevhub/ngx-tw/icon","symbols":[{"name":"IconComponent","kind":"component","description":"Renders SVG icons from a registry or from direct icon data. Icons inherit `currentColor` by default. Use the `color` input for semantic color variants that respond to theme changes. Register icons tree-shakably via `provideTwIcons()` (raw SVG data) or `provideTwLucideIcons()` from `ngx-tw/icon/lucide` (Lucide adapter).","selector":"tw-icon","usage":[{"form":"element","selector":"tw-icon","name":"tw-icon"}],"inputs":[{"name":"name","type":"string","description":"Icon name in kebab-case (e.g. `'chevron-right'`). Resolved via the registry. Defaults to `undefined`."},{"name":"img","type":"TwIconData","description":"Direct icon data (SVG element tuples). Takes precedence over `name`. Defaults to `undefined`."},{"name":"color","type":"TwIconColor","default":"'current'","description":"Semantic color. `'current'` inherits from parent text color. Defaults to `'current'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Icon size. Defaults to `'md'` (20px)."},{"name":"ariaLabel","type":"string","description":"Accessible label. When set, removes `aria-hidden` and applies `aria-label` to the SVG. Defaults to `undefined` (icon is decorative, `aria-hidden=\"true\"`)."},{"name":"svg","type":"TwIconSvgConfig","description":"SVG-author configuration: `strokeWidth`, `absoluteStrokeWidth`, `viewBox`. Unset fields fall back to `{ strokeWidth: 2, absoluteStrokeWidth: false, viewBox: '0 0 24 24' }`."}]},{"name":"IconRegistry","kind":"service","description":"Stores icon data (SVG element tuples) for use by `tw-icon`. Not provided in root — use `provideTwIcons()` or `provideTwLucideIcons()` to register icons and supply this service.","methods":[{"name":"register","signature":"register(icons: TwIconMap): void","description":"Merges the given icons into the registry."},{"name":"get","signature":"get(name: string): TwIconData | null","description":"Returns the icon data for the given PascalCase name, or `null` if not registered."}]},{"name":"provideTwIcons","kind":"function","description":"Registers icons for use with `tw-icon`. Returns an array of providers that includes the `IconRegistry` service and a multi-provider factory that merges the given icons into it. Angular deduplicates the class provider, so multiple calls are safe. Works at all injector levels: app root, lazy route, and component.","signature":"provideTwIcons(icons: TwIconMap): Provider[]"},{"name":"TW_ICON_REGISTRAR","kind":"token","description":"Multi-provider token whose factories register icons into the `IconRegistry`. Injected by `IconComponent` to trigger factory execution. The token value is never read."},{"name":"TwIconNode","kind":"type","description":"A single SVG element: [tagName, attributes].","definition":"readonly [string, Readonly<Record<string, string | number>>]"},{"name":"TwIconData","kind":"type","description":"Icon data: array of SVG child elements rendered inside the `<svg>` wrapper.","definition":"readonly TwIconNode[]"},{"name":"TwIconMap","kind":"type","description":"Map of PascalCase icon names to their SVG data.","definition":"Record<string, TwIconData>"},{"name":"TwIconColor","kind":"type","description":"Color options for the icon component: any semantic color plus `'current'` for color inheritance.","definition":"TwColor | 'current'"},{"name":"TwIconSvgConfig","kind":"interface","description":"SVG-author configuration grouped into a single input to keep the icon's top-level surface small. All fields are optional; unset fields fall back to sensible defaults (`strokeWidth: 2`, `absoluteStrokeWidth: false`, `viewBox: '0 0 24 24'`).","members":[{"name":"strokeWidth","type":"number","optional":true,"description":"SVG stroke width. Defaults to `2`."},{"name":"absoluteStrokeWidth","type":"boolean","optional":true,"description":"When true, stroke width scales inversely with icon size to maintain consistent visual weight. Defaults to `false`."},{"name":"viewBox","type":"string","optional":true,"description":"SVG viewBox attribute. Defaults to `'0 0 24 24'`."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TwIconNode = readonly [string, Readonly<Record<string, string | number>>];\ntype TwIconData = readonly TwIconNode[];\ntype TwIconMap = Record<string, TwIconData>;\ntype TwIconColor = TwColor | 'current';\n\ninterface TwIconSvgConfig {\n readonly strokeWidth?: number; // default 2\n readonly absoluteStrokeWidth?: boolean; // default false\n readonly viewBox?: string; // default '0 0 24 24'\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-icon name=\"star\" [color]=\"c\" size=\"lg\" />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-icon name=\"heart\" color=\"error\" [size]=\"s\" />\n}"},{"id":"strokeWidthSnippet","title":"Stroke Width","language":"html","code":"@for (sw of strokeWidths; track sw) {\n <tw-icon name=\"star\" size=\"lg\" [svg]=\"{ strokeWidth: sw }\" />\n}"},{"id":"absoluteStrokeWidthSnippet","title":"Absolute Stroke Width","language":"html","code":"<!-- Normal: heavier at small sizes -->\n@for (s of sizes; track s) {\n <tw-icon name=\"star\" [size]=\"s\" [svg]=\"{ strokeWidth: 2 }\" />\n}\n\n<!-- Absolute: compensates so perceived weight stays constant -->\n@for (s of sizes; track s) {\n <tw-icon name=\"star\" [size]=\"s\" [svg]=\"{ strokeWidth: 2, absoluteStrokeWidth: true }\" />\n}"},{"id":"inlineTextSnippet","title":"Inline with Text","language":"html","code":"<p class=\"text-sm flex items-center gap-1.5\">\n <tw-icon name=\"check-circle\" color=\"success\" size=\"sm\" /> Task completed successfully\n</p>\n<p class=\"text-sm flex items-center gap-1.5\">\n <tw-icon name=\"alert-triangle\" color=\"warning\" size=\"sm\" /> Proceed with caution\n</p>\n<p class=\"text-sm flex items-center gap-1.5\">\n <tw-icon name=\"x-circle\" color=\"error\" size=\"sm\" /> Something went wrong\n</p>"},{"id":"inheritanceSnippet","title":"Color Inheritance","language":"html","code":"<span class=\"text-primary-fg flex items-center gap-1.5\">\n <tw-icon name=\"star\" /> primary\n</span>\n<span class=\"text-success-fg flex items-center gap-1.5\">\n <tw-icon name=\"check-circle\" /> success\n</span>\n<span class=\"text-error-fg flex items-center gap-1.5\">\n <tw-icon name=\"x-circle\" /> error\n</span>"},{"id":"accessibilitySnippet","title":"Accessibility","language":"html","code":"<!-- Standalone icon: ariaLabel makes it announce as role=\"img\" -->\n<tw-icon name=\"check-circle\" color=\"success\" size=\"lg\" ariaLabel=\"Synchronised\" />\n\n<!-- Icon-only button: label the button, keep the icon aria-hidden -->\n<button twButton variant=\"outline\" size=\"sm\" aria-label=\"Open settings\">\n <tw-icon name=\"settings\" />\n</button>\n\n<!-- Icon beside visible text: icon stays decorative -->\n<button twButton variant=\"outline\" size=\"sm\">\n <tw-icon name=\"search\" /> Search\n</button>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-icon name=\"star\" />\n<tw-icon name=\"heart\" color=\"error\" />\n<tw-icon name=\"check-circle\" color=\"success\" size=\"lg\" />\n<tw-icon name=\"alert-triangle\" color=\"warning\" size=\"xl\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"// Component\nimport { IconComponent } from '@cdevhub/ngx-tw/icon';\n\n// Provider (app.config.ts)\nimport { provideTwLucideIcons } from '@cdevhub/ngx-tw/icon/lucide';\nimport { Star, Heart, CheckCircle } from 'lucide';\n\nexport const appConfig = {\n providers: [\n provideTwLucideIcons({ Star, Heart, CheckCircle }),\n ],\n};"}],"summary":"Renders an SVG glyph from a bring-your-own registry — there is no bundled icon set, so names must first be registered with `provideTwIcons()` (raw SVG data) or `provideTwLucideIcons()` (Lucide adapter) — on a consistent size scale with semantic color variants.","whenToUse":["Any inline glyph inside a button, alert, list row, menu entry, or table cell","After registering the specific glyphs the app uses, so only those end up in the bundle — never assume an icon name resolves without a matching provider entry","Icons that should inherit surrounding text color via `currentColor`, or take an explicit semantic color that follows the theme","Decorative glyphs that must stay `aria-hidden`, or meaningful ones that need an `ariaLabel`","Non-24x24 or differently-stroked SVG sources, via the `svg` config (`strokeWidth`, `absoluteStrokeWidth`, `viewBox`)"],"related":["button","alert","avatar","badge","empty-state"],"aliases":["svg","glyph","symbol","pictogram","lucide","iconography","icon registry","provideTwIcons","provideTwLucideIcons","material icon"],"hasMeta":true,"metaPath":"projects/ngx-tw/icon/icon.meta.ts"},{"name":"collapsible","importPath":"@cdevhub/ngx-tw/collapsible","symbols":[{"name":"CollapsibleComponent","kind":"component","description":"","selector":"tw-collapsible","usage":[{"form":"element","selector":"tw-collapsible","name":"tw-collapsible"}],"contentSlots":[{"select":"[twCollapsibleTrigger]"},{"select":null}],"inputs":[{"name":"value","type":"string","default":"''","description":"Unique identifier for this panel. Required when used inside a group."},{"name":"display","type":"CollapsibleDisplay","default":"{}","description":"Bundles decorative axes: `variant`, `color`, `size`. Accepts a partial; unset keys fall back to the defaults (`{ variant: 'default', color: 'neutral', size: 'md' }`)."},{"name":"disabled","type":"boolean","default":"false","description":"When true, the panel cannot be toggled and appears dimmed. Defaults to `false`.","transform":"booleanAttribute"},{"name":"keepAlive","type":"boolean","default":"false","description":"When true, content is rendered on first open and kept in the DOM across toggles. Defaults to `false`.","transform":"booleanAttribute"}],"outputs":[{"name":"toggled","payloadType":"boolean","description":"Fires after the panel is toggled. Payload is the new open state."}],"models":[{"name":"open","type":"boolean","default":"false","description":"Whether the panel is expanded. Two-way bindable. Defaults to `false`."}]},{"name":"CollapsibleGroupComponent","kind":"component","description":"","selector":"tw-collapsible-group","usage":[{"form":"element","selector":"tw-collapsible-group","name":"tw-collapsible-group"}],"contentSlots":[{"select":null}],"inputs":[{"name":"accordion","type":"boolean","default":"false","description":"When true, only one panel can be open at a time. `AccordionComponent` (subclass) ignores this input and drives the same behaviour from its own `type` input via the `isAccordionMode()` / `canCollapseSingleMode()` virtual hooks below. Consumers using `<tw-collapsible-group>` directly should bind this input; consumers using `<tw-accordion>` should bind `type=\"single\"` / `type=\"multiple\"` instead. Defaults to `false`.","transform":"booleanAttribute"}],"models":[{"name":"value","type":"string | string[] | null","default":"null","description":"The value(s) of currently open panels. String in accordion mode, string array in independent mode. `null` when no panel is open in accordion mode. Two-way bindable. Defaults to `null`."}]},{"name":"CollapsibleTriggerDirective","kind":"component","description":"Marks the toggle element inside a `<tw-collapsible>`. Apply this to a native `<button>`; the directive wires up ARIA, keyboard handling, and focus management. Custom hosts (e.g. `<div>`) are not supported.","selector":"[twCollapsibleTrigger]","usage":[{"form":"attribute","selector":"[twCollapsibleTrigger]","name":"twCollapsibleTrigger"}],"contentSlots":[{"select":null}]},{"name":"CollapsibleIconDirective","kind":"directive","description":"","selector":"[twCollapsibleIcon]","usage":[{"form":"attribute","selector":"[twCollapsibleIcon]","name":"twCollapsibleIcon"}]},{"name":"CollapsibleVariant","kind":"type","description":"Visual style of the collapsible container.","definition":"'default' | 'bordered' | 'ghost' | 'filled'"},{"name":"CollapsibleDisplay","kind":"interface","description":"Decorative axes bundled into a single collapsible input.","members":[{"name":"variant","type":"CollapsibleVariant","optional":true,"description":"Visual style of the panel container. Defaults to `'default'`."},{"name":"color","type":"TwColor","optional":true,"description":"Semantic color; applies to the `bordered` and `filled` variants. Defaults to `'neutral'`."},{"name":"size","type":"TwSize","optional":true,"description":"Padding scale for the trigger and content sections. Defaults to `'md'`."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type CollapsibleVariant = 'default' | 'bordered' | 'ghost' | 'filled';\n\ninterface CollapsibleDisplay {\n /** Visual style of the panel container. Defaults to 'default'. */\n variant?: CollapsibleVariant;\n /** Semantic color; applies to the bordered and filled variants. Defaults to 'neutral'. */\n color?: TwColor;\n /** Padding scale for the trigger and content sections. Defaults to 'md'. */\n size?: TwSize;\n}"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-collapsible [display]=\"{ variant: v }\">\n <button twCollapsibleTrigger>{{ v | titlecase }} variant</button>\n <p>This is the <strong>{{ v }}</strong> variant of the collapsible component.</p>\n </tw-collapsible>\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"<!-- Filled -->\n@for (c of colors; track c) {\n <tw-collapsible [display]=\"{ variant: 'filled', color: c }\">\n <button twCollapsibleTrigger>{{ c | titlecase }}</button>\n <p>Filled variant with {{ c }} color applied.</p>\n </tw-collapsible>\n}\n\n<!-- Bordered -->\n@for (c of colors; track c) {\n <tw-collapsible [display]=\"{ variant: 'bordered', color: c }\">\n <button twCollapsibleTrigger>{{ c | titlecase }}</button>\n <p>Bordered variant with {{ c }} color applied.</p>\n </tw-collapsible>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-collapsible [display]=\"{ variant: 'bordered', size: s }\">\n <button twCollapsibleTrigger>Size: {{ s }}</button>\n <p>Content with <strong>{{ s }}</strong> padding applied to trigger and body.</p>\n </tw-collapsible>\n}"},{"id":"statesSnippet","title":"States","language":"html","code":"<tw-collapsible [display]=\"{ variant: 'bordered' }\" [disabled]=\"true\">\n <button twCollapsibleTrigger>This panel is disabled</button>\n <p>You should never see this content.</p>\n</tw-collapsible>\n\n<tw-collapsible [display]=\"{ variant: 'bordered' }\">\n <button twCollapsibleTrigger>This panel is enabled</button>\n <p>This one toggles normally.</p>\n</tw-collapsible>"},{"id":"customIconSnippet","title":"Custom Icon","language":"html","code":"<tw-collapsible [display]=\"{ variant: 'bordered' }\" [(open)]=\"open\">\n <button twCollapsibleTrigger>\n Custom icon collapsible\n <svg twCollapsibleIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n @if (open()) {\n <path d=\"…minus path…\"/>\n } @else {\n <path d=\"…plus path…\"/>\n }\n </svg>\n </button>\n <p>This collapsible uses a plus/minus icon instead of the default chevron.</p>\n</tw-collapsible>"},{"id":"accordionSnippet","title":"Accordion Mode","language":"html","code":"<tw-collapsible-group [accordion]=\"true\" [(value)]=\"active\">\n <tw-collapsible value=\"about\" [display]=\"{ variant: 'filled', color: 'primary' }\">\n <button twCollapsibleTrigger>About</button>\n <p>Learn about the project…</p>\n </tw-collapsible>\n <tw-collapsible value=\"features\" [display]=\"{ variant: 'filled', color: 'primary' }\">\n <button twCollapsibleTrigger>Features</button>\n <p>Discover all the features…</p>\n </tw-collapsible>\n <tw-collapsible value=\"faq\" [display]=\"{ variant: 'filled', color: 'primary' }\">\n <button twCollapsibleTrigger>FAQ</button>\n <p>Frequently asked questions…</p>\n </tw-collapsible>\n</tw-collapsible-group>"},{"id":"independentGroupSnippet","title":"Independent Group","language":"html","code":"<tw-collapsible-group [(value)]=\"open\">\n <tw-collapsible value=\"html\" [display]=\"{ variant: 'bordered' }\">\n <button twCollapsibleTrigger>HTML</button>\n <p>HyperText Markup Language…</p>\n </tw-collapsible>\n <tw-collapsible value=\"css\" [display]=\"{ variant: 'bordered' }\">\n <button twCollapsibleTrigger>CSS</button>\n <p>Cascading Style Sheets…</p>\n </tw-collapsible>\n <tw-collapsible value=\"js\" [display]=\"{ variant: 'bordered' }\">\n <button twCollapsibleTrigger>JavaScript</button>\n <p>A programming language…</p>\n </tw-collapsible>\n</tw-collapsible-group>"},{"id":"keepAliveSnippet","title":"keepAlive Mode","language":"html","code":"<tw-collapsible [display]=\"{ variant: 'bordered' }\" [keepAlive]=\"true\" [(open)]=\"open\">\n <button twCollapsibleTrigger>State preserved across toggles</button>\n <div class=\"flex items-center gap-3\">\n <span>Counter: <strong>{{ counter() }}</strong></span>\n <button twButton variant=\"soft\" color=\"primary\" size=\"xs\" (click)=\"increment()\">\n Increment\n </button>\n </div>\n</tw-collapsible>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-collapsible [display]=\"{ variant: 'bordered' }\">\n <button twCollapsibleTrigger>What is ngx-tw?</button>\n <p>An Angular component library built with Tailwind CSS v4...</p>\n</tw-collapsible>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n CollapsibleComponent,\n CollapsibleGroupComponent,\n CollapsibleTriggerDirective,\n CollapsibleIconDirective,\n} from '@cdevhub/ngx-tw/collapsible';"}],"summary":"Disclosure widget that toggles one section of content open and closed, standing alone or composed into a group of siblings.","whenToUse":["A single standalone \"show more / show less\" section — advanced options, a details region, an inline expander","A trigger you author yourself: apply twCollapsibleTrigger to a native <button> and get aria-expanded, aria-controls, and focus wiring for free","Content that must keep its component state across toggles instead of being destroyed, via keepAlive","Coordinating siblings by hand with tw-collapsible-group — accordion behavior (one open) or independent (any number open)","Replacing the default chevron with a custom indicator via [twCollapsibleIcon]"],"whenNotToUse":[{"instead":"accordion","because":"several panels form one managed set that needs a shared open value, container variants, and heading conventions out of the box"},{"instead":"tabs","because":"the sections are mutually exclusive and the user expects horizontal navigation rather than vertical disclosure"},{"instead":"dialog","because":"the content should interrupt the page rather than expand inline beneath the trigger"},{"instead":"popover","because":"the extra content belongs in a floating layer anchored to the trigger, not in document flow"}],"related":["accordion","tabs","dialog","popover","card"],"aliases":["disclosure","expander","show more","details summary","toggle section","expand collapse","reveal","foldable"],"hasMeta":true,"metaPath":"projects/ngx-tw/collapsible/collapsible.meta.ts"},{"name":"accordion","importPath":"@cdevhub/ngx-tw/accordion","symbols":[{"name":"AccordionComponent","kind":"component","description":"Accordion — single- or multiple-open-panel group built on top of `<tw-collapsible>` children. Extends `CollapsibleGroupComponent` to inherit the keyboard navigation, value-sync, and toggle wiring; overrides the virtual `isAccordionMode()` / `canCollapseSingleMode()` hooks so the single-mode behaviour is driven from the local `type` + `collapsible` inputs instead of the parent's `accordion` input. The `hostRole` and `hostClasses` signals are also overridden so the accordion drops APG's `role=\"group\"` and renders the variant-driven container classes. The `providers` block exposes the accordion instance to its descendant collapsibles via the `CollapsibleGroupComponent` DI token — Angular DI uses class identity rather than the prototype chain, so the explicit `useExisting` is required even though `AccordionComponent extends CollapsibleGroupComponent`.","selector":"tw-accordion","usage":[{"form":"element","selector":"tw-accordion","name":"tw-accordion"}],"contentSlots":[{"select":null}],"inputs":[{"name":"type","type":"AccordionType","default":"'single'","description":"Open mode. `'single'` allows one panel open at a time; `'multiple'` allows many. Defaults to `'single'`."},{"name":"variant","type":"AccordionVariant","default":"'default'","description":"Visual style of the accordion container. Defaults to `'default'`."},{"name":"collapsible","type":"boolean","default":"true","description":"In `'single'` mode, whether re-clicking the open panel closes it. Defaults to `true` — accordions are collapsible by definition; opt-out only.","transform":"booleanAttribute"},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the accordion. Use when surrounding context doesn't make the purpose obvious.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID(s) of element(s) that label the accordion. Use instead of `aria-label` when a visible heading is available.","alias":"aria-labelledby"}],"extends":"CollapsibleGroupComponent"},{"name":"AccordionType","kind":"type","description":"Open mode of the accordion.","definition":"'single' | 'multiple'"},{"name":"AccordionVariant","kind":"type","description":"Visual style of the accordion container.","definition":"'default' | 'bordered' | 'ghost'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type AccordionType = 'single' | 'multiple';\n\ntype AccordionVariant = 'default' | 'bordered' | 'ghost';"},{"id":"usageSnippet","title":"Basic Usage","language":"html","code":"<tw-accordion variant=\"bordered\">\n <tw-collapsible value=\"a\">\n <button twCollapsibleTrigger>What is ngx-tw?</button>\n <p>An Angular component library...</p>\n </tw-collapsible>\n</tw-accordion>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { AccordionComponent } from '@cdevhub/ngx-tw/accordion';\nimport { CollapsibleComponent, CollapsibleTriggerDirective } from '@cdevhub/ngx-tw/collapsible';"}],"summary":"Coordinates a set of stacked disclosure panels under one managed open state, allowing either a single open panel at a time or several at once.","whenToUse":["An FAQ or help section where opening one answer should close the previous one (single mode)","A settings or filter sidebar where the user may keep several sections expanded at once (multiple mode)","The open panel must be readable and writable from the parent via two-way [(value)] — a string in single mode, string[] in multiple","A vertically stacked, always-visible set of headings the user expands in place, with arrow/Home/End roving focus across triggers","Single mode where one panel must always stay open, via [collapsible]=\"false\""],"whenNotToUse":[{"instead":"collapsible","because":"there is only one section to disclose, or the sections are independent and need no shared open state or container styling"},{"instead":"tabs","because":"the sections are mutually exclusive and the user expects a horizontal trigger strip above a single panel region"},{"instead":"tree","because":"the data nests to arbitrary depth rather than being one flat level of panels"}],"related":["collapsible","tabs","tree","item","icon"],"aliases":["faq","expandable panels","disclosure group","expander list","collapsible list","expand collapse sections","toggle panels"],"hasMeta":true,"metaPath":"projects/ngx-tw/accordion/accordion.meta.ts"},{"name":"popover","importPath":"@cdevhub/ngx-tw/popover","symbols":[{"name":"PopoverDirective","kind":"directive","description":"","selector":"[twPopover]","usage":[{"form":"attribute","selector":"[twPopover]","name":"twPopover"}],"exportAs":"twPopover","inputs":[{"name":"twPopover","type":"TemplateRef<PopoverTemplateContext> | Type<unknown>","required":true,"description":"The content to render. An `ng-template` receives context with `$implicit` (data) and `close` (function). A component class receives data and a ref via injection tokens."},{"name":"twPopoverPosition","type":"PopoverPosition","default":"'bottom'","description":"Preferred placement relative to the trigger. CDK handles fallback when space is insufficient. Defaults to `'bottom'`."},{"name":"twPopoverTriggerOn","type":"PopoverTrigger","default":"'click'","description":"What user interaction opens the popover. `'manual'` means consumers call `open()`/`close()` programmatically. Defaults to `'click'`."},{"name":"twPopoverDisabled","type":"boolean","default":"false","description":"When true, all trigger interactions are suppressed. Defaults to `false`."},{"name":"twPopoverSize","type":"TwSize","default":"'md'","description":"Controls panel padding using the standard spacing scale. Defaults to `'md'`."},{"name":"twPopoverOffset","type":"number","default":"8","description":"Pixel distance between trigger and panel edge. Defaults to `8`."},{"name":"twPopoverArrow","type":"boolean","default":"true","description":"Whether to render a directional arrow pointing at the trigger. Defaults to `true`."},{"name":"twPopoverBackdrop","type":"PopoverBackdrop","default":"'transparent'","description":"Backdrop behavior. `'transparent'` catches outside clicks invisibly. `'dimmed'` adds a semi-transparent overlay. `'none'` disables the backdrop. Defaults to `'transparent'`."},{"name":"twPopoverCloseOnOutside","type":"boolean","default":"true","description":"Whether clicking outside the panel closes the popover. Only relevant when backdrop is `'none'`. Defaults to `true`."},{"name":"twPopoverCloseOnEscape","type":"boolean","default":"true","description":"Whether pressing Escape closes the popover. Defaults to `true`."},{"name":"twPopoverScrollStrategy","type":"PopoverScrollStrategy","default":"'reposition'","description":"CDK scroll strategy for the overlay. Defaults to `'reposition'`."},{"name":"twPopoverTrapFocus","type":"boolean","default":"true","description":"Whether to trap focus inside the popover panel using CDK FocusTrapFactory. Defaults to `true`."},{"name":"twPopoverData","type":"unknown","default":"undefined","description":"Arbitrary data passed to template context or component via `POPOVER_DATA` token."},{"name":"twPopoverPanelClass","type":"string | string[]","default":"''","description":"Additional CSS classes applied to the overlay panel for consumer customization."},{"name":"twPopoverColor","type":"TwColor | undefined","default":"undefined","description":"Optional semantic color. When set, adds a colored top border accent to the panel."},{"name":"twPopoverAriaLabel","type":"string | undefined","default":"undefined","description":"Explicit `aria-label` for the dialog panel."}],"outputs":[{"name":"twPopoverOpened","payloadType":"void","description":"Fires after the popover becomes visible."},{"name":"twPopoverClosed","payloadType":"void","description":"Fires after the popover is fully removed."}],"models":[{"name":"twPopoverOpen","type":"boolean","default":"false","description":"Two-way bindable open state. Setting to `true` opens the popover; the popover sets it to `false` on close."}],"methods":[{"name":"open","signature":"open(): void","description":"Programmatically open the popover."},{"name":"close","signature":"close(): void","description":"Programmatically close the popover."},{"name":"toggle","signature":"toggle(): void","description":"Toggle open/close."},{"name":"reposition","signature":"reposition(): void","description":"Force CDK to recalculate overlay position."}]},{"name":"PopoverPosition","kind":"type","description":"Placement position of the popover relative to its trigger element.","definition":"| 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end'"},{"name":"PopoverScrollStrategy","kind":"type","description":"Scroll strategy for the popover overlay.","definition":"'reposition' | 'close' | 'block' | 'noop'"},{"name":"PopoverBackdrop","kind":"type","description":"Backdrop behavior for the popover.","definition":"'transparent' | 'dimmed' | 'none'"},{"name":"PopoverTrigger","kind":"type","description":"Trigger interaction that opens the popover.","definition":"'click' | 'focus' | 'manual'"},{"name":"PopoverCloseDirective","kind":"directive","description":"Convenience directive that closes the enclosing popover when the host element is clicked. Place on any element inside popover content. ```html <button twPopoverClose>Cancel</button> ```","selector":"[twPopoverClose]","usage":[{"form":"attribute","selector":"[twPopoverClose]","name":"twPopoverClose"}]},{"name":"PopoverTitleDirective","kind":"directive","description":"Popover title. Registers its ID with the enclosing popover overlay's `aria-labelledby` queue so screen readers announce it automatically. Mirrors `DialogTitleDirective` and is the recommended way to label a popover whose content has a heading. Use `twPopoverAriaLabel` when no heading is present.","selector":"[twPopoverTitle], tw-popover-title","usage":[{"form":"attribute","selector":"[twPopoverTitle]","name":"twPopoverTitle"},{"form":"element","selector":"tw-popover-title","name":"tw-popover-title"}],"inputs":[{"name":"id","type":"string","default":"this.generatedId","description":"Custom id for the title element. Defaults to a generated unique id."}]},{"name":"POPOVER_DATA","kind":"token","description":"Injection token providing arbitrary data to popover component content."},{"name":"POPOVER_REF","kind":"token","description":"Injection token providing a `PopoverRef` to popover component content."},{"name":"PopoverRef","kind":"interface","description":"Reference to control the popover from inside component content.","members":[{"name":"close","type":"void","optional":false,"description":"Closes the popover."},{"name":"_addAriaLabelledBy","type":"(id: string) => void","optional":true,"description":""},{"name":"_removeAriaLabelledBy","type":"(id: string) => void","optional":true,"description":""}]},{"name":"PopoverTemplateContext","kind":"interface","description":"Context object provided to popover template content.","members":[{"name":"$implicit","type":"T","optional":false,"description":"The data passed via `twPopoverData` input."},{"name":"close","type":"() => void","optional":false,"description":"Closes the popover."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type PopoverPosition =\n | 'top' | 'top-start' | 'top-end'\n | 'bottom' | 'bottom-start' | 'bottom-end'\n | 'left' | 'left-start' | 'left-end'\n | 'right' | 'right-start' | 'right-end';\n\ntype PopoverScrollStrategy = 'reposition' | 'close' | 'block' | 'noop';\ntype PopoverBackdrop = 'transparent' | 'dimmed' | 'none';\ntype PopoverTrigger = 'click' | 'focus' | 'manual';\n\ninterface PopoverTemplateContext<T = unknown> {\n $implicit: T;\n close: () => void;\n}\n\ninterface PopoverRef {\n close(): void;\n}\n\n// Shared library types (re-exported from '@cdevhub/ngx-tw/core'):\ntype TwColor = 'primary' | 'secondary' | 'accent' | 'neutral'\n | 'info' | 'success' | 'warning' | 'error';\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"positionsSnippet","title":"Positions","language":"html","code":"@for (pos of positions; track pos) {\n <button\n twButton variant=\"outline\" size=\"sm\"\n [twPopover]=\"posContent\"\n [twPopoverPosition]=\"pos\"\n >{{ pos }}</button>\n\n <ng-template #posContent>\n <p>Position: <strong>{{ pos }}</strong></p>\n </ng-template>\n}"},{"id":"triggersSnippet","title":"Triggers","language":"html","code":"<!-- click (default) -->\n<button twButton [twPopover]=\"content\" twPopoverTriggerOn=\"click\">click</button>\n\n<!-- focus — opens on focus, closes on blur -->\n<button twButton [twPopover]=\"content\" twPopoverTriggerOn=\"focus\">focus</button>\n\n<!-- manual — drive open/close from code -->\n<button twButton [twPopover]=\"content\" twPopoverTriggerOn=\"manual\" #p=\"twPopover\">manual</button>\n<button twButton (click)=\"p.toggle()\">toggle</button>"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <button twButton [twPopover]=\"content\" [twPopoverSize]=\"s\">{{ s }}</button>\n}"},{"id":"colorsSnippet","title":"Color Accents","language":"html","code":"@for (c of colors; track c) {\n <button twButton [twPopover]=\"content\" [twPopoverColor]=\"c\">{{ c }}</button>\n}"},{"id":"richContentSnippet","title":"Rich Content Patterns","language":"html","code":"<!-- Profile card -->\n<button twButton [twPopover]=\"profileContent\" twPopoverPosition=\"bottom-start\" twPopoverSize=\"lg\">\n <span class=\"inline-flex items-center gap-2\">\n <span class=\"size-6 rounded-full bg-primary-100 text-primary-700 flex items-center justify-center\">\n {{ user.avatar }}\n </span>\n {{ user.name }}\n </span>\n</button>\n<ng-template #profileContent>\n <div class=\"flex items-center gap-3 mb-3\">\n <div class=\"size-12 rounded-full bg-primary-100 text-primary-700\">{{ user.avatar }}</div>\n <div>\n <p class=\"font-semibold\">{{ user.name }}</p>\n <p class=\"text-xs text-fg-muted\">{{ user.handle }} · {{ user.role }}</p>\n </div>\n </div>\n <!-- details + actions -->\n <div class=\"flex justify-end gap-2\">\n <button twButton variant=\"ghost\" size=\"sm\" twPopoverClose>Close</button>\n <button twButton color=\"primary\" size=\"sm\">View profile</button>\n </div>\n</ng-template>\n\n<!-- Inline settings form -->\n<button twButton [twPopover]=\"settingsContent\" twPopoverSize=\"lg\">Notifications</button>\n<ng-template #settingsContent let-close=\"close\">\n <p class=\"font-semibold\">Notification preferences</p>\n <!-- checkboxes wired to this.pref() -->\n <div class=\"flex justify-end gap-2\">\n <button twButton variant=\"ghost\" size=\"sm\" (click)=\"close()\">Cancel</button>\n <button twButton color=\"primary\" size=\"sm\" (click)=\"savePrefs(close)\">Save</button>\n </div>\n</ng-template>"},{"id":"contextSnippet","title":"Template Context","language":"html","code":"<button\n twButton\n [twPopover]=\"content\"\n [twPopoverData]=\"contextData\"\n twPopoverColor=\"primary\"\n>Invite teammate</button>\n\n<ng-template #content let-data let-close=\"close\">\n <p class=\"font-semibold\">Invite {{ data.name }}?</p>\n <p class=\"text-fg-muted\">They'll receive an email with your team link.</p>\n <div class=\"flex justify-end gap-2\">\n <button twButton variant=\"ghost\" size=\"sm\" (click)=\"close()\">Cancel</button>\n <button twButton color=\"primary\" size=\"sm\" (click)=\"recordInvite(data.name, close)\">\n Send invite\n </button>\n </div>\n</ng-template>"},{"id":"componentTsSnippet","title":"Component Content","language":"ts","code":"@Component({\n selector: 'app-invite-card',\n imports: [ButtonDirective],\n template: `\n <p class=\"font-semibold\">Invite {{ data.name }}</p>\n <p class=\"text-fg-muted\">They'll join the <strong>{{ data.team }}</strong> team.</p>\n <div class=\"flex justify-end gap-2\">\n <button twButton variant=\"ghost\" size=\"sm\" (click)=\"ref.close()\">Cancel</button>\n <button twButton color=\"primary\" size=\"sm\" (click)=\"accept()\">Send invite</button>\n </div>\n `,\n})\nclass InviteCardComponent {\n protected readonly data = inject<InviteCardData>(POPOVER_DATA);\n protected readonly ref = inject<PopoverRef>(POPOVER_REF);\n\n protected accept(): void {\n // …save, then close\n this.ref.close();\n }\n}"},{"id":"componentHtmlSnippet","title":"Component Content","language":"html","code":"<button\n twButton\n [twPopover]=\"InviteCardComponent\"\n [twPopoverData]=\"{ name: 'Erin Shaw', team: 'Design systems' }\"\n twPopoverSize=\"lg\"\n twPopoverColor=\"primary\"\n>Invite via component</button>"},{"id":"closeSnippet","title":"Close Directive","language":"html","code":"<button twButton [twPopover]=\"confirm\" twPopoverColor=\"error\">Delete 3 items</button>\n\n<ng-template #confirm>\n <p class=\"font-semibold\">Delete 3 items?</p>\n <p class=\"text-fg-muted\">This permanently removes them from all your projects.</p>\n <div class=\"flex justify-end gap-2\">\n <!-- twPopoverClose fires close() on click with no wiring on your side -->\n <button twButton variant=\"ghost\" size=\"sm\" twPopoverClose>Cancel</button>\n <button twButton color=\"error\" size=\"sm\" twPopoverClose>Delete</button>\n </div>\n</ng-template>"},{"id":"dismissalSnippet","title":"Dismissal Behavior","language":"html","code":"<!-- Backdrop variants -->\n<button twButton [twPopover]=\"c\" twPopoverBackdrop=\"transparent\">transparent (default)</button>\n<button twButton [twPopover]=\"c\" twPopoverBackdrop=\"dimmed\">dimmed</button>\n<button twButton [twPopover]=\"c\" twPopoverBackdrop=\"none\">none</button>\n\n<!-- Stop closing on outside click (backdrop=\"none\" only) -->\n<button twButton [twPopover]=\"c\"\n twPopoverBackdrop=\"none\"\n [twPopoverCloseOnOutside]=\"false\"\n>Sticky popover</button>\n\n<!-- Arrow indicator -->\n<button twButton [twPopover]=\"c\" [twPopoverArrow]=\"false\">Hidden arrow</button>"},{"id":"programmaticSnippet","title":"Programmatic Control","language":"html","code":"<button\n twButton\n [twPopover]=\"content\"\n #pop=\"twPopover\"\n twPopoverTriggerOn=\"manual\"\n>Target</button>\n\n<button twButton (click)=\"pop.open()\">open()</button>\n<button twButton (click)=\"pop.close()\">close()</button>\n<button twButton (click)=\"pop.toggle()\">toggle()</button>\n<button twButton (click)=\"pop.reposition()\">reposition()</button>"},{"id":"modelSnippet","title":"Two-way Open Binding","language":"html","code":"protected readonly isOpen = signal(false);\n\n<button twButton [twPopover]=\"content\" [(twPopoverOpen)]=\"isOpen\">\n Toggle popover\n</button>\n<span>isOpen = {{ isOpen() }}</span>\n<button twButton (click)=\"isOpen.set(!isOpen())\">Toggle externally</button>"},{"id":"disabledSnippet","title":"Disabled","language":"html","code":"<button twButton [twPopover]=\"content\" [twPopoverDisabled]=\"true\">\n Disabled trigger\n</button>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<button twButton [twPopover]=\"confirm\" twPopoverColor=\"error\">\n Delete project\n</button>\n\n<ng-template #confirm>\n <p class=\"font-semibold\">Delete this project?</p>\n <p class=\"text-fg-muted\">This action cannot be undone.</p>\n <div class=\"flex justify-end gap-2\">\n <button twButton variant=\"ghost\" size=\"sm\" twPopoverClose>Cancel</button>\n <button twButton color=\"error\" size=\"sm\" twPopoverClose>Delete</button>\n </div>\n</ng-template>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n PopoverDirective,\n PopoverCloseDirective,\n POPOVER_DATA,\n POPOVER_REF,\n type PopoverRef,\n} from '@cdevhub/ngx-tw/popover';"}],"summary":"Floating panel anchored to a trigger element that holds rich, focusable content — a profile preview, a settings form, a confirmation prompt.","whenToUse":["The panel contains form controls or buttons the user must interact with","Content should stay open while the user works inside it","A confirmation prompt anchored to the control that triggered it","A contextual preview (user card, help text with a link) on hover or click"],"whenNotToUse":[{"instead":"tooltip","because":"the content is a short non-interactive hint that never needs focus"},{"instead":"menu","because":"the content is a list of actions driven by a trigger"},{"instead":"dialog","because":"the interaction is modal and should block the rest of the page"},{"instead":"toast","because":"the message is a global, transient status notification with no anchor"}],"related":["tooltip","menu","dialog","sheet"],"aliases":["overlay","flyout","floating panel","dropdown panel","hovercard","popup"],"hasMeta":true,"metaPath":"projects/ngx-tw/popover/popover.meta.ts"},{"name":"dialog","importPath":"@cdevhub/ngx-tw/dialog","symbols":[{"name":"TwDialog","kind":"service","description":"Opens Tailwind-styled modal dialogs. Composes `@angular/cdk/dialog` for focus trapping, portals, overlay plumbing, and adds a Tailwind container, richer ref API, and animation lifecycle. The rendering layer (`@angular/cdk/dialog` + the Tailwind container) is loaded through a dynamic `import()` on the first `open()` call, so merely registering this service costs nothing in the initial bundle. `open()` still returns its TwDialogRef synchronously — the dialog is rendered once the chunk lands. Read the rendered component via TwDialogRef.whenComponentReady. Not `providedIn: 'root'` — register it via provideTwDialog.","methods":[{"name":"open","signature":"open(content: ComponentType<C> | TemplateRef<C>, config?: TwDialogConfig<D, R>): TwDialogRef<R, C>","description":"Opens a dialog using the given component or template."},{"name":"closeAll","signature":"closeAll(): void","description":"Closes every open dialog managed by this service (and child services)."},{"name":"getDialogById","signature":"getDialogById(id: string): TwDialogRef<R, C> | undefined","description":"Looks up an open dialog by its id."}]},{"name":"provideTwDialog","kind":"function","description":"Registers the TwDialog service for dependency injection.","signature":"provideTwDialog(defaultOptions?: Partial<TwDialogConfig>): EnvironmentProviders"},{"name":"TwDialogRef","kind":"class","description":"Reference to a dialog opened via TwDialog.open. Drives the dialog lifecycle (close, state, observables) and forwards useful overlay streams. The ref is returned **synchronously** from `open()`, but the dialog's render layer (`@angular/cdk/dialog` + the Tailwind container) is loaded through a dynamic `import()`. The ref therefore starts *detached*: `id`, `state`, `close()`, the lifecycle observables, and panel/size mutations all work immediately (mutations are buffered and replayed on attach), but the rendered component instance does not exist yet — read it via whenComponentReady instead of a synchronous `componentInstance` field.","methods":[{"name":"whenComponentReady","signature":"whenComponentReady(): Promise<C | null>","description":"Resolves with the rendered content-component instance once the dialog's render chunk has loaded and attached. Resolves `null` for template dialogs, and for a dialog closed before it ever opened. Replaces the former synchronous `componentInstance` field, which cannot be populated before the deferred render chunk lands."},{"name":"close","signature":"close(result?: R): void","description":"Closes the dialog. The exit animation runs before the overlay is disposed."},{"name":"afterOpened","signature":"afterOpened(): Observable<void>","description":"Observable that emits once after the enter animation finishes."},{"name":"beforeClosed","signature":"beforeClosed(): Observable<R | undefined>","description":"Observable that emits once when the close animation starts."},{"name":"afterClosed","signature":"afterClosed(): Observable<R | undefined>","description":"Observable that emits once after the dialog has fully closed and the overlay is disposed."},{"name":"backdropClick","signature":"backdropClick(): Observable<MouseEvent>","description":"Backdrop click stream (emits even when `disableClose` is set). Buffered — a subscription made before the dialog attaches receives events once it does."},{"name":"keydownEvents","signature":"keydownEvents(): Observable<KeyboardEvent>","description":"Keydown event stream for the overlay. Buffered — a subscription made before the dialog attaches receives events once it does."},{"name":"updateSize","signature":"updateSize(width: string | number = '', height: string | number = ''): this","description":"Updates the dialog's width/height. Pass empty string to reset a dimension. Buffered until attach."},{"name":"addPanelClass","signature":"addPanelClass(classes: string | string[]): this","description":"Adds CSS classes to the overlay panel. Buffered until attach."},{"name":"removePanelClass","signature":"removePanelClass(classes: string | string[]): this","description":"Removes CSS classes from the overlay panel. Buffered until attach."}]},{"name":"DialogAnimationEvent","kind":"type","description":"Event emitted when the dialog's animation state transitions.","definition":"OverlayContainerAnimationEvent"},{"name":"DialogState","kind":"type","description":"Lifecycle states a dialog passes through.","definition":"OverlayContainerState"},{"name":"TwDialogConfig","kind":"class","description":"Configuration for opening a dialog with TwDialog.open. Mirrors the shape of `@angular/cdk/dialog`'s `DialogConfig` with additional tailwind-focused options (`size`, `enterAnimationDuration`, etc.).","extends":"CdkDialogConfig"},{"name":"TW_DIALOG_DATA","kind":"token","description":"Injection token carrying the `data` value passed via TwDialogConfig.data."},{"name":"TW_DIALOG_DEFAULT_OPTIONS","kind":"token","description":"Injection token for application-wide default dialog options. Set via `provideTwDialog()`."},{"name":"TwDialogSize","kind":"type","description":"Preset sizes mapped to width constraints for the dialog panel.","definition":"'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen'"},{"name":"TwDialogRole","kind":"type","description":"ARIA role of the dialog element. Use `'alertdialog'` for destructive confirmation prompts.","definition":"DialogRole"},{"name":"TwDialogAutoFocus","kind":"type","description":"Where to move focus when the dialog opens.","definition":"AutoFocusTarget | string | boolean"},{"name":"TwDialogRestoreFocus","kind":"type","description":"How to restore focus on close. `true` restores to the previously focused element; a selector or element targets a specific node.","definition":"RestoreFocusValue"},{"name":"TwDialogScrollStrategy","kind":"type","description":"Scroll behavior for content underneath the dialog.","definition":"'block' | 'close' | 'reposition' | 'noop'"},{"name":"DialogTitleDirective","kind":"directive","description":"Dialog title. Registers its ID with the container's `aria-labelledby` queue so screen readers announce it automatically.","selector":"[twDialogTitle], tw-dialog-title","usage":[{"form":"attribute","selector":"[twDialogTitle]","name":"twDialogTitle"},{"form":"element","selector":"tw-dialog-title","name":"tw-dialog-title"}],"inputs":[{"name":"id","type":"string","default":"this.generatedId","description":"Custom id for the title element. Defaults to a generated unique id."}]},{"name":"DialogSubtitleDirective","kind":"directive","description":"Secondary line beneath the dialog title — intended for a short description.","selector":"[twDialogSubtitle], tw-dialog-subtitle","usage":[{"form":"attribute","selector":"[twDialogSubtitle]","name":"twDialogSubtitle"},{"form":"element","selector":"tw-dialog-subtitle","name":"tw-dialog-subtitle"}]},{"name":"DialogDescriptionDirective","kind":"directive","description":"Dialog description. Registers its ID with the container's `aria-describedby` queue so screen readers announce the descriptive paragraph after the title. Mirrors DialogTitleDirective but for `aria-describedby`.","selector":"[twDialogDescription], tw-dialog-description","usage":[{"form":"attribute","selector":"[twDialogDescription]","name":"twDialogDescription"},{"form":"element","selector":"tw-dialog-description","name":"tw-dialog-description"}],"inputs":[{"name":"id","type":"string","default":"this.generatedId","description":"Custom id for the description element. Defaults to a generated unique id."}]},{"name":"DialogContentDirective","kind":"directive","description":"Scrollable content region of the dialog. Apply between the header and the actions bar. Inherits CDK's `CdkScrollable` to play nicely with scroll strategies and nested scrollables.","selector":"[twDialogContent], tw-dialog-content","usage":[{"form":"attribute","selector":"[twDialogContent]","name":"twDialogContent"},{"form":"element","selector":"tw-dialog-content","name":"tw-dialog-content"}]},{"name":"DialogActionsDirective","kind":"directive","description":"Bottom action bar of a dialog. Use inside the dialog content or template to host Cancel/Confirm buttons. Stays pinned below scrollable content.","selector":"[twDialogActions], tw-dialog-actions","usage":[{"form":"attribute","selector":"[twDialogActions]","name":"twDialogActions"},{"form":"element","selector":"tw-dialog-actions","name":"tw-dialog-actions"}],"inputs":[{"name":"align","type":"DialogActionsAlign","default":"'end'","description":"Horizontal alignment of the action buttons. Defaults to `'end'`."}]},{"name":"DialogCloseDirective","kind":"directive","description":"Closes the enclosing dialog when the host button is clicked. Provide a `[twDialogClose]` value to pass a result to `afterClosed()` subscribers.","selector":"[twDialogClose]","usage":[{"form":"attribute","selector":"[twDialogClose]","name":"twDialogClose"}],"inputs":[{"name":"twDialogClose","type":"unknown","default":"undefined","description":"Value passed to `afterClosed()` when the button is clicked."},{"name":"type","type":"'button' | 'submit' | 'reset'","default":"'button'","description":"Native button `type`. Defaults to `'button'` to avoid accidental form submission."}]},{"name":"DialogIconDirective","kind":"directive","description":"Decorative leading icon for a dialog header. Use with a semantic `color` to match destructive / informational / success dialogs.","selector":"[twDialogIcon]","usage":[{"form":"attribute","selector":"[twDialogIcon]","name":"twDialogIcon"}],"inputs":[{"name":"color","type":"TwColor | undefined","default":"undefined","description":"Semantic color for the icon container. Defaults to a neutral surface."}]},{"name":"DialogHeaderDirective","kind":"directive","description":"Header wrapper for a dialog. Provides consistent padding and spacing for title + subtitle + leading icon, and separates the header from scrollable content below.","selector":"[twDialogHeader], tw-dialog-header","usage":[{"form":"attribute","selector":"[twDialogHeader]","name":"twDialogHeader"},{"form":"element","selector":"tw-dialog-header","name":"tw-dialog-header"}]},{"name":"DialogActionsAlign","kind":"type","description":"Horizontal alignment for dialog action buttons.","definition":"'start' | 'center' | 'end'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TwDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';\ntype DialogState = 'opening' | 'open' | 'closing' | 'closed';\ntype TwDialogRole = 'dialog' | 'alertdialog';\ntype TwDialogScrollStrategy = 'block' | 'reposition' | 'close' | 'noop';\ntype TwDialogAutoFocus = AutoFocusTarget | string | boolean;\ntype TwDialogRestoreFocus = boolean | string | HTMLElement;\ntype DialogActionsAlign = 'start' | 'center' | 'end';\n\ninterface DialogAnimationEvent {\n state: DialogState;\n totalTime: number;\n}"},{"id":"sizesSnippet","title":"Sizes","language":"ts","code":"const SIZES: TwDialogSize[] = ['xs', 'sm', 'md', 'lg', 'xl', 'fullscreen'];\n\nprotected openSize(size: TwDialogSize): void {\n this.dialog.open(this.sizeTpl(), { size });\n}"},{"id":"confirmTsSnippet","title":"Confirmation (alertdialog)","language":"ts","code":"const ref = this.dialog.open<boolean>(this.confirmTpl(), {\n size: 'sm',\n role: 'alertdialog',\n});\nref.afterClosed().subscribe((result) => this.lastResult.set(result));"},{"id":"confirmHtmlSnippet","title":"Confirmation (alertdialog)","language":"html","code":"<ng-template #confirmTpl>\n <div twDialogHeader>\n <div twDialogIcon color=\"error\">\n <svg class=\"size-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n </div>\n <div class=\"flex-1 min-w-0\">\n <h2 twDialogTitle>Permanently delete acme-ledger?</h2>\n <p twDialogSubtitle>This removes the repository and everything it contains.</p>\n </div>\n </div>\n <div twDialogContent>\n <p twDialogDescription class=\"text-sm text-fg-muted\">\n The following will be permanently deleted and cannot be recovered.\n </p>\n <ul class=\"mt-3 space-y-1.5 text-sm\">\n <li>Open issues · 14</li>\n <li>Deploy environments · 3</li>\n <li>Automated runs · 128</li>\n <li>Contributor access · 6</li>\n </ul>\n </div>\n <div twDialogActions>\n <button twButton variant=\"ghost\" [twDialogClose]=\"false\">Cancel</button>\n <button twButton color=\"error\" [twDialogClose]=\"true\">Delete repository</button>\n </div>\n</ng-template>"},{"id":"scrollSnippet","title":"Long scrollable content","language":"html","code":"<ng-template #scrollTpl>\n <div twDialogHeader>\n <div twDialogIcon color=\"neutral\">…</div>\n <div class=\"flex-1 min-w-0\">\n <h2 twDialogTitle>Terms of service</h2>\n <p twDialogSubtitle>Last updated 12 March 2026.</p>\n </div>\n </div>\n <div twDialogContent>\n @for (t of termsSections; track t.heading) {\n <section class=\"pb-5 mb-5 border-b border-border-muted last:border-0\">\n <h3 class=\"text-sm font-semibold text-fg\">{{ t.heading }}</h3>\n <p class=\"mt-1.5 text-sm text-fg-muted leading-relaxed\">{{ t.body }}</p>\n </section>\n }\n </div>\n <div twDialogActions>\n <button twButton variant=\"ghost\" [twDialogClose]=\"false\">Decline</button>\n <button twButton [twDialogClose]=\"true\">Accept</button>\n </div>\n</ng-template>"},{"id":"componentTsSnippet","title":"Component content","language":"ts","code":"interface UserProfileData {\n name: string;\n handle: string;\n role: string;\n location: string;\n bio: string;\n stats: { followers: number; projects: number; reviews: number };\n activity: { label: string; meta: string }[];\n}\n\n@Component({\n selector: 'app-user-profile',\n imports: [DialogContentDirective, DialogActionsDirective, DialogCloseDirective, ButtonDirective],\n template: `\n <div twDialogContent>\n <!-- avatar + name + stats + bio + recent activity -->\n </div>\n <div twDialogActions>\n <button twButton variant=\"ghost\" twDialogClose>Dismiss</button>\n <button twButton variant=\"outline\" [twDialogClose]=\"'messaged'\">Message</button>\n <button twButton [twDialogClose]=\"'followed'\">Follow</button>\n </div>\n `,\n})\nclass UserProfileDialog {\n protected readonly data = inject<UserProfileData>(TW_DIALOG_DATA);\n protected readonly ref = inject<TwDialogRef<string>>(TwDialogRef);\n}"},{"id":"componentCallSnippet","title":"Component content","language":"ts","code":"const ref = this.dialog.open<string, UserProfileData>(UserProfileDialog, {\n size: 'sm',\n data: {\n name: 'Elena Moreau',\n handle: 'elena',\n role: 'Engineering lead',\n location: 'Lyon, France',\n bio: 'Runs the platform team…',\n stats: { followers: 1284, projects: 18, reviews: 342 },\n activity: [\n { label: 'Reviewed PR #1482', meta: '2h ago' },\n { label: 'Merged PR #1480', meta: 'yesterday' },\n { label: 'Opened issue #912', meta: '3d ago' },\n ],\n },\n});\nref.afterClosed().subscribe((result) => this.lastResult.set(result));"},{"id":"guardSnippet","title":"Close guard","language":"ts","code":"this.dialog.open(this.guardTpl(), {\n size: 'sm',\n closePredicate: () => this.dirtyForm() === false,\n});"},{"id":"lifecycleSnippet","title":"Lifecycle events","language":"ts","code":"const ref = this.dialog.open(this.tpl(), { size: 'sm' });\nref.afterOpened().subscribe(() => log('opened'));\nref.beforeClosed().subscribe(() => log('beforeClosed'));\nref.afterClosed().subscribe((result) => log('afterClosed', result));"},{"id":"stackedSnippet","title":"Stacked dialogs","language":"ts","code":"protected openParent(): void {\n this.dialog.open(this.parentTpl(), { size: 'md' });\n}\n\nprotected openChild(): void {\n // Called from inside the parent dialog — CDK handles stacking + focus transfer.\n this.dialog.open(this.childTpl(), { size: 'sm' });\n}\n\n// Reactive list + bulk actions:\nprotected readonly openCount = this.dialog.openDialogs; // Signal<readonly TwDialogRef[]>\nthis.dialog.closeAll();\nthis.dialog.afterAllClosed.subscribe(() => resetFilters());"},{"id":"basicUsageTsSnippet","title":"Keyboard shortcuts","language":"ts","code":"private readonly dialog = inject(TwDialog);\nprotected readonly tpl = viewChild.required<TemplateRef<unknown>>('tpl');\n\nprotected open(): void {\n this.dialog.open(this.tpl(), { size: 'md' });\n}"},{"id":"basicUsageHtmlSnippet","title":"Keyboard shortcuts","language":"html","code":"<button twButton (click)=\"open()\">Open keyboard shortcuts</button>\n\n<ng-template #tpl>\n <div twDialogContent>\n <h2 twDialogTitle>Keyboard shortcuts</h2>\n <p twDialogSubtitle>Press these anywhere in the app.</p>\n <dl class=\"mt-5 divide-y divide-border-muted text-sm\">\n <div class=\"flex items-center justify-between py-2.5\">\n <dt class=\"text-fg\">Open command palette</dt>\n <dd><kbd>⌘</kbd> <kbd>K</kbd></dd>\n </div>\n <!-- … -->\n </dl>\n </div>\n <div twDialogActions>\n <button twButton variant=\"ghost\" twDialogClose>Close</button>\n <button twButton [twDialogClose]=\"'open-palette'\">Open palette</button>\n </div>\n</ng-template>"},{"id":"provideSnippet","title":"Import","language":"ts","code":"import { provideTwDialog } from '@cdevhub/ngx-tw/dialog';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideTwDialog({ size: 'md' })],\n};"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n TwDialog,\n TwDialogRef,\n TW_DIALOG_DATA,\n DialogTitleDirective,\n DialogSubtitleDirective,\n DialogDescriptionDirective,\n DialogContentDirective,\n DialogActionsDirective,\n DialogCloseDirective,\n DialogHeaderDirective,\n DialogIconDirective,\n} from '@cdevhub/ngx-tw/dialog';"}],"summary":"Modal surface opened from a service and anchored to the viewport centre, with focus trapping, scroll blocking, and the WAI-ARIA dialog contract inherited from CDK Dialog.","whenToUse":["A decision the user must resolve before continuing — confirm, discard, destructive delete","A focused form that would derail the page if rendered inline","Content that should trap focus and mark the rest of the page inert","A flow opened imperatively from a service call rather than from markup"],"whenNotToUse":[{"instead":"sheet","because":"the surface should slide in from an edge and is closer to a side panel"},{"instead":"popover","because":"the panel belongs anchored to its trigger and should not block the page"},{"instead":"toast","because":"the message is a transient notification that needs no acknowledgement"},{"instead":"alert","because":"the message can sit inline in the page rather than interrupting the user"}],"related":["sheet","popover","toast","alert","button"],"aliases":["modal","confirm","prompt","lightbox","overlay window"],"hasMeta":true,"metaPath":"projects/ngx-tw/dialog/dialog.meta.ts"},{"name":"command-palette","importPath":"@cdevhub/ngx-tw/command-palette","symbols":[{"name":"CommandPaletteComponent","kind":"component","description":"","selector":"tw-command-palette","usage":[{"form":"element","selector":"tw-command-palette","name":"tw-command-palette"}],"contentSlots":[{"select":null}],"inputs":[{"name":"size","type":"TwSize","default":"'md'","description":"Controls item density and padding across the palette. Defaults to `'md'`."},{"name":"placeholder","type":"string","default":"'Type a command or search…'","description":"Placeholder text shown inside the search input. Defaults to `'Type a command or search…'`."},{"name":"commands","type":"readonly CommandPaletteItem[]","default":"[]","description":"Data-driven command list. Merged with projected items and filtered by `query`. Defaults to `[]`."},{"name":"filterFn","type":"CommandPaletteFilterFn | undefined","default":"undefined","description":"Custom filter function. Receives the merged item list and current query, returns the filtered result. Defaults to `undefined` (case-insensitive substring match)."},{"name":"closeOnSelect","type":"boolean","default":"true","description":"Whether the palette closes automatically after an item is activated. Defaults to `true` — a command palette is a fire-and-dismiss surface; the special case is a \"run many\" launcher that opts out."},{"name":"closeOnEscape","type":"boolean","default":"true","description":"Whether Escape closes the palette. Defaults to `true` — Escape is the universal dismiss key for modal surfaces; the special case is a non-dismissible palette."},{"name":"closeOnBackdropClick","type":"boolean","default":"true","description":"Whether clicking the backdrop closes the palette. Defaults to `true` — clicking outside a modal is the expected dismiss gesture; the special case is enforcing an explicit choice."},{"name":"autoFocus","type":"boolean","default":"true","description":"Whether the search input is auto-focused when the palette opens. Defaults to `true` — without auto-focus the user must click into the input before typing, defeating the keyboard-first design."},{"name":"ariaLabel","type":"string","default":"'Command palette'","description":"Accessible label used for the dialog role. Defaults to `'Command palette'`."},{"name":"searchAriaLabel","type":"string","default":"'Search commands'","description":"Accessible label applied to the search input (`role=\"combobox\"`). Defaults to `'Search commands'`."},{"name":"panelClass","type":"string | string[]","default":"''","description":"Additional classes appended to the overlay panel for consumer customization. Defaults to `''`."}],"outputs":[{"name":"itemSelected","payloadType":"CommandPaletteItem","description":"Fires when a command is activated (via click or Enter). Payload is the resolved `CommandPaletteItem`."},{"name":"opened","payloadType":"void","description":"Fires after the palette becomes fully visible."},{"name":"closed","payloadType":"void","description":"Fires after the palette is fully removed from the DOM."}],"models":[{"name":"query","type":"string","default":"''","description":"Two-way bindable search query. Reads or resets the current filter. Defaults to `''`."},{"name":"open","type":"boolean","default":"false","description":"Two-way bindable open state. Setting to `true` opens the palette; closing updates it to `false`. Defaults to `false`."}],"methods":[{"name":"show","signature":"show(): void","description":"Open the palette programmatically."},{"name":"hide","signature":"hide(): void","description":"Close the palette programmatically."},{"name":"toggle","signature":"toggle(): void","description":"Toggle the current open state."},{"name":"focusSearch","signature":"focusSearch(): void","description":"Force the palette to reapply focus to the search input."},{"name":"selectItem","signature":"selectItem(item: ResolvedItem): void","description":"Activate an item: run callbacks, emit events, optionally close."},{"name":"setActiveItem","signature":"setActiveItem(item: ResolvedItem): void","description":"Move the active descendant to the given item (used on hover)."}]},{"name":"CommandPaletteItemDirective","kind":"component","description":"A declarative palette item. Consumers project its rendered content as children.","selector":"tw-command-palette-item","usage":[{"form":"element","selector":"tw-command-palette-item","name":"tw-command-palette-item"}],"contentSlots":[{"select":null}],"inputs":[{"name":"id","type":"string","required":true,"description":"Stable identifier for the item. Used as DOM id and in selection payloads. Required."},{"name":"label","type":"string","default":"''","description":"Plain-text label used for filtering and default rendering. Defaults to `''`."},{"name":"keywords","type":"readonly string[]","default":"[]","description":"Additional search keywords that match the query but are not rendered. Defaults to `[]`."},{"name":"group","type":"string | undefined","default":"undefined","description":"Explicit group name. Overrides any enclosing `twCommandPaletteGroup`. Defaults to `undefined`."},{"name":"disabled","type":"boolean","default":"false","description":"Whether the item is disabled. Disabled items render but cannot be activated. Defaults to `false`."},{"name":"shortcut","type":"string | readonly string[] | undefined","default":"undefined","description":"Keyboard shortcut hint. A string renders as one kbd; an array renders each key separately. Defaults to `undefined`."},{"name":"description","type":"string","default":"''","description":"Secondary description text rendered under the label. Defaults to `''`."},{"name":"run","type":"(() => void) | undefined","default":"undefined","description":"Callback run before `activated` emits. Defaults to `undefined`."}],"outputs":[{"name":"activated","payloadType":"void","description":"Fires when this specific item is activated (Enter or click)."}]},{"name":"CommandPaletteGroupDirective","kind":"directive","description":"Optional explicit grouping wrapper. Items placed inside inherit its label.","selector":"[twCommandPaletteGroup]","usage":[{"form":"attribute","selector":"[twCommandPaletteGroup]","name":"twCommandPaletteGroup"}],"inputs":[{"name":"label","type":"string","required":true,"description":"Group heading text shown above the items. Required."}]},{"name":"CommandPaletteItemIconDirective","kind":"directive","description":"Styles a leading icon inside a palette item.","selector":"[twCommandPaletteItemIcon]","usage":[{"form":"attribute","selector":"[twCommandPaletteItemIcon]","name":"twCommandPaletteItemIcon"}]},{"name":"CommandPaletteItemDescriptionDirective","kind":"directive","description":"Styles secondary description text inside a palette item.","selector":"[twCommandPaletteItemDescription]","usage":[{"form":"attribute","selector":"[twCommandPaletteItemDescription]","name":"twCommandPaletteItemDescription"}]},{"name":"CommandPaletteEmptyDirective","kind":"directive","description":"Structural directive — consumer template rendered when no items match the query.","selector":"[twCommandPaletteEmpty]","usage":[{"form":"attribute","selector":"[twCommandPaletteEmpty]","name":"twCommandPaletteEmpty"}]},{"name":"CommandPaletteFooterDirective","kind":"directive","description":"Structural directive — consumer template rendered as a sticky footer inside the palette.","selector":"[twCommandPaletteFooter]","usage":[{"form":"attribute","selector":"[twCommandPaletteFooter]","name":"twCommandPaletteFooter"}]},{"name":"COMMAND_PALETTE_REF","kind":"token","description":"Injection token providing a `CommandPaletteRef` to palette overlay content."},{"name":"CommandPaletteItem","kind":"interface","description":"A single command palette entry.","members":[{"name":"id","type":"string","optional":false,"description":"Stable identifier used as the DOM id and for selection payloads."},{"name":"label","type":"string","optional":false,"description":"Visible label used for rendering and default filtering."},{"name":"keywords","type":"readonly string[]","optional":true,"description":"Additional search keywords that match the query but are not rendered."},{"name":"group","type":"string","optional":true,"description":"Group header name. Items sharing a group render under one section."},{"name":"disabled","type":"boolean","optional":true,"description":"Disabled items render but cannot be activated."},{"name":"shortcut","type":"string | readonly string[]","optional":true,"description":"Shortcut hint to render on the right edge."},{"name":"description","type":"string","optional":true,"description":"Secondary description text rendered under the label."},{"name":"icon","type":"string","optional":true,"description":"Optional icon name (consumers decide how to render)."},{"name":"run","type":"() => void","optional":true,"description":"Activation callback invoked before `itemSelected` is emitted."}]},{"name":"CommandPaletteFilterFn","kind":"type","description":"Filter signature. Receives the merged item list plus the current query; returns the filtered/reordered list.","definition":"( items: readonly CommandPaletteItem[], query: string, ) => readonly CommandPaletteItem[]"},{"name":"CommandPaletteRef","kind":"interface","description":"Control handle exposed inside the palette overlay (for future component-based content).","members":[{"name":"close","type":"void","optional":false,"description":"Closes the palette."},{"name":"setQuery","type":"void","optional":false,"description":"Updates the search query programmatically."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"interface CommandPaletteItem {\n id: string;\n label: string;\n keywords?: readonly string[];\n group?: string;\n disabled?: boolean;\n shortcut?: string | readonly string[];\n description?: string;\n icon?: string;\n run?: () => void;\n}\n\ntype CommandPaletteFilterFn = (\n items: readonly CommandPaletteItem[],\n query: string,\n) => readonly CommandPaletteItem[];\n\ninterface CommandPaletteRef {\n close(): void;\n setQuery(query: string): void;\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <button\n twButton\n variant=\"outline\"\n size=\"sm\"\n (click)=\"sizeSelected.set(s); sizeOpen.set(true)\"\n >Open {{ s }}</button>\n}\n\n<tw-command-palette\n [(open)]=\"sizeOpen\"\n [size]=\"sizeSelected()\"\n [commands]=\"dataCommands\"\n/>"},{"id":"groupsSnippet","title":"Groups Shortcuts","language":"html","code":"<tw-command-palette [(open)]=\"groupOpen\">\n <div twCommandPaletteGroup label=\"File\">\n <tw-command-palette-item id=\"g-new\" label=\"New file\" [shortcut]=\"['⌘','N']\">\n New file\n </tw-command-palette-item>\n <tw-command-palette-item id=\"g-open\" label=\"Open file\" [shortcut]=\"['⌘','O']\">\n Open file\n </tw-command-palette-item>\n </div>\n <div twCommandPaletteGroup label=\"Edit\">\n <tw-command-palette-item id=\"g-cut\" label=\"Cut\" [shortcut]=\"['⌘','X']\">\n Cut\n </tw-command-palette-item>\n <tw-command-palette-item\n id=\"g-paste\"\n label=\"Paste\"\n [shortcut]=\"['⌘','V']\"\n [disabled]=\"true\"\n >\n Paste\n </tw-command-palette-item>\n </div>\n</tw-command-palette>"},{"id":"fuzzyTsSnippet","title":"Data-driven with a custom filter","language":"ts","code":"const commands: readonly CommandPaletteItem[] = [\n { id: 'find', label: 'Find', group: 'Search', shortcut: ['⌘','F'] },\n { id: 'goto-file', label: 'Go to file', group: 'Navigation', shortcut: ['⌘','P'] },\n // …\n];\n\nconst fuzzyFilter: CommandPaletteFilterFn = (items, query) => {\n const q = query.trim().toLowerCase();\n if (!q) return items;\n return items.filter((item) => {\n const target = `${item.label} ${item.keywords?.join(' ') ?? ''}`.toLowerCase();\n let cursor = 0;\n for (const ch of q) {\n const idx = target.indexOf(ch, cursor);\n if (idx === -1) return false;\n cursor = idx + 1;\n }\n return true;\n });\n};"},{"id":"fuzzyHtmlSnippet","title":"Data-driven with a custom filter","language":"html","code":"<tw-command-palette\n [(open)]=\"open\"\n [commands]=\"commands\"\n [filterFn]=\"fuzzyFilter\"\n/>"},{"id":"hotkeyTsSnippet","title":"Global hotkey (⌘K / Ctrl+K)","language":"ts","code":"private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\nprivate readonly destroyRef = inject(DestroyRef);\nprotected readonly open = signal(false);\n\nconstructor() {\n if (!this.isBrowser) return;\n const handler = (event: KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {\n event.preventDefault();\n this.open.set(true);\n }\n };\n window.addEventListener('keydown', handler);\n this.destroyRef.onDestroy(() => window.removeEventListener('keydown', handler));\n}"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled item -->\n<tw-command-palette [(open)]=\"open\">\n <tw-command-palette-item id=\"build\" label=\"Build project\" [shortcut]=\"['⌘','B']\">Build project</tw-command-palette-item>\n <tw-command-palette-item id=\"deploy\" label=\"Deploy\" [shortcut]=\"['⌘','D']\" [disabled]=\"true\">Deploy</tw-command-palette-item>\n</tw-command-palette>\n\n<!-- Stay open after select -->\n<tw-command-palette\n [(open)]=\"open\"\n [closeOnSelect]=\"false\"\n [commands]=\"commands\"\n (itemSelected)=\"last.set($event.label)\"\n/>"},{"id":"descSnippet","title":"Descriptions","language":"html","code":"<tw-command-palette\n [(open)]=\"open\"\n searchAriaLabel=\"Search file actions\"\n>\n <tw-command-palette-item\n id=\"new\"\n label=\"New file\"\n description=\"Create an empty file in the current folder\"\n [shortcut]=\"['⌘','N']\"\n >\n New file\n </tw-command-palette-item>\n <tw-command-palette-item\n id=\"duplicate\"\n label=\"Duplicate\"\n description=\"Copy the active file alongside the original\"\n [shortcut]=\"['⌘','D']\"\n >\n Duplicate\n </tw-command-palette-item>\n</tw-command-palette>"},{"id":"templatesSnippet","title":"Custom empty state footer","language":"html","code":"<tw-command-palette [(open)]=\"open\" [commands]=\"commands\">\n <ng-template twCommandPaletteEmpty let-q>\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"size-8 text-fg-subtle\" viewBox=\"0 0 20 20\" fill=\"currentColor\">…</svg>\n <p class=\"text-sm text-fg-muted\">No commands match \"<strong>{{ q }}</strong>\"</p>\n <p class=\"text-2xs text-fg-subtle\">Try a different search term</p>\n </div>\n </ng-template>\n <ng-template twCommandPaletteFooter>\n <div class=\"flex items-center justify-between text-xs text-fg-subtle\">\n <span>↑↓ navigate · ↵ select · esc close</span>\n <span>{{ commands.length }} commands</span>\n </div>\n </ng-template>\n</tw-command-palette>"},{"id":"progTsSnippet","title":"Programmatic control","language":"ts","code":"protected readonly palette = viewChild.required<CommandPaletteComponent>('palette');"},{"id":"progHtmlSnippet","title":"Programmatic control","language":"html","code":"<button twButton (click)=\"palette().show()\">show()</button>\n<button twButton variant=\"ghost\" (click)=\"palette().hide()\">hide()</button>\n<button twButton variant=\"outline\" (click)=\"palette().toggle()\">toggle()</button>\n\n<tw-command-palette #palette [commands]=\"commands\" />"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<button twButton (click)=\"isOpen.set(true)\">Open palette</button>\n\n<tw-command-palette\n [(open)]=\"isOpen\"\n (itemSelected)=\"lastSelected.set($event.label)\"\n>\n <tw-command-palette-item id=\"new-file\" label=\"New file\" [shortcut]=\"['⌘', 'N']\">\n New file\n </tw-command-palette-item>\n <tw-command-palette-item id=\"open-file\" label=\"Open file\" [shortcut]=\"['⌘', 'O']\">\n Open file\n </tw-command-palette-item>\n <tw-command-palette-item id=\"save\" label=\"Save\" [shortcut]=\"['⌘', 'S']\">\n Save\n </tw-command-palette-item>\n <tw-command-palette-item id=\"settings\" label=\"Settings\">\n Settings\n </tw-command-palette-item>\n</tw-command-palette>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n CommandPaletteComponent,\n CommandPaletteItemDirective,\n CommandPaletteGroupDirective,\n CommandPaletteItemIconDirective,\n CommandPaletteItemDescriptionDirective,\n CommandPaletteEmptyDirective,\n CommandPaletteFooterDirective,\n} from '@cdevhub/ngx-tw/command-palette';"}],"summary":"Keyboard-driven modal search surface that filters a flat or grouped command list and runs the chosen action — the ⌘K launcher pattern from VS Code, Linear, and Raycast.","whenToUse":["A global ⌘K / Ctrl+K launcher reaching actions from anywhere in the app","The action set is large enough that type-ahead filtering beats scanning a list","Power users should navigate or jump to a record without touching the mouse","Commands span several groups (navigation, editing, settings) and need section headers plus shortcut hints","The result list is fetched or scored remotely via a custom `filterFn`"],"whenNotToUse":[{"instead":"menu","because":"the action set is short and anchored to the button that was clicked"},{"instead":"select","because":"the user is picking a value that writes to a form control rather than running a callback"},{"instead":"combobox","because":"the typed search belongs inline in a form field rather than in a modal overlay"},{"instead":"dialog","because":"the surface holds a form or confirmation that does not fit the one-action-per-row model"}],"related":["menu","dialog","select","combobox","icon"],"aliases":["cmdk","cmd+k","command k","command menu","spotlight","launcher","quick open","omnibox","action search","fuzzy finder"],"hasMeta":true,"metaPath":"projects/ngx-tw/command-palette/command-palette.meta.ts"},{"name":"toast","importPath":"@cdevhub/ngx-tw/toast","symbols":[{"name":"ToastService","kind":"service","description":"Opens Tailwind-styled toasts / snackbars inside CDK overlays. One overlay is created per `ToastPosition` on first use and reused for the lifetime of the service; toasts stack vertically inside their position container. The rendering layer (CDK overlay + the toast components) is loaded through a dynamic `import()` on the first `show()` call, so merely registering this service costs nothing in the initial bundle. Every open method still returns its ToastRef synchronously — the toast is attached once the chunk lands, and `update()` / `dismiss()` called in the meantime are honoured. Not `providedIn: 'root'` — register via provideToast.","methods":[{"name":"show","signature":"show(content: ToastContent, config?: ToastConfig<D, R>): ToastRef<unknown, R>","description":"Open a toast. Content may be a string (rendered inside the default `ToastComponent`), a `TemplateRef` (rendered via `TemplatePortal`), or a component class (rendered via `ComponentPortal` with `TW_TOAST_REF` + `TW_TOAST_DATA` injected)."},{"name":"success","signature":"success(message: string, config?: ToastConfig<unknown, R>): ToastRef<unknown, R>","description":"Shorthand for `show(message, { severity: 'success', ... })`."},{"name":"error","signature":"error(message: string, config?: ToastConfig<unknown, R>): ToastRef<unknown, R>","description":"Shorthand for `show(message, { severity: 'error', politeness: 'assertive', ... })`."},{"name":"warning","signature":"warning(message: string, config?: ToastConfig<unknown, R>): ToastRef<unknown, R>","description":"Shorthand for `show(message, { severity: 'warning', ... })`."},{"name":"info","signature":"info(message: string, config?: ToastConfig<unknown, R>): ToastRef<unknown, R>","description":"Shorthand for `show(message, { severity: 'info', ... })`."},{"name":"promise","signature":"promise(promise: Promise<T>, messages: PromiseMessages<T>, config?: ToastConfig<unknown, R>): ToastRef<unknown, R>","description":"Show a loading toast, then swap it to success / error when the promise settles. The same ref is returned and re-used across all three states. The loading toast is pinned (duration forced to 0, dismissible forced to false) until the promise resolves."},{"name":"dismiss","signature":"dismiss(id: string): void","description":"Dismiss a toast by id. No-op if no matching toast is active."},{"name":"dismissAll","signature":"dismissAll(): void","description":"Dismiss every active toast across every position."},{"name":"getToastById","signature":"getToastById(id: string): ToastRef<unknown, R> | undefined","description":"Look up an active toast by id."}]},{"name":"provideToast","kind":"function","description":"Registers ToastService for dependency injection and installs optional application-wide defaults.","signature":"provideToast(defaultOptions?: Partial<ToastConfig>): EnvironmentProviders"},{"name":"ToastComponent","kind":"component","description":"Visual toast / snackbar panel. Rendered internally by ToastService for string content, and exported for consumers who want to compose the same visual inside a custom `TemplateRef` or component class passed to `show()`.","selector":"tw-toast","usage":[{"form":"element","selector":"tw-toast","name":"tw-toast"}],"contentSlots":[{"select":"[twToastIcon]"},{"select":"[twToastTitle]"},{"select":"[twToastDescription]"},{"select":null},{"select":"[twToastAction]"}],"inputs":[{"name":"severity","type":"ToastSeverity","default":"'info'","description":"Severity variant. Drives color palette, default icon, and ARIA role / live politeness. Defaults to `'info'`."},{"name":"dismissible","type":"boolean","default":"true","description":"Whether to render the close button. Defaults to `true`."},{"name":"icon","type":"string | false | undefined","default":"undefined","description":"Icon override. Pass a string to render as text inside the icon slot, or `false` to hide the built-in severity icon. Ignored when a `[twToastIcon]` child is projected. When omitted, the severity-default icon renders. For arbitrary icon markup, project a `[twToastIcon]` child instead."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Explicit aria-label for the toast wrapper. When omitted, text content is used by assistive tech."}],"outputs":[{"name":"dismissed","payloadType":"void","description":"Fires when the close button is clicked."},{"name":"actionClicked","payloadType":"void","description":"Fires when a `[twToastAction]` button is clicked."}]},{"name":"ToastIconDirective","kind":"directive","description":"Slot directive for the icon. When projected, overrides the severity-default icon.","selector":"[twToastIcon]","usage":[{"form":"attribute","selector":"[twToastIcon]","name":"twToastIcon"}]},{"name":"ToastTitleDirective","kind":"directive","description":"Slot directive for the bold title line inside a toast.","selector":"[twToastTitle]","usage":[{"form":"attribute","selector":"[twToastTitle]","name":"twToastTitle"}]},{"name":"ToastDescriptionDirective","kind":"directive","description":"Slot directive for a secondary description line inside a toast.","selector":"[twToastDescription]","usage":[{"form":"attribute","selector":"[twToastDescription]","name":"twToastDescription"}]},{"name":"ToastActionDirective","kind":"directive","description":"Slot directive for the action button. Apply to a native `<button>` to inherit the severity-aware styling.","selector":"[twToastAction]","usage":[{"form":"attribute","selector":"[twToastAction]","name":"twToastAction"}]},{"name":"ToastRef","kind":"class","description":"Reference to a toast opened via ToastService. Use to dismiss the toast, pause / resume its auto-dismiss timer, or observe its lifecycle. The service returns a ref synchronously from every open method; the toast itself is attached asynchronously inside the CDK overlay.","methods":[{"name":"dismiss","signature":"dismiss(result?: R): void","description":"Dismiss the toast programmatically. The leave animation plays before the element is removed and subscribers are notified. Safe to call multiple times."},{"name":"pause","signature":"pause(): void","description":"Pause the auto-dismiss timer (if any). Called automatically on hover / focus when `pauseOnInteraction` is true."},{"name":"resume","signature":"resume(): void","description":"Resume the auto-dismiss timer with the remaining time."},{"name":"triggerAction","signature":"triggerAction(): void","description":"Invoke the configured action handler (or dismiss with reason `'action'` if none is set)."},{"name":"update","signature":"update(patch: ToastUpdatePatch): void","description":"Mutate the live toast in place. Used by the `promise()` helper to swap loading → success / error."},{"name":"afterOpened","signature":"afterOpened(): Observable<void>","description":"Observable that emits once after the enter animation completes."},{"name":"beforeDismissed","signature":"beforeDismissed(): Observable<R | undefined>","description":"Observable that emits once when the dismiss sequence starts."},{"name":"afterDismissed","signature":"afterDismissed(): Observable<ToastDismissal<R>>","description":"Observable that emits once after the leave animation completes and the ref is cleaned up."}]},{"name":"ToastUpdatePatch","kind":"interface","description":"Patch accepted by ToastRef.update. Mirrors the subset of `ToastConfig` that is safe to mutate live.","members":[{"name":"severity","type":"ToastSeverity","optional":true,"description":"New severity — updates color, default icon, and (for `'error'`) live-region politeness."},{"name":"duration","type":"number","optional":true,"description":"New auto-dismiss duration in ms. Restarts the timer if the toast is visible. `0` disables auto-dismiss."},{"name":"action","type":"ToastAction | null","optional":true,"description":"Replace or clear the action button. Pass `null` to remove."},{"name":"icon","type":"string | false | undefined","optional":true,"description":"Override the icon slot."},{"name":"data","type":"unknown","optional":true,"description":"Replace the `TW_TOAST_DATA` payload."},{"name":"ariaLabel","type":"string","optional":true,"description":"Replace the aria-label applied to the toast wrapper."},{"name":"content","type":"ToastContent","optional":true,"description":"Replace the toast's content (string, template, or component)."},{"name":"dismissible","type":"boolean","optional":true,"description":"Whether the close button is shown."}]},{"name":"ToastConfig","kind":"class","description":"Per-call configuration for ToastService methods. Also used as the payload of TW_TOAST_DEFAULT_OPTIONS for app-wide defaults."},{"name":"TW_TOAST_DATA","kind":"token","description":"Injection token providing `config.data` to projected component / template content."},{"name":"TW_TOAST_REF","kind":"token","description":"Injection token providing the owning ToastRef to projected component content."},{"name":"TW_TOAST_DEFAULT_OPTIONS","kind":"token","description":"Injection token carrying application-wide toast defaults. Set via `provideToast(defaults)`."},{"name":"ToastSeverity","kind":"type","description":"Severity variant — drives color palette, default icon, and live-region politeness.","definition":"'info' | 'success' | 'warning' | 'error' | 'neutral'"},{"name":"ToastPosition","kind":"type","description":"Screen anchor for a stack of toasts. One CDK overlay is created per position on first use.","definition":"| 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'"},{"name":"ToastState","kind":"type","description":"Lifecycle state of a single toast.","definition":"'entering' | 'visible' | 'paused' | 'dismissing' | 'dismissed'"},{"name":"ToastDismissReason","kind":"type","description":"Reason a toast was dismissed.","definition":"| 'action' | 'timeout' | 'swipe' | 'manual' | 'programmatic' | 'max-exceeded'"},{"name":"ToastDismissal","kind":"interface","description":"Payload forwarded to `afterDismissed()` subscribers.","members":[{"name":"reason","type":"ToastDismissReason","optional":false,"description":"Why the toast closed."},{"name":"result","type":"R","optional":true,"description":"Optional value forwarded from `ref.dismiss(result)`."}]},{"name":"ToastTemplateContext","kind":"interface","description":"Context object handed to `TemplateRef` content (`let-data let-ref=\"ref\"`).","members":[{"name":"$implicit","type":"T","optional":false,"description":"Data value from `config.data`, unwrapped as the template's `$implicit`."},{"name":"ref","type":"ToastRef<unknown, unknown>","optional":false,"description":"Reference to the toast itself, bound as `let-ref=\"ref\"`."}]},{"name":"ToastAction","kind":"interface","description":"Action button configuration. When `handler` is omitted the button dismisses the toast with reason `'action'`.","members":[{"name":"label","type":"string","optional":false,"description":"Visible label of the action button."},{"name":"handler","type":"(ref: ToastRef) => void","optional":true,"description":"Optional callback invoked when the action is clicked or `ref.triggerAction()` is called."}]},{"name":"ToastContent","kind":"type","description":"Accepted content forms for `ToastService.show()`.","definition":"string | TemplateRef<ToastTemplateContext> | Type<unknown>"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type ToastSeverity = 'info' | 'success' | 'warning' | 'error' | 'neutral';\n\ntype ToastPosition =\n | 'top-left'\n | 'top-center'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-center'\n | 'bottom-right';\n\ntype ToastState = 'entering' | 'visible' | 'paused' | 'dismissing' | 'dismissed';\n\ntype ToastDismissReason =\n | 'action'\n | 'timeout'\n | 'swipe'\n | 'manual'\n | 'programmatic'\n | 'max-exceeded';\n\ninterface ToastDismissal<R = unknown> {\n reason: ToastDismissReason;\n result?: R;\n}\n\ninterface ToastAction {\n label: string;\n handler?: (ref: ToastRef) => void;\n}\n\ninterface ToastTemplateContext<T = unknown> {\n $implicit: T;\n ref: ToastRef<unknown, unknown>;\n}\n\ninterface ToastUpdatePatch {\n severity?: ToastSeverity;\n duration?: number;\n action?: ToastAction | null;\n icon?: string | false | undefined;\n data?: unknown;\n ariaLabel?: string;\n content?: ToastContent;\n dismissible?: boolean;\n}\n\ntype ToastContent = string | TemplateRef<ToastTemplateContext> | Type<unknown>;"},{"id":"severitiesSnippet","title":"Severities","language":"ts","code":"protected readonly severitySamples: readonly SeveritySample[] = [\n { severity: 'info', message: 'Build #4812 started.' },\n { severity: 'success', message: 'Deployment to production finished in 18 seconds.' },\n { severity: 'warning', message: 'Disk is 92% full — old backups will be pruned tonight.' },\n { severity: 'error', message: 'Could not save the pull request.' },\n { severity: 'neutral', message: 'Settings synced from acme-production.' },\n];\n\nprotected openSeverity(sample: SeveritySample): void {\n this.toast.show(sample.message, { severity: sample.severity });\n}"},{"id":"positionsSnippet","title":"Positions","language":"ts","code":"@for (p of positions; track p) {\n <button twButton variant=\"outline\" size=\"sm\" (click)=\"openAt(p)\">\n {{ p }}\n </button>\n}\n\nprotected openAt(position: ToastPosition): void {\n this.toast.show(`Hello from ${position}`, { position });\n}"},{"id":"actionSnippet","title":"Action Button","language":"ts","code":"this.toast.show('Item archived — you have 8 seconds to undo.', {\n severity: 'neutral',\n duration: 8000,\n action: {\n label: 'Undo',\n handler: (ref) => {\n restoreItem();\n ref.dismiss();\n },\n },\n});"},{"id":"promiseSnippet","title":"promise()","language":"ts","code":"this.toast.promise(saveProject(), {\n loading: 'Saving changes…',\n success: (id) => `Saved ${id}.`,\n error: (err) => `Could not save: ${(err as Error).message}`,\n});"},{"id":"interactionSnippet","title":"Pause on Interaction Swipe","language":"ts","code":"// Pause timer on hover / focus (default)\nthis.toast.show('Read this carefully — hover to pause.', {\n duration: 3000,\n pauseOnInteraction: true,\n});\n\n// Swipe to dismiss (default on)\nthis.toast.show('Drag me horizontally to dismiss.', {\n duration: 0,\n swipeToDismiss: true,\n});"},{"id":"templateSnippet","title":"Custom Content","language":"html","code":"<ng-template #tmpl let-data let-ref=\"ref\">\n <strong>{{ data.items }} items updated.</strong>\n <button class=\"underline\" (click)=\"ref.dismiss()\">Dismiss</button>\n</ng-template>\n\n<!-- in the class -->\nthis.toast.show(this.tmpl(), {\n data: { items: 12 },\n severity: 'success',\n duration: 0,\n});"},{"id":"componentSnippet","title":"Custom Content","language":"ts","code":"@Component({\n selector: 'app-invite-toast',\n template: `<!-- custom markup -->`,\n})\nclass InviteToastComponent {\n protected readonly data = inject<{ name: string }>(TW_TOAST_DATA);\n protected readonly ref = inject<ToastRef>(TW_TOAST_REF);\n}\n\nthis.toast.show(InviteToastComponent, {\n data: { name: 'Tomás Aguilar' },\n duration: 0,\n dismissible: false,\n});"},{"id":"stackingSnippet","title":"Stacking maxVisible","language":"ts","code":"// Open past the cap — oldest dismisses with reason 'max-exceeded'\nfor (let i = 1; i <= 8; i++) {\n this.toast.info(`Notification #${i}`, { duration: 0 });\n}\n\n// Clear everything\nthis.toast.dismissAll();"},{"id":"lifecycleSnippet","title":"Lifecycle Observables","language":"ts","code":"const ref = this.toast.show('Watch the log update.', { duration: 2500 });\n\nref.afterOpened().subscribe(() => log('opened'));\nref.beforeDismissed().subscribe(() => log('beforeDismissed'));\nref.afterDismissed().subscribe((d) => log(`afterDismissed (${d.reason})`));"},{"id":"setupSnippet","title":"Setup","language":"ts","code":"import { provideToast } from '@cdevhub/ngx-tw/toast';\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideToast({ position: 'bottom-right', duration: 4000 }),\n ],\n};"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"ts","code":"const toast = inject(ToastService);\n\ntoast.info('Fetching the latest build.');\ntoast.success('Saved successfully.');\ntoast.warning('Disk is almost full.');\ntoast.error('Something went wrong.');"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n ToastService,\n ToastRef,\n TW_TOAST_DATA,\n TW_TOAST_REF,\n provideToast,\n} from '@cdevhub/ngx-tw/toast';"}],"summary":"Transient, non-modal notification opened from a service and stacked in a screen corner, with auto-dismiss, swipe-to-dismiss, and live-region announcements.","whenToUse":["Confirming the result of an async action — saved, copied, deleted, upload finished","Global feedback that belongs to the app rather than to one region of the page","Several messages that need to stack and expire on their own over time","Loading → success / error state for a promise, via the `promise()` helper","A short undo or retry affordance attached to the notification as an action button"],"whenNotToUse":[{"instead":"alert","because":"the message is anchored to a page region, must persist, and the user should be able to re-read it"},{"instead":"dialog","because":"the feedback must block the page until the user acknowledges or decides"},{"instead":"form-field","because":"the feedback is validation tied to one specific form field"}],"related":["alert","dialog","form-field","button","progress-bar"],"aliases":["snackbar","notification","flash message","growl","popup message","notify","banner notification"],"hasMeta":true,"metaPath":"projects/ngx-tw/toast/toast.meta.ts"},{"name":"transfer","importPath":"@cdevhub/ngx-tw/transfer","symbols":[{"name":"TransferComponent","kind":"component","description":"A dual-listbox shuttle: two panels (source / target) with a column of move buttons between them. The user ticks items in a panel and shuttles them across; an optional per-panel search and a tri-state header select-all accelerate bulk moves. The component's value is the set of keys on the target side, exposed through `ControlValueAccessor` + `FormFieldControl` so it works with reactive, template-driven, and signal forms and integrates with `<tw-form-field>`. Each panel composes `@angular/cdk/listbox` (`CdkListbox` / `CdkOption`) for focus-managed, accessible, keyboard-navigable selection. Consumers supply `keyFn` (required) and `labelFn` (defaulted to `String(item)`), and may project a `*twTransferItem` template for rich row content.","selector":"tw-transfer","usage":[{"form":"element","selector":"tw-transfer","name":"tw-transfer"}],"exportAs":"twTransfer","inputs":[{"name":"data","type":"readonly T[]","default":"[]","description":"All items across both panels. The source/target split derives from the value, not the reverse. Defaults to `[]`."},{"name":"keyFn","type":"(item: T) => K","required":true,"description":"Resolves a stable key for an item — drives membership (the value), checked state, tracking, the cdkOption value, and compareWith. Required."},{"name":"labelFn","type":"(item: T) => string","default":"(item: T) => String(item)","description":"Resolves an item's display label — used for the default row render, search filtering, and listbox typeahead. Defaults to `(item) => String(item)`."},{"name":"labels","type":"Partial<TwTransferLabels>","default":"{}","description":"Per-panel text + ARIA labels. Partial; unset keys fall back to the English defaults."},{"name":"display","type":"Partial<TwTransferDisplayConfig>","default":"{}","description":"Visual configuration — size, list viewport height, search / select-all visibility. Partial; unset keys fall back to the defaults."},{"name":"behavior","type":"Partial<TwTransferBehaviorConfig<T>>","default":"{}","description":"Behavioural configuration — oneWay, custom filterFn, per-item disabledItem predicate. Partial; unset keys fall back to the defaults."},{"name":"disabledInput","type":"boolean","default":"false","description":"Disables the entire control — panels non-interactive, search + buttons disabled, muted styling. Also driven by Forms `setDisabledState`. Defaults to `false`.","alias":"disabled"},{"name":"requiredInput","type":"boolean","default":"false","description":"Marks the control as required. Mirrored to `aria-required` chrome via the form-field; also inferred from `Validators.required` on a bound control. Defaults to `false`.","alias":"required"},{"name":"name","type":"string | undefined","default":"undefined","description":"Reserved for control identification (e.g. analytics, future native-submission parity). The transfer has no single native value element, so it is not currently reflected to the DOM. Defaults to `undefined`."},{"name":"idInput","type":"string | undefined","default":"undefined","description":"Id on the host element. Auto-generated as `tw-transfer-N` when unset. Used by the form-field's `<label for>` association.","alias":"id"},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name applied to the control when no visible label is wired. Mirrored to `aria-label`. Defaults to `undefined`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the control. Mirrored to `aria-labelledby`. Defaults to `undefined`.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external element that describes the control. The form-field merges its hint / error ids alongside. Defaults to `undefined`.","alias":"aria-describedby"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, uses the `TW_ERROR_STATE_MATCHER` token's value. Defaults to `undefined`."}],"outputs":[{"name":"valueChange","payloadType":"readonly K[]","description":"Fires when items move between panels by user interaction. Payload is the new full target-keys array. Does not fire on `writeValue`."},{"name":"moved","payloadType":"TwTransferMovedEvent<K>","description":"Fires after each directional move, identifying the moved keys and their direction. Does not fire on `writeValue`."}],"methods":[{"name":"moveToTarget","signature":"moveToTarget(keys: readonly K[]): void","description":"Moves the given items (by key) to the target side; ignores keys already there or whose item is disabled. Clears the source panel's pending checks, emits `valueChange` + `moved`, calls the CVA onChange, and announces the move. No-op when the control is disabled."},{"name":"moveToSource","signature":"moveToSource(keys: readonly K[]): void","description":"Moves the given items (by key) to the source side; ignores keys not on target or whose item is disabled. Clears the target panel's pending checks. No-op when the control is disabled or `behavior.oneWay` is on. Same emissions as moveToTarget."}],"formControl":true,"extends":"FormFieldControl"},{"name":"TransferItemDefDirective","kind":"directive","description":"Structural directive (`*twTransferItem=\"let item\"`) declaring the per-item template. Typed as `TwTransferItemContext<T>`.","selector":"[twTransferItem]","usage":[{"form":"attribute","selector":"[twTransferItem]","name":"twTransferItem"}]},{"name":"TwTransferSide","kind":"type","description":"Which panel of the transfer an item / event belongs to.","definition":"'source' | 'target'"},{"name":"TwTransferLabels","kind":"interface","description":"i18n strings for the transfer. Pass any subset; unset keys fall back to the English defaults.","members":[{"name":"sourceTitle","type":"string","optional":true,"description":"Source (left) panel title. Defaults to `'Source'`."},{"name":"targetTitle","type":"string","optional":true,"description":"Target (right) panel title. Defaults to `'Target'`."},{"name":"searchPlaceholder","type":"string","optional":true,"description":"Placeholder + accessible name for the per-panel search inputs. Defaults to `'Search'`."},{"name":"emptyText","type":"string","optional":true,"description":"Text shown in a panel body when it has no visible items. Defaults to `'No items'`."},{"name":"selectAllLabel","type":"string","optional":true,"description":"Accessible name for the header select-all checkbox. Defaults to `'Select all'`."},{"name":"moveToTargetLabel","type":"string","optional":true,"description":"Accessible name for the → (move-to-target) button. Defaults to `'Move selected to target'`."},{"name":"moveToSourceLabel","type":"string","optional":true,"description":"Accessible name for the ← (move-to-source) button. Defaults to `'Move selected to source'`."},{"name":"countFormat","type":"string","optional":true,"description":"Count template rendered in each panel header. Variables: `{total}`, `{selected}`. Defaults to `'{total} items'`."},{"name":"moveAnnouncement","type":"string","optional":true,"description":"LiveAnnouncer template announced after a move. Variables: `{count}`, `{target}`. Defaults to `'{count} items moved to {target}'`."}]},{"name":"TwTransferDisplayConfig","kind":"interface","description":"Visual configuration for the transfer. Pass any subset; unset keys fall back to the defaults.","members":[{"name":"size","type":"TwSize","optional":true,"description":"Row / control density across the design-system ramps. Defaults to `'md'`."},{"name":"listHeight","type":"number | 'auto'","optional":true,"description":"Scroll-viewport height of each list, in px (a number) or `'auto'` (flow with content). Defaults to `240`."},{"name":"showSearch","type":"boolean","optional":true,"description":"When true, each panel gets a labelled search input above its list. Defaults to `false`."},{"name":"showSelectAll","type":"boolean","optional":true,"description":"When true, each panel header shows a tri-state select-all checkbox. Defaults to `true`."}]},{"name":"TwTransferBehaviorConfig","kind":"interface","description":"Behavioural configuration for the transfer. Generic over item type `T`. Pass any subset; unset keys fall back to the defaults.","members":[{"name":"oneWay","type":"boolean","optional":true,"description":"When true, items only flow source → target; the ← button is not rendered. Defaults to `false`."},{"name":"filterFn","type":"(item: T, query: string) => boolean","optional":true,"description":"Custom search predicate. Defaults to a case-insensitive substring match of the query against `labelFn(item)`."},{"name":"disabledItem","type":"(item: T) => boolean","optional":true,"description":"Per-item disable predicate. A disabled item renders as a disabled option and is excluded from select-all and all moves. Defaults to `() => false`."}]},{"name":"TwTransferItemContext","kind":"interface","description":"Context surfaced to a `*twTransferItem` template. Generic over the item type `T`.","members":[{"name":"$implicit","type":"T","optional":false,"description":"The item data (implicit `let-item`)."},{"name":"label","type":"string","optional":false,"description":"Resolved `labelFn(item)`."},{"name":"checked","type":"boolean","optional":false,"description":"Ephemeral pending-move checked state of this row's option."},{"name":"disabled","type":"boolean","optional":false,"description":"Whether the item is disabled (via `behavior.disabledItem`)."},{"name":"side","type":"TwTransferSide","optional":false,"description":"Which panel this row renders in."}]},{"name":"TwTransferMovedEvent","kind":"interface","description":"Payload emitted by `moved`. Generic over the key type `K`.","members":[{"name":"keys","type":"readonly K[]","optional":false,"description":"The keys that moved in this interaction."},{"name":"direction","type":"'toTarget' | 'toSource'","optional":false,"description":"The direction of the move."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TwTransferSide = 'source' | 'target';\n\ninterface TwTransferLabels {\n /** Source / target panel titles. Default 'Source' / 'Target'. */\n sourceTitle?: string;\n targetTitle?: string;\n /** Search placeholder + empty text. Default 'Search' / 'No items'. */\n searchPlaceholder?: string;\n emptyText?: string;\n /** ARIA labels for the select-all and move buttons. */\n selectAllLabel?: string;\n moveToTargetLabel?: string;\n moveToSourceLabel?: string;\n /** Header count template, vars {total} {selected}. Default '{total} items'. */\n countFormat?: string;\n /** Move announcement template, vars {count} {target}. */\n moveAnnouncement?: string;\n}\n\ninterface TwTransferDisplayConfig {\n /** Row density. Default 'md'. */\n size?: TwSize;\n /** List viewport height in px, or 'auto'. Default 240. */\n listHeight?: number | 'auto';\n /** Per-panel search input. Default false. */\n showSearch?: boolean;\n /** Tri-state header select-all. Default true. */\n showSelectAll?: boolean;\n}\n\ninterface TwTransferBehaviorConfig<T = unknown> {\n /** Source → target only; hides the ← button. Default false. */\n oneWay?: boolean;\n /** Custom search predicate. Default substring match on labelFn. */\n filterFn?: (item: T, query: string) => boolean;\n /** Per-item disable predicate. Default () => false. */\n disabledItem?: (item: T) => boolean;\n}\n\ninterface TwTransferItemContext<T> {\n $implicit: T;\n label: string;\n checked: boolean;\n disabled: boolean;\n side: TwTransferSide;\n}\n\ninterface TwTransferMovedEvent<K> {\n keys: readonly K[];\n direction: 'toTarget' | 'toSource';\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (size of sizes; track size) {\n <tw-transfer\n [data]=\"scopes\"\n [keyFn]=\"scopeKey\"\n [labelFn]=\"scopeLabel\"\n [display]=\"{ size: size, listHeight: 'auto' }\"\n [ngModel]=\"['read:billing']\"\n />\n}"},{"id":"searchSnippet","title":"Search Select-all","language":"html","code":"<tw-transfer\n [data]=\"timezones\"\n [keyFn]=\"identity\"\n [(ngModel)]=\"zones\"\n [display]=\"{ showSearch: true, listHeight: 260 }\"\n [labels]=\"{ sourceTitle: 'All time zones', targetTitle: 'Working hours' }\"\n/>"},{"id":"templateHtmlSnippet","title":"Custom Item Template","language":"html","code":"<tw-transfer\n [data]=\"team\"\n [keyFn]=\"personKey\"\n [labelFn]=\"personLabel\"\n [(ngModel)]=\"reviewers\"\n [display]=\"{ showSearch: true, listHeight: 300 }\"\n>\n <ng-template twTransferItem let-item let-side=\"side\">\n <tw-avatar [initials]=\"item.initials\" [color]=\"item.color\" size=\"sm\" />\n <div class=\"min-w-0 flex-1\">\n <p class=\"truncate\">{{ item.name }}</p>\n <p class=\"truncate text-xs text-fg-muted\">{{ item.email }}</p>\n </div>\n </ng-template>\n</tw-transfer>"},{"id":"templateTsSnippet","title":"Custom Item Template","language":"ts","code":"interface Person {\n id: string;\n name: string;\n email: string;\n initials: string;\n color: TwColor;\n}\n\nprotected readonly team: Person[] = [/* … */];\nprotected readonly reviewers = signal<readonly string[]>(['grace']);\nprotected readonly personKey = (p: Person) => p.id;\nprotected readonly personLabel = (p: Person) => p.name;"},{"id":"oneWaySnippet","title":"One-Way","language":"html","code":"<tw-transfer\n [data]=\"scopes\"\n [keyFn]=\"scopeKey\"\n [labelFn]=\"scopeLabel\"\n [(ngModel)]=\"channels\"\n [behavior]=\"{ oneWay: true }\"\n [labels]=\"{ sourceTitle: 'Available', targetTitle: 'Enabled (no undo)' }\"\n/>"},{"id":"disabledHtmlSnippet","title":"Disabled Items","language":"html","code":"<tw-transfer\n [data]=\"scopes\"\n [keyFn]=\"scopeKey\"\n [labelFn]=\"scopeLabel\"\n [behavior]=\"{ disabledItem: isLocked }\"\n [ngModel]=\"['read:members', 'deploy']\"\n/>"},{"id":"disabledTsSnippet","title":"Disabled Items","language":"ts","code":"interface Scope { key: string; label: string; locked?: boolean; }\n\n// Locked scopes can't be granted or revoked.\nprotected readonly isLocked = (s: Scope) => !!s.locked;"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly tdGranted = signal<readonly string[]>(['read:members']);"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-transfer\n name=\"tdScopes\"\n [data]=\"scopes\"\n [keyFn]=\"scopeKey\"\n [labelFn]=\"scopeLabel\"\n [(ngModel)]=\"tdGranted\"\n/>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly reactiveCtrl = new FormControl<readonly string[]>(\n ['deploy'],\n { nonNullable: true },\n);"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-transfer\n [data]=\"scopes\"\n [keyFn]=\"scopeKey\"\n [labelFn]=\"scopeLabel\"\n [formControl]=\"reactiveCtrl\"\n/>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"import { form } from '@angular/forms/signals';\n\nprotected readonly signalModel = signal<{ scopes: readonly string[] }>({ scopes: ['audit'] });\nprotected readonly signalForm = form(this.signalModel);"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-transfer\n [data]=\"scopes\"\n [keyFn]=\"scopeKey\"\n [labelFn]=\"scopeLabel\"\n [formField]=\"signalForm.scopes\"\n/>"},{"id":"formFieldSnippet","title":"Inside form-field","language":"html","code":"<tw-form-field>\n <label twLabel>Required reviewers</label>\n <tw-transfer\n [data]=\"team\"\n [keyFn]=\"personKey\"\n [labelFn]=\"personLabel\"\n [formControl]=\"ffReviewers\"\n />\n <span twHint>Assign at least one reviewer before publishing.</span>\n <span twError match=\"required\">Select at least one reviewer.</span>\n</tw-form-field>"},{"id":"basicUsageHtmlSnippet","title":"Basic Usage","language":"html","code":"<tw-transfer\n [data]=\"scopes\"\n [keyFn]=\"scopeKey\"\n [labelFn]=\"scopeLabel\"\n [(ngModel)]=\"granted\"\n [labels]=\"{ sourceTitle: 'Available scopes', targetTitle: 'Granted' }\"\n aria-label=\"API scopes\"\n/>"},{"id":"basicUsageTsSnippet","title":"Basic Usage","language":"ts","code":"interface Scope {\n key: string;\n label: string;\n}\n\nprotected readonly scopes: Scope[] = [/* … */];\nprotected readonly granted = signal<readonly string[]>(['read:members']);\n\nprotected readonly scopeKey = (s: Scope) => s.key;\nprotected readonly scopeLabel = (s: Scope) => s.label;"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n TransferComponent,\n TransferItemDefDirective,\n} from '@cdevhub/ngx-tw/transfer';"}],"summary":"Dual-listbox shuttle — a source panel and a target panel with move controls between them — whose form value is the set of keys currently on the target side.","whenToUse":["Assigning a subset out of a long fixed catalogue: permissions and scopes, feature flags, group members, mailing-list recipients","Selections where the user benefits from seeing what is chosen and what remains side by side","Bulk moves accelerated by a per-panel search field and a tri-state select-all header","A multi-value form control bound with template-driven, reactive, or signal forms","One-way assignment (items can leave the source but never come back) via the behavior config"],"whenNotToUse":[{"instead":"select","because":"the option list is short and an overlay listbox with chips is enough — a full dual list is overkill"},{"instead":"tags-input","because":"the user types free-form values rather than picking from a fixed catalogue"},{"instead":"checkbox","because":"there are only a handful of options and a simple checkbox group reads clearer"}],"related":["select","tags-input","checkbox","form-field","combobox"],"aliases":["dual listbox","dual list","shuttle","pick list","picklist","multi select","move items","assign","two panel selector","side by side list"],"hasMeta":true,"metaPath":"projects/ngx-tw/transfer/transfer.meta.ts"},{"name":"form-field","importPath":"@cdevhub/ngx-tw/form-field","symbols":[{"name":"FormFieldComponent","kind":"component","description":"","selector":"tw-form-field","usage":[{"form":"element","selector":"tw-form-field","name":"tw-form-field"}],"contentSlots":[{"select":"[twPrefix]"},{"select":"[twPrefixIcon]"},{"select":null},{"select":"[twSuffix]"},{"select":"[twSuffixIcon]"},{"select":"[twLabel]"},{"select":"[twError]"},{"select":"[twHint]"}],"inputs":[{"name":"appearance","type":"FormFieldAppearance","default":"'outline'","description":"Visual appearance of the field container. `'outline'` draws a full border around the control; `'filled'` uses a filled surface with a bottom border. Defaults to `'outline'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Density of the field container. Maps to the inline-padding scale (`px-2 py-1` xs … `px-5 py-3` xl) and the floating-label font scale. Defaults to `'md'`."},{"name":"floatLabel","type":"FloatLabel","default":"'auto'","description":"Floating label behavior. `'auto'` floats when focused or non-empty; `'always'` stays floated; `'never'` disables floating entirely (the label wrapper is not rendered and the wrapped control's placeholder is always visible). Defaults to `'auto'`."},{"name":"subscriptSizing","type":"SubscriptSizing","default":"'fixed'","description":"Subscript sizing strategy. `'fixed'` always reserves a 20px row for hints/errors so adjacent fields align; `'dynamic'` collapses the row when no hint/error is projected. Defaults to `'fixed'`."},{"name":"hideRequiredMarker","type":"boolean","default":"false","description":"Hides the visual required marker (`*`) even when the wrapped control is required. `aria-required` on the control is unaffected. Defaults to `false`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color for focused/active accents (focused border, floated label). Defaults to `'primary'`."},{"name":"hintAlign","type":"'start' | 'end'","default":"'start'","description":"Default alignment for a `twHint` element that does not specify its own `align`. Defaults to `'start'`."}]},{"name":"LabelDirective","kind":"directive","description":"","selector":"[twLabel]","usage":[{"form":"attribute","selector":"[twLabel]","name":"twLabel"}]},{"name":"HintDirective","kind":"directive","description":"","selector":"[twHint]","usage":[{"form":"attribute","selector":"[twHint]","name":"twHint"}],"inputs":[{"name":"align","type":"'start' | 'end' | undefined","default":"undefined","description":"Alignment within the subscript row. `'end'` pushes the hint to the right. When unset, the form-field's `hintAlign` input supplies the default."}]},{"name":"ErrorDirective","kind":"directive","description":"","selector":"[twError]","usage":[{"form":"attribute","selector":"[twError]","name":"twError"}],"inputs":[{"name":"match","type":"string | undefined","default":"undefined","description":"Validation error key this message is bound to (e.g. `'required'`, `'email'`, `'minlength'`). When set, the error renders only while the surrounding control reports that key in its active validation errors. When omitted, the error displays whenever the form-field is in an error state — useful as a generic fallback."}]},{"name":"PrefixDirective","kind":"directive","description":"","selector":"[twPrefix]","usage":[{"form":"attribute","selector":"[twPrefix]","name":"twPrefix"}]},{"name":"SuffixDirective","kind":"directive","description":"","selector":"[twSuffix]","usage":[{"form":"attribute","selector":"[twSuffix]","name":"twSuffix"}]},{"name":"PrefixIconDirective","kind":"directive","description":"","selector":"[twPrefixIcon]","usage":[{"form":"attribute","selector":"[twPrefixIcon]","name":"twPrefixIcon"}],"extends":"PrefixDirective"},{"name":"SuffixIconDirective","kind":"directive","description":"","selector":"[twSuffixIcon]","usage":[{"form":"attribute","selector":"[twSuffixIcon]","name":"twSuffixIcon"}],"extends":"SuffixDirective"},{"name":"FormFieldControl","kind":"class","description":"Contract every ngx-tw form-field-compatible control must implement. A concrete control provides itself under TW_FORM_FIELD_CONTROL so the surrounding `FormFieldComponent` can mirror its state and wire ARIA.","methods":[{"name":"setDescribedByIds","signature":"setDescribedByIds(ids: string[]): void","description":"Called by the form-field to push the merged `aria-describedby` ids back onto the control's host element."},{"name":"onContainerClick","signature":"onContainerClick(event: MouseEvent): void","description":"Called when the form-field container is clicked. Concrete controls typically focus their underlying native element or open a panel."},{"name":"setLabelledByIds","signature":"setLabelledByIds(_ids: string[]): void","description":"Called by the form-field to push the merged `aria-labelledby` ids onto the control's host element. Default is a no-op — native `<input>` controls rely on the label's `for=` attribute for the canonical association. Non-native controls (combobox triggers, date-pickers, etc.) override this to set the attribute explicitly."}]},{"name":"TW_FORM_FIELD_CONTROL","kind":"token","description":"Injection token matching FormFieldControl. Controls register via `providers: [{ provide: TW_FORM_FIELD_CONTROL, useExisting: MyControl }]`."},{"name":"TW_FORM_FIELD","kind":"token","description":"Injection token exposing the wrapping TwFormFieldParent. The FormFieldComponent provides itself under this token; controls inject it with `{ optional: true }` to detect (and read label state from) a parent `tw-form-field` without referencing the concrete component class — keeping the form-field component out of each control's bundle."},{"name":"FormFieldAppearance","kind":"type","description":"Visual appearance of the form-field container.","definition":"'outline' | 'filled'"},{"name":"FloatLabel","kind":"type","description":"Floating label behavior. `'never'` disables floating entirely; the label wrapper is not rendered and the wrapped control's placeholder is always visible.","definition":"'auto' | 'always' | 'never'"},{"name":"SubscriptSizing","kind":"type","description":"Subscript (hint/error row) sizing strategy. `'fixed'` always reserves vertical space; `'dynamic'` collapses the row when no hint/error is projected.","definition":"'fixed' | 'dynamic'"},{"name":"TwFormFieldParent","kind":"interface","description":"Minimal surface a wrapped control reads from its surrounding form-field — presence (via optional injection) plus whether a label is projected — without depending on the concrete FormFieldComponent. Controls inject TW_FORM_FIELD typed as this interface so detecting a parent form-field never pins the heavier component class into the control's own bundle.","members":[{"name":"hasLabel","type":"Signal<boolean>","optional":false,"description":"Whether a `twLabel` element is projected into the form-field."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type FormFieldAppearance = 'outline' | 'filled';\ntype FloatLabel = 'auto' | 'always' | 'never';\ntype SubscriptSizing = 'fixed' | 'dynamic';\n\nabstract class FormFieldControl<T = unknown> {\n abstract readonly id: Signal<string>;\n abstract readonly value: Signal<T | null>;\n abstract readonly focused: Signal<boolean>;\n abstract readonly empty: Signal<boolean>;\n abstract readonly disabled: Signal<boolean>;\n abstract readonly required: Signal<boolean>;\n abstract readonly errorState: Signal<boolean>;\n abstract readonly controlType?: string;\n abstract readonly userAriaDescribedBy?: Signal<string | undefined>;\n readonly userAriaLabelledby?: Signal<string | undefined>;\n\n abstract setDescribedByIds(ids: string[]): void;\n abstract onContainerClick(event: MouseEvent): void;\n // Default no-op; override for non-native controls that need explicit label pushdown.\n setLabelledByIds(ids: string[]): void {}\n}"},{"id":"appearanceSnippet","title":"Variants","language":"html","code":"<tw-form-field appearance=\"outline\">\n <label twLabel>Outline</label>\n <input twInput placeholder=\"Acme Industries\" />\n <span twHint>Full legal company name.</span>\n</tw-form-field>\n\n<tw-form-field appearance=\"filled\">\n <label twLabel>Filled</label>\n <input twInput placeholder=\"Acme Industries\" />\n <span twHint>Full legal company name.</span>\n</tw-form-field>"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-form-field [color]=\"c\" floatLabel=\"always\">\n <label twLabel>{{ labelFor(c) }}</label>\n <input twInput [placeholder]=\"placeholderFor(c)\" />\n </tw-form-field>\n}"},{"id":"floatLabelSnippet","title":"Floating Label","language":"html","code":"<tw-form-field floatLabel=\"auto\">\n <label twLabel>Auto (default)</label>\n <input twInput />\n</tw-form-field>\n\n<tw-form-field floatLabel=\"always\">\n <label twLabel>Always floated</label>\n <input twInput placeholder=\"e.g. Remote, EU\" />\n</tw-form-field>\n\n<tw-form-field floatLabel=\"never\" appearance=\"filled\">\n <label twLabel>Never floated</label>\n <input twInput placeholder=\"Search…\" />\n</tw-form-field>"},{"id":"sizeSnippet","title":"Size","language":"html","code":"@for (s of sizes; track s) {\n <tw-form-field [size]=\"s\">\n <label twLabel>{{ s }} density</label>\n <input twInput [placeholder]=\"'Size ' + s\" />\n </tw-form-field>\n}"},{"id":"subscriptSizingSnippet","title":"Subscript Sizing","language":"html","code":"<!-- Default: reserves space for hints/errors -->\n<tw-form-field>\n <label twLabel>First name</label>\n <input twInput />\n</tw-form-field>\n\n<!-- Dense: collapses the subscript when nothing to show -->\n<tw-form-field subscriptSizing=\"dynamic\">\n <label twLabel>First name</label>\n <input twInput />\n</tw-form-field>"},{"id":"iconAdornmentsSnippet","title":"Icon Adornments","language":"html","code":"<tw-form-field>\n <label twLabel>Search</label>\n <svg twPrefixIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n <input twInput placeholder=\"Search invoices…\" />\n</tw-form-field>\n\n<tw-form-field>\n <label twLabel>Email</label>\n <input twInput type=\"email\" placeholder=\"you@company.com\" />\n <svg twSuffixIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">…</svg>\n</tw-form-field>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled -->\n<tw-form-field>\n <label twLabel>Account tier</label>\n <input twInput [disabled]=\"true\" value=\"Enterprise — managed by billing\" />\n <span twHint>Contact your administrator to upgrade.</span>\n</tw-form-field>\n\n<!-- Required (marker visible) -->\n<tw-form-field>\n <label twLabel>Username</label>\n <input twInput required placeholder=\"jane-doe\" />\n</tw-form-field>\n\n<!-- Required, marker hidden -->\n<tw-form-field [hideRequiredMarker]=\"true\">\n <label twLabel>Work email</label>\n <input twInput type=\"email\" required placeholder=\"you@company.com\" />\n</tw-form-field>"},{"id":"prefixSuffixSnippet","title":"Prefix Suffix","language":"html","code":"<tw-form-field>\n <label twLabel>Amount</label>\n <span twPrefix>$</span>\n <input twInput type=\"number\" placeholder=\"0.00\" />\n <span twSuffix>USD</span>\n <span twHint>Pre-tax total.</span>\n</tw-form-field>\n\n<tw-form-field appearance=\"filled\">\n <label twLabel>Search</label>\n <svg twPrefix class=\"size-4\">…</svg>\n <input twInput placeholder=\"Search invoices, clients, or projects\" />\n <kbd twSuffix>⌘K</kbd>\n</tw-form-field>"},{"id":"hintsSnippet","title":"Hints","language":"html","code":"<tw-form-field>\n <label twLabel>Project name</label>\n <input twInput [(ngModel)]=\"projectName\" name=\"projectName\" maxlength=\"48\" />\n <span twHint>Visible to every workspace member.</span>\n <span twHint align=\"end\">{{ projectName().length }} / 48</span>\n</tw-form-field>"},{"id":"textareaSnippet","title":"Textarea","language":"html","code":"<tw-form-field>\n <label twLabel>Release notes</label>\n <textarea\n twInput\n rows=\"4\"\n maxlength=\"280\"\n [(ngModel)]=\"releaseNotes\"\n name=\"releaseNotes\"\n placeholder=\"What changed, why it matters, and what to watch for.\"\n ></textarea>\n <span twHint>Markdown supported.</span>\n <span twHint align=\"end\">{{ releaseNotes().length }} / 280</span>\n</tw-form-field>"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly displayName = signal('');"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Display name</label>\n <input twInput name=\"displayName\" [(ngModel)]=\"displayName\" required minlength=\"2\" />\n <span twHint>Shown next to your avatar in the sidebar.</span>\n</tw-form-field>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly inviteForm = new FormGroup({\n name: new FormControl<string>('', {\n nonNullable: true,\n validators: [Validators.required, Validators.minLength(2)],\n }),\n email: new FormControl<string>('', {\n nonNullable: true,\n validators: [Validators.required, Validators.email],\n }),\n seats: new FormControl<number | null>(null, {\n validators: [Validators.required, Validators.min(1), Validators.max(50)],\n }),\n});"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<form [formGroup]=\"inviteForm\" (ngSubmit)=\"submitInvite()\">\n <tw-form-field>\n <label twLabel>Full name</label>\n <input twInput formControlName=\"name\" placeholder=\"Alex Morgan\" />\n <span twHint>How teammates will see you in the workspace.</span>\n <span twError match=\"required\">Enter your full name.</span>\n <span twError match=\"minlength\">Name must be at least 2 characters.</span>\n </tw-form-field>\n\n <tw-form-field>\n <label twLabel>Work email</label>\n <input twInput type=\"email\" formControlName=\"email\" />\n <span twHint>Invitation and receipts go here.</span>\n <span twError match=\"required\">Email is required.</span>\n <span twError match=\"email\">Enter a valid email address.</span>\n </tw-form-field>\n\n <tw-form-field appearance=\"filled\">\n <label twLabel>Seat count</label>\n <input twInput type=\"number\" formControlName=\"seats\" min=\"1\" max=\"50\" />\n <span twSuffix>seats</span>\n <span twError match=\"required\">Pick a seat count.</span>\n <span twError match=\"min\">Must be at least 1.</span>\n <span twError match=\"max\">Must be 50 or fewer.</span>\n </tw-form-field>\n</form>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly model = signal({ email: '' });\nprotected readonly inviteForm = form(this.model, (p) => {\n required(p.email);\n email(p.email);\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Email</label>\n <input twInput type=\"email\" [formField]=\"inviteForm.email\" />\n <span twHint>Used for account recovery.</span>\n <span twError match=\"required\">Email is required.</span>\n <span twError match=\"email\">Enter a valid email.</span>\n</tw-form-field>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-form-field>\n <label twLabel>Work email</label>\n <input twInput type=\"email\" placeholder=\"you@company.com\" />\n <span twHint>We'll send a confirmation link to this address.</span>\n</tw-form-field>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n FormFieldComponent,\n LabelDirective,\n HintDirective,\n ErrorDirective,\n PrefixDirective,\n SuffixDirective,\n PrefixIconDirective,\n SuffixIconDirective,\n FormFieldControl,\n TW_FORM_FIELD_CONTROL,\n} from '@cdevhub/ngx-tw/form-field';\nimport { InputDirective } from '@cdevhub/ngx-tw/input';"}],"summary":"Presentational wrapper that pairs any form control with its label, required marker, prefix and suffix adornments, hint text, and validation errors.","whenToUse":["Giving a text field, textarea, select, or custom control a consistent labelled row with hint and error regions","A floating label in auto, always, or never mode, over an outline or filled appearance","Prefix and suffix adornments — a currency symbol, a unit, a search icon, or a stepper button group","Automatic accessibility wiring: label-for association plus aria-describedby merging for hints and errors that preserves consumer ids","A subscript that swaps hint text for an error announced via role=\"alert\" once the control enters its error state","Plugging a consumer-authored control into the same chrome by implementing FormFieldControl and providing TW_FORM_FIELD_CONTROL"],"related":["input","textarea","select","number-input","combobox","date-picker","core"],"aliases":["field wrapper","form group","form row","label wrapper","floating label","hint","helper text","error message","validation message","prefix suffix","adornment"],"hasMeta":true,"metaPath":"projects/ngx-tw/form-field/form-field.meta.ts"},{"name":"switch","importPath":"@cdevhub/ngx-tw/switch","symbols":[{"name":"SwitchComponent","kind":"component","description":"","selector":"tw-switch","usage":[{"form":"element","selector":"tw-switch","name":"tw-switch"}],"contentSlots":[{"select":"[slot="},{"select":"[slot="},{"select":null},{"select":"[slot="}],"inputs":[{"name":"color","type":"TwColor","default":"'primary'","description":"Sets the semantic color for the active (checked) track. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls the overall scale of the track, thumb, and label typography. Defaults to `'md'`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, prevents interaction and applies muted styling. Defaults to `false`."},{"name":"required","type":"boolean","default":"false","description":"When true, sets `aria-required=\"true\"` so assistive tech announces the control as required. Defaults to `false`."},{"name":"label","type":"string | undefined","default":"undefined","description":"Optional inline label rendered next to the switch. Use default content projection for rich label content instead."},{"name":"description","type":"string | undefined","default":"undefined","description":"Optional secondary description rendered under the label. Use `[slot=\"description\"]` content projection for rich content instead."},{"name":"labelPosition","type":"SwitchLabelPosition","default":"'after'","description":"Position of the label/description relative to the switch. Defaults to `'after'`."},{"name":"name","type":"string | undefined","default":"undefined","description":"Optional name attribute, mirrored to the host for form association."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name when no visible label is provided. Mirrored to `aria-label`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the switch. Mirrored to `aria-labelledby`.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external element that describes the switch. Mirrored to `aria-describedby`.","alias":"aria-describedby"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, the switch uses the `TW_ERROR_STATE_MATCHER` token's value."}],"outputs":[{"name":"change","payloadType":"boolean","description":"Fires after the checked state changes from a user interaction. Does not fire when the value is updated programmatically via `writeValue`."}],"models":[{"name":"checked","type":"boolean","default":"false","description":"Two-way bound checked state. Updates when the user toggles via click or Space."}],"methods":[{"name":"toggle","signature":"toggle(): void","description":"Toggles the checked state. No-op when disabled."},{"name":"onKeydown","signature":"onKeydown(event: KeyboardEvent): void","description":"Handles keyboard activation. Space toggles the switch — matches the ARIA `switch` role pattern (Enter is intentionally NOT handled, mirroring `<tw-checkbox>`)."}],"formControl":true},{"name":"SwitchLabelPosition","kind":"type","description":"Position of the label relative to the switch control.","definition":"'before' | 'after'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type SwitchLabelPosition = 'before' | 'after';"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-switch [color]=\"c\" [(checked)]=\"colorValues[c]\" [label]=\"c\" />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-switch [size]=\"s\" [(checked)]=\"sizeValues[s]\" [label]=\"s\" />\n}"},{"id":"iconsSnippet","title":"With Icons","language":"html","code":"<tw-switch label=\"Sound\" color=\"info\" size=\"lg\" [(checked)]=\"sound\">\n <svg slot=\"on-icon\" class=\"size-3\" viewBox=\"0 0 20 20\" fill=\"currentColor\">…</svg>\n <svg slot=\"off-icon\" class=\"size-3\" viewBox=\"0 0 20 20\" fill=\"currentColor\">…</svg>\n</tw-switch>"},{"id":"descriptionSnippet","title":"With Description","language":"html","code":"<tw-switch\n label=\"Auto-sync\"\n description=\"Sync changes every minute\"\n color=\"success\"\n [(checked)]=\"syncValue\"\n/>\n\n<tw-switch\n label=\"Beta features\"\n description=\"Opt in to experimental functionality\"\n color=\"accent\"\n [(checked)]=\"betaValue\"\n/>"},{"id":"labelPositionSnippet","title":"Label Position","language":"html","code":"<tw-switch label=\"Label after (default)\" labelPosition=\"after\" [(checked)]=\"afterValue\" />\n<tw-switch label=\"Label before\" labelPosition=\"before\" [(checked)]=\"beforeValue\" />"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled (off) -->\n<tw-switch label=\"Notifications\" [disabled]=\"true\" />\n\n<!-- Disabled (on) -->\n<tw-switch label=\"Analytics\" [disabled]=\"true\" [checked]=\"true\" color=\"info\" />\n\n<!-- Required -->\n<tw-switch\n label=\"I agree to the terms\"\n description=\"Required to continue\"\n [required]=\"true\"\n [(checked)]=\"agreedValue\"\n/>"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly notifications = signal(true);"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-switch\n name=\"notifications\"\n label=\"Notifications\"\n color=\"info\"\n [(ngModel)]=\"notifications\"\n/>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly marketingControl = new FormControl<boolean>(false, { nonNullable: true });"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-switch\n label=\"Marketing emails\"\n color=\"info\"\n [formControl]=\"marketingControl\"\n/>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly settings = signal({ darkMode: false });\nprotected readonly settingsForm = form(this.settings);"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-switch\n label=\"Dark mode\"\n description=\"Use the dark color palette\"\n color=\"accent\"\n [formField]=\"settingsForm.darkMode\"\n/>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-switch label=\"Enable notifications\" [(checked)]=\"enabled\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { SwitchComponent } from '@cdevhub/ngx-tw/switch';"}],"summary":"Two-state toggle for a setting that takes effect the moment it is flipped, implementing the WAI-ARIA switch pattern.","whenToUse":["Settings panels and preference rows — dark mode, notifications, auto-save","A binary control whose change applies immediately rather than waiting for a form submit","A toggle that needs on/off indicator icons projected into the track","A labelled row where the label sits before or after the control and may carry a description line","A state assistive tech should announce as a switch rather than a checkbox, via role=\"switch\" and aria-checked"],"whenNotToUse":[{"instead":"checkbox","because":"the value is a form-submission truth the user reviews before commit, such as accepting terms"},{"instead":"radio","because":"the user is picking exactly one option from a small enumerated set rather than an on/off state"},{"instead":"segmented-control","because":"there are more than two labelled states but they still belong on one control surface"}],"related":["checkbox","radio","segmented-control","form-field","core"],"aliases":["toggle","toggle switch","on off","on/off","flip","setting toggle","boolean toggle","ios switch"],"hasMeta":true,"metaPath":"projects/ngx-tw/switch/switch.meta.ts"},{"name":"checkbox","importPath":"@cdevhub/ngx-tw/checkbox","symbols":[{"name":"CheckboxComponent","kind":"component","description":"","selector":"tw-checkbox","usage":[{"form":"element","selector":"tw-checkbox","name":"tw-checkbox"}],"contentSlots":[{"select":"[slot="},{"select":"[slot="},{"select":null},{"select":"[slot="}],"inputs":[{"name":"color","type":"TwColor","default":"'primary'","description":"Sets the semantic color for the checked and indeterminate box fill/border. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls the overall scale of the box, check icon, and label typography. Defaults to `'md'`."},{"name":"variant","type":"CheckboxVariant","default":"'solid'","description":"Visual style when checked or indeterminate. `'solid'` fills the box with the color; `'outline'` keeps a transparent fill with a colored border and check. Defaults to `'solid'`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, prevents interaction and applies muted styling. Defaults to `false`."},{"name":"requiredInput","type":"boolean","default":"false","description":"When true, sets `aria-required=\"true\"` so assistive tech announces the control as required. Defaults to `false`.","alias":"required"},{"name":"label","type":"string | undefined","default":"undefined","description":"Optional inline label rendered next to the checkbox. Use default content projection for rich label content instead. Projection takes precedence."},{"name":"description","type":"string | undefined","default":"undefined","description":"Optional secondary description rendered under the label. Use `[slot=\"description\"]` content projection for rich content instead. Projection takes precedence."},{"name":"labelPosition","type":"CheckboxLabelPosition","default":"'after'","description":"Position of the label/description relative to the checkbox. Defaults to `'after'`."},{"name":"name","type":"string | undefined","default":"undefined","description":"Optional name attribute, applied to the hidden native `<input type=\"checkbox\">` so native form submission includes the control."},{"name":"idInput","type":"string | undefined","default":"undefined","description":"Id on the host element. Auto-generated as `tw-checkbox-N` when not provided. Used by the form-field's `<label for>` attribute.","alias":"id"},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name when no visible label is provided. Mirrored to `aria-label`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the checkbox. Mirrored to `aria-labelledby`.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external element that describes the checkbox. Mirrored to `aria-describedby`. Form-field merges its hint/error ids alongside.","alias":"aria-describedby"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, the directive uses the `TW_ERROR_STATE_MATCHER` token's value."}],"outputs":[{"name":"change","payloadType":"boolean","description":"Fires after the checked state changes from a user interaction. Does not fire when the value is updated programmatically via `writeValue`."}],"models":[{"name":"checked","type":"boolean","default":"false","description":"Two-way bound checked state. Updates when the user toggles via click or Space."},{"name":"indeterminate","type":"boolean","default":"false","description":"Two-way bound indeterminate state. When true, the box shows a dash and the host exposes `aria-checked=\"mixed\"`. Any user toggle clears indeterminate and sets `checked` to `true`."}],"methods":[{"name":"toggle","signature":"toggle(): void","description":"Toggles the checked state. Clears indeterminate if set. No-op when disabled."},{"name":"onKeydown","signature":"onKeydown(event: KeyboardEvent): void","description":"Handles keyboard activation. Only Space toggles — matches native checkbox semantics."}],"formControl":true,"extends":"FormFieldControl"},{"name":"CheckboxVariant","kind":"type","description":"Visual style of the checkbox when checked or indeterminate.","definition":"'solid' | 'outline'"},{"name":"CheckboxLabelPosition","kind":"type","description":"Position of the label relative to the checkbox control.","definition":"'before' | 'after'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type CheckboxVariant = 'solid' | 'outline';\ntype CheckboxLabelPosition = 'before' | 'after';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-checkbox [variant]=\"v\" color=\"primary\" [(checked)]=\"variantValues[v]\" [label]=\"v\" />\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-checkbox [color]=\"c\" [(checked)]=\"colorValues[c]\" [label]=\"c\" />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-checkbox [size]=\"s\" [(checked)]=\"sizeValues[s]\" [label]=\"'Size ' + s\" />\n}"},{"id":"indeterminateTsSnippet","title":"Indeterminate \"Select all\"","language":"ts","code":"protected readonly values: Record<string, WritableSignal<boolean>> = {\n read: signal(true),\n write: signal(false),\n admin: signal(false),\n};\n\nprotected readonly allChecked = computed(() =>\n permissions.every(p => this.values[p.key]()),\n);\n\nprotected readonly someChecked = computed(() => {\n const checked = permissions.filter(p => this.values[p.key]()).length;\n return checked > 0 && checked < permissions.length;\n});\n\nprotected toggleAll(next: boolean): void {\n for (const p of permissions) this.values[p.key].set(next);\n}"},{"id":"indeterminateHtmlSnippet","title":"Indeterminate \"Select all\"","language":"html","code":"<tw-checkbox\n [checked]=\"allChecked()\"\n [indeterminate]=\"someChecked()\"\n (change)=\"toggleAll($event)\"\n label=\"Grant all permissions\"\n description=\"Toggle every scope below at once.\"\n/>\n@for (p of permissions; track p.key) {\n <tw-checkbox\n [(checked)]=\"values[p.key]\"\n [label]=\"p.label\"\n [description]=\"p.hint\"\n size=\"sm\"\n />\n}"},{"id":"descriptionSnippet","title":"With description","language":"html","code":"<tw-checkbox\n label=\"Subscribe to the product newsletter\"\n description=\"We'll send a short monthly digest. You can unsubscribe in one click.\"\n color=\"info\"\n [(checked)]=\"newsletter\"\n/>"},{"id":"multilineSnippet","title":"Long multi-line labels","language":"html","code":"<!-- Box stays aligned to the first line of the label regardless of length. -->\n<tw-checkbox\n size=\"md\"\n label=\"I agree to receive transactional emails about my account, billing reminders, security alerts, and occasional product updates from the team.\"\n description=\"You can change this preference any time from the notification settings page in your dashboard.\"\n [(checked)]=\"agreed\"\n/>"},{"id":"labelPositionSnippet","title":"Label position","language":"html","code":"<tw-checkbox\n labelPosition=\"after\"\n label=\"Send me product updates\"\n [(checked)]=\"updates\"\n/>\n\n<tw-checkbox\n labelPosition=\"before\"\n label=\"Show desktop notifications for new messages\"\n [(checked)]=\"notifications\"\n/>"},{"id":"customIconSnippet","title":"Custom check icon","language":"html","code":"<tw-checkbox label=\"Draft project brief\" color=\"success\" size=\"lg\" [(checked)]=\"done\">\n <svg slot=\"check-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M9.937 15.5A2 2 0 0 1 8.523 14.914l-3.523-3.5a1 1 0 0 1 1.414-1.414l3.523 3.5 7.05-7a1 1 0 0 1 1.414 1.414l-7.05 7a2 2 0 0 1-1.414.586z\"/>\n </svg>\n</tw-checkbox>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled -->\n<tw-checkbox label=\"Disabled, unchecked\" [disabled]=\"true\" />\n<tw-checkbox label=\"Disabled, checked\" [disabled]=\"true\" [checked]=\"true\" />\n<tw-checkbox label=\"Disabled, indeterminate\" [disabled]=\"true\" [indeterminate]=\"true\" />\n\n<!-- Required -->\n<tw-checkbox\n label=\"Accept the terms and privacy policy\"\n description=\"Required to create your account.\"\n [required]=\"true\"\n [(checked)]=\"accepted\"\n/>"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly newsletter = signal(false);"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-checkbox\n name=\"newsletter\"\n label=\"Subscribe to weekly digest\"\n description=\"Friday roundup of new articles and releases.\"\n color=\"info\"\n [(ngModel)]=\"newsletter\"\n/>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly remember = new FormControl<boolean>(false, { nonNullable: true });"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-checkbox\n label=\"Remember this device for 30 days\"\n color=\"info\"\n [formControl]=\"remember\"\n/>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly model = signal({ accepted: false });\nprotected readonly termsForm = form(this.model, (p) => {\n required(p.accepted);\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-checkbox\n label=\"Accept the terms and conditions\"\n description=\"Required to continue.\"\n [formField]=\"termsForm.accepted\"\n/>"},{"id":"formFieldSnippet","title":"Inside tw-form-field","language":"html","code":"<form [formGroup]=\"termsGroup\" (ngSubmit)=\"submit()\">\n <tw-form-field>\n <label twLabel>I accept the privacy policy</label>\n <tw-checkbox formControlName=\"accepted\" color=\"primary\" />\n <span twHint>Required to create the account.</span>\n <span twError>You must accept the policy before continuing.</span>\n </tw-form-field>\n</form>"},{"id":"errorStateSnippet","title":"Error state","language":"ts","code":"protected readonly confirmDestructive = new FormControl<boolean>(false, {\n nonNullable: true,\n validators: [Validators.requiredTrue],\n});\n\n// In the template\n<tw-checkbox\n label=\"Confirm the destructive action\"\n description=\"Required — the operation cannot be undone.\"\n color=\"error\"\n [formControl]=\"confirmDestructive\"\n/>"},{"id":"nativeFormSnippet","title":"Native form submission","language":"html","code":"<form>\n <tw-checkbox name=\"newsletter\" label=\"Subscribe\" [(checked)]=\"newsletter\" />\n <tw-checkbox name=\"terms\" label=\"Accept terms\" [(checked)]=\"terms\" />\n <button type=\"submit\">Submit</button>\n</form>\n\n<!-- The hidden <input type=\"checkbox\" name=\"...\"> ensures\n FormData includes both fields on submit. -->"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-checkbox label=\"I agree to the terms and conditions\" [(checked)]=\"accepted\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { CheckboxComponent } from '@cdevhub/ngx-tw/checkbox';"}],"summary":"Three-state selection control — unchecked, checked, and indeterminate — for an answer that is independent of the other options in view.","whenToUse":["An opt-in the user reviews before committing the form — accept terms, remember me, subscribe","A list of independent options where any number may be on at once","A parent \"select all\" that shows aria-checked=\"mixed\" while only some children are checked","Row selection in a list or table where each row toggles on its own","Reactive validation with Validators.requiredTrue, where aria-invalid follows the ErrorStateMatcher","Native form submission, via the hidden <input type=\"checkbox\"> that carries the name input"],"whenNotToUse":[{"instead":"radio","because":"exactly one of a small, fully visible set must be chosen"},{"instead":"switch","because":"the toggle takes effect immediately rather than being submitted with the form"},{"instead":"select","because":"the enumeration is long enough that inline options would overwhelm the layout"},{"instead":"transfer","because":"the user is moving many items between a source and a chosen list"}],"related":["radio","switch","select","form-field","core"],"aliases":["tickbox","tick box","check box","toggle option","multi-select option","indeterminate","mixed state","select all","opt-in","consent"],"hasMeta":true,"metaPath":"projects/ngx-tw/checkbox/checkbox.meta.ts"},{"name":"radio","importPath":"@cdevhub/ngx-tw/radio","symbols":[{"name":"RadioComponent","kind":"component","description":"","selector":"tw-radio","usage":[{"form":"element","selector":"tw-radio","name":"tw-radio"}],"contentSlots":[{"select":"[slot="},{"select":null},{"select":"[slot="}],"inputs":[{"name":"value","type":"unknown","default":"undefined","description":"The value this radio contributes when selected inside a `tw-radio-group`. Required when nested in a group; ignored when used standalone."},{"name":"color","type":"TwColor | undefined","default":"undefined","description":"Overrides the parent group's color for this radio. When undefined, inherits from the group (or defaults to `'primary'` standalone)."},{"name":"size","type":"TwSize | undefined","default":"undefined","description":"Overrides the parent group's size for this radio. When undefined, inherits from the group (or defaults to `'md'` standalone)."},{"name":"variant","type":"RadioVariant | undefined","default":"undefined","description":"Overrides the parent group's variant for this radio. When undefined, inherits from the group (or defaults to `'solid'` standalone)."},{"name":"disabled","type":"boolean","default":"false","description":"When true, disables this radio regardless of the group's state. Group-disabled always wins as an OR. Defaults to `false`."},{"name":"label","type":"string | undefined","default":"undefined","description":"Optional inline label rendered next to the radio. Use default content projection for rich label content instead."},{"name":"description","type":"string | undefined","default":"undefined","description":"Optional secondary description rendered under the label. Use `[slot=\"description\"]` content projection for rich content instead."},{"name":"labelPosition","type":"RadioLabelPosition","default":"'after'","description":"Position of the label/description relative to the radio. Defaults to `'after'`."},{"name":"name","type":"string | undefined","default":"undefined","description":"Optional name attribute for standalone use. Ignored when the parent group provides a name."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name when no visible label is provided. Mirrored to `aria-label`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the radio. Mirrored to `aria-labelledby`.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external element that describes the radio. Mirrored to `aria-describedby`.","alias":"aria-describedby"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, the radio uses the `TW_ERROR_STATE_MATCHER` token's value."}],"outputs":[{"name":"change","payloadType":"boolean","description":"Fires after the checked state changes from a user interaction on this radio. In grouped mode only fires when this radio becomes the selected one. Does not fire when selection is updated programmatically."}],"models":[{"name":"checked","type":"boolean","default":"false","description":"Two-way bound checked state. Authoritative only in standalone mode; when inside a `tw-radio-group`, this model reflects group selection but does NOT drive it."}],"methods":[{"name":"onActivate","signature":"onActivate(): void","description":"Called by user click or group keyboard handler. Selects this radio."},{"name":"onKeydown","signature":"onKeydown(event: KeyboardEvent): void","description":"Handles keyboard activation. Space selects; Enter does NOT — matches native `<input type=\"radio\">` semantics."},{"name":"focus","signature":"focus(): void","description":"Focuses the host element. Required for keyboard navigation from the group."}],"formControl":true},{"name":"RadioGroupComponent","kind":"component","description":"","selector":"tw-radio-group","usage":[{"form":"element","selector":"tw-radio-group","name":"tw-radio-group"}],"contentSlots":[{"select":null}],"inputs":[{"name":"color","type":"TwColor","default":"'primary'","description":"Sets the semantic color applied to the selected radio's dot/ring. Propagated to children unless a child overrides it. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls the overall scale of radios inside the group. Propagated to children unless a child overrides it. Defaults to `'md'`."},{"name":"variant","type":"RadioVariant","default":"'solid'","description":"Visual style of the selected indicator. `'solid'` fills the dot with the color against a colored ring; `'outline'` keeps a transparent fill with a colored ring and colored dot. Propagated to children unless a child overrides it. Defaults to `'solid'`."},{"name":"orientation","type":"RadioOrientation","default":"'vertical'","description":"Layout direction of the group. Drives `aria-orientation` and the arrow-key model. Defaults to `'vertical'`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, disables every radio in the group and blocks keyboard interaction. Defaults to `false`."},{"name":"required","type":"boolean","default":"false","description":"When true, sets `aria-required=\"true\"` on the group for assistive tech. Defaults to `false`."},{"name":"name","type":"string | undefined","default":"undefined","description":"Optional form-association name. Propagated to each child radio's host `name` attribute so standard HTML form semantics still apply."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the group when no visible label is provided. Mirrored to `aria-label`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the group. Mirrored to `aria-labelledby`.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external element that describes the group. Mirrored to `aria-describedby`.","alias":"aria-describedby"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, the group uses the `TW_ERROR_STATE_MATCHER` token's value."}],"outputs":[{"name":"change","payloadType":"T | null","description":"Fires after the selected value changes from a user interaction. Does not fire when the value is updated programmatically via `writeValue`."}],"models":[{"name":"value","type":"T | null","default":"null","description":"Two-way bound selected value. Updates when the user picks a radio; fires `valueChange`. `null` means no selection."}],"methods":[{"name":"selectValue","signature":"selectValue(next: unknown): void","description":"Selects the given value from a user interaction. Updates the model and notifies forms."}],"formControl":true},{"name":"RadioVariant","kind":"type","description":"Visual style of the selected radio indicator.","definition":"'solid' | 'outline'"},{"name":"RadioOrientation","kind":"type","description":"Layout direction of a radio group.","definition":"'horizontal' | 'vertical'"},{"name":"RadioLabelPosition","kind":"type","description":"Position of the label relative to the radio control.","definition":"'before' | 'after'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type RadioVariant = 'solid' | 'outline';\ntype RadioOrientation = 'horizontal' | 'vertical';\ntype RadioLabelPosition = 'before' | 'after';\n\n// Shared library types (re-exported from '@cdevhub/ngx-tw/core'):\ntype TwColor = 'primary' | 'secondary' | 'accent' | 'neutral'\n | 'info' | 'success' | 'warning' | 'error';\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"<tw-radio-group variant=\"solid\" aria-label=\"Solid variant\">\n <tw-radio value=\"a\" label=\"Option A\" />\n <tw-radio value=\"b\" label=\"Option B\" />\n <tw-radio value=\"c\" label=\"Option C\" />\n</tw-radio-group>\n\n<tw-radio-group variant=\"outline\" aria-label=\"Outline variant\">\n <tw-radio value=\"a\" label=\"Option A\" />\n <tw-radio value=\"b\" label=\"Option B\" />\n <tw-radio value=\"c\" label=\"Option C\" />\n</tw-radio-group>"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"<tw-radio-group [(value)]=\"colorValue\" orientation=\"horizontal\" aria-label=\"Colors\">\n @for (c of colors; track c) {\n <tw-radio [value]=\"c\" [color]=\"c\" [label]=\"c\" />\n }\n</tw-radio-group>"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"<tw-radio-group [(value)]=\"sizeValue\" orientation=\"horizontal\" aria-label=\"Sizes\">\n @for (s of sizes; track s) {\n <tw-radio [value]=\"s\" [size]=\"s\" [label]=\"s\" />\n }\n</tw-radio-group>"},{"id":"orientationSnippet","title":"Orientation","language":"html","code":"<tw-radio-group orientation=\"vertical\" aria-label=\"Vertical\">\n <tw-radio value=\"low\" label=\"Low\" />\n <tw-radio value=\"med\" label=\"Medium\" />\n <tw-radio value=\"high\" label=\"High\" />\n</tw-radio-group>\n\n<tw-radio-group orientation=\"horizontal\" aria-label=\"Horizontal\">\n <tw-radio value=\"low\" label=\"Low\" />\n <tw-radio value=\"med\" label=\"Medium\" />\n <tw-radio value=\"high\" label=\"High\" />\n</tw-radio-group>"},{"id":"descriptionSnippet","title":"With Description","language":"html","code":"<tw-radio-group [(value)]=\"billing\" color=\"success\" aria-label=\"Billing cycle\">\n <tw-radio value=\"monthly\" label=\"Monthly\" description=\"Billed every month\" />\n <tw-radio value=\"annual\" label=\"Annual\" description=\"Save 20% with yearly billing\" />\n <tw-radio value=\"lifetime\" label=\"Lifetime\" description=\"One-time payment, forever\" />\n</tw-radio-group>"},{"id":"labelPositionSnippet","title":"Label Position","language":"html","code":"<tw-radio-group [(value)]=\"labelPos\" aria-label=\"Label position\">\n <tw-radio value=\"after\" label=\"Label after (default)\" labelPosition=\"after\" />\n <tw-radio value=\"before\" label=\"Label before\" labelPosition=\"before\" />\n</tw-radio-group>"},{"id":"overridesSnippet","title":"Per-Radio Overrides","language":"html","code":"<tw-radio-group [(value)]=\"priority\" aria-label=\"Priority\">\n <tw-radio value=\"low\" label=\"Low priority\" />\n <tw-radio value=\"med\" label=\"Medium priority\" color=\"warning\" />\n <tw-radio value=\"high\" label=\"High priority\" color=\"error\" variant=\"outline\" />\n</tw-radio-group>"},{"id":"dotSnippet","title":"Custom Dot Glyph","language":"html","code":"<tw-radio-group [(value)]=\"dot\" color=\"accent\" size=\"lg\" aria-label=\"Custom dots\">\n <tw-radio value=\"star\" label=\"Star\" color=\"warning\">\n <svg slot=\"dot\" viewBox=\"0 0 20 20\" fill=\"currentColor\" class=\"size-full text-warning-500\" aria-hidden=\"true\">\n <path d=\"M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.966a1 1 0 00.95.69h4.175c.969 0 1.371 1.24.588 1.81l-3.376 2.454a1 1 0 00-.363 1.118l1.287 3.966c.3.922-.755 1.688-1.54 1.118l-3.376-2.454a1 1 0 00-1.175 0l-3.376 2.454c-.784.57-1.838-.196-1.539-1.118l1.287-3.966a1 1 0 00-.364-1.118L2.05 9.393c-.783-.57-.38-1.81.588-1.81h4.175a1 1 0 00.95-.69l1.286-3.966z\"/>\n </svg>\n </tw-radio>\n <!-- …more radios with custom glyphs -->\n</tw-radio-group>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Whole group disabled -->\n<tw-radio-group [value]=\"'a'\" [disabled]=\"true\" aria-label=\"Locked group\">\n <tw-radio value=\"a\" label=\"Option A\" />\n <tw-radio value=\"b\" label=\"Option B\" />\n <tw-radio value=\"c\" label=\"Option C\" />\n</tw-radio-group>\n\n<!-- Individual radio disabled — arrow keys skip it -->\n<tw-radio-group [(value)]=\"mixed\" aria-label=\"Mixed disabled\">\n <tw-radio value=\"a\" label=\"Available\" />\n <tw-radio value=\"b\" label=\"Unavailable\" [disabled]=\"true\" />\n <tw-radio value=\"c\" label=\"Available\" />\n</tw-radio-group>\n\n<!-- Required -->\n<tw-radio-group [(value)]=\"answer\" [required]=\"true\" aria-label=\"Required group\">\n <tw-radio value=\"yes\" label=\"Yes\" />\n <tw-radio value=\"no\" label=\"No\" />\n</tw-radio-group>"},{"id":"standaloneSnippet","title":"Standalone Radio","language":"html","code":"<tw-radio [(checked)]=\"confirmed\" label=\"I confirm this action\" color=\"info\" />"},{"id":"ngModelTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly tdColor = signal<string | null>('red');"},{"id":"ngModelHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-radio-group name=\"color-td\" [(ngModel)]=\"tdColor\" aria-label=\"Favorite color\">\n <tw-radio value=\"red\" label=\"Red\" />\n <tw-radio value=\"green\" label=\"Green\" />\n <tw-radio value=\"blue\" label=\"Blue\" />\n</tw-radio-group>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly shippingControl = new FormControl<string | null>('standard');"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-radio-group [formControl]=\"shippingControl\" aria-label=\"Shipping method\">\n <tw-radio value=\"standard\" label=\"Standard\" description=\"3–5 business days\" />\n <tw-radio value=\"express\" label=\"Express\" description=\"Arrives tomorrow\" />\n <tw-radio value=\"pickup\" label=\"In-store pickup\" description=\"Ready in 2 hours\" />\n</tw-radio-group>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly signalModel = signal<{ plan: string | null }>({ plan: null });\nprotected readonly signalForm = form(this.signalModel, (p) => {\n required(p.plan);\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-radio-group [formField]=\"signalForm.plan\" aria-label=\"Subscription plan\">\n <tw-radio value=\"free\" label=\"Free\" description=\"Basic features, no cost\" />\n <tw-radio value=\"pro\" label=\"Pro\" description=\"Everything in Free, plus advanced tools\" />\n <tw-radio value=\"team\" label=\"Team\" description=\"Pro features for up to 10 seats\" />\n</tw-radio-group>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-radio-group [(value)]=\"plan\" aria-label=\"Subscription plan\">\n <tw-radio value=\"free\" label=\"Free\" />\n <tw-radio value=\"pro\" label=\"Pro\" />\n <tw-radio value=\"team\" label=\"Team\" />\n</tw-radio-group>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { RadioComponent, RadioGroupComponent } from '@cdevhub/ngx-tw/radio';"}],"summary":"Single-selection control and its group container, implementing the ARIA radiogroup pattern with arrow-key navigation and roving tabindex.","whenToUse":["Exactly one choice from a small set where every option should stay visible — a plan picker, a shipping method, a payment type","A choice list that needs rich per-option content: a description line, a custom dot, or projected markup beside the label","A group laid out horizontally or vertically whose disabled state cascades to every child","Native radio keyboard semantics: arrows move focus and selection together with wrap, Home / End jump to the ends, Enter still submits the form","A one-shot standalone toggle bound with [(checked)], with no surrounding group"],"whenNotToUse":[{"instead":"checkbox","because":"each option is independent and more than one can be on at the same time"},{"instead":"select","because":"the list is long enough that showing every option would overwhelm the layout"},{"instead":"segmented-control","because":"the mutually exclusive options should read as a compact inline button group"},{"instead":"switch","because":"the choice is a binary on/off setting that takes effect immediately"}],"related":["checkbox","switch","select","segmented-control","form-field","core"],"aliases":["radio button","radiogroup","radio group","option button","single choice","exclusive choice","either/or","choice group"],"hasMeta":true,"metaPath":"projects/ngx-tw/radio/radio.meta.ts"},{"name":"select","importPath":"@cdevhub/ngx-tw/select","symbols":[{"name":"SelectComponent","kind":"component","description":"ARIA combobox with listbox popup. Supports single/multi selection, in-panel search filtering, custom option/trigger templates, and full integration with Angular forms (reactive, template-driven, signal-forms) plus `tw-form-field`.","selector":"tw-select","usage":[{"form":"element","selector":"tw-select","name":"tw-select"}],"contentSlots":[{"select":null}],"inputs":[{"name":"options","type":"readonly unknown[]","default":"[]","description":"Array of options to render in the panel. Accepts either `TwSelectOption<T>` objects or arbitrary records read via the accessor inputs. Defaults to an empty array."},{"name":"optionLabel","type":"(option: unknown) => string","default":"defaultOptionLabel","description":"Accessor returning the visible label for an option. Override when passing arbitrary objects."},{"name":"optionValue","type":"(option: unknown) => T","default":"defaultOptionValue as (option: unknown) => T","description":"Accessor returning the value for an option. The result is what `value` / `valueChange` emit."},{"name":"optionDisabled","type":"(option: unknown) => boolean","default":"defaultOptionDisabled","description":"Accessor returning the disabled state for an option. Defaults to reading `.disabled`."},{"name":"optionGroup","type":"(option: unknown) => string | undefined","default":"defaultOptionGroup","description":"Accessor returning the group name for an option. Options sharing a group render under a labelled `role=\"group\"` region."},{"name":"multiple","type":"boolean","default":"false","description":"When true, enables multi-selection. The `value` model becomes a `T[]` and the panel renders checkable options. Defaults to `false`."},{"name":"searchable","type":"boolean","default":"false","description":"When true, renders a search input at the top of the panel that filters options using `filterPredicate`. Defaults to `false`."},{"name":"filterPredicate","type":"(option: unknown, search: string) => boolean","default":"(option, search) => {\n const label = this.optionLabel()(option);\n return label.toLowerCase().includes(search.toLowerCase());\n }","description":"Custom filter function for the search input. Defaults to a case-insensitive substring match on the option label."},{"name":"placeholder","type":"string | undefined","default":"undefined","description":"Placeholder text shown in the trigger when no value is selected."},{"name":"disabledInput","type":"boolean","default":"false","description":"When true, the trigger cannot be activated and the panel cannot open. Defaults to `false`.","alias":"disabled"},{"name":"requiredInput","type":"boolean","default":"false","description":"When true, exposes `aria-required=\"true\"` on the trigger. Defaults to `false`.","alias":"required"},{"name":"size","type":"TwSize","default":"'md'","description":"Controls trigger padding, font size, and panel option density. Defaults to `'md'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color for focused trigger border, active-option background, and checkmarks. Defaults to `'primary'`."},{"name":"variant","type":"SelectVariant | undefined","default":"undefined","description":"Visual style of the trigger. When inside a `tw-form-field` and left unset, auto-resolves to `'naked'`. Otherwise defaults to `'default'`."},{"name":"panelWidth","type":"'trigger' | 'auto' | number | string","default":"'trigger'","description":"Overlay panel width. `'trigger'` matches the trigger's measured width; `'auto'` lets content decide; a number is applied as pixels; a string is passed through as a CSS length. Defaults to `'trigger'`."},{"name":"panelClass","type":"string | readonly string[]","default":"''","description":"Extra class(es) applied to the overlay panel element."},{"name":"panelMaxHeight","type":"number","default":"256","description":"Maximum height of the listbox scroll region in pixels. Defaults to `256`."},{"name":"closeOnSelect","type":"boolean | undefined","default":"undefined","description":"Whether the panel closes after a selection is made. When unset, resolves to `true` for single-select and `false` for multi-select."},{"name":"scrollStrategy","type":"'reposition' | 'close' | 'block'","default":"'reposition'","description":"CDK scroll strategy for the overlay. Defaults to `'reposition'`."},{"name":"offset","type":"number","default":"4","description":"Pixel distance between trigger and panel. Defaults to `4`."},{"name":"emptyMessage","type":"string","default":"'No results'","description":"Fallback message rendered when the filter yields no options and no `*twSelectEmpty` template is provided."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the combobox trigger.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the combobox.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external element that describes the combobox.","alias":"aria-describedby"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, the select uses the `TW_ERROR_STATE_MATCHER` token's value."},{"name":"compareWith","type":"(a: T, b: T) => boolean","default":"Object.is","description":"Equality comparator for option values. Defaults to `Object.is`."}],"outputs":[{"name":"openedChange","payloadType":"TwSelectOpenedEvent","description":"Fires when the panel's visibility finishes changing."},{"name":"selectionChange","payloadType":"TwSelectSelectionChangeEvent<T>","description":"Fires after any selection change, with `added`, `removed`, `previousValue`, and a `source` discriminator."},{"name":"searchChange","payloadType":"TwSelectSearchEvent","description":"Fires whenever the search input changes (only when `searchable` is true)."}],"models":[{"name":"value","type":"T | readonly T[] | null","default":"null","description":"Two-way bound selected value(s). Single-select: `T | null`. Multi-select: `T[]`."},{"name":"open","type":"boolean","default":"false","description":"Two-way bound open state of the panel."}],"methods":[{"name":"openPanel","signature":"openPanel(): void","description":"Opens the overlay panel. No-op when disabled or already open."},{"name":"closePanel","signature":"closePanel(): void","description":"Closes the overlay panel. No-op when already closed."},{"name":"toggle","signature":"toggle(): void","description":"Toggles the panel's open state."},{"name":"clear","signature":"clear(): void","description":"Clears the current selection and emits `selectionChange` with `source: 'reset'`."}],"formControl":true},{"name":"SelectOptionTemplateDirective","kind":"directive","description":"Structural directive projecting a custom template for each option in the panel.","selector":"[twSelectOption]","usage":[{"form":"attribute","selector":"[twSelectOption]","name":"twSelectOption"}]},{"name":"SelectTriggerTemplateDirective","kind":"directive","description":"Structural directive projecting a custom template for the select's trigger content.","selector":"[twSelectTrigger]","usage":[{"form":"attribute","selector":"[twSelectTrigger]","name":"twSelectTrigger"}]},{"name":"SelectEmptyTemplateDirective","kind":"directive","description":"Structural directive projecting a custom template for the panel's empty state.","selector":"[twSelectEmpty]","usage":[{"form":"attribute","selector":"[twSelectEmpty]","name":"twSelectEmpty"}]},{"name":"SelectHeaderTemplateDirective","kind":"directive","description":"Structural directive projecting a custom header at the top of the panel.","selector":"[twSelectHeader]","usage":[{"form":"attribute","selector":"[twSelectHeader]","name":"twSelectHeader"}]},{"name":"SelectFooterTemplateDirective","kind":"directive","description":"Structural directive projecting a custom footer at the bottom of the panel.","selector":"[twSelectFooter]","usage":[{"form":"attribute","selector":"[twSelectFooter]","name":"twSelectFooter"}]},{"name":"TwSelectOption","kind":"interface","description":"Canonical option shape. Consumers using arbitrary objects override the accessor inputs instead.","members":[{"name":"label","type":"string","optional":false,"description":"Visible label."},{"name":"value","type":"T","optional":false,"description":"Value emitted via `value` / `valueChange`."},{"name":"disabled","type":"boolean","optional":true,"description":"When true, the option cannot be focused or selected."},{"name":"group","type":"string","optional":true,"description":"Optional group name. Options sharing a group render under a labelled `role=\"group\"` region."}]},{"name":"SelectVariant","kind":"type","description":"Visual style of the select trigger.","definition":"'default' | 'naked'"},{"name":"TwSelectSelectionSource","kind":"type","description":"Origin of a selection change, used to distinguish user input from programmatic writes.","definition":"'user' | 'reset' | 'programmatic'"},{"name":"TwSelectSelectionChangeEvent","kind":"interface","description":"Emitted by `selectionChange`. Generic over the option-value type.","members":[{"name":"value","type":"T | readonly T[] | null","optional":false,"description":"The current value. `null` when single-select has no selection; `T[]` in multi-select (possibly empty)."},{"name":"previousValue","type":"T | readonly T[] | null","optional":false,"description":"The previous value, before this change."},{"name":"added","type":"readonly T[]","optional":false,"description":"Values newly added to the selection. Always empty when `source: 'reset'`."},{"name":"removed","type":"readonly T[]","optional":false,"description":"Values removed from the selection."},{"name":"source","type":"TwSelectSelectionSource","optional":false,"description":"What triggered the change."}]},{"name":"TwSelectOpenedEvent","kind":"interface","description":"Emitted by `openedChange`.","members":[{"name":"open","type":"boolean","optional":false,"description":"Whether the panel is now open."},{"name":"trigger","type":"HTMLElement","optional":false,"description":"The combobox trigger element — handy when coordinating multiple open panels."}]},{"name":"TwSelectSearchEvent","kind":"interface","description":"Emitted by `searchChange`.","members":[{"name":"search","type":"string","optional":false,"description":"The current search text passed to `filterPredicate`."},{"name":"visibleCount","type":"number","optional":false,"description":"Number of options currently visible after filtering."}]},{"name":"TwSelectOptionContext","kind":"interface","description":"Context provided to an `*twSelectOption` template.","members":[{"name":"$implicit","type":"O","optional":false,"description":"The raw option object (or arbitrary record when using accessors)."},{"name":"label","type":"string","optional":false,"description":"Resolved label from `optionLabel`."},{"name":"value","type":"T","optional":false,"description":"Resolved value from `optionValue`."},{"name":"selected","type":"boolean","optional":false,"description":"Whether this option is currently selected."},{"name":"active","type":"boolean","optional":false,"description":"Whether this option is the active-descendant for keyboard nav."},{"name":"disabled","type":"boolean","optional":false,"description":"Whether this option is disabled."},{"name":"index","type":"number","optional":false,"description":"Index within `visibleOptions()`."}]},{"name":"TwSelectTriggerContext","kind":"interface","description":"Context provided to a `*twSelectTrigger` template.","members":[{"name":"$implicit","type":"T | readonly T[] | null","optional":false,"description":"The current value."},{"name":"open","type":"boolean","optional":false,"description":"Whether the panel is open."},{"name":"empty","type":"boolean","optional":false,"description":"Whether the current value is empty."},{"name":"selectedOptions","type":"readonly O[]","optional":false,"description":"The resolved option objects for the current value."}]},{"name":"SelectRenderedRow","kind":"type","description":"Rendered row in the panel — either a group-label header or an option.","definition":"| { readonly kind: 'group-label'; readonly group: string } | { readonly kind: 'option'; readonly option: O; readonly index: number; readonly group?: string }"},{"name":"SelectVisibleOption","kind":"interface","description":"Internal: resolved option view paired with its position in `visibleOptions()`.","members":[{"name":"option","type":"O","optional":false,"description":""},{"name":"label","type":"string","optional":false,"description":""},{"name":"value","type":"T","optional":false,"description":""},{"name":"disabled","type":"boolean","optional":false,"description":""},{"name":"group","type":"string","optional":true,"description":""}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"interface TwSelectOption<T> {\n label: string;\n value: T;\n disabled?: boolean;\n group?: string;\n}\n\ntype SelectVariant = 'default' | 'naked';\ntype TwSelectSelectionSource = 'user' | 'reset' | 'programmatic';\n\ninterface TwSelectSelectionChangeEvent<T> {\n value: T | readonly T[] | null;\n previousValue: T | readonly T[] | null;\n added: readonly T[];\n removed: readonly T[];\n source: TwSelectSelectionSource;\n}\n\ninterface TwSelectOpenedEvent {\n open: boolean;\n trigger: HTMLElement;\n}\n\ninterface TwSelectSearchEvent {\n search: string;\n visibleCount: number;\n}\n\ninterface TwSelectOptionContext<T, O = TwSelectOption<T>> {\n $implicit: O;\n label: string;\n value: T;\n selected: boolean;\n active: boolean;\n disabled: boolean;\n index: number;\n}\n\ninterface TwSelectTriggerContext<T, O = TwSelectOption<T>> {\n $implicit: T | readonly T[] | null;\n open: boolean;\n empty: boolean;\n selectedOptions: readonly O[];\n}"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-select\n [options]=\"simple\"\n [variant]=\"v\"\n [(value)]=\"variantValues[v]\"\n placeholder=\"Choose a fruit\"\n [attr.aria-label]=\"'Variant ' + v\"\n />\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-select\n [options]=\"simple\"\n [color]=\"c\"\n [(value)]=\"colorValues[c]\"\n [placeholder]=\"c\"\n [attr.aria-label]=\"'Color ' + c\"\n />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-select\n [options]=\"simple\"\n [size]=\"s\"\n [(value)]=\"sizeValues[s]\"\n [placeholder]=\"'Choose a fruit (' + s + ')'\"\n [attr.aria-label]=\"'Size ' + s\"\n />\n}"},{"id":"searchableSnippet","title":"Searchable","language":"html","code":"<tw-select\n [options]=\"countries\"\n [(value)]=\"searchValue\"\n [searchable]=\"true\"\n placeholder=\"Search for a country\"\n aria-label=\"Country\"\n/>"},{"id":"multiSelectSnippet","title":"Multi-select","language":"html","code":"<tw-select\n [options]=\"tags\"\n [(value)]=\"tagValues\"\n [multiple]=\"true\"\n [searchable]=\"true\"\n placeholder=\"Add tags\"\n aria-label=\"Tags\"\n color=\"accent\"\n/>"},{"id":"groupedSnippet","title":"Grouped options","language":"html","code":"// options: each has a `group` string; one has `disabled: true`\nconst countries = [\n { label: 'United States', value: 'us', group: 'Americas' },\n { label: 'Germany', value: 'de', group: 'Europe' },\n { label: 'India', value: 'in', group: 'Asia', disabled: true },\n // …\n];\n\n<tw-select\n [options]=\"countries\"\n [(value)]=\"groupValue\"\n [searchable]=\"true\"\n placeholder=\"Select a country\"\n aria-label=\"Country\"\n/>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled with value -->\n<tw-select [options]=\"simple\" [value]=\"'apple'\" [disabled]=\"true\" aria-label=\"Disabled whole\" />\n\n<!-- Disabled without value -->\n<tw-select [options]=\"simple\" [disabled]=\"true\" placeholder=\"Not available\" aria-label=\"Disabled empty\" />\n\n<!-- Required -->\n<tw-select [options]=\"simple\" [required]=\"true\" placeholder=\"Choose a fruit\" aria-label=\"Required fruit\" />"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly fruit = signal<string | null>('apple');"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-select\n name=\"fruit\"\n [options]=\"simple\"\n [(ngModel)]=\"fruit\"\n placeholder=\"Choose a fruit\"\n aria-label=\"Fruit\"\n/>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly fruitCtrl = new FormControl<string | null>('banana');"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-select\n [options]=\"simple\"\n [formControl]=\"fruitCtrl\"\n placeholder=\"Choose a fruit\"\n aria-label=\"Fruit\"\n/>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly model = signal<{ fruit: string | null }>({ fruit: null });\nprotected readonly fruitForm = form(this.model, (p) => {\n required(p.fruit);\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-select\n [options]=\"simple\"\n [formField]=\"fruitForm.fruit\"\n placeholder=\"Choose a fruit\"\n aria-label=\"Fruit\"\n/>"},{"id":"formFieldSnippet","title":"Inside form-field (auto-naked)","language":"html","code":"<tw-form-field>\n <label twLabel>Country</label>\n <tw-select\n [options]=\"countries\"\n [(value)]=\"country\"\n [searchable]=\"true\"\n aria-label=\"Country\"\n />\n <span twHint>Pick the country where you live.</span>\n</tw-form-field>\n\n<tw-form-field appearance=\"filled\" color=\"success\">\n <label twLabel>Priority</label>\n <tw-select [options]=\"simple\" [(value)]=\"priority\" aria-label=\"Priority\" />\n</tw-form-field>"},{"id":"customOptionSnippet","title":"Custom option template","language":"html","code":"<tw-select\n [options]=\"users\"\n [optionLabel]=\"userLabel\"\n [optionValue]=\"userValue\"\n [optionDisabled]=\"userDisabled\"\n [(value)]=\"assigneeValue\"\n [searchable]=\"true\"\n [filterPredicate]=\"userFilter\"\n placeholder=\"Assign a teammate\"\n aria-label=\"Assignee\"\n>\n <ng-template twSelectOption let-u let-selected=\"selected\">\n <span class=\"avatar-chip\">{{ u.avatar }}</span>\n <span class=\"flex-1 min-w-0\">\n <span class=\"block truncate text-sm\">{{ u.name }}</span>\n <span class=\"block truncate text-xs text-fg-muted\">{{ u.email }}</span>\n </span>\n @if (selected) { <svg class=\"size-4 text-primary-600\">…</svg> }\n </ng-template>\n</tw-select>"},{"id":"customTriggerSnippet","title":"Custom trigger","language":"html","code":"<tw-select\n [options]=\"tags\"\n [(value)]=\"chipValues\"\n [multiple]=\"true\"\n [searchable]=\"true\"\n placeholder=\"Choose tags\"\n aria-label=\"Tags\"\n>\n <ng-template twSelectTrigger let-empty=\"empty\" let-selectedOptions=\"selectedOptions\">\n @if (empty) {\n <span class=\"text-fg-subtle\">Pick one or more tags…</span>\n } @else {\n <span class=\"flex flex-wrap gap-1\">\n @for (opt of selectedOptions; track opt.value) {\n <span class=\"tag-chip\">{{ opt.label }}</span>\n }\n </span>\n }\n </ng-template>\n</tw-select>"},{"id":"customEmptySnippet","title":"Custom empty state header","language":"html","code":"<tw-select [options]=\"tags\" [(value)]=\"label\" [searchable]=\"true\" aria-label=\"Labels\">\n <ng-template twSelectHeader>\n <p class=\"text-xs text-fg-muted font-medium\">Project labels</p>\n </ng-template>\n <ng-template twSelectEmpty let-search>\n <div class=\"p-4 text-center text-sm text-fg-muted\">\n No label matches <strong>\"{{ search }}\"</strong>.\n <button twButton variant=\"ghost\" color=\"primary\" size=\"xs\">Create</button>\n </div>\n </ng-template>\n</tw-select>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-select\n [options]=\"countries\"\n [(value)]=\"country\"\n placeholder=\"Select a country\"\n aria-label=\"Country\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { SelectComponent } from '@cdevhub/ngx-tw/select';"}],"summary":"Form control for choosing one or many values from a list, implementing the WAI-ARIA combobox-with-listbox-popup pattern.","whenToUse":["Choosing a value for a form field from a known set of options","Multi-select where the chosen values display as chips in the trigger","Option lists that benefit from grouping or an in-panel search box","Any of the three Angular form strategies — template-driven, reactive, or signal forms"],"whenNotToUse":[{"instead":"menu","because":"the entries run actions rather than setting a form value"},{"instead":"radio","because":"there are only a few options and all of them should stay visible"},{"instead":"combobox","because":"the user must be able to type a free-form value that is not in the list"},{"instead":"segmented-control","because":"there are two or three mutually exclusive options that belong inline"}],"related":["combobox","form-field","menu","radio","segmented-control"],"aliases":["dropdown","picker","listbox","multiselect","choice","option list"],"hasMeta":true,"metaPath":"projects/ngx-tw/select/select.meta.ts"},{"name":"input","importPath":"@cdevhub/ngx-tw/input","symbols":[{"name":"InputDirective","kind":"directive","description":"Adapts a native `<input>` or `<textarea>` into an ngx-tw form-field-compatible control. Works standalone (applies its own border and focus styling) or inside a `<tw-form-field>` (strips its chrome and provides signals the form-field uses to float the label and wire `aria-describedby`). Extension points: - Implement a completely custom control → extend FormFieldControl and provide under `TW_FORM_FIELD_CONTROL`. - Swap value storage for an existing `<input twInput>` (masked input, date parser, etc.) → provide TW_INPUT_VALUE_ACCESSOR. - Change when errors show → override TW_ERROR_STATE_MATCHER at any injector level, or pass `errorStateMatcher` per instance. Forms integration is inherited: Angular's native value accessors attach to the underlying element, so the directive works with template-driven, reactive, and signal-based forms without additional glue.","selector":"input[twInput], textarea[twInput]","usage":[{"form":"element-with-attribute","selector":"input[twInput]","name":"input"},{"form":"element-with-attribute","selector":"textarea[twInput]","name":"textarea"}],"exportAs":"twInput","inputs":[{"name":"idInput","type":"string | undefined","default":"undefined","description":"Id on the underlying element. Auto-generated as `tw-input-N` when not provided. Used by the form-field's `<label for>` attribute.","alias":"id"},{"name":"type","type":"string","default":"'text'","description":"Native HTML input `type`. Defaults to `'text'`. Dev-mode throws on unsupported values (`checkbox`, `radio`, `submit`, etc.) — use the dedicated component instead. Ignored on `<textarea>`."},{"name":"size","type":"TwSize","default":"'md'","description":"Density of a standalone field. Maps to the inline-padding scale (`px-2 py-1` xs … `px-6 py-3` xl) and font scale (`text-xs` xs, `text-sm` sm/md, `text-base` lg/xl). Ignored inside a `<tw-form-field>` — the wrapper's `size` carries density. Defaults to `'md'`."},{"name":"disabledInput","type":"boolean","default":"false","description":"Disables the control. Also reflects `ngControl.disabled` when the element is bound to a reactive form. Defaults to `false`.","alias":"disabled","transform":"booleanAttribute"},{"name":"requiredInput","type":"boolean","default":"false","description":"Marks the control as required. Also inferred from `Validators.required` on a bound `NgControl`. Defaults to `false`.","alias":"required","transform":"booleanAttribute"},{"name":"readonlyInput","type":"boolean","default":"false","description":"Makes the control read-only (native `readonly` attribute). Defaults to `false`.","alias":"readonly","transform":"booleanAttribute"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, the directive uses the `TW_ERROR_STATE_MATCHER` token's value."},{"name":"userAriaDescribedBy","type":"string | undefined","default":"undefined","description":"Consumer-supplied `aria-describedby` ids. The form-field preserves these when merging hint and error ids. Alias: `aria-describedby`.","alias":"aria-describedby"},{"name":"userAriaLabelledby","type":"string | undefined","default":"undefined","description":"Consumer-supplied `aria-labelledby` ids. The form-field preserves these when merging in the projected label id. Alias: `aria-labelledby`.","alias":"aria-labelledby"}],"methods":[{"name":"focus","signature":"focus(options?: FocusOptions): void","description":"Moves focus to the underlying element."}],"extends":"FormFieldControl"},{"name":"TW_INPUT_VALUE_ACCESSOR","kind":"token","description":"Extension point for directives that wrap an `<input twInput>` and need to own how its value is read and written. The directive's `value` property can be a plain object slot (`{ value: T }`) or a `WritableSignal<T>`. Mirrors Angular Material's `MAT_INPUT_VALUE_ACCESSOR`."}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"// Exported from ngx-tw/input:\nconst TW_INPUT_VALUE_ACCESSOR: InjectionToken<{\n value: unknown | WritableSignal<unknown>;\n}>;\n\n// Exported from ngx-tw/core (relevant to Input):\ninterface ErrorStateMatcher {\n isErrorState(\n control: AbstractControl | null,\n form: FormGroupDirective | NgForm | null,\n ): boolean;\n}\n\nconst TW_ERROR_STATE_MATCHER: InjectionToken<ErrorStateMatcher>;\n\n// Exported from ngx-tw/form-field (relevant to Input):\nabstract class FormFieldControl<T> {\n readonly id: Signal<string>;\n readonly value: Signal<T | null>;\n readonly focused: Signal<boolean>;\n readonly empty: Signal<boolean>;\n readonly disabled: Signal<boolean>;\n readonly required: Signal<boolean>;\n readonly errorState: Signal<boolean>;\n readonly controlType: string;\n focus(options?: FocusOptions): void;\n setDescribedByIds(ids: string[]): void;\n onContainerClick(event: MouseEvent): void;\n}\n\nconst TW_FORM_FIELD_CONTROL: InjectionToken<FormFieldControl<unknown>>;"},{"id":"standaloneVsFieldSnippet","title":"Standalone vs Form Field","language":"html","code":"<!-- Standalone — directive paints its own chrome -->\n<input twInput placeholder=\"Search projects…\" />\n<textarea twInput rows=\"3\" placeholder=\"Notes on this release…\"></textarea>\n\n<!-- Inside tw-form-field — directive strips its chrome; wrapper owns it -->\n<tw-form-field>\n <label twLabel>Email</label>\n <input twInput type=\"email\" />\n <span twHint>We'll never share your email.</span>\n</tw-form-field>"},{"id":"typesSnippet","title":"Input Types","language":"html","code":"<tw-form-field>\n <label twLabel>Email</label>\n <input twInput type=\"email\" placeholder=\"you@example.com\" />\n</tw-form-field>\n<tw-form-field>\n <label twLabel>Password</label>\n <input twInput type=\"password\" />\n</tw-form-field>\n<tw-form-field>\n <label twLabel>Age</label>\n <input twInput type=\"number\" min=\"0\" max=\"120\" />\n</tw-form-field>\n\n<!-- Date controls are \"never empty\" — float the label always -->\n<tw-form-field floatLabel=\"always\">\n <label twLabel>Date of birth</label>\n <input twInput type=\"date\" />\n</tw-form-field>"},{"id":"prefixSuffixSnippet","title":"Prefix Suffix","language":"html","code":"<tw-form-field>\n <label twLabel>Amount</label>\n <span twPrefix>$</span>\n <input twInput type=\"number\" />\n <span twSuffix>USD</span>\n</tw-form-field>\n\n<tw-form-field>\n <label twLabel>Website</label>\n <span twPrefix>https://</span>\n <input twInput placeholder=\"example.com\" />\n</tw-form-field>"},{"id":"textareaSnippet","title":"Textarea","language":"html","code":"<tw-form-field>\n <label twLabel>Bio</label>\n <textarea twInput rows=\"3\" maxlength=\"240\" [(ngModel)]=\"bio\" name=\"bio\"></textarea>\n <span twHint>A short blurb for your profile page.</span>\n <span twHint align=\"end\">{{ bio().length }} / 240</span>\n</tw-form-field>\n\n<!-- Standalone textarea — default chrome -->\n<textarea twInput rows=\"4\" placeholder=\"…\"></textarea>"},{"id":"sizeSnippet","title":"Size","language":"html","code":"<input twInput size=\"xs\" placeholder=\"xs\" />\n<input twInput size=\"sm\" placeholder=\"sm\" />\n<input twInput size=\"md\" placeholder=\"md (default)\" />\n<input twInput size=\"lg\" placeholder=\"lg\" />\n<input twInput size=\"xl\" placeholder=\"xl\" />\n\n<!-- Inside a form-field, the wrapper's size carries density: -->\n<tw-form-field size=\"sm\">\n <label twLabel>Compact</label>\n <input twInput />\n</tw-form-field>"},{"id":"autosizeSnippet","title":"Textarea Autosize","language":"html","code":"import { CdkTextareaAutosize } from '@angular/cdk/text-field';\n// add CdkTextareaAutosize to the component's `imports` array\n\n<tw-form-field>\n <label twLabel>Release notes</label>\n <textarea\n twInput\n cdkTextareaAutosize\n cdkAutosizeMinRows=\"2\"\n cdkAutosizeMaxRows=\"8\"\n ></textarea>\n</tw-form-field>"},{"id":"clearableSnippet","title":"Clear Button (Composition)","language":"html","code":"<tw-form-field>\n <label twLabel>Search</label>\n <input twInput type=\"search\" [formControl]=\"searchCtrl\" />\n @if (searchCtrl.value) {\n <button\n twSuffix\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"xs\"\n aria-label=\"Clear search\"\n (click)=\"searchCtrl.reset('')\"\n >\n <svg class=\"size-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\">…</svg>\n </button>\n }\n</tw-form-field>"},{"id":"disabledSnippet","title":"Disabled Read-only","language":"html","code":"<!-- Read-only: focusable and copyable, not editable -->\n<tw-form-field>\n <label twLabel>Account ID</label>\n <input twInput readonly value=\"acct_1Kj8dFZ9oX2p\" />\n <span twHint>Read-only — select to copy.</span>\n</tw-form-field>\n\n<!-- Disabled: removed from the tab order, dimmed, not submitted -->\n<tw-form-field>\n <label twLabel>Legacy slug</label>\n <input twInput [disabled]=\"true\" value=\"acme-corp-2019\" />\n</tw-form-field>"},{"id":"ngModelTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly displayName = signal('');\nprotected readonly displayNameDisabled = signal(false);"},{"id":"ngModelHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Display name</label>\n <input\n twInput\n name=\"displayName\"\n [(ngModel)]=\"displayName\"\n [disabled]=\"displayNameDisabled()\"\n required\n />\n</tw-form-field>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly usernameCtrl = new FormControl<string>('', {\n nonNullable: true,\n validators: [Validators.required, Validators.minLength(3)],\n});"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Username</label>\n <input twInput [formControl]=\"usernameCtrl\" />\n <span twHint>At least 3 characters.</span>\n @if (usernameCtrl.hasError('required')) {\n <span twError>Username is required.</span>\n }\n @if (usernameCtrl.hasError('minlength')) {\n <span twError>Too short.</span>\n }\n</tw-form-field>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly signalModel = signal({ fullName: '' });\nprotected readonly signalForm = form(this.signalModel, (p) => {\n required(p.fullName);\n minLength(p.fullName, 2);\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Full name</label>\n <input twInput [formField]=\"signalForm.fullName\" />\n <span twHint>Minimum 2 characters.</span>\n @if (signalForm.fullName().errors().length && signalForm.fullName().touched()) {\n <span twError>Full name is required (min 2 chars).</span>\n }\n</tw-form-field>\n\n<!-- Note: FieldState.reset() resets only touched/dirty by default; pass an\n explicit value if you want to clear the model too. -->\n<button (click)=\"signalForm.fullName().reset('')\">Reset</button>"},{"id":"matcherTsSnippet","title":"Custom Error-State Matcher","language":"ts","code":"import type { ErrorStateMatcher } from '@cdevhub/ngx-tw/core';\n\nconst SUBMIT_ONLY_MATCHER: ErrorStateMatcher = {\n isErrorState(control, form) {\n return !!control?.invalid && !!form?.submitted;\n },\n};\n\nprotected readonly submitOnlyCtrl = new FormControl<string>('', {\n nonNullable: true,\n validators: [Validators.required, Validators.email],\n});\nprotected readonly submitOnlyMatcher = SUBMIT_ONLY_MATCHER;"},{"id":"matcherHtmlSnippet","title":"Custom Error-State Matcher","language":"html","code":"<form (ngSubmit)=\"onSubmit()\" #f=\"ngForm\">\n <tw-form-field>\n <label twLabel>Email</label>\n <input\n twInput\n [formControl]=\"submitOnlyCtrl\"\n [errorStateMatcher]=\"submitOnlyMatcher\"\n />\n @if (submitOnlyCtrl.hasError('required')) {\n <span twError>Email is required.</span>\n }\n </tw-form-field>\n <button twButton type=\"submit\">Submit</button>\n</form>"},{"id":"accessorTsSnippet","title":"Custom Value Accessor","language":"ts","code":"@Directive({\n selector: 'input[uppercaseValue]',\n providers: [\n { provide: TW_INPUT_VALUE_ACCESSOR, useExisting: UppercaseValueDirective },\n ],\n host: {\n '(input)': '_onInput($event)',\n '[value]': 'value()',\n },\n})\nclass UppercaseValueDirective {\n readonly value: WritableSignal<string> = signal('');\n\n _onInput(event: Event): void {\n this.value.set((event.target as HTMLInputElement).value.toUpperCase());\n }\n}"},{"id":"accessorHtmlSnippet","title":"Custom Value Accessor","language":"html","code":"<tw-form-field>\n <label twLabel>Product code</label>\n <input twInput uppercaseValue />\n <span twHint>Every character is normalized to uppercase.</span>\n</tw-form-field>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-form-field>\n <label twLabel>Email</label>\n <input twInput type=\"email\" />\n <span twHint>We'll never share your email.</span>\n</tw-form-field>\n\n<!-- Standalone — no form-field wrapper -->\n<input twInput placeholder=\"Search…\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n InputDirective,\n TW_INPUT_VALUE_ACCESSOR,\n} from '@cdevhub/ngx-tw/input';"}],"summary":"Attribute directive that adapts a native single-line text field into a themed, form-field-compatible control without taking over value I/O.","whenToUse":["Any single-line text, email, password, search, url, or tel field in a form","A field that should render its own border and focus ring standalone, then strip that chrome automatically inside a <tw-form-field> wrapper","Keeping Angular native value accessors in charge so the same element works with ngModel, a reactive FormControl, or a signal-forms formField binding","A field whose error styling must follow an ErrorStateMatcher, per-instance or via the global TW_ERROR_STATE_MATCHER token","Masked or transformed text values, supplied through a custom TW_INPUT_VALUE_ACCESSOR provider"],"whenNotToUse":[{"instead":"textarea","because":"the value is multi-line and needs autosize, a resize handle, or a character counter"},{"instead":"number-input","because":"the value is numeric and must round-trip as a real number with locale-aware formatting and arrow-key stepping"},{"instead":"checkbox","because":"the value is a boolean — the directive throws in dev mode on type=\"checkbox\""},{"instead":"radio","because":"the value is one choice from a small enumerated set — the directive throws in dev mode on type=\"radio\""},{"instead":"combobox","because":"typing should filter a list of suggestions rather than capture free text alone"}],"related":["form-field","textarea","number-input","select","date-picker","core"],"aliases":["text field","textbox","text box","text input","field","entry","search box","password field","email field"],"hasMeta":true,"metaPath":"projects/ngx-tw/input/input.meta.ts"},{"name":"textarea","importPath":"@cdevhub/ngx-tw/textarea","symbols":[{"name":"TextareaDirective","kind":"directive","description":"Adapts a native `<textarea>` into an ngx-tw form-field-compatible multi-line control. Extends InputDirective — inherits its form-field integration, error-state machinery, autofill / focus tracking, ARIA wiring, and standalone styling. Adds textarea-specific behavior: autosize composition via CDK's `CdkTextareaAutosize`, a `resize` axis for the user-resize handle, `rows` / `minRows` / `maxRows`, and `maxLength` with a `valueLength` signal for character counters. Like `InputDirective`, this directive deliberately does NOT implement `ControlValueAccessor`. Angular's built-in `DefaultValueAccessor` attaches to the native `<textarea>` and handles value I/O for template-driven (`ngModel`), reactive (`FormControl` / `formControlName`), and signal-forms (`formField`) bindings — no library glue required. Form-control input-cap exception applies (codified in CLAUDE.md): ARIA + forms baseline plus textarea-specific surface exceeds the 5–6 cap; `checkbox` is the canonical 12+ exemplar.","selector":"textarea[twTextarea]","usage":[{"form":"element-with-attribute","selector":"textarea[twTextarea]","name":"textarea"}],"exportAs":"twTextarea","inputs":[{"name":"minRows","type":"unknown","description":"Re-exposed from the `CdkTextareaAutosize` host directive.","from":"CdkTextareaAutosize"},{"name":"maxRows","type":"unknown","description":"Re-exposed from the `CdkTextareaAutosize` host directive.","from":"CdkTextareaAutosize"},{"name":"size","type":"TwSize","default":"'md'","description":"Density of a standalone textarea. Maps to the inline-padding + font scale (`xs` … `xl`). Ignored inside a `<tw-form-field>` — the wrapper's `size` carries density. Defaults to `'md'`."},{"name":"autosize","type":"boolean","default":"false","description":"Grows the textarea with its content (composed from CDK's `CdkTextareaAutosize`). When `true` the user-resize handle is forced off — autosize owns the height. Defaults to `false`.","transform":"booleanAttribute"},{"name":"minRows","type":"number","default":"1","description":"Minimum number of rows the textarea collapses to when `autosize` is `true`. Ignored when autosize is off. Defaults to `1`.","transform":"numberAttribute"},{"name":"maxRows","type":"number | undefined","default":"undefined","description":"Maximum number of rows the textarea expands to before scrolling, when `autosize` is `true`. `undefined` removes the cap. Ignored when autosize is off. Defaults to `undefined`.","transform":"(v) => (v === undefined || v === null || v === '' ? undefined : numberAttribute(v))"},{"name":"rows","type":"number","default":"3","description":"Number of rows for the initial render height (native `rows` attribute). Browsers honor this even when `autosize` is `true`, so first paint uses this value. Defaults to `3`.","transform":"numberAttribute"},{"name":"resize","type":"TwTextareaResize","default":"'vertical'","description":"Controls the user-resize handle: `'none'` locks the size, `'vertical'` (default) allows vertical drag, `'both'` allows both axes. Forced to `'none'` when `autosize` is `true`. Horizontal-only is intentionally not supported."},{"name":"maxLength","type":"number | undefined","default":"undefined","description":"Maximum character count. Mirrors to the native `maxlength` attribute when defined and is exposed via the `valueLength` signal so consumers can render a \"X / N\" hint. Defaults to `undefined`.","transform":"(v) => (v === undefined || v === null || v === '' ? undefined : numberAttribute(v))"}],"methods":[{"name":"resizeToFitContent","signature":"resizeToFitContent(force = false): void","description":"Triggers a CDK autosize recalculation. Useful after programmatic value changes that bypass the native `(input)` event (e.g., clipboard write APIs). No-op when `autosize` is `false`."}],"extends":"InputDirective"},{"name":"TwTextareaResize","kind":"type","description":"How the user-resize handle behaves on the textarea. `'vertical'` (the default) lets users drag the bottom edge to grow the textarea; `'none'` locks the size; `'both'` allows free resizing on both axes (rarely useful inside a form-field — the textarea can overflow the wrapper). Horizontal is intentionally omitted — it breaks form-field layout in practice.","definition":"'none' | 'vertical' | 'both'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"// Exported from ngx-tw/textarea:\ntype TwTextareaResize = 'none' | 'vertical' | 'both';\n\nclass TextareaDirective extends InputDirective {\n readonly autosize: Signal<boolean>;\n readonly minRows: Signal<number>;\n readonly maxRows: Signal<number | undefined>;\n readonly rows: Signal<number>;\n readonly resize: Signal<TwTextareaResize>;\n readonly maxLength: Signal<number | undefined>;\n readonly valueLength: Signal<number>;\n resizeToFitContent(force?: boolean): void;\n}"},{"id":"standaloneVsFieldSnippet","title":"Standalone vs Form Field","language":"html","code":"<!-- Standalone — directive paints its own chrome -->\n<textarea twTextarea rows=\"3\" placeholder=\"Notes on this release…\"></textarea>\n\n<!-- Inside tw-form-field — directive strips its chrome; wrapper owns it -->\n<tw-form-field>\n <label twLabel>Bio</label>\n <textarea twTextarea rows=\"3\" placeholder=\"Tell us about yourself…\"></textarea>\n <span twHint>A short blurb for your profile page.</span>\n</tw-form-field>"},{"id":"sizeSnippet","title":"Size","language":"html","code":"<textarea twTextarea size=\"xs\" rows=\"2\" placeholder=\"xs\"></textarea>\n<textarea twTextarea size=\"sm\" rows=\"2\" placeholder=\"sm\"></textarea>\n<textarea twTextarea size=\"md\" rows=\"2\" placeholder=\"md (default)\"></textarea>\n<textarea twTextarea size=\"lg\" rows=\"2\" placeholder=\"lg\"></textarea>\n<textarea twTextarea size=\"xl\" rows=\"2\" placeholder=\"xl\"></textarea>"},{"id":"autosizeSnippet","title":"Autosize","language":"html","code":"<tw-form-field>\n <label twLabel>Release notes</label>\n <textarea\n twTextarea\n [autosize]=\"true\"\n [minRows]=\"2\"\n [maxRows]=\"8\"\n placeholder=\"Type to see it grow…\"\n ></textarea>\n <span twHint>Grows between 2 and 8 rows.</span>\n</tw-form-field>"},{"id":"charCountSnippet","title":"Character Count","language":"html","code":"<tw-form-field>\n <label twLabel>Bio</label>\n <textarea\n #bioTa=\"twTextarea\"\n twTextarea\n rows=\"3\"\n [maxLength]=\"240\"\n [(ngModel)]=\"bio\"\n name=\"bio\"\n ></textarea>\n <span twHint>A short blurb for your profile page.</span>\n <span twHint align=\"end\">{{ '{{' }} bioTa.valueLength() {{ '}}' }} / 240</span>\n</tw-form-field>"},{"id":"resizeSnippet","title":"Resize Axis","language":"html","code":"<textarea twTextarea resize=\"none\" rows=\"2\"></textarea>\n<textarea twTextarea resize=\"vertical\" rows=\"2\"></textarea>\n<textarea twTextarea resize=\"both\" rows=\"2\"></textarea>\n\n<!-- With autosize the user-resize handle is forced off -->\n<textarea twTextarea [autosize]=\"true\"></textarea>"},{"id":"disabledSnippet","title":"Disabled Read-only","language":"html","code":"<tw-form-field>\n <label twLabel>Commit message</label>\n <textarea twTextarea readonly rows=\"3\">feat(textarea): add multi-line input with autosize</textarea>\n <span twHint>Read-only — select to copy.</span>\n</tw-form-field>\n\n<tw-form-field>\n <label twLabel>Legacy notes</label>\n <textarea twTextarea [disabled]=\"true\" rows=\"3\">Locked after the v3 migration.</textarea>\n</tw-form-field>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly descCtrl = new FormControl<string>('', {\n nonNullable: true,\n validators: [Validators.required, Validators.maxLength(80)],\n});"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Description</label>\n <textarea twTextarea rows=\"3\" [formControl]=\"descCtrl\"></textarea>\n <span twHint>Required, max 80 characters.</span>\n @if (descCtrl.hasError('required')) {\n <span twError>Description is required.</span>\n }\n @if (descCtrl.hasError('maxlength')) {\n <span twError>Too long — keep it under 80 characters.</span>\n }\n</tw-form-field>"},{"id":"ngModelTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly comment = signal('');\nprotected readonly commentDisabled = signal(false);"},{"id":"ngModelHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Comment</label>\n <textarea\n twTextarea\n rows=\"3\"\n name=\"comment\"\n [(ngModel)]=\"comment\"\n [disabled]=\"commentDisabled()\"\n required\n ></textarea>\n</tw-form-field>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly signalModel = signal({ notes: '' });\nprotected readonly signalForm = form(this.signalModel, (p) => {\n required(p.notes);\n minLength(p.notes, 2);\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Notes</label>\n <textarea twTextarea rows=\"3\" [formField]=\"signalForm.notes\"></textarea>\n <span twHint>Minimum 2 characters.</span>\n @if (signalForm.notes().errors().length && signalForm.notes().touched()) {\n <span twError>Notes are required (min 2 chars).</span>\n }\n</tw-form-field>"},{"id":"matcherTsSnippet","title":"Custom Error-State Matcher","language":"ts","code":"import type { ErrorStateMatcher } from '@cdevhub/ngx-tw/core';\n\nconst SUBMIT_ONLY_MATCHER: ErrorStateMatcher = {\n isErrorState(control, form) {\n return !!control?.invalid && !!form?.submitted;\n },\n};\n\nprotected readonly submitOnlyCtrl = new FormControl<string>('', {\n nonNullable: true,\n validators: [Validators.required],\n});\nprotected readonly submitOnlyMatcher = SUBMIT_ONLY_MATCHER;"},{"id":"matcherHtmlSnippet","title":"Custom Error-State Matcher","language":"html","code":"<form (ngSubmit)=\"onSubmit()\" #f=\"ngForm\">\n <tw-form-field>\n <label twLabel>Feedback</label>\n <textarea\n twTextarea rows=\"3\"\n [formControl]=\"submitOnlyCtrl\"\n [errorStateMatcher]=\"submitOnlyMatcher\"\n ></textarea>\n @if (submitOnlyCtrl.hasError('required')) {\n <span twError>Feedback is required.</span>\n }\n </tw-form-field>\n <button twButton type=\"submit\">Submit</button>\n</form>"},{"id":"suffixSnippet","title":"Suffix Slot (Composition)","language":"html","code":"<tw-form-field>\n <label twLabel>Draft</label>\n <textarea\n twTextarea\n [autosize]=\"true\"\n [minRows]=\"2\"\n [maxRows]=\"6\"\n [formControl]=\"draftCtrl\"\n ></textarea>\n @if (draftCtrl.value) {\n <button twSuffix type=\"button\" twButton variant=\"ghost\" color=\"neutral\" size=\"xs\"\n aria-label=\"Clear draft\" (click)=\"draftCtrl.reset('')\">\n <svg class=\"size-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\">…</svg>\n </button>\n }\n</tw-form-field>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-form-field>\n <label twLabel>Bio</label>\n <textarea twTextarea rows=\"3\" placeholder=\"Tell us about yourself…\"></textarea>\n <span twHint>A short blurb for your profile page.</span>\n</tw-form-field>\n\n<!-- Standalone — no form-field wrapper -->\n<textarea twTextarea rows=\"4\" placeholder=\"Standalone textarea — default chrome.\"></textarea>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { TextareaDirective } from '@cdevhub/ngx-tw/textarea';"}],"summary":"Attribute directive for multi-line free text, adding autosize, a resize axis, row bounds, and a character-count signal on top of the shared text-field surface.","whenToUse":["Free-form prose fields — a bio, a comment, a description, a support message","A field that should grow with its content, using CDK autosize capped by minRows / maxRows","Letting the user drag the field taller, or locking the resize handle off entirely","A live character counter, wiring the valueLength() signal into a projected hint next to maxLength","Multi-line entry where Enter must insert a newline instead of submitting the surrounding form"],"whenNotToUse":[{"instead":"input","because":"the value is a single line and none of the autosize, resize, or counter surface is needed"},{"instead":"code-block","because":"the multi-line text is read-only content to display rather than a value to edit"}],"related":["input","form-field","core"],"aliases":["multiline","multi-line input","text area","comment box","message box","notes field","autosize","autogrow","long text"],"hasMeta":true,"metaPath":"projects/ngx-tw/textarea/textarea.meta.ts"},{"name":"file-upload","importPath":"@cdevhub/ngx-tw/file-upload","symbols":[{"name":"FileUploadComponent","kind":"component","description":"","selector":"tw-file-upload","usage":[{"form":"element","selector":"tw-file-upload","name":"tw-file-upload"}],"exportAs":"twFileUpload","contentSlots":[{"select":"[twFileUploadIcon]"},{"select":"[twFileUploadHeadline]"},{"select":"[twFileUploadDescription]"}],"inputs":[{"name":"multiple","type":"boolean","default":"false","description":"When true, the user can select more than one file. Mirrors to the hidden `<input multiple>` attribute. Defaults to `false`.","transform":"booleanAttribute"},{"name":"accept","type":"string | undefined","default":"undefined","description":"Comma-separated list of accepted file types using the native `<input accept>` syntax (e.g., `'image/*,.pdf'`). Forwarded to the hidden input and used to reject dropped files that do not match. Defaults to `undefined` (accept any)."},{"name":"maxSize","type":"number | undefined","default":"undefined","description":"Maximum size per file, in bytes. Files exceeding this are rejected with reason `'size'` and never reach `value`. Defaults to `undefined` (no limit)."},{"name":"maxFiles","type":"number | undefined","default":"undefined","description":"Maximum total file count. Drops that would push the count over this limit are rejected with reason `'count'`. Ignored when `multiple` is `false` (the limit is always 1). Defaults to `undefined` (no limit)."},{"name":"variant","type":"FileUploadVariant","default":"'outline'","description":"Visual style of the drop zone. `'outline'` renders a dashed-bordered transparent region; `'soft'` renders a filled muted background. Defaults to `'outline'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls overall density: drop-zone padding, icon scale, headline typography. Defaults to `'md'`."},{"name":"disabledInput","type":"boolean","default":"false","description":"When true, blocks file selection (button, click, drop, keyboard) and applies muted styling. Reactive forms also propagate `disabled` via `setDisabledState`. Defaults to `false`.","alias":"disabled","transform":"booleanAttribute"},{"name":"requiredInput","type":"boolean","default":"false","description":"Marks the control as required. Mirrors to `aria-required`. Also inferred from `Validators.required` on a bound `NgControl`. Defaults to `false`.","alias":"required","transform":"booleanAttribute"},{"name":"label","type":"string | undefined","default":"undefined","description":"Headline text rendered inside the drop zone (e.g., `'Drag files here'`). Projected `[twFileUploadHeadline]` content takes precedence over this input."},{"name":"description","type":"string | undefined","default":"undefined","description":"Secondary text rendered under the label (e.g., `'PDF up to 10MB'`). Projected `[twFileUploadDescription]` content takes precedence."},{"name":"triggerLabel","type":"string","default":"'Choose files'","description":"Label rendered inside the trigger button. Defaults to `'Choose files'`. Use `'Choose file'` when `multiple` is `false` (consumer responsibility)."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name applied to the host when no visible label is projected. Mirrored to `aria-label`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the control. Mirrored to `aria-labelledby`.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external element that describes the control. Form-field merges its hint/error ids alongside.","alias":"aria-describedby"},{"name":"name","type":"string | undefined","default":"undefined","description":"Optional `name` attribute on the hidden `<input type=\"file\">` for native form submissions."},{"name":"idInput","type":"string | undefined","default":"undefined","description":"Id on the host element. Auto-generated as `tw-file-upload-N` when not provided. Used by the form-field's `<label for>` attribute.","alias":"id"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, the component uses the `TW_ERROR_STATE_MATCHER` token's value."}],"outputs":[{"name":"filesAdded","payloadType":"FileUploadItem[]","description":"Fires once per add operation with the items that were accepted and appended. Does not fire for rejected files (see `fileRejected`). Does not fire on `writeValue`."},{"name":"fileRemoved","payloadType":"FileUploadItem","description":"Fires when a file is removed via the remove button or programmatic `remove()`. Does not fire on `writeValue`."},{"name":"fileRejected","payloadType":"FileUploadRejection","description":"Fires for each file that failed validation during add. Payload includes a human-readable message suitable for assistive-tech announcement."},{"name":"cleared","payloadType":"void","description":"Fires when all files are cleared at once via programmatic `clear()`. Does not fire on `writeValue`, even when the new value is empty."}],"methods":[{"name":"open","signature":"open(): void","description":"Programmatically opens the OS file picker. No-op when disabled."},{"name":"remove","signature":"remove(id: string): void","description":"Removes the item with the given id. Emits `fileRemoved`. No-op when the id is unknown or the component is disabled."},{"name":"clear","signature":"clear(): void","description":"Removes all items. Emits `cleared` and calls `onChange([])`. No-op when already empty or disabled."},{"name":"setItemProgress","signature":"setItemProgress(id: string, percent: number): void","description":"Updates the per-item progress (0–100, clamped). Does not change status. No-op when the id is unknown."},{"name":"setItemStatus","signature":"setItemStatus(id: string, status: FileUploadStatus, error?: string): void","description":"Updates the per-item status and (optionally) the error message. Announces status transitions via `LiveAnnouncer`. No-op when the id is unknown."}],"formControl":true,"extends":"FormFieldControl"},{"name":"FileUploadItemDirective","kind":"directive","description":"Structural directive that captures a template used to render each item row. Apply with `*twFileUploadItem=\"let item\"` to override the default row layout. The template context exposes the current `FileUploadItem` as both `$implicit` and `item`, so both `let-item` and `let-item=\"item\"` work.","selector":"ng-template[twFileUploadItem]","usage":[{"form":"element-with-attribute","selector":"ng-template[twFileUploadItem]","name":"ng-template"}]},{"name":"FileUploadItem","kind":"interface","description":"A single tracked entry in the file-upload's internal state.","members":[{"name":"id","type":"string","optional":false,"description":"Stable opaque id for this entry. Generated by the component on add. Use as the key when calling `setItemProgress` / `setItemStatus`."},{"name":"file","type":"File","optional":false,"description":"The underlying `File` object as provided by the browser (or by `writeValue`)."},{"name":"progress","type":"number","optional":false,"description":"Current upload progress, integer 0–100. `0` when no progress has been reported."},{"name":"status","type":"FileUploadStatus","optional":false,"description":"Current lifecycle status. Defaults to `'pending'`."},{"name":"error","type":"string","optional":true,"description":"Optional error message attached when `status === 'error'`. Displayed in the file row and announced via `LiveAnnouncer`."}]},{"name":"FileUploadStatus","kind":"type","description":"Lifecycle status of a single selected file.","definition":"'pending' | 'uploading' | 'success' | 'error'"},{"name":"FileUploadVariant","kind":"type","description":"Visual variant of the drop zone container.","definition":"'outline' | 'soft'"},{"name":"FileUploadRejection","kind":"interface","description":"Payload emitted on `fileRejected`.","members":[{"name":"file","type":"File","optional":false,"description":"The browser-supplied `File` that was rejected."},{"name":"reason","type":"FileUploadRejectionReason","optional":false,"description":"Why the file was rejected."},{"name":"message","type":"string","optional":false,"description":"Human-readable message suitable for assistive-tech announcement and inline display."}]},{"name":"FileUploadRejectionReason","kind":"type","description":"Reason a file was rejected during validation.","definition":"'accept' | 'size' | 'count'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type FileUploadStatus = 'pending' | 'uploading' | 'success' | 'error';\n\ntype FileUploadVariant = 'outline' | 'soft';\n\ntype FileUploadRejectionReason = 'accept' | 'size' | 'count';\n\ninterface FileUploadItem {\n readonly id: string;\n readonly file: File;\n readonly progress: number;\n readonly status: FileUploadStatus;\n readonly error?: string;\n}\n\ninterface FileUploadRejection {\n readonly file: File;\n readonly reason: FileUploadRejectionReason;\n readonly message: string;\n}"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-file-upload\n [variant]=\"v\"\n [label]=\"'Variant: ' + v\"\n description=\"Drop a file or click to browse.\"\n />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-file-upload\n [size]=\"s\"\n label=\"Drop a file\"\n description=\"Click to browse.\"\n />\n}"},{"id":"statesSnippet","title":"States","language":"html","code":"<tw-file-upload\n [disabled]=\"true\"\n label=\"Upload locked\"\n description=\"Sign in to upload.\"\n/>\n\n<tw-file-upload\n [required]=\"true\"\n label=\"Attachments\"\n description=\"At least one file is required.\"\n/>"},{"id":"validationSnippet","title":"Validation","language":"html","code":"<tw-file-upload\n multiple\n accept=\"image/*,.pdf\"\n [maxSize]=\"5 * 1024 * 1024\"\n [maxFiles]=\"3\"\n label=\"Attachments\"\n description=\"PDF or image, up to 5 MB each. Maximum 3 files.\"\n (fileRejected)=\"onRejected($event)\"\n/>"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly tdFiles = signal<File[] | null>(null);"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-file-upload\n name=\"attachmentsTd\"\n multiple\n [(ngModel)]=\"tdFiles\"\n label=\"Attachments\"\n/>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly reactiveCtrl = new FormControl<File[] | null>(null);"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-file-upload\n multiple\n [formControl]=\"reactiveCtrl\"\n label=\"Attachments\"\n/>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly signalModel = signal<{ files: File[] | null }>({ files: null });\nprotected readonly signalForm = form(this.signalModel, (p) => {\n required(p.files);\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-file-upload\n multiple\n [formField]=\"signalForm.files\"\n label=\"Attachments\"\n/>"},{"id":"formFieldSnippet","title":"Inside form-field","language":"html","code":"<tw-form-field>\n <label twLabel>Supporting documents</label>\n <tw-file-upload multiple accept=\".pdf,image/*\" [formControl]=\"ctrl\" />\n <span twHint>PDF or image, up to 10 MB each.</span>\n</tw-form-field>"},{"id":"customItemTsSnippet","title":"Custom item template","language":"ts","code":"// Mint a thumbnail URL per image item and revoke when the item disappears.\n// linkedSignal reconciles the URL map against upload.items() — previous URLs\n// carry forward, new image items mint a URL, removed items release theirs.\nprivate readonly upload = viewChild<FileUploadComponent>('thumbUpload');\n\nprotected readonly thumbnails = linkedSignal<\n readonly FileUploadItem[],\n ReadonlyMap<string, string>\n>({\n source: () => this.upload()?.items() ?? [],\n computation: (items, previous) => {\n const prev = previous?.value ?? new Map<string, string>();\n const next = new Map(prev);\n const keep = new Set<string>();\n for (const item of items) {\n keep.add(item.id);\n if (!next.has(item.id) && item.file.type.startsWith('image/')) {\n next.set(item.id, URL.createObjectURL(item.file));\n }\n }\n for (const [id, url] of prev) {\n if (!keep.has(id)) {\n URL.revokeObjectURL(url);\n next.delete(id);\n }\n }\n return next;\n },\n});\n\nconstructor() {\n inject(DestroyRef).onDestroy(() => {\n for (const url of this.thumbnails().values()) URL.revokeObjectURL(url);\n });\n}"},{"id":"customItemHtmlSnippet","title":"Custom item template","language":"html","code":"<tw-file-upload #u=\"twFileUpload\" multiple accept=\"image/*\"\n label=\"Add product images\">\n <ng-template twFileUploadItem let-item>\n <div class=\"flex items-center gap-3 px-3 py-2 rounded-md hover:bg-surface-muted\">\n @if (thumbnails().get(item.id); as src) {\n <img [src]=\"src\" alt=\"\" class=\"size-12 rounded-md object-cover\" />\n } @else {\n <div class=\"size-12 rounded-md bg-surface-muted flex items-center justify-center\">\n <tw-icon name=\"image\" size=\"md\" class=\"text-fg-muted\" />\n </div>\n }\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-sm font-medium truncate\">{{ '{{' }} item.file.name {{ '}}' }}</p>\n </div>\n <button twButton variant=\"ghost\" color=\"neutral\" size=\"sm\"\n (click)=\"u.remove(item.id)\">\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </ng-template>\n</tw-file-upload>"},{"id":"progressSnippet","title":"Per-item progress status","language":"ts","code":"// Wire to your real HTTP pipeline; this example simulates ticks.\nsimulateUpload(upload: FileUploadComponent): void {\n for (const item of upload.items()) {\n this.http.post('/upload', toFormData(item.file), {\n reportProgress: true,\n observe: 'events',\n }).subscribe({\n next: (event) => {\n if (event.type === HttpEventType.UploadProgress && event.total) {\n upload.setItemProgress(item.id, Math.round((event.loaded / event.total) * 100));\n } else if (event.type === HttpEventType.Response) {\n upload.setItemStatus(item.id, 'success');\n }\n },\n error: (err) => upload.setItemStatus(item.id, 'error', err.message),\n });\n }\n}"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-file-upload\n multiple\n accept=\"image/*,.pdf\"\n label=\"Drop attachments or click to browse\"\n description=\"Up to 10 MB per file.\"\n (filesAdded)=\"onAdded($event)\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { FileUploadComponent } from '@cdevhub/ngx-tw/file-upload';"}],"summary":"Drop zone and file picker that owns selection, validation, and per-file progress while leaving the actual HTTP transfer to the consumer.","whenToUse":["Attaching one or more documents or images to a form, by browsing or by dragging from the desktop","Enforcing accepted MIME types, a maximum file size, or a maximum file count before anything is sent","Showing per-file progress and success or error status while a custom upload pipeline runs","A file field that must participate in reactive, template-driven, or signal forms as a File array"],"related":["form-field","progress-bar","button"],"aliases":["dropzone","drag and drop","file picker","attachment","uploader","file input","browse files","image upload"],"hasMeta":true,"metaPath":"projects/ngx-tw/file-upload/file-upload.meta.ts"},{"name":"number-input","importPath":"@cdevhub/ngx-tw/number-input","symbols":[{"name":"NumberInputDirective","kind":"directive","description":"Turns a plain `<input twInput twNumberInput>` into a robust numeric field without `type=\"number\"` — fixing the broken-on-mobile, browser-inconsistent native control. It composes the sibling InputDirective: that directive keeps owning the form-field chrome, error-state, and `aria-invalid` / `aria-required`, while this directive owns the numeric value transport, locale-aware parse/format, spinbutton ARIA, `inputmode`, and keyboard stepping. Pair it with the companion `<tw-number-stepper [for]=\"ref\">` for visible up/down spinner buttons. Forms: implements `ControlValueAccessor`, so the value round-trips as a real `number | null` (never a string) through template-driven, reactive, and signal-based forms.","selector":"input[twNumberInput]","usage":[{"form":"element-with-attribute","selector":"input[twNumberInput]","name":"input"}],"exportAs":"twNumberInput","inputs":[{"name":"min","type":"number | undefined","default":"undefined","description":"Smallest accepted value. Clamps the committed value (on blur, Enter, and stepping) and sets `aria-valuemin`. Does not clamp per keystroke. Defaults to `undefined` (no lower bound)."},{"name":"max","type":"number | undefined","default":"undefined","description":"Largest accepted value. Clamps the committed value (on blur, Enter, and stepping) and sets `aria-valuemax`. Does not clamp per keystroke. Defaults to `undefined` (no upper bound)."},{"name":"step","type":"number","default":"1","description":"Amount added or subtracted by ArrowUp/ArrowDown and the stepper buttons. Defaults to `1`. Values `<= 0` or non-finite fall back to `1`."},{"name":"format","type":"Intl.NumberFormatOptions | undefined","default":"undefined","description":"`Intl.NumberFormat` options driving the blurred display (grouping, decimals, currency). The formatter's resolved `maximumFractionDigits` also sets commit-time rounding precision and switches `inputmode` to `'numeric'` when it is `0`. Percent style is not supported in v1. Defaults to `undefined` (locale default number formatting, grouping on)."},{"name":"locale","type":"string | undefined","default":"undefined","description":"BCP-47 locale for `Intl.NumberFormat` formatting and for locale-aware parsing (decimal and group separators). Defaults to `undefined` (the runtime default locale)."}],"outputs":[{"name":"valueChange","payloadType":"number | null","description":"Fires when the committed numeric value changes through user interaction (typing, stepping, clamping on blur/Enter). Does not fire on `writeValue` (programmatic form writes). Useful for non-form / template-ref usage alongside the `value` signal."}],"methods":[{"name":"increment","signature":"increment(): void","description":"Steps the value up: treats an empty field as `0`, adds `step`, clamps to `[min,max]`, rounds to the formatter's resolved precision, formats, and writes both the display and the model. Emits `valueChange`. No-op when the host input is disabled or readonly. Does not move focus."},{"name":"decrement","signature":"decrement(): void","description":"Steps the value down: treats an empty field as `0`, subtracts `step`, clamps to `[min,max]`, rounds, formats, writes display and model. Emits `valueChange`. No-op when disabled or readonly. Does not move focus."},{"name":"focus","signature":"focus(options?: FocusOptions): void","description":"Moves focus to the underlying input element."}],"formControl":true},{"name":"NumberStepperComponent","kind":"component","description":"The visible up/down spinner column for a NumberInputDirective. A directive cannot emit sibling DOM, so the spinner buttons live in this tiny companion. Bind it to the directive via a template ref and drop it into a `<tw-form-field>`'s `[twSuffix]` slot (or a standalone flex row): ```html <input twInput twNumberInput #qty=\"twNumberInput\" [formControl]=\"ctrl\" /> <tw-number-stepper twSuffix [for]=\"qty\" /> ``` The buttons are kept out of the tab order (`tabindex=\"-1\"`) — the spinbutton input owns value + keyboard semantics — and refocus the input after stepping.","selector":"tw-number-stepper","usage":[{"form":"element","selector":"tw-number-stepper","name":"tw-number-stepper"}],"exportAs":"twNumberStepper","contentSlots":[{"select":"[slot=up]"},{"select":"[slot=down]"}],"inputs":[{"name":"for","type":"NumberInputDirective | undefined","default":"undefined","description":"The number-input directive instance this stepper controls. Bind to a template ref, e.g. `[for]=\"qty\"` with `#qty=\"twNumberInput\"`. When omitted, the buttons render but do nothing (no-op) and disable."},{"name":"size","type":"TwSize","default":"'md'","description":"Button + glyph density. Match the field's `size` for visual alignment. Defaults to `'md'`."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"// 'format' accepts the standard library type:\ninterface Intl.NumberFormatOptions { /* style, currency, minimumFractionDigits, … */ }\n\n// The stepper's 'size' reuses the library-global axis (ngx-tw/core):\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n\n// The form value round-trips as:\ntype NumberInputValue = number | null;"},{"id":"minMaxStepSnippet","title":"Min, Max Step","language":"html","code":"<tw-form-field>\n <label twLabel>Quantity</label>\n <input twInput twNumberInput [(ngModel)]=\"quantity\" [min]=\"1\" [max]=\"10\" [step]=\"1\" />\n <span twHint>1–10, step 1</span>\n</tw-form-field>\n\n<tw-form-field>\n <label twLabel>Volume</label>\n <input twInput twNumberInput [(ngModel)]=\"volume\" [min]=\"0\" [max]=\"100\" [step]=\"5\" />\n</tw-form-field>"},{"id":"stepperSnippet","title":"Spinner Buttons","language":"html","code":"<!-- Standalone — wrap the input and stepper in a flex row -->\n<div class=\"flex items-stretch gap-1\">\n <input twInput twNumberInput #seats=\"twNumberInput\" [(ngModel)]=\"seats\" [min]=\"1\" [max]=\"8\" />\n <tw-number-stepper [for]=\"seats\" />\n</div>"},{"id":"formatSnippet","title":"Formatted Display","language":"html","code":"<!-- Currency (EUR, German locale) -->\n<input twInput twNumberInput [(ngModel)]=\"price\"\n [format]=\"{ style: 'currency', currency: 'EUR' }\" locale=\"de-DE\" [min]=\"0\" />\n\n<!-- Fixed 2 decimals + a unit suffix -->\n<tw-form-field>\n <label twLabel>Weight</label>\n <input twInput twNumberInput [(ngModel)]=\"weight\"\n [format]=\"{ minimumFractionDigits: 2, maximumFractionDigits: 2 }\" [step]=\"0.25\" />\n <span twSuffix>kg</span>\n</tw-form-field>\n\n<!-- Integer, grouped — switches inputmode to numeric -->\n<input twInput twNumberInput [(ngModel)]=\"population\"\n [format]=\"{ maximumFractionDigits: 0 }\" [step]=\"1000\" />"},{"id":"formFieldSnippet","title":"Inside a Form Field","language":"html","code":"<tw-form-field>\n <label twLabel>Tickets</label>\n <input twInput twNumberInput #tickets=\"twNumberInput\" [formControl]=\"ticketsCtrl\" [min]=\"1\" [max]=\"6\" required />\n <tw-number-stepper twSuffix [for]=\"tickets\" />\n <span twHint>Up to 6 per order.</span>\n @if (ticketsCtrl.hasError('required')) {\n <span twError>Pick at least one ticket.</span>\n }\n</tw-form-field>"},{"id":"disabledSnippet","title":"Disabled Read-only","language":"html","code":"<!-- Disabled — field and stepper both inert -->\n<div class=\"flex items-stretch gap-1\">\n <input twInput twNumberInput #n=\"twNumberInput\" [ngModel]=\"42\" disabled />\n <tw-number-stepper [for]=\"n\" />\n</div>\n\n<!-- Read-only — value selectable, edits blocked -->\n<input twInput twNumberInput [ngModel]=\"3.5\" readonly [format]=\"{ minimumFractionDigits: 1 }\" />"},{"id":"ngModelTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected ngQty: number | null = 2;"},{"id":"ngModelHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Servings</label>\n <input twInput twNumberInput #servings=\"twNumberInput\" [(ngModel)]=\"ngQty\" name=\"servings\" [min]=\"1\" [max]=\"12\" />\n <tw-number-stepper twSuffix [for]=\"servings\" />\n</tw-form-field>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly budgetCtrl = new FormControl<number | null>(null, {\n validators: [Validators.required, Validators.min(0)],\n});"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Budget</label>\n <input twInput twNumberInput #budget=\"twNumberInput\" [formControl]=\"budgetCtrl\"\n [format]=\"{ style: 'currency', currency: 'USD' }\" [min]=\"0\" [step]=\"50\" />\n <tw-number-stepper twSuffix [for]=\"budget\" />\n @if (budgetCtrl.hasError('required')) {\n <span twError>A budget is required.</span>\n }\n</tw-form-field>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly ratingModel = signal<{ rating: number | null }>({ rating: 3 });\nprotected readonly ratingForm = form(this.ratingModel);"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Rating</label>\n <input twInput twNumberInput #rating=\"twNumberInput\" [formField]=\"ratingForm.rating\" [min]=\"0\" [max]=\"5\" [step]=\"1\" />\n <tw-number-stepper twSuffix [for]=\"rating\" />\n</tw-form-field>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-form-field>\n <label twLabel>Quantity</label>\n <input twInput twNumberInput #qty=\"twNumberInput\" [(ngModel)]=\"quantity\" [min]=\"1\" [max]=\"99\" />\n <tw-number-stepper twSuffix [for]=\"qty\" />\n <span twHint>Between 1 and 99.</span>\n</tw-form-field>\n\n<!-- Standalone — no form-field, no stepper -->\n<input twInput twNumberInput [(ngModel)]=\"count\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n NumberInputDirective,\n NumberStepperComponent,\n} from '@cdevhub/ngx-tw/number-input';"}],"summary":"Numeric field that replaces the browser-inconsistent native number input with a spinbutton-patterned text field, locale-aware formatting, and optional up/down stepper buttons.","whenToUse":["Any numeric form value that must round-trip as a real number | null and never a string or NaN","Quantities, prices, percentages, and currency amounts needing Intl.NumberFormat grouping, decimals, or a currency symbol","A bounded value where min / max clamp on commit and step drives arrow keys, Home / End, and the spinner","Mobile entry that needs the right keypad — inputmode numeric for integers, decimal otherwise","Visible increment / decrement buttons, via the companion <tw-number-stepper> mounted in a form-field suffix slot"],"whenNotToUse":[{"instead":"slider","because":"the user should pick a number by dragging along a range rather than typing an exact value"},{"instead":"time-picker","because":"the number is an hour, minute, or second component of a time value"},{"instead":"input","because":"the value is text that merely looks numeric, such as a phone number, postcode, or account reference"}],"related":["input","form-field","slider","time-picker","core"],"aliases":["numeric input","number field","spinbutton","spinner","stepper","quantity","currency input","price input","increment decrement","numeric stepper"],"hasMeta":true,"metaPath":"projects/ngx-tw/number-input/number-input.meta.ts"},{"name":"tags-input","importPath":"@cdevhub/ngx-tw/tags-input","symbols":[{"name":"TagsInputComponent","kind":"component","description":"Free-text multi-value input. The user types a token and commits it via Enter, a separator key, paste, or blur; committed tokens render as dismissible chips (composing `[twBadge]`) inline with the text input. The value is exposed as a real `T[]` (default `string[]`) through `ControlValueAccessor`, so it works with template-driven, reactive, and signal forms and integrates with `<tw-form-field>` for label / hint / error chrome. The control is a single tab stop with a roving-tabindex chip strip (Material `MatChipGrid` interaction model): Arrow keys traverse chips and the input, Delete / a two-step Backspace remove chips, and `LiveAnnouncer` voices add / remove. Autocomplete suggestions are out of scope (use `tw-combobox`).","selector":"tw-tags-input","usage":[{"form":"element","selector":"tw-tags-input","name":"tw-tags-input"}],"exportAs":"twTagsInput","inputs":[{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color for the container focus-within ring and the chip accent. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls overall density: container padding, chip size, and text scale. Defaults to `'md'`."},{"name":"disabledInput","type":"boolean","default":"false","description":"When true, blocks typing, committing, and chip removal, and applies muted styling. Reactive forms also propagate disabled via `setDisabledState`. Defaults to `false`.","alias":"disabled"},{"name":"requiredInput","type":"boolean","default":"false","description":"Marks the control as required. Mirrored to the input's `aria-required`. Also inferred from `Validators.required` on a bound control. Defaults to `false`.","alias":"required"},{"name":"placeholder","type":"string | undefined","default":"undefined","description":"Placeholder shown in the text input only while there are no chips and the input is empty."},{"name":"separatorKeys","type":"readonly string[]","default":"DEFAULT_SEPARATORS","description":"Keys that commit the in-progress text as a tag. Each entry is a `KeyboardEvent.key` value (`'Enter'`) or a single separator character (`','`). Single-character separators also split pasted text. Defaults to `['Enter', ',']`."},{"name":"addOnBlur","type":"boolean","default":"false","description":"When true, blurring the control while the input holds non-empty text commits it as a tag. Defaults to `false`."},{"name":"maxTags","type":"number | undefined","default":"undefined","description":"Maximum number of tags. Once reached, further commits are blocked and announced. Does not truncate an oversized `writeValue`. Defaults to `undefined` (no limit)."},{"name":"allowDuplicates","type":"boolean","default":"false","description":"When false (default), a committed tag equal (per `compareWith`) to an existing tag is dropped silently. When true, duplicates are kept. Defaults to `false`."},{"name":"createTag","type":"TwTagFactory<T>","default":"((text: string) => text) as unknown as TwTagFactory<T>","description":"Maps committed text to a tag value. Default is identity — the trimmed string. Override to build object tags."},{"name":"tagLabel","type":"TwTagLabelFn<T>","default":"(tag: T) => String(tag)","description":"Maps a tag value to its visible chip label and the remove-button accessible name. Defaults to `String(tag)`."},{"name":"compareWith","type":"TwTagCompareFn<T>","default":"(a: T, b: T) => Object.is(a, b)","description":"Equality comparator used for dedup when `allowDuplicates` is false. Defaults to `Object.is` (reference / value identity). String tags dedupe case-sensitively by default; pass `(a, b) => a.toLowerCase() === b.toLowerCase()` for case-insensitive dedup."},{"name":"name","type":"string | undefined","default":"undefined","description":"Applied to the text input for labeling and identification only; does not submit the tag array via native (non-Angular) form posting."},{"name":"idInput","type":"string | undefined","default":"undefined","description":"Id on the host element. Auto-generated as `tw-tags-input-N` when not provided. Used by the form-field's `<label for>` association.","alias":"id"},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name applied to the control when no visible label is wired. Mirrored to `aria-label`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the control. Mirrored to `aria-labelledby`.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external element that describes the control. Form-field merges its hint / error ids alongside.","alias":"aria-describedby"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, uses the `TW_ERROR_STATE_MATCHER` token's value."}],"outputs":[{"name":"valueChange","payloadType":"T[]","description":"Fires when the tag array changes through user interaction (add, remove, clear). Emits a fresh array reference. Does not fire on `writeValue`."},{"name":"tagAdded","payloadType":"TwTagAddedEvent<T>","description":"Fires when a tag is committed via Enter, a separator key, paste, or `addTag()`. Does not fire for dropped duplicates, blocked-by-max commits, empty commits, or `writeValue`."},{"name":"tagRemoved","payloadType":"TwTagRemovedEvent<T>","description":"Fires when a tag is removed via the remove button, Backspace / Delete, or `removeTag()`. Does not fire on `writeValue` or `clear()`."}],"methods":[{"name":"addTag","signature":"addTag(text: string): boolean","description":"Commits `text` as a tag (trim → `createTag` → dedup unless `allowDuplicates` → `maxTags` check). Returns true if a tag was added, false if dropped (empty / duplicate / max). Emits `tagAdded` + `valueChange` on success. No-op when disabled."},{"name":"removeTag","signature":"removeTag(tag: T | number): void","description":"Removes a tag by value (first match via `compareWith`) or by index when a number is passed. Emits `tagRemoved` + `valueChange` and restores focus. No-op when disabled or when no match. Note: when the tag type is `number`, the argument is treated as an index."},{"name":"clear","signature":"clear(): void","description":"Removes all tags and clears the in-progress input text. Emits `valueChange` with `[]`. Does NOT emit `tagRemoved` per tag (bulk reset). No-op when already empty or disabled."},{"name":"focus","signature":"focus(): void","description":"Moves focus to the text input."}],"formControl":true,"extends":"FormFieldControl"},{"name":"TwTagAddedEvent","kind":"interface","description":"Payload of the `tagAdded` output.","members":[{"name":"tag","type":"T","optional":false,"description":"The tag that was committed."},{"name":"value","type":"T[]","optional":false,"description":"The full tag array after the addition. A fresh array reference."}]},{"name":"TwTagCompareFn","kind":"type","description":"Equality comparator used to dedupe tags when `allowDuplicates` is `false` and to resolve a tag passed to `removeTag`. Defaults to `Object.is`.","definition":"(a: T, b: T) => boolean"},{"name":"TwTagFactory","kind":"type","description":"Maps the committed input text to a tag value. The default returns the trimmed string, so the value type defaults to `string`. Override to build object tags (e.g. `(text) => ({ id: crypto.randomUUID(), name: text })`).","definition":"(text: string) => T"},{"name":"TwTagLabelFn","kind":"type","description":"Maps a tag value to its visible chip label and the remove-button accessible name. Defaults to `String(tag)`.","definition":"(tag: T) => string"},{"name":"TwTagRemovedEvent","kind":"interface","description":"Payload of the `tagRemoved` output.","members":[{"name":"tag","type":"T","optional":false,"description":"The tag that was removed."},{"name":"value","type":"T[]","optional":false,"description":"The full tag array after the removal. A fresh array reference."},{"name":"index","type":"number","optional":false,"description":"The index the removed tag occupied before removal."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TwTagFactory<T> = (text: string) => T;\n\ntype TwTagLabelFn<T> = (tag: T) => string;\n\ntype TwTagCompareFn<T> = (a: T, b: T) => boolean;\n\ninterface TwTagAddedEvent<T> {\n tag: T;\n value: T[];\n}\n\ninterface TwTagRemovedEvent<T> {\n tag: T;\n value: T[];\n index: number;\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-tags-input\n [color]=\"c\"\n [(ngModel)]=\"colorValues[c]\"\n [placeholder]=\"c + '…'\"\n [attr.aria-label]=\"'Color ' + c\"\n />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-tags-input\n [size]=\"s\"\n [(ngModel)]=\"sizeValues[s]\"\n placeholder=\"Add a tag…\"\n [attr.aria-label]=\"'Size ' + s\"\n />\n}"},{"id":"separatorsSnippet","title":"Separators paste","language":"html","code":"<!-- Default: Enter + comma -->\n<tw-tags-input [(ngModel)]=\"cities\" placeholder=\"Paste comma-separated cities…\" aria-label=\"Cities\" />\n\n<!-- Custom: Enter + space + semicolon -->\n<tw-tags-input\n [separatorKeys]=\"['Enter', ' ', ';']\"\n [(ngModel)]=\"hashtags\"\n color=\"accent\"\n placeholder=\"Type hashtags, space to commit…\"\n aria-label=\"Hashtags\"\n/>"},{"id":"limitsSnippet","title":"Limits duplicates","language":"html","code":"<!-- Cap at 5 tags -->\n<tw-tags-input [maxTags]=\"5\" [(ngModel)]=\"skills\" placeholder=\"Add up to 5 skills…\" aria-label=\"Skills\" />\n\n<!-- Keep duplicates -->\n<tw-tags-input [allowDuplicates]=\"true\" [(ngModel)]=\"tags\" aria-label=\"With duplicates\" />"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled with values -->\n<tw-tags-input [disabled]=\"true\" [(ngModel)]=\"tags\" aria-label=\"Disabled\" />\n\n<!-- Required -->\n<tw-tags-input [required]=\"true\" [(ngModel)]=\"tags\" placeholder=\"At least one tag…\" aria-label=\"Required tags\" />"},{"id":"objectTsSnippet","title":"Object tags","language":"ts","code":"interface Assignee { id: string; name: string; }\n\nprotected readonly assignees = signal<Assignee[]>([\n { id: 'alice', name: 'Alice' },\n { id: 'ben', name: 'Ben' },\n]);\n\nprotected readonly factory = (text: string): Assignee => ({\n id: text.trim().toLowerCase(),\n name: text.trim(),\n});\nprotected readonly label = (a: Assignee) => a.name;\nprotected readonly compare = (a: Assignee, b: Assignee) => a.id === b.id;"},{"id":"objectHtmlSnippet","title":"Object tags","language":"html","code":"<tw-tags-input\n [createTag]=\"factory\"\n [tagLabel]=\"label\"\n [compareWith]=\"compare\"\n [(ngModel)]=\"assignees\"\n color=\"info\"\n placeholder=\"Add an assignee…\"\n aria-label=\"Assignees\"\n/>"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly tags = signal<string[]>(['bug']);"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-tags-input\n name=\"tags\"\n [(ngModel)]=\"tags\"\n placeholder=\"Add a label…\"\n aria-label=\"Labels\"\n/>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly tagsCtrl = new FormControl<string[]>(['urgent'], { nonNullable: true });"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-tags-input [formControl]=\"tagsCtrl\" placeholder=\"Add a label…\" aria-label=\"Labels\" />"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly model = signal<{ labels: string[] }>({ labels: ['docs'] });\nprotected readonly tagsForm = form(this.model);"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-tags-input [formField]=\"tagsForm.labels\" placeholder=\"Add a label…\" aria-label=\"Labels\" />"},{"id":"formFieldSnippet","title":"Inside form-field","language":"html","code":"<tw-form-field>\n <label twLabel>Recipients</label>\n <tw-tags-input [(ngModel)]=\"recipients\" aria-label=\"Recipients\" />\n <span twHint>Press Enter or comma after each address.</span>\n</tw-form-field>\n\n<tw-form-field color=\"success\">\n <label twLabel>Tags</label>\n <tw-tags-input [formControl]=\"tagsCtrl\" aria-label=\"Tags\" />\n <span twError match=\"required\">Add at least one tag.</span>\n</tw-form-field>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-tags-input\n [(ngModel)]=\"recipients\"\n placeholder=\"Add a recipient…\"\n aria-label=\"Recipients\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { TagsInputComponent } from '@cdevhub/ngx-tw/tags-input';"}],"summary":"Multi-value text field where each typed token is committed with Enter, a separator key, or paste and renders as a dismissible chip.","whenToUse":["Email recipient fields where addresses are typed rather than picked","Free-form labels, keywords, or filter terms with no predefined option list","Pasting a comma- or newline-separated list and having it split into individual values","A form value that must round-trip as a real array through any Angular form strategy"],"whenNotToUse":[{"instead":"select","because":"the values come from a fixed list of known options rather than being typed"},{"instead":"input","because":"the field holds a single free-form string, not a collection"},{"instead":"badge","because":"the chips are read-only annotations the user cannot add to or remove"}],"related":["select","input","form-field","badge","combobox"],"aliases":["chips input","chip grid","token input","tag editor","keywords","multi value input","recipients","pills"],"hasMeta":true,"metaPath":"projects/ngx-tw/tags-input/tags-input.meta.ts"},{"name":"spinner","importPath":"@cdevhub/ngx-tw/spinner","symbols":[{"name":"SpinnerComponent","kind":"component","description":"Indeterminate loading indicator. Designed to compose inside other ngx-tw components: - Inside `<button twButton [loading]=\"true\">` — inherits the button's text color via `color=\"current\"`. - Inside `<tw-form-field>` as `twSuffix` — indicates async validation / pending load. - Inline with text using `size=\"inherit\"` — scales with the surrounding font. - Centered inside cards or overlay wrappers for region-level loading states.","selector":"tw-spinner","usage":[{"form":"element","selector":"tw-spinner","name":"tw-spinner"}],"inputs":[{"name":"variant","type":"SpinnerVariant","default":"'circular'","description":"Controls the visual style of the spinner. Defaults to `'circular'`."},{"name":"color","type":"SpinnerColor","default":"'current'","description":"Semantic color. `'current'` inherits the surrounding text color — required for composition inside buttons and form-field prefix/suffix slots. Defaults to `'current'`."},{"name":"size","type":"SpinnerSize","default":"'md'","description":"Sets the spinner dimensions. `'inherit'` sizes the spinner to `1em` so it matches the surrounding font size (useful for inline text indicators). Defaults to `'md'`."},{"name":"track","type":"boolean","default":"true","description":"When true, renders a subtle ring behind the rotating stroke. Without the track ring the spinner reads as a partial arc, not a loading indicator. Only applies to the `'circular'` variant. Defaults to `true`."},{"name":"label","type":"string","default":"'Loading'","description":"Accessible label announced by assistive technology. Rendered in a visually hidden span. Defaults to `'Loading'`."}]},{"name":"SpinnerVariant","kind":"type","description":"Visual style of the spinner.","definition":"'circular' | 'dots' | 'bars'"},{"name":"SpinnerColor","kind":"type","description":"Semantic color for the spinner. `'current'` inherits from parent `currentColor`.","definition":"TwColor | 'current'"},{"name":"SpinnerSize","kind":"type","description":"Size of the spinner. `'inherit'` sizes the spinner to `1em` so it scales with the surrounding font.","definition":"TwSize | 'inherit'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type SpinnerVariant = 'circular' | 'dots' | 'bars';\n\ntype SpinnerColor = TwColor | 'current';\n\ntype SpinnerSize = TwSize | 'inherit';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-spinner [variant]=\"v\" color=\"primary\" size=\"lg\" />\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-spinner variant=\"circular\" [color]=\"c\" size=\"md\" />\n <tw-spinner variant=\"dots\" [color]=\"c\" size=\"md\" />\n <tw-spinner variant=\"bars\" [color]=\"c\" size=\"md\" />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-spinner [size]=\"s\" color=\"primary\" />\n}"},{"id":"trackSnippet","title":"Track ring","language":"html","code":"<tw-spinner [track]=\"true\" color=\"primary\" size=\"lg\" />\n<tw-spinner [track]=\"false\" color=\"primary\" size=\"lg\" />"},{"id":"buttonSnippet","title":"Inside buttons","language":"html","code":"<button twButton color=\"primary\" [loading]=\"saving()\" (click)=\"save()\">\n @if (saving()) { <tw-spinner size=\"sm\" /> }\n Save\n</button>"},{"id":"formFieldSnippet","title":"Inside form fields (async validation)","language":"html","code":"<tw-form-field>\n <label twLabel>Username</label>\n <input twInput [formControl]=\"usernameCtrl\" />\n @if (usernameCtrl.pending) {\n <tw-spinner twSuffix size=\"sm\" label=\"Checking availability\" />\n }\n</tw-form-field>"},{"id":"inlineSnippet","title":"Inline with text","language":"html","code":"<p class=\"text-xs inline-flex items-center gap-1.5\">\n <tw-spinner size=\"inherit\" variant=\"dots\" /> Syncing…\n</p>\n<p class=\"text-lg text-success-700 inline-flex items-center gap-2.5\">\n <tw-spinner size=\"inherit\" /> Backing up files\n</p>"},{"id":"centeredSnippet","title":"Centered loading region","language":"html","code":"<div class=\"flex items-center justify-center min-h-40 rounded-lg border border-border bg-surface\">\n <div class=\"flex flex-col items-center gap-3\">\n <tw-spinner size=\"xl\" color=\"primary\" label=\"Loading report\" />\n <span class=\"text-sm text-fg-muted\">Loading report…</span>\n </div>\n</div>"},{"id":"overlaySnippet","title":"Overlay over existing content","language":"html","code":"<div class=\"relative\">\n <!-- stale content stays visible -->\n <dashboard-panel />\n\n @if (loading()) {\n <div class=\"absolute inset-0 flex items-center justify-center bg-surface/70 backdrop-blur-sm rounded-[inherit]\"\n role=\"status\" aria-live=\"polite\">\n <tw-spinner size=\"xl\" color=\"primary\" label=\"Refreshing data\" />\n </div>\n }\n</div>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-spinner />\n<tw-spinner variant=\"dots\" color=\"info\" />\n<tw-spinner variant=\"bars\" color=\"success\" />\n<tw-spinner size=\"lg\" color=\"warning\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { SpinnerComponent } from '@cdevhub/ngx-tw/spinner';"}],"summary":"Indeterminate activity indicator for work whose duration cannot be predicted, rendered as a circular arc, dots, or bars that inherit the surrounding text color.","whenToUse":["A single point in the UI is pending — a button submitting, a field validating async","The request has no measurable percentage to report","An inline indicator must scale with the text around it and adopt its color","A small region is refreshing and the surrounding layout already exists"],"whenNotToUse":[{"instead":"progress-bar","because":"the completion percentage is known and should be shown as a filling bar"},{"instead":"skeleton","because":"a whole region of content is loading and its final layout is known in advance"}],"related":["progress-bar","skeleton","button","form-field","empty-state"],"aliases":["loader","loading","busy","throbber","activity indicator","progress circle","circular progress","pending"],"hasMeta":true,"metaPath":"projects/ngx-tw/spinner/spinner.meta.ts"},{"name":"progress-bar","importPath":"@cdevhub/ngx-tw/progress-bar","symbols":[{"name":"ProgressBarComponent","kind":"component","description":"Indicates progress toward the completion of a task.","selector":"tw-progress-bar","usage":[{"form":"element","selector":"tw-progress-bar","name":"tw-progress-bar"}],"inputs":[{"name":"value","type":"number | null | undefined","default":"null","description":"Current progress value. When null or undefined, the bar renders indeterminate. Values outside `[options.min, options.max]` are clamped."},{"name":"variant","type":"ProgressBarVariant","default":"'linear'","description":"Visual style of the bar. `'linear'` renders a single fill; `'segmented'` splits the rail into discrete steps. Defaults to `'linear'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color of the filled portion. Defaults to `'primary'`. Use status colors (`success`/`warning`/`error`) to reflect task outcome."},{"name":"size","type":"ProgressBarSize","default":"'md'","description":"Bar thickness. `'sm'` = h-1, `'md'` = h-2, `'lg'` = h-3. Defaults to `'md'`."},{"name":"label","type":"string | undefined","default":"undefined","description":"Visible label rendered above the bar. When set, the bar is wired to the label via `aria-labelledby`."},{"name":"options","type":"ProgressBarOptions | undefined","default":"undefined","description":"Bundles non-visual configuration: value range (`min`/`max`/`segments`), value readout (`showValue`/`formatter`), and accessibility fallbacks (`ariaLabel`/`ariaLabelledby`). Every field is optional."}]},{"name":"ProgressBarVariant","kind":"type","description":"Visual style of the progress bar.","definition":"'linear' | 'segmented'"},{"name":"ProgressBarSize","kind":"type","description":"Size scale specific to progress bars (bar thickness).","definition":"'sm' | 'md' | 'lg'"},{"name":"ProgressBarValueFormatter","kind":"type","description":"Function signature for formatting the visible / announced progress value.","definition":"(value: number, max: number, min: number) => string"},{"name":"ProgressBarOptions","kind":"interface","description":"Non-visual configuration bundled into a single input to keep the component's public surface small. Every field is optional and falls back to a sensible default.","members":[{"name":"min","type":"number","optional":true,"description":"Lower bound of the value range. Defaults to `0`."},{"name":"max","type":"number","optional":true,"description":"Upper bound of the value range. Defaults to `100`."},{"name":"segments","type":"number","optional":true,"description":"Number of equal cells when `variant` is `'segmented'`. Ignored for `'linear'`. Defaults to `5`."},{"name":"showValue","type":"boolean","optional":true,"description":"When true, renders the formatted progress value next to the label. Defaults to `false`."},{"name":"formatter","type":"ProgressBarValueFormatter","optional":true,"description":"Custom formatter for the displayed and announced value. Defaults to an integer percentage, e.g. `'42%'`."},{"name":"ariaLabel","type":"string","optional":true,"description":"Accessible name when no visible `label` is provided. Mirrored to `aria-label` on the progressbar element."},{"name":"ariaLabelledby","type":"string","optional":true,"description":"ID of an external element that labels the progress bar. Mirrored to `aria-labelledby`."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type ProgressBarVariant = 'linear' | 'segmented';\n\ntype ProgressBarSize = 'sm' | 'md' | 'lg';\n\ntype ProgressBarValueFormatter = (value: number, max: number, min: number) => string;\n\ninterface ProgressBarOptions {\n min?: number;\n max?: number;\n segments?: number;\n showValue?: boolean;\n formatter?: ProgressBarValueFormatter;\n ariaLabel?: string;\n ariaLabelledby?: string;\n}"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"<!-- Linear (continuous fill) -->\n<tw-progress-bar label=\"Linear\" [value]=\"60\" [options]=\"{ showValue: true }\" />\n\n<!-- Segmented (discrete steps) -->\n<tw-progress-bar\n [label]=\"'Onboarding step ' + step() + ' of 4'\"\n variant=\"segmented\"\n [value]=\"step() * 25\"\n [options]=\"{ segments: 4 }\"\n/>"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-progress-bar [label]=\"c\" [color]=\"c\" [value]=\"65\" [options]=\"{ showValue: true }\" />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-progress-bar [label]=\"s\" [size]=\"s\" [value]=\"50\" [options]=\"{ showValue: true }\" />\n}"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Determinate -->\n<tw-progress-bar label=\"Rendering report\" [value]=\"42\" [options]=\"{ showValue: true }\" />\n\n<!-- Indeterminate: omit value, or pass null -->\n<tw-progress-bar label=\"Waiting for server\" />\n\n<!-- Outcome states -->\n<tw-progress-bar label=\"Backup complete\" [value]=\"100\" color=\"success\" [options]=\"{ showValue: true }\" />\n<tw-progress-bar label=\"Backup paused\" [value]=\"45\" color=\"warning\" [options]=\"{ showValue: true }\" />\n<tw-progress-bar label=\"Backup failed at 63%\" [value]=\"63\" color=\"error\" [options]=\"{ showValue: true }\" />"},{"id":"formatterTsSnippet","title":"Custom value formatter","language":"ts","code":"const TEN_MB = 10 * 1_048_576;\nprotected readonly uploaded = signal(3_355_443);\n\nprotected readonly uploadOptions: ProgressBarOptions = {\n max: TEN_MB,\n showValue: true,\n formatter: (value, max) => {\n const toMB = (b: number) => (b / 1_048_576).toFixed(1);\n return `${toMB(value)} MB / ${toMB(max)} MB`;\n },\n};"},{"id":"formatterHtmlSnippet","title":"Custom value formatter","language":"html","code":"<tw-progress-bar\n label=\"annual-report-2026.pdf\"\n [value]=\"uploaded()\"\n color=\"info\"\n [options]=\"uploadOptions\"\n/>"},{"id":"inContextSnippet","title":"In context","language":"html","code":"<ul class=\"divide-y divide-border-muted\">\n @for (file of files; track file.name) {\n <li class=\"flex items-center gap-4 px-4 py-3\">\n <svg class=\"size-4 shrink-0 text-fg-muted\">…</svg>\n <span class=\"text-sm text-fg flex-1 min-w-0 truncate\">{{ file.name }}</span>\n <tw-progress-bar\n class=\"w-40 shrink-0\"\n [value]=\"file.progress\"\n size=\"sm\"\n [options]=\"{ ariaLabel: file.name + ' upload progress' }\"\n />\n <span class=\"text-xs text-fg-muted font-mono tabular-nums w-10 text-right\">{{ file.progress }}%</span>\n </li>\n }\n</ul>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-progress-bar\n label=\"Syncing files\"\n [value]=\"42\"\n [options]=\"{ showValue: true }\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { ProgressBarComponent } from '@cdevhub/ngx-tw/progress-bar';"}],"summary":"Horizontal bar visualising measurable task completion — continuous or segmented — implementing the WAI-ARIA progressbar pattern.","whenToUse":["A file upload or download reporting bytes transferred","A multi-step wizard showing how far through the flow the user is","A quota, score, or skill level rendered as a proportion of a maximum","A discrete step count better shown as segments than a continuous fill","Long-running work with no percentage yet, using the indeterminate sweep"],"whenNotToUse":[{"instead":"spinner","because":"the wait is open-ended and there is no bar worth filling"},{"instead":"skeleton","because":"the point is to placeholder content that is being fetched, not to report completion"},{"instead":"stepper","because":"the multi-step flow needs per-step labels, status, and navigation rather than a single fill"},{"instead":"slider","because":"the user needs to set the value rather than read it"}],"related":["spinner","skeleton","stepper","slider","file-upload"],"aliases":["progress","loading bar","meter","completion","percentage","determinate","linear progress","upload progress"],"hasMeta":true,"metaPath":"projects/ngx-tw/progress-bar/progress-bar.meta.ts"},{"name":"skeleton","importPath":"@cdevhub/ngx-tw/skeleton","symbols":[{"name":"SkeletonComponent","kind":"component","description":"Placeholder loading shape (the standard \"shimmer\" / \"pulse\") shown where real content will appear. Use inside lists, cards, tables, and detail views to communicate \"this region is loading\" without layout jump when content arrives. Three shapes (`text`, `rectangle`, `circle`), three animations (`pulse`, `wave`, `none`), arbitrary `width` / `height`, and an optional multi-line text mode. By default the skeleton is hidden from assistive technology (parent owns the announcement); set `announce` to expose `role=\"status\"`.","selector":"tw-skeleton","usage":[{"form":"element","selector":"tw-skeleton","name":"tw-skeleton"}],"inputs":[{"name":"shape","type":"SkeletonShape","default":"'text'","description":"Geometric shape of the placeholder. `'text'` renders a short rectangle with text-line proportions; `'rectangle'` a free-form block sized by `width`/`height`; `'circle'` a perfect circle (avatar / icon placeholder). Defaults to `'text'`."},{"name":"animation","type":"SkeletonAnimation","default":"'pulse'","description":"Animation style. `'pulse'` fades opacity in and out; `'wave'` sweeps a shimmer across; `'none'` renders a static block. All animations are halted under `prefers-reduced-motion`. Defaults to `'pulse'`."},{"name":"width","type":"string | number | undefined","default":"undefined","description":"Optional explicit width. Numbers are treated as pixels; strings pass through (e.g. `'50%'`, `'12rem'`, `'auto'`). When undefined, the skeleton fills its container's width (or its shape default for circle)."},{"name":"height","type":"string | number | undefined","default":"undefined","description":"Optional explicit height. Numbers are treated as pixels; strings pass through. When undefined, the skeleton uses its shape default (text-line height for text, `1rem` for rectangle, equal-to-width for circle)."},{"name":"lines","type":"number","default":"1","description":"Number of stacked text rows to render. Only applies when `shape` is `'text'`. Values greater than 1 render N rows in a vertical stack with a `0.5rem` gap; the last row is rendered at 60% width to mimic a paragraph's final line. Ignored for `'rectangle'` and `'circle'`. Defaults to `1`."},{"name":"announce","type":"boolean","default":"false","description":"When true, the skeleton announces itself as a busy live region via `role=\"status\"`, `aria-busy=\"true\"`, `aria-live=\"polite\"`, and a visually-hidden label. When false, the skeleton is fully hidden from assistive technology with `aria-hidden=\"true\"` — appropriate when a parent already owns the loading announcement. Defaults to `false`.","transform":"booleanAttribute"}]},{"name":"SkeletonShape","kind":"type","description":"Geometric shape of the skeleton placeholder.","definition":"'text' | 'rectangle' | 'circle'"},{"name":"SkeletonAnimation","kind":"type","description":"Animation style applied to the skeleton.","definition":"'pulse' | 'wave' | 'none'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type SkeletonShape = 'text' | 'rectangle' | 'circle';\n\ntype SkeletonAnimation = 'pulse' | 'wave' | 'none';"},{"id":"shapesSnippet","title":"Shapes","language":"html","code":"@for (s of shapes; track s) {\n @switch (s) {\n @case ('text') { <tw-skeleton shape=\"text\" /> }\n @case ('rectangle') { <tw-skeleton shape=\"rectangle\" height=\"4rem\" /> }\n @case ('circle') { <tw-skeleton shape=\"circle\" [width]=\"56\" [height]=\"56\" /> }\n }\n}"},{"id":"animationsSnippet","title":"Animations","language":"html","code":"@for (a of animations; track a) {\n <tw-skeleton shape=\"rectangle\" [animation]=\"a\" height=\"3rem\" />\n}"},{"id":"multiLineSnippet","title":"Multi-line text","language":"html","code":"<tw-skeleton />\n<tw-skeleton [lines]=\"3\" />\n<tw-skeleton [lines]=\"5\" />"},{"id":"dimensionsSnippet","title":"Custom dimensions","language":"html","code":"<tw-skeleton shape=\"rectangle\" [width]=\"240\" [height]=\"24\" />\n<tw-skeleton shape=\"rectangle\" width=\"80%\" height=\"2rem\" />\n<tw-skeleton shape=\"rectangle\" width=\"100%\" height=\"3rem\" class=\"rounded-lg\" />"},{"id":"announceSnippet","title":"Stand-alone announcement","language":"html","code":"<tw-skeleton shape=\"rectangle\" height=\"10rem\" announce />"},{"id":"cardSnippet","title":"Card placeholder","language":"html","code":"<div class=\"rounded-lg border border-border bg-surface p-4 max-w-sm\">\n <tw-skeleton shape=\"rectangle\" height=\"9rem\" class=\"mb-4\" />\n <div class=\"flex items-center gap-3 mb-3\">\n <tw-skeleton shape=\"circle\" [width]=\"32\" [height]=\"32\" />\n <div class=\"flex-1\">\n <tw-skeleton width=\"60%\" />\n </div>\n </div>\n <tw-skeleton [lines]=\"3\" />\n</div>"},{"id":"listTsSnippet","title":"List placeholder with reload","language":"ts","code":"protected readonly loading = signal(false);\nprotected readonly placeholderRows = [0, 1, 2];\n\nprotected reload(): void {\n this.loading.set(true);\n setTimeout(() => this.loading.set(false), 1800);\n}"},{"id":"listHtmlSnippet","title":"List placeholder with reload","language":"html","code":"<ul [attr.aria-busy]=\"loading()\" aria-live=\"polite\">\n @if (loading()) {\n @for (_ of placeholderRows; track $index) {\n <li>\n <tw-skeleton shape=\"circle\" [width]=\"40\" [height]=\"40\" />\n <div>\n <tw-skeleton width=\"55%\" class=\"mb-2\" />\n <tw-skeleton [lines]=\"2\" />\n </div>\n </li>\n }\n } @else {\n @for (article of articles; track article.title) {\n <li>…real content…</li>\n }\n }\n</ul>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-skeleton shape=\"circle\" [width]=\"40\" [height]=\"40\" />\n<tw-skeleton [lines]=\"2\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { SkeletonComponent } from '@cdevhub/ngx-tw/skeleton';"}],"summary":"Pulsing placeholder shape standing in for content that has not loaded yet, sized to the real thing so the layout does not jump when data arrives.","whenToUse":["First paint of a list, table, or card grid whose row shape is known ahead of the data","Preventing layout shift while a region of the page is fetched","Mirroring a specific composition — avatar circle plus two text lines","Several regions of a page loading independently, each showing its own placeholder"],"whenNotToUse":[{"instead":"spinner","because":"a single point is pending and there is no content shape to stand in for"},{"instead":"progress-bar","because":"the load reports a measurable percentage worth showing"},{"instead":"empty-state","because":"the fetch finished and there is genuinely nothing to display"}],"related":["spinner","progress-bar","empty-state","card","table","avatar"],"aliases":["placeholder","shimmer","ghost","loading placeholder","content loader","stub","pulse","wireframe"],"hasMeta":true,"metaPath":"projects/ngx-tw/skeleton/skeleton.meta.ts"},{"name":"calendar","importPath":"@cdevhub/ngx-tw/calendar","symbols":[{"name":"CalendarComponent","kind":"component","description":"Main calendar orchestrator. Composes the header and view components (day, month, year) with view switching, navigation, focus management, and the §8 internal state model. Every date operation is delegated to an injected `DateAdapter` — call `provideNativeDateAdapter()` in your app to bootstrap the default one. Generic parameters (§7.3): - `M` — selection mode. Narrows `value` to the mode-specific shape. - `D` — adapter date type. - `TOut` — post-transformer form value type (Phase 14 wires it; until then it defaults to `CalendarValue<M, D>` so forms see raw adapter values).","selector":"tw-calendar","usage":[{"form":"element","selector":"tw-calendar","name":"tw-calendar"}],"contentSlots":[{"select":"[twCalendarPresets]"}],"inputs":[{"name":"modeInput","type":"CalendarMode","default":"'single'","description":"Selection mode (§5). Changing this at runtime clears the value and emits `modeChange`.","alias":"mode"},{"name":"valueInput","type":"CalendarValue<M, D>","default":"null as CalendarValue<M, D>","description":"Consumer-bound value. Shape narrows by `M`: `D | null` for single, `D[]` for multiple, `{ start; end }` for range.","alias":"value"},{"name":"startAt","type":"D | null","default":"null","description":"Anchor date for the displayed view. Defaults to today on mount."},{"name":"minDate","type":"D | null","default":"null","description":"Minimum selectable date."},{"name":"maxDate","type":"D | null","default":"null","description":"Maximum selectable date."},{"name":"dateFilter","type":"DateFilterFn<D> | null","default":"null","description":"Per-date predicate — return `false` to disable."},{"name":"disabledDates","type":"DisabledDates<D> | null","default":"null","description":"Explicitly disabled dates (§10.1). Either an array (each entry compared via `adapter.sameDate`) or a predicate returning `true` for disabled dates. OR-combined with `dateFilter`, `disabledDaysOfWeek`, and `[minDate, maxDate]`."},{"name":"disabledDaysOfWeek","type":"readonly number[]","default":"[]","description":"Days of the week to disable (0=Sun … 6=Sat). Empty array = no day-of-week disabling."},{"name":"constraints","type":"CalendarConstraints<D> | null","default":"null","description":"Bundle of date constraints (§10.1). Lets a consumer pass `minDate`, `maxDate`, `disabledDates`, `disabledDaysOfWeek`, and `dateFilter` as a single object — useful for sharing a constraint preset across multiple calendars. Each field is optional. Individual inputs (`minDate`, `maxDate`, `disabledDates`, `disabledDaysOfWeek`, `dateFilter`) take precedence over fields here when both are set non-null."},{"name":"minRangeLength","type":"number | null","default":"null","description":"Minimum range length in days, inclusive (`mode: 'range'` only). `null` = no minimum."},{"name":"maxRangeLength","type":"number | null","default":"null","description":"Maximum range length in days, inclusive (`mode: 'range'` only). `null` = no maximum."},{"name":"maxSelections","type":"number | null","default":"null","description":"Maximum number of selections in `mode: 'multiple'`. `null` = unlimited."},{"name":"maxSelectionBehavior","type":"MaxSelectionBehavior","default":"'emit-limit-reached'","description":"What happens when the user tries to select past `maxSelections` (§10.1). `'emit-limit-reached'` (default) emits `selectionLimitReached` and ignores the click; `'replace-oldest'` drops the first entry; `'ignore'` is silent."},{"name":"errorAriaDescribedBy","type":"string | null","default":"null","description":"IDREF list for the form-error live region (§28.3). When set, the calendar root carries `aria-describedby=\"<value>\"` so screen readers read consumer- rendered error messages alongside the focused cell announcement."},{"name":"rangeClickBehavior","type":"RangeClickBehavior","default":"'restart'","description":"How `mode: 'range'` reacts to a click after a complete range (§21.2): - `'restart'` (default): start a new range with the clicked cell, emit `selectionRestart`. - `'nearest-edge'`: move the closer endpoint (start vs end) to the clicked date and re-commit (§21.3); emits `selectionComplete({reason: 'nearest-edge'})`. - `'require-clear'`: ignore the click + flash the cell as invalid (`data-state-invalid-flash`); user must call `clear()` first."},{"name":"rangeBehavior","type":"Partial<RangeBehaviorConfig>","default":"{}","description":"Range-mode behavior knobs. Accepts a partial object — unset fields use the defaults documented on each property of `RangeBehaviorConfig`. Defaults: `{ allowSingleDayRange: true, persistPartialRange: true, allowBackwardRange: false, disableRangesCrossingDisabledDates: false }`."},{"name":"dateClass","type":"DateClassFn<D> | null","default":"null","description":"Function producing per-cell CSS classes."},{"name":"bordered","type":"boolean","default":"true","description":"When `true`, the calendar renders with a border and a soft shadow."},{"name":"startView","type":"CalendarViewState","default":"'day'","description":"Which view opens first. Defaults to `'day'`. Phase 8 will derive the default from `rangeGranularity`."},{"name":"firstDayOfWeek","type":"number | null","default":"null","description":"Override first day of week (0=Sun, 1=Mon). Falls back to the adapter's default."},{"name":"monthColumns","type":"number","default":"1","description":"Number of months to display side-by-side (1 or 2). Phase 9 replaces this with a full `numberOfMonths: 1..12+` surface (§23)."},{"name":"disabled","type":"boolean","default":"false","description":"Disabled state. Merged with any `setDisabledState` call from a bound form control via `effectiveDisabled`."},{"name":"readonly","type":"boolean","default":"false","description":"Read-only state (§33.1). When `true`, focus and keyboard navigation still work but selections cannot be committed."},{"name":"resetBehavior","type":"ResetBehavior","default":"'full'","description":"How a form reset restores internal state (§6.5). `'full'` (default) also restores view + active-date; `'value-only'` clears the draft but leaves navigation where the user left it."},{"name":"cellTemplate","type":"TemplateRef<{ $implicit: CalendarCell<D> }> | null","default":"null","description":"Optional cell-content template."},{"name":"locale","type":"string | null","default":"null","description":"Per-instance locale override. Falls back to Angular `LOCALE_ID` when `null` (§19.1)."},{"name":"intl","type":"Partial<CalendarIntl> | null","default":"null","description":"Per-instance `CalendarIntl` override (§19.4). Per-field merge — unspecified fields inherit the DI-provided default. Pass a partial bag (e.g. one of the shipped locale packs `de`, `fr`, `es`, `pt`, `ja`) or a full custom instance."}],"outputs":[{"name":"valueChange","payloadType":"CalendarValue<M, D>","description":"Fires whenever the committed value changes (cell click, keyboard select, preset, mode change, programmatic clear). Payload is the untransformed mode-shaped `CalendarValue<M, D>` — `D | null` for single, `D[]` for multiple, `{ start, end }` for range (§7.6)."},{"name":"selectionStart","payloadType":"{ start: D }","description":"Fires on the first click of a range selection (transitions selection state to SELECTING). Payload carries the chosen `start` date."},{"name":"rangePreview","payloadType":"RangePreviewEvent<D>","description":"Fires as the hover / keyboard cursor moves during range SELECTING. Payload carries the `tentativeRange` and an `invalidPreview` flag set when the range crosses a disabled date or violates `min`/`maxRangeLength`."},{"name":"selectionComplete","payloadType":"SelectionCompleteEvent<M, D>","description":"Fires after `valueChange` once the selection commits. Payload carries the committed `value` and a `reason` flagging the commit path (`'commit' | 'auto-swap' | 'nearest-edge' | 'preset'`)."},{"name":"selectionRestart","payloadType":"{ start: D }","description":"Fires when a range selection restarts after a complete range (e.g. third click with `rangeClickBehavior='restart'`). Payload carries the new draft `start` date."},{"name":"selectionCleared","payloadType":"SelectionClearedEvent","description":"Fires whenever the value clears. Payload carries a `reason` distinguishing user clear, programmatic, mode change, form reset, or a disabled flip."},{"name":"selectionLimitReached","payloadType":"{ limit: number; attempted: D }","description":"Fires in `mode: 'multiple'` when the user attempts to select past `maxSelections` (only with `maxSelectionBehavior: 'emit-limit-reached'`). Payload carries the configured `limit` and the rejected `attempted` date."},{"name":"presetChange","payloadType":"string | null","description":"Fires when the selected preset changes (user click on a preset chip, or programmatic clear). Payload is the new preset id, or `null` when no preset is active. Phase 12 wires it."},{"name":"viewChange","payloadType":"ViewChangeEvent","description":"Fires on every view transition (day ↔ month ↔ year). Payload carries `from`, `to`, and a `reason` distinguishing drill-down, drill-up, user header click, and programmatic changes."},{"name":"activeDateChange","payloadType":"D","description":"Fires when keyboard navigation, mouse hover, or programmatic action moves the focused (active) cell. Payload is the new active date."},{"name":"monthChange","payloadType":"{ year: number; month: number }","description":"Fires when the displayed primary month changes (page-nav, drill-up, or programmatic). Payload carries the new `year` and zero-based `month`."},{"name":"yearChange","payloadType":"{ year: number }","description":"Fires when the displayed year changes (month/year-view nav or year-page scroll). Payload carries the new `year`."},{"name":"cellClick","payloadType":"{ date: D; event: PointerEvent }","description":"Fires on every pointer click of any cell, including disabled ones — analytics only. Payload carries the clicked `date` and the underlying `PointerEvent`. Does NOT indicate a selection; subscribe to `valueChange` for that."},{"name":"cellHover","payloadType":"{ date: D }","description":"Fires on every pointer hover of any cell — analytics only. Payload carries the hovered `date`. Does NOT indicate a preview; subscribe to `rangePreview` for range-mode hover state."},{"name":"modeChange","payloadType":"ModeChangeEvent","description":"Fires when `mode` changes at runtime, in canonical order `selectionCleared` → `modeChange` → `valueChange` (§11.2). Payload carries `{ from, to }` modes."}],"methods":[{"name":"focusDate","signature":"focusDate(date: D, opts?: { navigate?: boolean }): void","description":"Focuses `date` and optionally navigates the view to render it."},{"name":"setView","signature":"setView(view: CalendarViewState): void","description":"Sets the current view state and emits a `viewChange` event."},{"name":"goToDate","signature":"goToDate(date: D): void","description":"Navigates to a specific date (anchor). Does not emit a selection event."},{"name":"goToToday","signature":"goToToday(): void","description":"Navigates to today."},{"name":"clear","signature":"clear(): void","description":"Clears the current selection. Alias: `clearSelection`."},{"name":"clearSelection","signature":"clearSelection(): void","description":"Alias of `clear()` exported per §33.4 for discoverability."},{"name":"reset","signature":"reset(): void","description":"Resets to initial state — value + view + active date per `resetBehavior`. Phase 3 extends this with form-reset integration."},{"name":"revalidate","signature":"revalidate(): void","description":"Re-runs validation against the current constraints and cell filters. Call this after mutating a constraint imperatively (rather than through an input binding), so the bound control's errors reflect the new rules."},{"name":"open","signature":"open(): void","description":"Opens the overlay."},{"name":"close","signature":"close(): void","description":"Closes the overlay."},{"name":"toggle","signature":"toggle(): void","description":"Toggles the overlay."},{"name":"focusActiveCell","signature":"focusActiveCell(): void","description":"Imperatively focuses the currently active cell."}],"formControl":true},{"name":"CalendarSingleDirective","kind":"directive","description":"Signal Forms strict binding for `CalendarComponent` in `single` mode. Implements `FormValueControl<D | null>` so `[field]=\"form.someDate\"` infers `FieldTree<D | null>` with no casts. Applies automatically when `<tw-calendar mode=\"single\">` appears in the template. When the mode attribute is omitted, consumers bind the component directly — the CVA path handles reactive / template-driven forms.","selector":"tw-calendar[mode=\"single\"]","usage":[{"form":"element-with-attribute","selector":"tw-calendar[mode=\"single\"]","name":"tw-calendar"}],"exportAs":"twCalendarSingle","inputs":[{"name":"disabled","type":"boolean","default":"false","description":"Mirrors the bound field's disabled flag."},{"name":"readonly","type":"boolean","default":"false","description":"Mirrors the bound field's readonly flag."},{"name":"required","type":"boolean","default":"false","description":"Mirrors the bound field's required flag."},{"name":"invalid","type":"boolean","default":"false","description":"Mirrors the bound field's invalid flag."},{"name":"hidden","type":"boolean","default":"false","description":"Mirrors the bound field's hidden flag."},{"name":"errors","type":"readonly ValidationError.WithOptionalFieldTree[]","default":"[]","description":"Mirrors the bound field's errors."},{"name":"disabledReasons","type":"readonly WithOptionalFieldTree<DisabledReason>[]","default":"[]","description":"Mirrors the bound field's disablement reasons."}],"models":[{"name":"touched","type":"boolean","default":"false","description":"Two-way `touched` flag — drives and reflects the field's touched state."}]},{"name":"CalendarMultipleDirective","kind":"directive","description":"Signal Forms strict binding for `CalendarComponent` in `multiple` mode. Implements `FormValueControl<D[]>`.","selector":"tw-calendar[mode=\"multiple\"]","usage":[{"form":"element-with-attribute","selector":"tw-calendar[mode=\"multiple\"]","name":"tw-calendar"}],"exportAs":"twCalendarMultiple","inputs":[{"name":"disabled","type":"boolean","default":"false","description":"Mirrors the bound field's disabled flag."},{"name":"readonly","type":"boolean","default":"false","description":"Mirrors the bound field's readonly flag."},{"name":"required","type":"boolean","default":"false","description":"Mirrors the bound field's required flag."},{"name":"invalid","type":"boolean","default":"false","description":"Mirrors the bound field's invalid flag."},{"name":"hidden","type":"boolean","default":"false","description":"Mirrors the bound field's hidden flag."},{"name":"errors","type":"readonly ValidationError.WithOptionalFieldTree[]","default":"[]","description":"Mirrors the bound field's errors."},{"name":"disabledReasons","type":"readonly WithOptionalFieldTree<DisabledReason>[]","default":"[]","description":"Mirrors the bound field's disablement reasons."}],"models":[{"name":"touched","type":"boolean","default":"false","description":"Two-way `touched` flag — drives and reflects the field's touched state."}]},{"name":"CalendarRangeDirective","kind":"directive","description":"Signal Forms strict binding for `CalendarComponent` in `range` mode. Implements `FormValueControl<{ start: D | null; end: D | null }>`.","selector":"tw-calendar[mode=\"range\"]","usage":[{"form":"element-with-attribute","selector":"tw-calendar[mode=\"range\"]","name":"tw-calendar"}],"exportAs":"twCalendarRange","inputs":[{"name":"disabled","type":"boolean","default":"false","description":"Mirrors the bound field's disabled flag."},{"name":"readonly","type":"boolean","default":"false","description":"Mirrors the bound field's readonly flag."},{"name":"required","type":"boolean","default":"false","description":"Mirrors the bound field's required flag."},{"name":"invalid","type":"boolean","default":"false","description":"Mirrors the bound field's invalid flag."},{"name":"hidden","type":"boolean","default":"false","description":"Mirrors the bound field's hidden flag."},{"name":"errors","type":"readonly ValidationError.WithOptionalFieldTree[]","default":"[]","description":"Mirrors the bound field's errors."},{"name":"disabledReasons","type":"readonly WithOptionalFieldTree<DisabledReason>[]","default":"[]","description":"Mirrors the bound field's disablement reasons."}],"models":[{"name":"touched","type":"boolean","default":"false","description":"Two-way `touched` flag — drives and reflects the field's touched state."}]},{"name":"calendarValidator","kind":"function","description":"Built-in synchronous validator (§6.1, §10.2). Emits the full v1 error code set: Phase 3 codes: - `calendarRequired` when the hosting control carries `Validators.required` and the calendar value is mode-specific empty. - `calendarInvalidValue` when the most recent `writeValue` was rejected as wrong-shape (§7.2) — the raw value is held on the component's `lastInvalidFormValue` until a valid write clears it. Phase 4 codes (active when `ctx.constraints` + `ctx.adapter` are supplied): - `calendarMinDate` / `calendarMaxDate` — value (or any range endpoint / array entry) falls outside `[minDate, maxDate]`. - `calendarDisabledDate` — value matches `dateFilter` / `disabledDates` / `disabledDaysOfWeek` (constraint resolver). - `calendarRangeTooShort` / `calendarRangeTooLong` — `mode: 'range'` length violates `minRangeLength` / `maxRangeLength`. - `calendarMaxSelections` — `mode: 'multiple'` array exceeds `maxSelections`. - `calendarInvalidRange` — committed range has `start > end` (the orchestrator normalizes via auto-swap; this only triggers on programmatic writeValue). Each code's payload mirrors §10.2 — `{ min, actual }`, `{ max, actual }`, `{ length, min }`, etc.","signature":"calendarValidator(ctx: CalendarValidatorContext<M, D>): ValidatorFn"},{"name":"calendarRequiredValidator","kind":"function","description":"Standalone required-validator helper for consumers who want to compose calendar-required on a control that is **not** bound directly to a `CalendarComponent`. The rule is identical: mode-specific empty ⇒ error.","signature":"calendarRequiredValidator(mode: M): ValidatorFn"},{"name":"isCalendarValueEmpty","kind":"function","description":"`true` when `value` matches the mode-specific empty state (§7.1, §6.4).","signature":"isCalendarValueEmpty(mode: M, value: CalendarValue<M, D> | null | undefined): boolean"},{"name":"CalendarValidatorContext","kind":"interface","description":"Shape of the read-only context the calendar validator needs from the hosting component. Kept as a structural type so tests and custom consumers can synthesize one without subclassing `CalendarComponent`.","members":[{"name":"mode","type":"M","optional":false,"description":"Current mode — used to decide which shape counts as \"empty\"."},{"name":"lastInvalidFormValue","type":"unknown","optional":false,"description":"The raw form value whose last write was rejected as wrong-shape (§7.2). `null` when no rejected write is outstanding."},{"name":"constraints","type":"CalendarConstraints<D>","optional":true,"description":"Aggregated constraint inputs (§10.1). Optional — when omitted, only `calendarRequired` / `calendarInvalidValue` codes are produced."},{"name":"adapter","type":"DateAdapter<D>","optional":true,"description":"Adapter used to evaluate constraint codes. Required when `constraints` is set."},{"name":"minRangeLength","type":"number | null","optional":true,"description":"Minimum range length, days. `mode: 'range'` only."},{"name":"maxRangeLength","type":"number | null","optional":true,"description":"Maximum range length, days. `mode: 'range'` only."},{"name":"maxSelections","type":"number | null","optional":true,"description":"Maximum number of selections. `mode: 'multiple'` only."}]},{"name":"CalendarPresetsDirective","kind":"directive","description":"Marker directive for a slot positioned above the calendar grid. Used to style an external preset rail; the calendar renders projected content with `<ng-content select=\"[twCalendarPresets]\">`.","selector":"[twCalendarPresets]","usage":[{"form":"attribute","selector":"[twCalendarPresets]","name":"twCalendarPresets"}]},{"name":"CalendarHeaderComponent","kind":"component","description":"Calendar navigation header — previous / period / next button row. The period label button toggles between views (month → year → multi-year).","selector":"tw-calendar-header","usage":[{"form":"element","selector":"tw-calendar-header","name":"tw-calendar-header"}],"inputs":[{"name":"periodLabel","type":"string","required":true,"description":"Display text for the period button (e.g. \"January 2026\")."},{"name":"periodAriaLabel","type":"string","required":true,"description":"Accessible label for the period button."},{"name":"prevAriaLabel","type":"string","required":true,"description":"Accessible label for the previous button."},{"name":"nextAriaLabel","type":"string","required":true,"description":"Accessible label for the next button."},{"name":"prevDisabled","type":"boolean","default":"false","description":"Disables the previous button."},{"name":"nextDisabled","type":"boolean","default":"false","description":"Disables the next button."},{"name":"canSwitchView","type":"boolean","default":"true","description":"When false, the period button is disabled (e.g. when already at the top view)."}],"outputs":[{"name":"prevClicked","payloadType":"void","description":"Fires on previous button click."},{"name":"nextClicked","payloadType":"void","description":"Fires on next button click."},{"name":"periodClicked","payloadType":"void","description":"Fires on period button click (switch to the next higher view)."}]},{"name":"CalendarIntl","kind":"service","description":"Localized strings + ARIA announcements consumed by the calendar (§19.4). Override per-field via Angular DI — provide a custom instance, or pass a `Partial<CalendarIntl>` into the calendar component's `intl` input. Unset fields fall through to the English defaults shipped here. All members take primitives so consumers can write straight string literals or template helpers without depending on internal types.","methods":[{"name":"previousYearsLabel","signature":"previousYearsLabel(yearsPerPage: number): string","description":"Aria label for the \"previous N years\" navigation button (year view)."},{"name":"nextYearsLabel","signature":"nextYearsLabel(yearsPerPage: number): string","description":"Aria label for the \"next N years\" navigation button (year view)."},{"name":"switchToMonthViewLabel","signature":"switchToMonthViewLabel(period: string): string","description":"Tooltip / aria text for the \"switch to month view\" affordance."},{"name":"switchToYearViewLabel","signature":"switchToYearViewLabel(period: string): string","description":"Tooltip / aria text for the \"switch to year view\" affordance."},{"name":"cellAccessibleName","signature":"cellAccessibleName(ctx: CalendarCellAccessibleNameContext): string","description":"Returns the accessible name a screen reader announces for any cell. Default implementation appends \"today\" / \"selected\" / \"disabled\" suffixes to the supplied `display` string."},{"name":"navigatedTo","signature":"navigatedTo(direction: 'previous' | 'next', period: string): string","description":"Live-region message after navigating the displayed period."},{"name":"selectedAnnouncement","signature":"selectedAnnouncement(value: string): string","description":"Live-region message after a single-mode selection commits."},{"name":"rangeStartAnnouncement","signature":"rangeStartAnnouncement(start: string): string","description":"Live-region message after the start of a range is picked."},{"name":"rangeUpdateAnnouncement","signature":"rangeUpdateAnnouncement(start: string, end: string, lengthDays: number): string","description":"Live-region message after a range commits. Plural-marked on `lengthDays`."},{"name":"multipleSelectionAnnouncement","signature":"multipleSelectionAnnouncement(count: number): string","description":"Live-region message after a `mode: 'multiple'` selection commits. Plural-marked."},{"name":"viewSwitched","signature":"viewSwitched(view: 'day' | 'month' | 'year', period: string): string","description":"Live-region message after the active view changes."},{"name":"inProgressTemplate","signature":"inProgressTemplate(state: string): string","description":"Live-region template used while a range pick is in progress."},{"name":"skippedPeriods","signature":"skippedPeriods(count: number, destination: string): string","description":"Live-region message announcing how many empty periods auto-skip jumped over. Plural-marked."},{"name":"minDateError","signature":"minDateError(date: string): string","description":"Message for the `calendarMinDate` validation error."},{"name":"maxDateError","signature":"maxDateError(date: string): string","description":"Message for the `calendarMaxDate` validation error."},{"name":"rangeTooShortError","signature":"rangeTooShortError(min: number): string","description":"Message for the `calendarRangeTooShort` validation error. Plural-marked on `min`."},{"name":"rangeTooLongError","signature":"rangeTooLongError(max: number): string","description":"Message for the `calendarRangeTooLong` validation error. Plural-marked on `max`."},{"name":"maxSelectionsError","signature":"maxSelectionsError(limit: number): string","description":"Message for the `calendarMaxSelections` validation error."}]},{"name":"provideCalendarIntl","kind":"function","description":"Provides a custom `CalendarIntl` instance. Place at any injector level — the calendar resolves the closest one and merges any per-instance `intl` input on top of it (§19.4 per-field merge semantics).","signature":"provideCalendarIntl(custom: Partial<CalendarIntl>): Provider"},{"name":"CalendarCellAccessibleNameContext","kind":"interface","description":"Context payload passed to `cellAccessibleName` so consumers can produce a locale-appropriate accessible name for any cell in any view (§15.6, §19.4).","members":[{"name":"date","type":"Date","optional":false,"description":"The date the cell represents."},{"name":"mode","type":"'day' | 'month' | 'year'","optional":false,"description":"Which view the cell lives in."},{"name":"selected","type":"boolean","optional":true,"description":"True when the cell is part of the committed selection."},{"name":"today","type":"boolean","optional":true,"description":"True when the cell is today."},{"name":"disabled","type":"boolean","optional":true,"description":"True when the cell is non-interactive (constraint or filter)."},{"name":"display","type":"string","optional":true,"description":"Optional pre-formatted display string the consumer may incorporate."}]},{"name":"de","kind":"const","description":"German (de) locale pack for the calendar's user-visible strings. Pass into the `intl` input or via provideCalendarIntl. Per-field merge semantics (§19.4) — unspecified fields fall back to English defaults.","type":"Partial<CalendarIntl>"},{"name":"fr","kind":"const","description":"French (fr) locale pack for the calendar's user-visible strings. Pass into the `intl` input or via provideCalendarIntl. Per-field merge semantics (§19.4) — unspecified fields fall back to English defaults.","type":"Partial<CalendarIntl>"},{"name":"es","kind":"const","description":"Spanish (es) locale pack for the calendar's user-visible strings. Pass into the `intl` input or via provideCalendarIntl. Per-field merge semantics (§19.4) — unspecified fields fall back to English defaults.","type":"Partial<CalendarIntl>"},{"name":"pt","kind":"const","description":"Portuguese (pt) locale pack for the calendar's user-visible strings. Pass into the `intl` input or via provideCalendarIntl. Per-field merge semantics (§19.4) — unspecified fields fall back to English defaults.","type":"Partial<CalendarIntl>"},{"name":"ja","kind":"const","description":"Japanese (ja) locale pack for the calendar's user-visible strings. Pass into the `intl` input or via provideCalendarIntl. Per-field merge semantics (§19.4) — unspecified fields fall back to English defaults. Note: Japanese has no grammatical plural form, so plural-marked methods collapse to a single form.","type":"Partial<CalendarIntl>"},{"name":"CalendarCellComponent","kind":"component","description":"Renders a single cell in any calendar view. The cell is a stateless presentation component — selection / preview / today state is all derived from the `CalendarCell<D>` data object. It emits events for clicks, keyboard navigation, mouse hover, and focus.","selector":"tw-calendar-cell","usage":[{"form":"element","selector":"tw-calendar-cell","name":"tw-calendar-cell"}],"inputs":[{"name":"cell","type":"CalendarCell<D>","required":true,"description":"The cell data object to render."},{"name":"view","type":"CalendarViewState","default":"'day'","description":"The active calendar view — drives cell dimensions and radius."},{"name":"outside","type":"boolean","default":"false","description":"`true` when the cell's date is outside the currently displayed month."},{"name":"tabindex","type":"number","default":"-1","description":"Tab index for roving focus (`0` for the active cell, `-1` otherwise)."},{"name":"cellTemplate","type":"TemplateRef<{ $implicit: CalendarCell<D> }> | null","default":"null","description":"Optional template rendered inside the button — receives `{ $implicit: CalendarCell<D> }`."}],"outputs":[{"name":"selected","payloadType":"CalendarCell<D>","description":"Emitted when the user activates the cell (click, Enter, or Space)."},{"name":"focused","payloadType":"CalendarCell<D>","description":"Emitted when the cell gains focus."},{"name":"previewed","payloadType":"CalendarCell<D>","description":"Emitted on pointer hover — parent uses this to drive range preview."},{"name":"keyNav","payloadType":"CalendarCellKeyNavEvent<D>","description":"Emitted on navigation keys (arrows, Home/End, PageUp/PageDown)."}],"methods":[{"name":"focusButton","signature":"focusButton(): void","description":"Imperatively focuses this cell's button."}]},{"name":"CalendarCellKeyNavEvent","kind":"interface","description":"Emitted on keyboard navigation keys (arrows, Home/End, PageUp/PageDown).","members":[{"name":"direction","type":"'left' | 'right' | 'up' | 'down' | 'home' | 'end' | 'pageUp' | 'pageDown'","optional":false,"description":""},{"name":"shiftKey","type":"boolean","optional":false,"description":"True when the originating KeyboardEvent had Shift held — drives year-jump variants of Page navigation."},{"name":"cell","type":"CalendarCell<D>","optional":false,"description":""}]},{"name":"CalendarViewBase","kind":"directive","description":"Abstract base for the three calendar views. Holds shared inputs/outputs (selection, preview, min/max, filters), cell-hit helpers, and roving-focus machinery. Subclasses implement `cells`, `gridLabel`, `onKeyNav`, and `getActiveCompareValue`.","inputs":[{"name":"activeDate","type":"D","required":true,"description":"The date that anchors the grid being rendered."},{"name":"selected","type":"D | D[] | DateRange<D> | null","default":"null","description":"Current selection — scalar, array (multi), or range."},{"name":"minDate","type":"D | null","default":"null","description":"Minimum selectable date."},{"name":"maxDate","type":"D | null","default":"null","description":"Maximum selectable date."},{"name":"dateFilter","type":"DateFilterFn<D> | null","default":"null","description":"Per-date predicate — return `false` to disable."},{"name":"disabledDates","type":"DisabledDates<D> | null","default":"null","description":"Explicitly disabled dates — array (compared via `adapter.sameDate`) or predicate (returns `true` for disabled)."},{"name":"disabledDaysOfWeek","type":"readonly number[]","default":"[]","description":"Days of the week to disable (0=Sun … 6=Sat). Empty array = no day-of-week disabling."},{"name":"dateClass","type":"DateClassFn<D> | null","default":"null","description":"Per-cell class override."},{"name":"previewStart","type":"D | null","default":"null","description":"Hover-preview start (set while user is mid-range pick)."},{"name":"previewEnd","type":"D | null","default":"null","description":"Hover-preview end."},{"name":"invalidFlashDate","type":"D | null","default":"null","description":"Phase 6 — date that briefly flashes as invalid (rejected commit). Cleared by the orchestrator."},{"name":"multiSelectable","type":"boolean","default":"false","description":"`true` when the parent calendar allows more than one cell to be selected at once. Drives `aria-multiselectable` on the grid host."},{"name":"readonlyGrid","type":"boolean","default":"false","description":"`true` when the parent calendar is in read-only mode. Drives `aria-readonly` on the grid host."},{"name":"cellTemplate","type":"TemplateRef<{ $implicit: CalendarCell<D> }> | null","default":"null","description":"Optional cell-content override."}],"outputs":[{"name":"selectedChange","payloadType":"D","description":"Fires when the user activates an enabled cell (click, Enter, or Space). Payload is the activated cell's date."},{"name":"activeDateChange","payloadType":"D","description":"Fires when keyboard navigation or programmatic focus moves the roving cursor to a new cell. Payload is the new active date."},{"name":"previewChange","payloadType":"D | null","description":"Fires on pointer hover with the hovered date, and again with `null` when the pointer leaves the grid. The parent orchestrator drives any range-preview state from this stream."}],"methods":[{"name":"onCellSelected","signature":"onCellSelected(cell: CalendarCell<D>): void","description":"Routes a cell activation up to the parent."},{"name":"onCellPreviewed","signature":"onCellPreviewed(cell: CalendarCell<D>): void","description":"Routes hover events up to the parent (parent drives preview state)."},{"name":"onGridMouseLeave","signature":"onGridMouseLeave(): void","description":"Clears the preview when the pointer leaves the grid."},{"name":"onKeyNav","signature":"onKeyNav(event: { direction: string; cell: CalendarCell<D> }): void","description":"Subclass implements view-specific keyboard navigation."},{"name":"isActiveCell","signature":"isActiveCell(cell: CalendarCell<D>): boolean","description":"Returns `true` when the cell owns the roving cursor."},{"name":"focusCell","signature":"focusCell(compareValue: number): void","description":"Imperatively focuses the cell matching `compareValue` after the next render."},{"name":"focusActiveCell","signature":"focusActiveCell(): void","description":"Focuses the cell that currently owns the roving cursor."}]},{"name":"MonthViewComponent","kind":"component","description":"Month view — renders a 7×6 grid of day cells, with leading/trailing days from adjacent months marked as \"outside\".","selector":"tw-calendar-month-view","usage":[{"form":"element","selector":"tw-calendar-month-view","name":"tw-calendar-month-view"}],"inputs":[{"name":"firstDayOfWeek","type":"number","default":"0","description":"Override first day of week (0=Sun, 1=Mon)."},{"name":"gridIndex","type":"number","default":"0","description":"Index within a multi-month row (0=left, 1+=right) — used by the orchestrator to disambiguate focus."}],"methods":[{"name":"isOutsideMonth","signature":"isOutsideMonth(cell: CalendarCell<D>): boolean","description":"True when the cell's date sits in a neighbouring month (leading/trailing)."}],"extends":"CalendarViewBase"},{"name":"YearViewComponent","kind":"component","description":"Year view — 4×3 grid of month cells inside the active year.","selector":"tw-calendar-year-view","usage":[{"form":"element","selector":"tw-calendar-year-view","name":"tw-calendar-year-view"}],"extends":"CalendarViewBase"},{"name":"YearsViewComponent","kind":"component","description":"Year view — 4×6 grid (24 years by default). A user clicks a year to drill down to the month-of-year view. In the spec vocabulary this is the `'year'` view state (§7.4, §22).","selector":"tw-calendar-years-view","usage":[{"form":"element","selector":"tw-calendar-years-view","name":"tw-calendar-years-view"}],"extends":"CalendarViewBase"},{"name":"yearsPerPage","kind":"const","description":"Default years per page — kept until Phase 7 introduces the `yearsPerPage` input."},{"name":"CalendarMode","kind":"type","description":"Selection mode exposed via the `mode` input. `'week'` is `[WONT] v1` — week-as-unit selection remains available via the DI-only `WeekSelectionStrategy`.","definition":"'single' | 'multiple' | 'range'"},{"name":"CalendarSingleValue","kind":"type","description":"Value shape for `mode: 'single'`.","definition":"D | null"},{"name":"CalendarMultipleValue","kind":"type","description":"Value shape for `mode: 'multiple'`.","definition":"D[]"},{"name":"CalendarRangeValue","kind":"interface","description":"Value shape for `mode: 'range'`. Endpoints are independent — either can be `null`.","members":[{"name":"start","type":"D | null","optional":false,"description":""},{"name":"end","type":"D | null","optional":false,"description":""}]},{"name":"CalendarValue","kind":"type","description":"Mode-parameterized value. `mode: 'single' → D | null`, `'multiple' → D[]`, `'range' → { start; end }`.","definition":"M extends 'single' ? CalendarSingleValue<D> : M extends 'multiple' ? CalendarMultipleValue<D> : M extends 'range' ? CalendarRangeValue<D> : never"},{"name":"CalendarSelectionState","kind":"type","description":"Selection lifecycle.","definition":"'EMPTY' | 'SELECTING' | 'COMPLETE'"},{"name":"CalendarViewState","kind":"type","description":"Which grid the user is looking at. `'day'` = 7×6 day grid, `'month'` = 4×3 months in a year, `'year'` = a page of years.","definition":"'day' | 'month' | 'year'"},{"name":"CalendarOverlayState","kind":"type","description":"Overlay presentation lifecycle (Phase 10 wires the transitions). In inline mode this signal resolves to `null`.","definition":"'closed' | 'opening' | 'open' | 'closing'"},{"name":"CalendarErrorCode","kind":"type","description":"Union of validation error keys emitted by the built-in validator. Stable across versions.","definition":"| 'calendarRequired' | 'calendarMinDate' | 'calendarMaxDate' | 'calendarDisabledDate' | 'calendarRangeTooShort' | 'calendarRangeTooLong' | 'calendarMaxSelections' | 'calendarInvalidRange' | 'calendarParseError' | 'calendarInvalidValue'"},{"name":"CalendarValidationErrors","kind":"type","description":"Shape returned by the built-in validator. Each code carries the context payload described in §10.2.","definition":"Partial<{ calendarRequired: true; calendarMinDate: { min: unknown; actual: unknown }; calendarMaxDate: { max: unknown; actual: unknown }; calendarDisabledDate: { actual: unknown }; calendarRangeTooShort: { length: number; min: number }; calendarRangeTooLong: { length: number; max: number }; calendarMaxSelections: { limit: number; actual: number }; calendarInvalidRange: { start: unknown; end: unknown }; calendarParseError: { raw: string }; calendarInvalidValue: { expected: 'single' | 'multiple' | 'range'; actual: unknown; reason: 'shape' | 'transformer'; }; }>"},{"name":"RangeClickBehavior","kind":"type","description":"How `mode: 'range'` handles the third click after a complete range.","definition":"'restart' | 'nearest-edge' | 'require-clear'"},{"name":"RangeGranularity","kind":"type","description":"Granularity at which `mode: 'range'` commits (day-of-month, month-of-year, year).","definition":"'day' | 'month' | 'year'"},{"name":"MaxSelectionBehavior","kind":"type","description":"What happens when the user tries to select past `maxSelections` in `mode: 'multiple'`.","definition":"'emit-limit-reached' | 'replace-oldest' | 'ignore'"},{"name":"ResetBehavior","kind":"type","description":"How a form reset restores the calendar's internal state. `'full'` also resets view / active-date; `'value-only'` leaves navigation in place.","definition":"'full' | 'value-only'"},{"name":"MobileMode","kind":"type","description":"Presentation on small viewports. `'auto'` = native overlay on ≥ 600 px, fullscreen otherwise.","definition":"'overlay' | 'fullscreen' | 'bottom-sheet' | 'auto'"},{"name":"SelectionCompleteEvent","kind":"interface","description":"Payload of `selectionComplete`. `reason` flags the commit path.","members":[{"name":"value","type":"CalendarValue<M, D>","optional":false,"description":""},{"name":"reason","type":"'commit' | 'auto-swap' | 'nearest-edge' | 'preset'","optional":false,"description":""}]},{"name":"SelectionClearedEvent","kind":"interface","description":"Payload of `selectionCleared`. `reason` disambiguates user action from programmatic / mode-change / reset / disabled.","members":[{"name":"reason","type":"'user' | 'programmatic' | 'mode-change' | 'reset' | 'disabled'","optional":false,"description":""}]},{"name":"RangePreviewEvent","kind":"interface","description":"Payload of `rangePreview`. `invalidPreview` is set when the hover crosses a disabled date and `disableRangesCrossingDisabledDates` is true, or the range length violates `min`/`maxRangeLength` (Phase 4 / 6 set this).","members":[{"name":"tentativeRange","type":"{ readonly start: D; readonly end: D }","optional":false,"description":""},{"name":"invalidPreview","type":"boolean","optional":false,"description":""}]},{"name":"ViewChangeEvent","kind":"interface","description":"Payload of `viewChange`. Distinguishes drill-down (day→month→year) from drill-up and from programmatic changes.","members":[{"name":"from","type":"CalendarViewState","optional":false,"description":""},{"name":"to","type":"CalendarViewState","optional":false,"description":""},{"name":"reason","type":"'user' | 'programmatic' | 'drill-down' | 'drill-up'","optional":false,"description":""}]},{"name":"ModeChangeEvent","kind":"interface","description":"Payload of `modeChange`.","members":[{"name":"from","type":"CalendarMode","optional":false,"description":""},{"name":"to","type":"CalendarMode","optional":false,"description":""}]},{"name":"DateRange","kind":"interface","description":"A date range with optional endpoints. Kept as a structural type for backward-compat with the existing view implementation; `CalendarRangeValue<D>` is the spec-canonical alias.","members":[{"name":"start","type":"D | null","optional":false,"description":""},{"name":"end","type":"D | null","optional":false,"description":""}]},{"name":"CalendarCellState","kind":"type","description":"Visual state of a single cell in any view. Phase 4 replaces this with the `data-state-*` attribute surface (§34.5).","definition":"| 'default' | 'today' | 'selected' | 'range-start' | 'range-middle' | 'range-end' | 'disabled' | 'preview-start' | 'preview-middle' | 'preview-end'"},{"name":"CalendarCell","kind":"interface","description":"Data describing one renderable cell. Phase 13 replaces this with `DayCellContext<D, T>` (§24.1) — kept in place through Phases 1–12 so the existing month/year/years views compile without a wholesale view rewrite.","members":[{"name":"value","type":"D","optional":false,"description":"The date value this cell represents."},{"name":"displayValue","type":"string","optional":false,"description":"Display text inside the cell (day number, short month name, year)."},{"name":"ariaLabel","type":"string","optional":false,"description":"Accessible label announced to screen readers."},{"name":"enabled","type":"boolean","optional":false,"description":"Whether the cell is interactive. Disabled cells still render."},{"name":"cssClasses","type":"string","optional":false,"description":"Consumer-provided CSS classes (via `dateClass`)."},{"name":"isToday","type":"boolean","optional":false,"description":"True when this cell is today's date."},{"name":"isSelected","type":"boolean","optional":false,"description":"True when this cell is the committed selection (or part of a range)."},{"name":"isRangeStart","type":"boolean","optional":false,"description":"True when this cell starts a committed range."},{"name":"isRangeMiddle","type":"boolean","optional":false,"description":"True when this cell is strictly inside a committed range."},{"name":"isRangeEnd","type":"boolean","optional":false,"description":"True when this cell ends a committed range."},{"name":"isPreviewStart","type":"boolean","optional":false,"description":"True when this cell starts the hover-preview range."},{"name":"isPreviewMiddle","type":"boolean","optional":false,"description":"True when this cell is inside the hover-preview range."},{"name":"isPreviewEnd","type":"boolean","optional":false,"description":"True when this cell ends the hover-preview range."},{"name":"isOutOfMonth","type":"boolean","optional":false,"description":"True when this cell falls outside the currently displayed month (day view only)."},{"name":"isWeekend","type":"boolean","optional":false,"description":"True when this cell is a weekend day (Sat/Sun by default; locale-aware override planned)."},{"name":"isInvalidPreview","type":"boolean","optional":false,"description":"True when this cell is part of a tentative range that violates a constraint (e.g., crosses a disabled date or exceeds `maxRangeLength`)."},{"name":"isInvalidFlash","type":"boolean","optional":false,"description":"True when the cell briefly flashes to indicate a rejected click (e.g., disabled commit, `rangeClickBehavior: 'require-clear'`). Auto-cleared by the orchestrator."},{"name":"compareValue","type":"number","optional":false,"description":"Numeric comparison key used for focus tracking."}]},{"name":"CalendarCellConfig","kind":"interface","description":"Configuration accepted by `createCalendarCell`.","members":[{"name":"value","type":"D","optional":false,"description":""},{"name":"displayValue","type":"string","optional":false,"description":""},{"name":"ariaLabel","type":"string","optional":false,"description":""},{"name":"enabled","type":"boolean","optional":true,"description":""},{"name":"cssClasses","type":"string","optional":true,"description":""},{"name":"compareValue","type":"number","optional":false,"description":""}]},{"name":"NameStyle","kind":"type","description":"Style for month and weekday names.","definition":"'long' | 'short' | 'narrow'"},{"name":"DateFilterFn","kind":"type","description":"Predicate for per-date disabling — return `false` to disable.","definition":"(date: D) => boolean"},{"name":"DateClassFn","kind":"type","description":"Function producing extra per-cell CSS classes.","definition":"(date: D, view: CalendarViewState) => string"},{"name":"DisabledDates","kind":"type","description":"Source for `disabledDates` (§10.1) — accepts either an explicit array of disabled dates (compared via `adapter.sameDate`) or a predicate returning `true` for disabled dates. Note: this is the inverse of `dateFilter` (which returns `true` for ENABLED dates). Both inputs are honored — a date is disabled if either source flags it.","definition":"readonly D[] | ((date: D) => boolean)"},{"name":"CalendarConstraints","kind":"interface","description":"Aggregated constraint inputs (§10.1). Doubles as the shorthand object accepted by `CalendarComponent`'s `constraints` input — every field is optional so consumers can supply only what they need (e.g. `{ minDate, maxDate }` or `{ dateFilter }`). The orchestrator's resolver normalizes missing fields to neutral values (`null` / empty array) before evaluating cell state. Resolution rule on the `constraints` input: the individual `minDate` / `maxDate` / `disabledDates` / `disabledDaysOfWeek` / `dateFilter` inputs win when both are set non-null. This lets a consumer pass `[constraints]=\"defaults\"` for a base set and still override one field via the dedicated input.","members":[{"name":"minDate","type":"D | null","optional":true,"description":""},{"name":"maxDate","type":"D | null","optional":true,"description":""},{"name":"disabledDates","type":"DisabledDates<D> | null","optional":true,"description":""},{"name":"disabledDaysOfWeek","type":"readonly number[] | null","optional":true,"description":""},{"name":"dateFilter","type":"DateFilterFn<D> | null","optional":true,"description":""}]},{"name":"emptyCalendarValue","kind":"function","description":"Mode-agnostic empty value — `null` for single, `[]` for multiple, `{ start: null, end: null }` for range.","signature":"emptyCalendarValue(mode: M): CalendarValue<M, D>"},{"name":"createDateRange","kind":"function","description":"Factory for a plain `DateRange<D>`.","signature":"createDateRange(start: D | null, end: D | null): DateRange<D>"},{"name":"createCalendarCell","kind":"function","description":"Factory for a `CalendarCell` with boolean state fields zeroed out.","signature":"createCalendarCell(config: CalendarCellConfig<D>): CalendarCell<D>"},{"name":"DAYS_PER_WEEK","kind":"const","description":"Days in a week."},{"name":"WEEKS_PER_MONTH","kind":"const","description":"Rows in a month grid."},{"name":"YEARS_PER_PAGE","kind":"const","description":"Default years shown per page in the year view. Phase 7 replaces this constant with the `yearsPerPage` input (default 20 per §33.1)."},{"name":"YEARS_PER_ROW","kind":"const","description":"Years per row in the year view."},{"name":"MONTHS_PER_ROW","kind":"const","description":"Months per row in the month (of year) view."},{"name":"createGrid","kind":"function","description":"Arranges a flat array of items into a 2D grid with `columns` items per row.","signature":"createGrid(items: readonly T[], columns: number): T[][]"},{"name":"navigateGrid","kind":"function","description":"Returns the new flat index after an arrow-key navigation step, or `null` on out of bounds.","signature":"navigateGrid(currentIndex: number, direction: 'left' | 'right' | 'up' | 'down', totalItems: number, columns: number): number | null"},{"name":"getWeekdayHeaders","kind":"function","description":"Builds seven weekday headers rotated to start at `firstDayOfWeek` (or adapter default).","signature":"getWeekdayHeaders(adapter: DateAdapter<D>, style: NameStyle = 'long', firstDayOfWeek?: number): WeekdayHeader[]"},{"name":"isDateDisabled","kind":"function","description":"True when `date` falls outside `[minDate, maxDate]` or fails the filter. Phase 4 superset: also OR-checks `disabledDates` (array OR predicate) and `disabledDaysOfWeek`. Legacy callers passing only the first four arguments keep working — the new sources default to neutral values.","signature":"isDateDisabled(date: D, minDate: D | null, maxDate: D | null, dateFilter: DateFilterFn<D> | null, adapter: DateAdapter<D>, disabledDates: DisabledDates<D> | null = null, disabledDaysOfWeek: readonly number[] = []): boolean"},{"name":"isMonthDisabled","kind":"function","description":"True when the whole month falls outside `[minDate, maxDate]`. `month` is 0-based (matches `adapter.getMonth`).","signature":"isMonthDisabled(year: number, month: number, minDate: D | null, maxDate: D | null, adapter: DateAdapter<D>): boolean"},{"name":"isYearDisabled","kind":"function","description":"True when the whole year falls outside `[minDate, maxDate]`.","signature":"isYearDisabled(year: number, minDate: D | null, maxDate: D | null, adapter: DateAdapter<D>): boolean"},{"name":"getMultiYearStartingYear","kind":"function","description":"Aligns `activeYear` to the start of its `yearsPerPage`-block.","signature":"getMultiYearStartingYear(activeYear: number, yearsPerPage = 24): number"},{"name":"isDateInRange","kind":"function","description":"True when `date` sits inside `[start, end]` (inclusive).","signature":"isDateInRange(date: D, start: D | null, end: D | null, adapter: DateAdapter<D>): boolean"},{"name":"getFirstDayOfMonth","kind":"function","description":"Returns the first day of the month for `date`.","signature":"getFirstDayOfMonth(date: D, adapter: DateAdapter<D>): D"},{"name":"WeekdayHeader","kind":"interface","description":"Label pair for a weekday header cell.","members":[{"name":"label","type":"string","optional":false,"description":""},{"name":"narrow","type":"string","optional":false,"description":""}]},{"name":"DateAdapter","kind":"class","description":"Abstract date adapter. Ship a subclass to swap the underlying date library (Luxon, date-fns, Temporal). The calendar consumes only the methods defined here — nothing else. `D` is the native date type understood by the underlying library. Method contract aligned with spec §20.1 / §20.2: `addYears`/`addMonths`/ `addDays`, `compare`, 1-based-month `create`, `toIso`/`fromIso`, and optional TZ virtuals.","methods":[{"name":"today","signature":"today(): D","description":"Returns today at midnight, local time (or in the adapter's configured TZ)."},{"name":"create","signature":"create(year: number, month: number, day: number): D","description":"Constructs a date. **`month` is 1-based** — pass `1` for January, `12` for December. Callers migrating from the pre-v1 `createDate(year, zeroBasedMonth, day)` must bump the month argument by one. `day` is 1-based."},{"name":"clone","signature":"clone(date: D): D","description":"Returns a defensive copy."},{"name":"setLocale","signature":"setLocale(locale: string): void","description":"Locale for label formatting. Subclasses may honour this or ignore."},{"name":"getYear","signature":"getYear(date: D): number","description":"Field getters. `month` is zero-based."},{"name":"getDayOfWeek","signature":"getDayOfWeek(date: D): number","description":"0 = Sunday, 6 = Saturday."},{"name":"getHours","signature":"getHours(date: D): number","description":"Time getters — hours are 0–23, minutes and seconds are 0–59."},{"name":"withTime","signature":"withTime(date: D, hours: number, minutes: number, seconds: number): D","description":"Returns a new date with the same year/month/day as `date` but the supplied time-of-day. Adapters must not mutate `date`. `hours` is 0–23. **Not part of the calendar's v1 spec surface** — the calendar does not consume time-of-day. Retained for the companion time-picker component."},{"name":"getFirstDayOfWeek","signature":"getFirstDayOfWeek(): number","description":"0 = Sunday, 1 = Monday, etc."},{"name":"getDaysInWeek","signature":"getDaysInWeek(): number","description":"Days per week. Override only for non-Gregorian adapters (WONT v1)."},{"name":"getMonthNames","signature":"getMonthNames(style: TwDateNameStyle): string[]","description":"Returns month names for the current locale."},{"name":"getDayOfWeekNames","signature":"getDayOfWeekNames(style: TwDateNameStyle): string[]","description":"Returns day-of-week names for the current locale, starting at Sunday."},{"name":"getDateNames","signature":"getDateNames(_style: TwDateNameStyle = 'short'): string[]","description":"Returns 1-based day-of-month names (`[\"1\", \"2\", ..., \"31\"]`) for locales that localize numerals. Default implementation returns Latin digits."},{"name":"getYearName","signature":"getYearName(date: D): string","description":"Returns the localised year label (e.g. `\"2026\"`)."},{"name":"format","signature":"format(date: D, displayFormat: unknown): string","description":"Formats a date for display. Subclasses pick their own format tokens."},{"name":"addYears","signature":"addYears(date: D, years: number): D","description":"Date arithmetic — preserve original in immutable adapters."},{"name":"addHours","signature":"addHours(date: D, hours: number): D","description":"Time arithmetic. Default implementation derives hours/minutes from day arithmetic via `addDays` — adapters with better precision should override."},{"name":"compare","signature":"compare(first: D, second: D): number","description":"Returns negative when `first` is earlier, 0 when equal, positive when later."},{"name":"isValid","signature":"isValid(date: D): boolean","description":"Whether a raw value can be turned into a valid date."},{"name":"invalid","signature":"invalid(): D","description":"Returns an invalid sentinel of type `D` (NaN-date for native)."},{"name":"parse","signature":"parse(value: unknown, parseFormat?: unknown): D | null","description":"Attempt to parse an ISO or free-form string. Return `null` on failure."},{"name":"deserialize","signature":"deserialize(value: unknown): D | null","description":"Coerce an incoming value from form writes."},{"name":"toIso","signature":"toIso(date: D): string","description":"ISO 8601 (`YYYY-MM-DD`) serialization. Phases using serialized forms call this."},{"name":"fromIso","signature":"fromIso(iso: string): D | null","description":"ISO 8601 (`YYYY-MM-DD`) deserialization counterpart to `toIso`."},{"name":"startOfDay","signature":"startOfDay(date: D): D","description":"Start-of-day normalization. Default implementation rebuilds the date at `00:00:00.000` local time; DST-skipped hours (e.g., 02:00 on DST start) are resolved to the wall-clock midnight the adapter considers valid. TZ-aware adapters MUST override to normalize in the adapter's TZ."},{"name":"startOfWeek","signature":"startOfWeek(date: D, firstDayOfWeek?: number): D","description":"First day of the week containing `date`. `firstDayOfWeek` defaults to `getFirstDayOfWeek()`."},{"name":"endOfWeek","signature":"endOfWeek(date: D, firstDayOfWeek?: number): D","description":"Last day of the week containing `date`."},{"name":"sameMonth","signature":"sameMonth(first: D, second: D): boolean","description":"Equality on year + month."},{"name":"sameYear","signature":"sameYear(first: D, second: D): boolean","description":"Equality on year only."},{"name":"clampDate","signature":"clampDate(date: D, min?: D | null, max?: D | null): D","description":"Clamp a date into the [min, max] window."},{"name":"getTimezone","signature":"getTimezone(): string | null","description":"Returns the adapter's configured timezone, or `null` for floating adapters."},{"name":"withTimezone","signature":"withTimezone(date: D, _tz: string): D","description":"Returns a copy of `date` reinterpreted in `tz`. Floating adapters pass through."},{"name":"isDST","signature":"isDST(_date: D): boolean","description":"Returns `true` when `date` sits inside a DST-transition. Floating adapters return `false`."},{"name":"resolveAmbiguous","signature":"resolveAmbiguous(date: D, _prefer: 'earlier' | 'later'): D","description":"Resolves an ambiguous wall-clock time (DST fall-back) to one of the two candidates. Floating adapters pass through."}]},{"name":"DATE_ADAPTER","kind":"token","description":"Injection token for the calendar's `DateAdapter`. Prefer `provideNativeDateAdapter()` or `provideTwCalendar({ adapter })` over binding this token directly."},{"name":"DATE_FORMATS","kind":"token","description":"DI token carrying the default `DateFormats`. The calendar's `dateFormats` input overrides."},{"name":"TZ_OVERRIDE","kind":"token","description":"Injection token for a per-instance timezone override (§7.4, §4.3). When set, TZ-aware adapters resolve `today()` and date-range math against the supplied IANA timezone. Floating (naive) adapters ignore the token."},{"name":"DATE_SERIALIZATION","kind":"token","description":"Injection token for the default value transformer (§7.4, §7.6). Phase 14 wires the component-level `valueTransformer` input to override this DI default. Shipped as a token shell now so later phases can bind against it without a breaking change."},{"name":"TwDateNameStyle","kind":"type","description":"Names of month / day-of-week labels, in three densities.","definition":"'long' | 'short' | 'narrow'"},{"name":"DateFormats","kind":"interface","description":"Format definitions consumed by the calendar header, views, and text-input directive. Each field accepts an adapter-specific format token — the default NativeDateAdapter interprets them as `Intl.DateTimeFormatOptions` bags wrapped in a NativeDateFormat. Phases 2+ only require the `input`, `display`, `monthLabel`, and `yearLabel` members; the rest ship as slots now so later phases (5 — CalendarIntl, 9 — multi-month, 11 — text input) can populate them without an API break.","members":[{"name":"input","type":"unknown","optional":true,"description":"Format used when parsing user-typed input."},{"name":"display","type":"unknown","optional":true,"description":"Format used when rendering a date into a text input's display value."},{"name":"monthLabel","type":"unknown","optional":true,"description":"Format used for the header's month label (e.g., `\"January 2026\"`)."},{"name":"yearLabel","type":"unknown","optional":true,"description":"Format used for the header's year label in month view (e.g., `\"2026\"`)."},{"name":"decadeLabel","type":"unknown","optional":true,"description":"Format used for the years-view header (e.g., `\"2020 – 2039\"`)."},{"name":"a11yLabel","type":"unknown","optional":true,"description":"Format used for a day-cell's accessible label."},{"name":"monthA11yLabel","type":"unknown","optional":true,"description":"Format used for a month-cell's accessible label."},{"name":"dayA11yLabel","type":"unknown","optional":true,"description":"Format used for a day-of-week column header's accessible label."},{"name":"yearViewMonthA11yLabel","type":"unknown","optional":true,"description":"Format used for a month name inside the year view's accessible label."}]},{"name":"serializeCalendarValue","kind":"function","description":"","signature":"serializeCalendarValue(value: unknown, adapter: DateAdapter<D>): string | null | string[] | { start: string | null; end: string | null }"},{"name":"NativeDateAdapter","kind":"service","description":"`Intl`-driven `Date` adapter. Zero runtime dependencies.","methods":[{"name":"create","signature":"create(year: number, month: number, day: number): Date","description":"Constructs a `Date`. **`month` is 1-based** — `create(2026, 1, 1)` yields January 1, 2026."},{"name":"startOfDay","signature":"startOfDay(date: Date): Date","description":"Start-of-day normalization. Rebuilds the date at `00:00:00.000` local time. On DST spring-forward days where midnight is valid, this is a no-op beyond zeroing h/m/s/ms. On DST fall-back days the repeated-01:00 hour does not affect `00:00`, so this is also safe. The edge case of an entire DST-skipped midnight (rare — exists in some historical zones) is resolved by `new Date(year, month, day)` selecting the next valid instant forward."}],"extends":"DateAdapter"},{"name":"provideNativeDateAdapter","kind":"function","description":"Provides the default `Date`-based adapter.","signature":"provideNativeDateAdapter(): EnvironmentProviders"},{"name":"provideTwCalendar","kind":"function","description":"Provides a custom `DateAdapter` implementation.","signature":"provideTwCalendar(config: { adapter: new (...args: never[]) => DateAdapter<D>; extraProviders?: Provider[]; }): EnvironmentProviders"},{"name":"TwNativeDateFormat","kind":"interface","description":"Display-format descriptor used by `NativeDateAdapter.format()`.","members":[{"name":"dateTimeFormat","type":"Intl.DateTimeFormatOptions","optional":true,"description":"Any option bag accepted by `Intl.DateTimeFormat`."}]},{"name":"TwDateRange","kind":"class","description":"Convenience class that satisfies the `DateRange<D>` interface and carries `complete` / `empty` getters. Interchangeable with a plain `{ start, end }` object anywhere a `DateRange<D>` is expected."},{"name":"toTwDateRange","kind":"function","description":"","signature":"toTwDateRange(value: TwDateRangeInput<D>): TwDateRange<D> | null"},{"name":"TwDateRangeInput","kind":"type","description":"Shape accepted when writing a range value — either a `TwDateRange` or a plain object.","definition":"| TwDateRange<D> | { readonly start: D | null; readonly end: D | null } | null"}],"snippets":[{"id":"typesSnippet","title":"Exported types (Phase 1)","language":"ts","code":"import type {\n CalendarMode, // 'single' | 'multiple' | 'range'\n CalendarValue, // narrow by mode\n CalendarSingleValue, // D | null\n CalendarMultipleValue,// D[]\n CalendarRangeValue, // { start; end }\n CalendarViewState, // 'day' | 'month' | 'year'\n CalendarSelectionState, // 'EMPTY' | 'SELECTING' | 'COMPLETE'\n CalendarOverlayState, // 'closed' | 'opening' | 'open' | 'closing'\n CalendarErrorCode, // validation codes per §10.2\n DateFilterFn,\n DateClassFn,\n} from '@cdevhub/ngx-tw/calendar';"},{"id":"singleSnippet","title":"Single selection","language":"html","code":"<tw-calendar aria-label=\"Pick a date\" [(value)]=\"value\" />"},{"id":"rangeSnippet","title":"Range selection","language":"html","code":"<tw-calendar\n aria-label=\"Pick a range\"\n mode=\"range\"\n [(value)]=\"range\"\n/>"},{"id":"multipleSnippet","title":"Multiple selection","language":"html","code":"<tw-calendar\n aria-label=\"Pick multiple dates\"\n mode=\"multiple\"\n [(value)]=\"dates\"\n/>"},{"id":"templateDrivenSnippet","title":"Template-Driven Forms","language":"html","code":"<form #tdForm=\"ngForm\">\n <tw-calendar\n name=\"meetingDate\"\n [(ngModel)]=\"meetingDate\"\n required\n />\n</form>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-calendar aria-label=\"Pick a date\" [(value)]=\"value\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { CalendarComponent, provideNativeDateAdapter } from '@cdevhub/ngx-tw/calendar';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideNativeDateAdapter()],\n};"}],"summary":"Inline month, year, and multi-year grid primitive that the date pickers are built on, with pluggable date adapter and swappable selection strategy.","whenToUse":["A date grid that stays permanently visible on the page rather than behind a trigger","Selection shapes beyond a single date — multiple loose dates, whole weeks, or a range","Decorating cells with dots, badges, or prices through a cell template, or highlighting holidays and events with per-cell classes","Custom selection behaviour such as business-days-only or anchored ranges, via the selection-strategy token","Composing your own picker shell, using headerless mode and the projected preset rail"],"whenNotToUse":[{"instead":"date-picker","because":"the page only needs a compact field, with the grid appearing in a popover on demand"},{"instead":"date-range-picker","because":"a start and end date are captured from a trigger, with presets and an action bar already assembled"},{"instead":"time-picker","because":"only the time of day is being edited and no date grid is involved"}],"related":["date-picker","date-range-picker","time-picker","form-field"],"aliases":["date grid","month view","day grid","inline datepicker","year picker","month picker","week picker","date adapter"],"hasMeta":true,"metaPath":"projects/ngx-tw/calendar/calendar.meta.ts"},{"name":"date-picker","importPath":"@cdevhub/ngx-tw/date-picker","symbols":[{"name":"DatePickerComponent","kind":"component","description":"ARIA date-picker dialog. Combines a typable text input with a popover calendar. All three Angular forms strategies (reactive, template-driven, signal-forms) are supported via `ControlValueAccessor`, and the component integrates with `<tw-form-field>` by implementing `FormFieldControl`. All date operations go through the injected `DateAdapter<D>`; call `provideNativeDateAdapter()` in your app providers to bootstrap the default.","selector":"tw-date-picker","usage":[{"form":"element","selector":"tw-date-picker","name":"tw-date-picker"}],"contentSlots":[{"select":"[slot=trigger]"},{"select":"[slot=trigger-icon]"}],"inputs":[{"name":"idInput","type":"string | undefined","default":"undefined","description":"Id on the date-picker's input element. Auto-generated when not provided. Used by the form-field's `<label for>` attribute.","alias":"id"},{"name":"minDate","type":"D | null","default":"null","description":"Minimum selectable date. Typed input earlier than this commits the value and marks the bound control invalid with `calendarMinDate`; the calendar disables the cell. Defaults to `null`."},{"name":"maxDate","type":"D | null","default":"null","description":"Maximum selectable date. Typed input later than this commits the value and marks the bound control invalid with `calendarMaxDate`; the calendar disables the cell. Defaults to `null`."},{"name":"dateFilter","type":"DateFilterFn<D> | null","default":"null","description":"Per-date predicate — return `false` to disable. Applied in the calendar, the text-parse path, and the `calendarDisabledDate` validation error."},{"name":"startView","type":"CalendarViewState","default":"'day'","description":"Which calendar view opens first — `'day'`, `'month'`, or `'year'`. Defaults to `'day'`."},{"name":"startAt","type":"D | null","default":"null","description":"Date to focus when the calendar opens with no selection. Falls back to today."},{"name":"format","type":"unknown","default":"DEFAULT_DISPLAY_FORMAT","description":"Display format passed to `DateAdapter.format()`. With the default adapter, accepts `{ dateTimeFormat: Intl.DateTimeFormatOptions }`. When `withTime` is true and this is left at the default, hour/minute (and optional seconds) are folded in automatically."},{"name":"parseFormat","type":"unknown | undefined","default":"undefined","description":"Optional format hint passed to `DateAdapter.parse()`. Ignored by the native adapter."},{"name":"placeholder","type":"string | undefined","default":"undefined","description":"Placeholder text shown in the input when no value is entered."},{"name":"disabledInput","type":"boolean","default":"false","description":"When true, the input is disabled, the trigger cannot open the calendar, and `aria-disabled=\"true\"` is set. Defaults to `false`.","alias":"disabled","transform":"booleanAttribute"},{"name":"requiredInput","type":"boolean","default":"false","description":"When true, exposes `aria-required=\"true\"`. Validators.required on a bound NgControl is also honoured. Defaults to `false`.","alias":"required","transform":"booleanAttribute"},{"name":"readonlyInput","type":"boolean","default":"false","description":"When true, blocks typing but still allows picking via the calendar trigger. Defaults to `false`.","alias":"readonly","transform":"booleanAttribute"},{"name":"size","type":"TwSize","default":"'md'","description":"Trigger padding, font size, and calendar cell density. Defaults to `'md'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color for focused border, calendar selection ring, and today marker. Defaults to `'primary'`."},{"name":"variant","type":"DatePickerVariant | undefined","default":"undefined","description":"Visual style of the trigger. `'default'` draws its own border; `'naked'` strips chrome. Auto-resolves to `'naked'` when inside `<tw-form-field>`."},{"name":"showClear","type":"boolean","default":"true","description":"Whether to show a clear-button affordance inside the trigger when a value is set. Defaults to `true`."},{"name":"showActions","type":"boolean","default":"false","description":"When true, renders a `Today / Clear / Cancel / Apply` action bar at the bottom of the overlay. Defaults to `false`."},{"name":"todayLabel","type":"string","default":"'Today'","description":"Label for the `Today` action in the overlay's action bar."},{"name":"clearLabel","type":"string","default":"'Clear'","description":"Label for the `Clear` action in the overlay's action bar."},{"name":"cancelLabel","type":"string","default":"'Cancel'","description":"Label for the `Cancel` action in the overlay's action bar."},{"name":"applyLabel","type":"string","default":"'Apply'","description":"Label for the `Apply` action in the overlay's action bar."},{"name":"openOnFocus","type":"boolean","default":"false","description":"When true, focusing the text input opens the overlay. Defaults to `false`."},{"name":"panelClass","type":"string | readonly string[]","default":"''","description":"Extra class(es) applied to the overlay panel element."},{"name":"scrollStrategy","type":"'reposition' | 'close' | 'block'","default":"'reposition'","description":"CDK scroll strategy for the overlay. Defaults to `'reposition'`."},{"name":"offset","type":"number","default":"4","description":"Pixel distance between trigger and overlay. Defaults to `4`."},{"name":"triggerAriaLabel","type":"string","default":"'Open calendar'","description":"Accessible name for the calendar trigger button. Defaults to `'Open calendar'`."},{"name":"timeConfig","type":"DatePickerTimeConfig<D> | null","default":"null","description":"Bundles the time-of-day configuration. Passing a non-null object turns on the embedded `<tw-time-picker>` and forwards each field. Defaults to `null` (no time field). Pass an empty object `{}` to enable the time picker with all defaults."},{"name":"locale","type":"string | null","default":"null","description":"Per-instance locale override. Forwarded to the embedded calendar and the underlying `DateAdapter` for parse/format. Falls back to Angular `LOCALE_ID` when `null`."},{"name":"dateClass","type":"DateClassFn<D> | null","default":"null","description":"Function producing per-cell CSS classes, forwarded to the embedded calendar. Useful for visually marking special dates (holidays, billing cycles)."},{"name":"cellTemplate","type":"TemplateRef<{ $implicit: CalendarCell<D> }> | null","default":"null","description":"Optional cell-content template, forwarded to the embedded calendar. Use to customize cell visuals beyond `dateClass`."},{"name":"presets","type":"readonly DatePickerPreset<D>[]","default":"[]","description":"Optional quick-select presets rendered as a vertical list before the calendar. Each preset provides a `label` and a `date` factory. An empty array hides the preset panel."},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the `ErrorStateMatcher`."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the input. Required when no visible label is supplied. Alias: `aria-label`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the input. Alias: `aria-labelledby`.","alias":"aria-labelledby"},{"name":"userAriaDescribedByInput","type":"string | undefined","default":"undefined","description":"Consumer-supplied `aria-describedby` ids. The form-field preserves these when merging hint/error ids. Alias: `aria-describedby`.","alias":"aria-describedby"}],"outputs":[{"name":"opened","payloadType":"DatePickerOpenedEvent","description":"Fires after the overlay's enter animation completes. Payload is the trigger element."},{"name":"closed","payloadType":"DatePickerCloseReason","description":"Fires after the overlay's leave animation completes. Payload is the reason it closed."},{"name":"dateInput","payloadType":"DatePickerInputEvent<D>","description":"Fires on every keystroke in the text input (before parsing). Does NOT mean the value has committed."},{"name":"dateChange","payloadType":"DatePickerChangeEvent<D>","description":"Fires after a commit — from parsing typed input or picking in the calendar."},{"name":"presetSelected","payloadType":"DatePickerPreset<D>","description":"Fires when the user picks one of the entries from `presets`. Payload is the preset that fired."}],"models":[{"name":"value","type":"D | null","default":"null","description":"Two-way bound selected date. `null` when no selection. Setting programmatically updates the display; does NOT trigger `onChange`."},{"name":"open","type":"boolean","default":"false","description":"Two-way bound open state of the calendar overlay."}],"methods":[{"name":"openPicker","signature":"openPicker(): void","description":"Opens the overlay. No-op when disabled or already open."},{"name":"closePicker","signature":"closePicker(): void","description":"Closes the overlay. No-op when already closed."},{"name":"toggle","signature":"toggle(): void","description":"Toggles the overlay's open state."},{"name":"clear","signature":"clear(): void","description":"Clears the current value and emits `dateChange` with `source: 'clear'`."}],"formControl":true,"extends":"FormFieldControl"},{"name":"DatePickerVariant","kind":"type","description":"Visual style of the date-picker trigger.","definition":"'default' | 'naked'"},{"name":"DatePickerChangeSource","kind":"type","description":"Origin of a value change, used to distinguish user input from programmatic writes.","definition":"| 'input' | 'calendar' | 'apply' | 'clear' | 'today' | 'programmatic'"},{"name":"DatePickerCloseReason","kind":"type","description":"Reason the overlay closed.","definition":"| 'select' | 'apply' | 'cancel' | 'escape' | 'backdrop' | 'programmatic'"},{"name":"DatePickerInputEvent","kind":"interface","description":"Emitted by `dateInput`.","members":[{"name":"rawText","type":"string","optional":false,"description":"The raw string currently in the input (pre-parse)."},{"name":"parsed","type":"D | null","optional":false,"description":"The parsed value if parsing succeeded and it's in range; otherwise `null`."},{"name":"target","type":"HTMLInputElement","optional":false,"description":"The input element itself."}]},{"name":"DatePickerChangeEvent","kind":"interface","description":"Emitted by `dateChange`.","members":[{"name":"value","type":"D | null","optional":false,"description":"The committed value (`null` when cleared)."},{"name":"previousValue","type":"D | null","optional":false,"description":"The value before this change."},{"name":"source","type":"DatePickerChangeSource","optional":false,"description":"What triggered the change."}]},{"name":"DatePickerOpenedEvent","kind":"interface","description":"Emitted by `opened` and `closed`.","members":[{"name":"trigger","type":"HTMLElement","optional":false,"description":"The trigger (input) element."}]},{"name":"DatePickerPreset","kind":"interface","description":"A quick-select preset rendered above the calendar in the overlay. Mirrors `DateRangePreset`.","members":[{"name":"label","type":"string","optional":false,"description":"Label shown on the preset button."},{"name":"date","type":"() => D","optional":false,"description":"Factory returning the date to apply when this preset is chosen. Called fresh each click so \"today\"-relative presets stay current."},{"name":"id","type":"string","optional":true,"description":"Optional identifier — surfaced in `presetSelected` and used to detect the active preset for visual state."}]},{"name":"DatePickerTimeConfig","kind":"interface","description":"Bundle of time-of-day configuration forwarded to the embedded `<tw-time-picker>`. Passing a non-null object turns the time-picker on and forwards each field; pass `{}` to enable with all defaults.","members":[{"name":"format","type":"TimePickerFormat","optional":true,"description":"Clock format. Defaults to `'24h'`."},{"name":"showSeconds","type":"boolean","optional":true,"description":"Whether to expose a seconds field. Defaults to `false`."},{"name":"hourStep","type":"number","optional":true,"description":"Hour step. Defaults to `1`."},{"name":"minuteStep","type":"number","optional":true,"description":"Minute step. Defaults to `1`."},{"name":"secondStep","type":"number","optional":true,"description":"Second step. Defaults to `1`."},{"name":"minTime","type":"D | null","optional":true,"description":"Earliest accepted time-of-day. Values earlier than this set `errorState`."},{"name":"maxTime","type":"D | null","optional":true,"description":"Latest accepted time-of-day. Values later than this set `errorState`."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type DatePickerVariant = 'default' | 'naked';\n\ntype DatePickerChangeSource =\n | 'input' | 'calendar' | 'apply' | 'clear' | 'today' | 'programmatic';\n\ntype DatePickerCloseReason =\n | 'select' | 'apply' | 'cancel' | 'escape' | 'backdrop' | 'programmatic';\n\ninterface DatePickerInputEvent<D> {\n rawText: string;\n parsed: D | null;\n target: HTMLInputElement;\n}\n\ninterface DatePickerChangeEvent<D> {\n value: D | null;\n previousValue: D | null;\n source: DatePickerChangeSource;\n}\n\ninterface DatePickerOpenedEvent {\n trigger: HTMLElement;\n}\n\ninterface DatePickerPreset<D = Date> {\n label: string;\n date: () => D;\n id?: string;\n}\n\ninterface DatePickerTimeConfig<D = Date> {\n format?: TimePickerFormat;\n showSeconds?: boolean;\n hourStep?: number;\n minuteStep?: number;\n secondStep?: number;\n minTime?: D | null;\n maxTime?: D | null;\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (size of sizes; track size) {\n <tw-date-picker\n [size]=\"size\"\n [(value)]=\"sizeValues[size]\"\n [placeholder]=\"'Size: ' + size\"\n [aria-label]=\"'Date picker ' + size\"\n />\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (color of colors; track color) {\n <tw-date-picker\n [color]=\"color\"\n [(value)]=\"colorValues[color]\"\n [placeholder]=\"color\"\n [aria-label]=\"color\"\n />\n}"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled: blocks typing and opening -->\n<tw-date-picker [value]=\"today\" [disabled]=\"true\" aria-label=\"Disabled\" />\n\n<!-- Readonly: blocks typing, calendar still opens -->\n<tw-date-picker [value]=\"today\" [readonly]=\"true\" aria-label=\"Readonly\" />"},{"id":"constraintsSnippet","title":"Min, max filter","language":"html","code":"<tw-date-picker\n [(value)]=\"appointment\"\n [minDate]=\"today\"\n [maxDate]=\"thirtyDaysOut\"\n [dateFilter]=\"weekdayFilter\"\n placeholder=\"Select a weekday\"\n aria-label=\"Appointment\"\n/>"},{"id":"withTimeSnippet","title":"With time","language":"html","code":"<!-- 24h, minute precision (empty config opts in with defaults) -->\n<tw-date-picker\n [(value)]=\"meeting\"\n [timeConfig]=\"{}\"\n placeholder=\"Pick a date and time\"\n aria-label=\"Meeting\"\n/>\n\n<!-- 12h + seconds + action bar -->\n<tw-date-picker\n [(value)]=\"deadline\"\n [timeConfig]=\"{ format: '12h', showSeconds: true }\"\n [showActions]=\"true\"\n color=\"accent\"\n placeholder=\"Pick a deadline\"\n aria-label=\"Deadline\"\n/>"},{"id":"actionBarSnippet","title":"Action bar","language":"html","code":"<tw-date-picker\n [(value)]=\"eventDate\"\n [showActions]=\"true\"\n color=\"accent\"\n size=\"lg\"\n placeholder=\"Pick event date\"\n aria-label=\"Event date\"\n/>"},{"id":"tdTsSnippet","title":"Template-driven forms","language":"ts","code":"protected birthday: Date | null = null;"},{"id":"tdHtmlSnippet","title":"Template-driven forms","language":"html","code":"<tw-form-field>\n <label twLabel>Birthday</label>\n <tw-date-picker name=\"birthday\" [(ngModel)]=\"birthday\" required />\n <span twHint>Used for age-based offers.</span>\n</tw-form-field>"},{"id":"reactiveTsSnippet","title":"Reactive forms","language":"ts","code":"protected readonly deliveryCtrl = new FormControl<Date | null>(null, [\n Validators.required,\n]);"},{"id":"reactiveHtmlSnippet","title":"Reactive forms","language":"html","code":"<tw-form-field>\n <label twLabel>Delivery date</label>\n <tw-date-picker\n [formControl]=\"deliveryCtrl\"\n [minDate]=\"today\"\n [maxDate]=\"thirtyDaysOut\"\n />\n <span twHint>Within the next 30 days.</span>\n <span twError>Please pick a valid date.</span>\n</tw-form-field>"},{"id":"signalTsSnippet","title":"Signal forms","language":"ts","code":"protected readonly shipModel = signal<{ shipDate: Date | null }>({ shipDate: null });\nprotected readonly shipForm = form(this.shipModel, (path) => {\n required(path.shipDate, { message: 'Ship date is required.' });\n});"},{"id":"signalHtmlSnippet","title":"Signal forms","language":"html","code":"<tw-form-field>\n <label twLabel>Ship date</label>\n <tw-date-picker\n [formField]=\"shipForm.shipDate\"\n [minDate]=\"today\"\n placeholder=\"Pick ship date\"\n />\n <span twHint>Must be today or later.</span>\n <span twError>Ship date is required.</span>\n</tw-form-field>"},{"id":"formFieldSnippet","title":"Inside form-field (auto-naked)","language":"html","code":"<tw-form-field>\n <label twLabel>Start date</label>\n <tw-date-picker [(value)]=\"startValue\" [minDate]=\"today\" aria-label=\"Start date\" />\n <span twHint>When the project kicks off.</span>\n</tw-form-field>\n\n<tw-form-field appearance=\"filled\" color=\"success\">\n <label twLabel>Launch date</label>\n <tw-date-picker [(value)]=\"launchValue\" aria-label=\"Launch date\" />\n</tw-form-field>"},{"id":"presetsTsSnippet","title":"Presets","language":"ts","code":"protected readonly quickPresets: DatePickerPreset<Date>[] = [\n { id: 'today', label: 'Today', date: () => startOfToday() },\n { id: 'tomorrow', label: 'Tomorrow', date: () => addDays(startOfToday(), 1) },\n { id: 'next-week', label: 'Next week', date: () => addDays(startOfToday(), 7) },\n { id: 'next-month', label: 'Next month', date: () => addDays(startOfToday(), 30) },\n];"},{"id":"presetsHtmlSnippet","title":"Presets","language":"html","code":"<tw-date-picker\n [(value)]=\"presetValue\"\n [presets]=\"quickPresets\"\n placeholder=\"Pick or use a preset\"\n aria-label=\"Quick presets\"\n/>"},{"id":"timeConfigSnippet","title":"Time config (recommended)","language":"html","code":"<tw-date-picker\n [(value)]=\"value\"\n [timeConfig]=\"{ format: '12h', minuteStep: 15 }\"\n placeholder=\"Pick a 12h date + time\"\n aria-label=\"Time config\"\n/>"},{"id":"localeSnippet","title":"Per-instance locale","language":"html","code":"<tw-date-picker locale=\"de-DE\" [(value)]=\"dateDE\" aria-label=\"German\" />\n<tw-date-picker locale=\"fr-FR\" [(value)]=\"dateFR\" aria-label=\"French\" />"},{"id":"customTriggerSnippet","title":"Custom trigger","language":"html","code":"<tw-date-picker [(value)]=\"value\" aria-label=\"Custom trigger\">\n <button twButton slot=\"trigger\" variant=\"outline\" type=\"button\">\n {{ value()?.toDateString() ?? 'Choose a date' }}\n </button>\n</tw-date-picker>"},{"id":"dateClassSnippet","title":"Date class cell template","language":"ts","code":"protected readonly weekendClass = (d: Date) =>\n d.getDay() === 0 || d.getDay() === 6 ? 'text-error-600 font-semibold' : '';\n\n// in template:\n// <tw-date-picker [dateClass]=\"weekendClass\" [(value)]=\"value\" />"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-date-picker\n [(value)]=\"date\"\n placeholder=\"Pick a date\"\n aria-label=\"Basic date\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"// 1. Provide the adapter once in your app config:\nimport { provideNativeDateAdapter } from '@cdevhub/ngx-tw/calendar';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideNativeDateAdapter()],\n};\n\n// 2. Import the component where you use it:\nimport { DatePickerComponent } from '@cdevhub/ngx-tw/date-picker';"}],"summary":"Single-date form control combining a typable text input with a modal popover calendar, following the ARIA date-picker dialog pattern.","whenToUse":["A form field that captures one date, such as a birth date, due date, or appointment day","Layouts where a permanently visible month grid would take too much room, so the calendar lives behind a trigger","Users who prefer to type the date but still want a grid to fall back on","A full timestamp in one control, by enabling the built-in time fields alongside the date","Constraining entry with min/max bounds or a date filter and surfacing violations through the standard error state"],"whenNotToUse":[{"instead":"date-range-picker","because":"the value has a start and an end rather than a single day"},{"instead":"calendar","because":"the month grid should be permanently visible inline, or you are composing a custom date UI on the primitive"},{"instead":"time-picker","because":"only the time of day matters and there is no date to capture"},{"instead":"input","because":"a free-form typed string is enough and no calendar affordance is wanted"}],"related":["calendar","date-range-picker","time-picker","form-field","input"],"aliases":["datepicker","date input","date field","day picker","calendar input","date selector","due date"],"hasMeta":true,"metaPath":"projects/ngx-tw/date-picker/date-picker.meta.ts"},{"name":"date-range-picker","importPath":"@cdevhub/ngx-tw/date-range-picker","symbols":[{"name":"DateRangePickerComponent","kind":"component","description":"ARIA date-picker dialog for two-endpoint date ranges. Composes `tw-calendar` in `selectionMode=\"range\"` inside a CDK overlay. Supports optional time selection via embedded `tw-time-picker`s and optional quick-select presets. All three Angular forms strategies (reactive, template-driven, signal-forms) are supported via `ControlValueAccessor`, and the component integrates with `<tw-form-field>` by implementing `FormFieldControl`. All date operations go through the injected `DateAdapter<D>`; call `provideNativeDateAdapter()` in your app providers to bootstrap the default.","selector":"tw-date-range-picker","usage":[{"form":"element","selector":"tw-date-range-picker","name":"tw-date-range-picker"}],"contentSlots":[{"select":"[slot=trigger-icon]"}],"inputs":[{"name":"idInput","type":"string | undefined","default":"undefined","description":"Id on the trigger element. Auto-generated when not provided. Used by the form-field's `<label for>` attribute.","alias":"id"},{"name":"minDate","type":"D | null","default":"null","description":"Earliest selectable date for either endpoint. Presets and calendar cells earlier than this are rejected or disabled. Defaults to `null`."},{"name":"maxDate","type":"D | null","default":"null","description":"Latest selectable date for either endpoint. Presets and calendar cells later than this are rejected or disabled. Defaults to `null`."},{"name":"dateFilter","type":"DateFilterFn<D> | null","default":"null","description":"Per-date predicate — return `false` to disable. Applied in both calendars. Presets that fall on a filtered date are skipped."},{"name":"startView","type":"CalendarViewState","default":"'day'","description":"Which calendar view opens first — `'day'`, `'month'`, or `'year'`. Defaults to `'day'`."},{"name":"startAt","type":"D | null","default":"null","description":"Date the left calendar focuses on when opened with no value. Falls back to today. Ignored when a value is already set."},{"name":"format","type":"unknown","default":"DEFAULT_DISPLAY_FORMAT","description":"Display format for each endpoint, passed to `DateAdapter.format()`. When `showTime` is true and this is left at the default, hour/minute (and optional seconds) are folded in automatically."},{"name":"rangeSeparator","type":"string","default":"' – '","description":"Separator rendered between the two formatted endpoints in the trigger. Defaults to `\" – \"`."},{"name":"emptyStartLabel","type":"string","default":"'Start date'","description":"Placeholder text shown in the trigger for an empty `start` endpoint."},{"name":"emptyEndLabel","type":"string","default":"'End date'","description":"Placeholder text shown in the trigger for an empty `end` endpoint."},{"name":"placeholder","type":"string | undefined","default":"undefined","description":"When set, overrides the composed `${emptyStartLabel}${rangeSeparator}${emptyEndLabel}` placeholder with a single string."},{"name":"disabledInput","type":"boolean","default":"false","description":"When true, the trigger cannot open the overlay and `aria-disabled=\"true\"` is set. Defaults to `false`.","alias":"disabled","transform":"booleanAttribute"},{"name":"requiredInput","type":"boolean","default":"false","description":"When true, exposes `aria-required=\"true\"`. `Validators.required` on a bound `NgControl` is also honoured. Defaults to `false`.","alias":"required","transform":"booleanAttribute"},{"name":"size","type":"TwSize","default":"'md'","description":"Trigger padding, font size, and calendar cell density. Uses the shared `TwSize` scale. Defaults to `'md'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color for focused border, calendar range fill, and preset active state. Defaults to `'primary'`."},{"name":"variant","type":"DateRangePickerVariant | undefined","default":"undefined","description":"Visual style of the trigger. `'default'` draws its own border; `'naked'` strips chrome so a parent (e.g. `tw-form-field`) owns it. Auto-resolves to `'naked'` when inside a form-field; otherwise `'default'`."},{"name":"numberOfMonths","type":"DateRangePickerMonths","default":"2","description":"How many months the overlay shows side-by-side. `2` is the standard range-picker layout; use `1` for compact contexts. Defaults to `2`."},{"name":"presets","type":"readonly DateRangePreset<D>[]","default":"[]","description":"Optional quick-select presets rendered as a vertical list before the calendars. Each preset provides a label and a factory returning a `TwDateRange<D>`. An empty array hides the preset panel."},{"name":"showClear","type":"boolean","default":"true","description":"Whether to show a clear-button affordance inside the trigger when a value is set. Defaults to `true`."},{"name":"minRangeLength","type":"number | null","default":"null","description":"Minimum range length in days, inclusive. Commits shorter than this are rejected and surface `calendarRangeTooShort` on the bound `NgControl`. `null` = no minimum. Defaults to `null`."},{"name":"maxRangeLength","type":"number | null","default":"null","description":"Maximum range length in days, inclusive. Commits longer than this are rejected and surface `calendarRangeTooLong` on the bound `NgControl`. `null` = no maximum. Defaults to `null`."},{"name":"rangeBehavior","type":"Partial<RangeBehaviorConfig>","default":"{}","description":"Range-mode behavior knobs forwarded to the embedded calendar. Accepts a partial config — unset fields use the documented defaults on `RangeBehaviorConfig`. Defaults: `{ allowSingleDayRange: true, persistPartialRange: true, allowBackwardRange: false, disableRangesCrossingDisabledDates: false }`."},{"name":"rangeClickBehavior","type":"RangeClickBehavior","default":"'restart'","description":"How the embedded calendar reacts to a click after a complete range. `'restart'` (default) starts a fresh draft; `'nearest-edge'` moves the nearer endpoint; `'require-clear'` blocks until cleared."},{"name":"firstDayOfWeek","type":"number | null","default":"null","description":"Override first day of week (0=Sun, 1=Mon) on the embedded calendar. Falls back to the adapter's default."},{"name":"locale","type":"string | null","default":"null","description":"Per-instance locale override. Forwarded to the embedded calendar and the underlying `DateAdapter` so the trigger display tracks the picker's locale. Falls back to Angular `LOCALE_ID` when `null`."},{"name":"dateClass","type":"DateClassFn<D> | null","default":"null","description":"Function producing per-cell CSS classes on the embedded calendar."},{"name":"cellTemplate","type":"TemplateRef<{ $implicit: CalendarCell<D> }> | null","default":"null","description":"Optional cell-content template, forwarded to the embedded calendar. Use to customize cell visuals beyond `dateClass`."},{"name":"showActions","type":"boolean","default":"false","description":"When true, renders a `Today / Clear / Cancel / Apply` action bar at the bottom of the overlay. The calendar commits on the second click by default — turn this on for touch-heavy contexts. Defaults to `false`."},{"name":"showTime","type":"boolean","default":"false","description":"When true, the overlay renders two `<tw-time-picker>` instances so users can pick times for the start and end of the range. Defaults to `false`."},{"name":"timeFormat","type":"TimePickerFormat","default":"'24h'","description":"Format of the embedded time-pickers. `'24h'` renders 00–23; `'12h'` adds an AM/PM toggle. Defaults to `'24h'`."},{"name":"showSeconds","type":"boolean","default":"false","description":"Whether the embedded time-pickers expose a seconds field. Defaults to `false`."},{"name":"hourStep","type":"number","default":"1","description":"Step for the embedded time-pickers' hour fields. Defaults to `1`."},{"name":"minuteStep","type":"number","default":"1","description":"Step for the embedded time-pickers' minute fields. Defaults to `1`."},{"name":"secondStep","type":"number","default":"1","description":"Step for the embedded time-pickers' second fields. Defaults to `1`."},{"name":"todayLabel","type":"string","default":"'Today'","description":"Label for the `Today` action in the overlay's action bar."},{"name":"clearLabel","type":"string","default":"'Clear'","description":"Label for the `Clear` action in the overlay's action bar."},{"name":"cancelLabel","type":"string","default":"'Cancel'","description":"Label for the `Cancel` action in the overlay's action bar."},{"name":"applyLabel","type":"string","default":"'Apply'","description":"Label for the `Apply` action in the overlay's action bar."},{"name":"panelClass","type":"string | readonly string[]","default":"''","description":"Extra class(es) applied to the overlay panel element. `twMerge` resolves conflicts with internal classes."},{"name":"scrollStrategy","type":"'reposition' | 'close' | 'block'","default":"'reposition'","description":"CDK scroll strategy for the overlay. Defaults to `'reposition'`."},{"name":"offset","type":"number","default":"4","description":"Pixel distance between trigger and overlay. Defaults to `4`."},{"name":"clearAriaLabel","type":"string","default":"'Clear date range'","description":"Accessible label for the clear button. Defaults to `'Clear date range'`."},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the `ErrorStateMatcher`. When omitted, uses the injected `TW_ERROR_STATE_MATCHER`."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the trigger. Required when no visible label is supplied via `tw-form-field` or an external `aria-labelledby`. Alias: `aria-label`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the trigger. Alias: `aria-labelledby`.","alias":"aria-labelledby"},{"name":"userAriaDescribedByInput","type":"string | undefined","default":"undefined","description":"Consumer-supplied `aria-describedby` ids. The form-field preserves these when merging hint/error ids. Alias: `aria-describedby`.","alias":"aria-describedby"}],"outputs":[{"name":"opened","payloadType":"DateRangePickerOpenedEvent","description":"Fires after the overlay's enter animation completes. Payload is the trigger element."},{"name":"closed","payloadType":"DateRangePickerCloseReason","description":"Fires after the overlay's leave animation completes. Payload is the reason it closed."},{"name":"rangeChange","payloadType":"DateRangePickerChangeEvent<D>","description":"Fires after a commit — either from completing a range in the calendar, picking a preset, or applying via the action bar."},{"name":"presetSelected","payloadType":"DateRangePreset<D>","description":"Fires when the user picks a preset from the list. Payload is the preset descriptor. Fires in addition to `rangeChange`."}],"models":[{"name":"value","type":"TwDateRange<D> | null","default":"null","description":"Two-way bound selected range. `null` when no selection. Setting programmatically updates the trigger display and the calendar selection; it does NOT trigger `onChange`."},{"name":"open","type":"boolean","default":"false","description":"Two-way bound open state of the overlay. Setting to `true` opens; setting to `false` closes."}],"methods":[{"name":"openPicker","signature":"openPicker(): void","description":"Opens the overlay. No-op when disabled or already open."},{"name":"closePicker","signature":"closePicker(): void","description":"Closes the overlay. No-op when already closed."},{"name":"toggle","signature":"toggle(): void","description":"Toggles the overlay's open state."},{"name":"clear","signature":"clear(): void","description":"Clears the current range and emits `rangeChange` with `source: 'clear'`."}],"formControl":true,"extends":"FormFieldControl"},{"name":"DateRangePickerVariant","kind":"type","description":"Visual style of the date-range-picker trigger.","definition":"'default' | 'naked'"},{"name":"DateRangePickerMonths","kind":"type","description":"How many months the overlay displays side-by-side.","definition":"1 | 2"},{"name":"DateRangePickerChangeSource","kind":"type","description":"Origin of a value change, used to distinguish user input from programmatic writes.","definition":"| 'calendar' | 'preset' | 'time' | 'apply' | 'clear' | 'programmatic'"},{"name":"DateRangePickerCloseReason","kind":"type","description":"Reason the overlay closed.","definition":"| 'select' | 'apply' | 'cancel' | 'escape' | 'backdrop' | 'programmatic'"},{"name":"DateRangePickerChangeEvent","kind":"interface","description":"Emitted by `rangeChange` after a committed value update.","members":[{"name":"value","type":"TwDateRange<D> | null","optional":false,"description":"The committed range (`null` when cleared)."},{"name":"previousValue","type":"TwDateRange<D> | null","optional":false,"description":"The range before this change."},{"name":"source","type":"DateRangePickerChangeSource","optional":false,"description":"What triggered the change."}]},{"name":"DateRangePickerOpenedEvent","kind":"interface","description":"Emitted by `opened`.","members":[{"name":"trigger","type":"HTMLElement","optional":false,"description":"The trigger element."}]},{"name":"DateRangePreset","kind":"interface","description":"A quick-select preset rendered in the overlay's preset list.","members":[{"name":"label","type":"string","optional":false,"description":"Label shown on the preset button."},{"name":"range","type":"() => TwDateRange<D>","optional":false,"description":"Factory returning the range to apply when this preset is chosen. Called fresh each click so \"today\"-relative presets stay current."},{"name":"id","type":"string","optional":true,"description":"Optional identifier — surfaced in `presetSelected` and used to detect the active preset for visual state."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"/** Visual style of the trigger. */\nexport type DateRangePickerVariant = 'default' | 'naked';\n\n/** How many months the overlay displays side-by-side. */\nexport type DateRangePickerMonths = 1 | 2;\n\n/** Origin of a value change, used to distinguish user input from programmatic writes. */\nexport type DateRangePickerChangeSource =\n | 'calendar'\n | 'preset'\n | 'time'\n | 'apply'\n | 'clear'\n | 'programmatic';\n\n/** Reason the overlay closed. */\nexport type DateRangePickerCloseReason =\n | 'select'\n | 'apply'\n | 'cancel'\n | 'escape'\n | 'backdrop'\n | 'programmatic';\n\n/** Emitted by rangeChange after a committed value update. */\nexport interface DateRangePickerChangeEvent<D> {\n readonly value: TwDateRange<D> | null;\n readonly previousValue: TwDateRange<D> | null;\n readonly source: DateRangePickerChangeSource;\n}\n\n/** Emitted by opened. */\nexport interface DateRangePickerOpenedEvent {\n readonly trigger: HTMLElement;\n}\n\n/** A quick-select preset rendered in the overlay's preset list. */\nexport interface DateRangePreset<D = Date> {\n readonly label: string;\n readonly range: () => TwDateRange<D>;\n readonly id?: string;\n}\n\n// Re-exported from '@cdevhub/ngx-tw/calendar' — use directly:\n// import { TwDateRange } from '@cdevhub/ngx-tw/calendar';\nexport class TwDateRange<D> {\n constructor(\n public readonly start: D | null,\n public readonly end: D | null,\n ) {}\n get complete(): boolean;\n get empty(): boolean;\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (size of sizes; track size) {\n <tw-date-range-picker\n [size]=\"size\"\n [(value)]=\"sizeValues[size]\"\n [emptyStartLabel]=\"'Size: ' + size\"\n emptyEndLabel=\"End date\"\n [aria-label]=\"'Date range picker ' + size\"\n />\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (color of colors; track color) {\n <tw-date-range-picker\n [color]=\"color\"\n [(value)]=\"colorValues[color]\"\n [emptyStartLabel]=\"color\"\n emptyEndLabel=\"end\"\n [aria-label]=\"color\"\n />\n}"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled: blocks opening the overlay -->\n<tw-date-range-picker [(value)]=\"disabledValue\" [disabled]=\"true\" aria-label=\"Disabled\" />\n\n<!-- Required: surfaces aria-required=\"true\" and participates in form validation -->\n<tw-date-range-picker [(value)]=\"requiredValue\" [required]=\"true\" aria-label=\"Required\" />"},{"id":"monthLayoutSnippet","title":"Month layout","language":"html","code":"<!-- Two months (default) -->\n<tw-date-range-picker [(value)]=\"twoMonthValue\" [numberOfMonths]=\"2\" />\n\n<!-- One month (compact) -->\n<tw-date-range-picker [(value)]=\"oneMonthValue\" [numberOfMonths]=\"1\" />"},{"id":"presetsTsSnippet","title":"Presets","language":"ts","code":"protected readonly reportPresets: readonly DateRangePreset<Date>[] = [\n { id: 'today', label: 'Today', range: () => new TwDateRange(today, today) },\n { id: 'last-7', label: 'Last 7 days', range: () => new TwDateRange(addDays(today, -6), today) },\n { id: 'last-30', label: 'Last 30 days', range: () => new TwDateRange(addDays(today, -29), today) },\n { id: 'this-month', label: 'This month', range: () => new TwDateRange(firstOfMonth(today), today) },\n { id: 'ytd', label: 'Year to date', range: () => new TwDateRange(firstOfYear(today), today) },\n];"},{"id":"presetsHtmlSnippet","title":"Presets","language":"html","code":"<tw-date-range-picker\n [(value)]=\"reportRange\"\n [presets]=\"reportPresets\"\n aria-label=\"Report period\"\n/>"},{"id":"constraintsSnippet","title":"Min, max filter","language":"html","code":"<tw-date-range-picker\n [(value)]=\"vacation\"\n [minDate]=\"today\"\n [maxDate]=\"sixtyDaysOut\"\n [dateFilter]=\"weekdayFilter\"\n aria-label=\"Vacation\"\n/>"},{"id":"withTimeSnippet","title":"With time","language":"html","code":"<!-- 24h, minute precision -->\n<tw-date-range-picker\n [(value)]=\"meetingWindow\"\n [showTime]=\"true\"\n aria-label=\"Meeting window\"\n/>\n\n<!-- 12h + seconds + action bar -->\n<tw-date-range-picker\n [(value)]=\"bookingWindow\"\n [showTime]=\"true\"\n timeFormat=\"12h\"\n [showSeconds]=\"true\"\n [showActions]=\"true\"\n color=\"accent\"\n aria-label=\"Booking window\"\n/>"},{"id":"actionBarSnippet","title":"Action bar","language":"html","code":"<tw-date-range-picker\n [(value)]=\"eventRange\"\n [showActions]=\"true\"\n color=\"accent\"\n size=\"lg\"\n aria-label=\"Event range\"\n/>"},{"id":"lengthConstraintsSnippet","title":"Length click behavior","language":"html","code":"<tw-form-field>\n <label twLabel>Stay (3 – 14 nights)</label>\n <tw-date-range-picker\n [formControl]=\"stayCtrl\"\n [minDate]=\"today\"\n [minRangeLength]=\"3\"\n [maxRangeLength]=\"14\"\n rangeClickBehavior=\"nearest-edge\"\n aria-label=\"Stay window\"\n />\n <span twHint>Pick a window between 3 and 14 nights.</span>\n <span twError>Range is too short or too long.</span>\n</tw-form-field>"},{"id":"tdTsSnippet","title":"Template-driven forms","language":"ts","code":"protected holiday: TwDateRange<Date> | null = null;"},{"id":"tdHtmlSnippet","title":"Template-driven forms","language":"html","code":"<tw-form-field>\n <label twLabel>Holiday</label>\n <tw-date-range-picker name=\"holiday\" [(ngModel)]=\"holiday\" required />\n <span twHint>Pick a start and end day.</span>\n</tw-form-field>"},{"id":"reactiveTsSnippet","title":"Reactive forms","language":"ts","code":"protected readonly reportCtrl = new FormControl<TwDateRange<Date> | null>(null, [\n Validators.required,\n]);"},{"id":"reactiveHtmlSnippet","title":"Reactive forms","language":"html","code":"<tw-form-field>\n <label twLabel>Report period</label>\n <tw-date-range-picker\n [formControl]=\"reportCtrl\"\n [minDate]=\"ninetyDaysAgo\"\n [maxDate]=\"today\"\n [presets]=\"reportPresets\"\n />\n <span twHint>Any 90-day window ending today.</span>\n <span twError>Please pick a valid range.</span>\n</tw-form-field>"},{"id":"signalTsSnippet","title":"Signal forms","language":"ts","code":"protected readonly campaignModel = signal<{ window: TwDateRange<Date> | null }>({ window: null });\nprotected readonly campaignForm = form(this.campaignModel, (path) => {\n required(path.window, { message: 'Campaign window is required.' });\n});"},{"id":"signalHtmlSnippet","title":"Signal forms","language":"html","code":"<tw-form-field>\n <label twLabel>Campaign window</label>\n <tw-date-range-picker\n [formField]=\"campaignForm.window\"\n [minDate]=\"today\"\n />\n <span twHint>Start no earlier than today.</span>\n <span twError>Campaign window is required.</span>\n</tw-form-field>"},{"id":"formFieldSnippet","title":"Inside form-field (auto-naked)","language":"html","code":"<tw-form-field>\n <label twLabel>Trip window</label>\n <tw-date-range-picker [(value)]=\"tripRange\" [minDate]=\"today\" aria-label=\"Trip window\" />\n <span twHint>When you'll be away.</span>\n</tw-form-field>\n\n<tw-form-field appearance=\"filled\" color=\"success\">\n <label twLabel>Launch window</label>\n <tw-date-range-picker [(value)]=\"launchRange\" aria-label=\"Launch window\" />\n</tw-form-field>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-date-range-picker\n [(value)]=\"range\"\n aria-label=\"Booking window\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"// 1. Provide the adapter once in your app config:\nimport { provideNativeDateAdapter } from '@cdevhub/ngx-tw/calendar';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideNativeDateAdapter()],\n};\n\n// 2. Import the component where you use it:\nimport { DateRangePickerComponent } from '@cdevhub/ngx-tw/date-range-picker';\nimport { TwDateRange } from '@cdevhub/ngx-tw/calendar';"}],"summary":"Two-endpoint range form control that opens a modal overlay with one or two linked month grids, hover preview, and optional quick-select presets.","whenToUse":["Booking windows, reporting periods, or filters defined by a start and an end date","Range selection that reads better across two side-by-side months with lockstep pagination","Offering shortcuts such as Today, Last 7 days, or This month next to the grid","Capturing a time of day on each endpoint as well as the dates"],"whenNotToUse":[{"instead":"date-picker","because":"only one date is being captured, not a span"},{"instead":"calendar","because":"the range grid should stay visible inline, or the selection behaviour needs a custom strategy on the primitive"},{"instead":"slider","because":"the range is over a plain numeric axis rather than over dates"}],"related":["date-picker","calendar","time-picker","form-field"],"aliases":["date range","daterangepicker","range picker","period picker","from to dates","start end date","booking dates","reporting period"],"hasMeta":true,"metaPath":"projects/ngx-tw/date-range-picker/date-range-picker.meta.ts"},{"name":"time-picker","importPath":"@cdevhub/ngx-tw/time-picker","symbols":[{"name":"TimePickerComponent","kind":"component","description":"","selector":"tw-time-picker","usage":[{"form":"element","selector":"tw-time-picker","name":"tw-time-picker"}],"contentSlots":[{"select":"[slot=stepper-up]"},{"select":"[slot=stepper-down]"}],"inputs":[{"name":"idInput","type":"string | undefined","default":"undefined","description":"Id on the time-picker's host element. Auto-generated when not provided. Used by the form-field's `<label for>` attribute.","alias":"id"},{"name":"disabledInput","type":"boolean","default":"false","description":"When true, the whole component is disabled and every field sets `aria-disabled=\"true\"`. Defaults to `false`.","alias":"disabled","transform":"booleanAttribute"},{"name":"requiredInput","type":"boolean","default":"false","description":"When true, exposes `aria-required=\"true\"`. Validators.required on a bound NgControl is also honoured. Defaults to `false`.","alias":"required","transform":"booleanAttribute"},{"name":"readonlyInput","type":"boolean","default":"false","description":"When true, blocks typing, stepping, and the AM/PM toggle — the value is still read-only visible. Defaults to `false`.","alias":"readonly","transform":"booleanAttribute"},{"name":"size","type":"TwSize","default":"'md'","description":"Controls field height, font size, and stepper density. Defaults to `'md'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color used for the focused border and focus ring. Defaults to `'primary'`."},{"name":"variant","type":"TimePickerVariant | undefined","default":"undefined","description":"Visual style of the chrome. `'default'` draws its own border; `'naked'` strips chrome. Auto-resolves to `'naked'` inside `<tw-form-field>`."},{"name":"format","type":"TimePickerFormat","default":"'24h'","description":"`'12h'` renders 1–12 hours with an AM/PM toggle; `'24h'` renders 00–23. Defaults to `'24h'`."},{"name":"showSeconds","type":"boolean","default":"false","description":"When true, renders a seconds field after minutes. Defaults to `false`."},{"name":"hourStep","type":"number","default":"1","description":"Amount to add/subtract when stepping hours. Defaults to `1`."},{"name":"minuteStep","type":"number","default":"1","description":"Amount to add/subtract when stepping minutes. Defaults to `1`."},{"name":"secondStep","type":"number","default":"1","description":"Amount to add/subtract when stepping seconds. Defaults to `1`."},{"name":"minTime","type":"D | null","default":"null","description":"Earliest accepted time-of-day. Values earlier than this set `errorState` and mark the bound control invalid with `timePickerMin`. Defaults to `null`. Note: when `showSeconds` is `false`, the seconds component of any value still participates in the range comparison — supply `minTime` with zeroed seconds to match the 2-field display."},{"name":"maxTime","type":"D | null","default":"null","description":"Latest accepted time-of-day. Values later than this set `errorState` and mark the bound control invalid with `timePickerMax`. Defaults to `null`. Note: when `showSeconds` is `false`, the seconds component of any value still participates in the range comparison — supply `maxTime` with zeroed seconds to match the 2-field display."},{"name":"referenceDate","type":"D | null","default":"null","description":"Date portion used when the user types a time while `value` is `null`. Defaults to today."},{"name":"placeholder","type":"string | undefined","default":"undefined","description":"Placeholder shown in each field when empty. Defaults to `'--'`."},{"name":"showSteppers","type":"boolean","default":"true","description":"Whether to render the up/down stepper column. Defaults to `true`."},{"name":"showClear","type":"boolean","default":"true","description":"Whether to render the clear affordance when a value is set. Defaults to `true`."},{"name":"clearLabel","type":"string","default":"'Clear time'","description":"Accessible label for the clear button. Defaults to `'Clear time'`."},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the `ErrorStateMatcher`."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the fields group. Required when no visible label is supplied. Alias: `aria-label`.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element that labels the group. Alias: `aria-labelledby`.","alias":"aria-labelledby"},{"name":"userAriaDescribedByInput","type":"string | undefined","default":"undefined","description":"Consumer-supplied `aria-describedby` ids. The form-field preserves these when merging hint/error ids. Alias: `aria-describedby`.","alias":"aria-describedby"}],"outputs":[{"name":"timeInput","payloadType":"TimePickerInputEvent<D>","description":"Fires on every keystroke, stepper press, or AM/PM toggle — before the new value is committed."},{"name":"timeChange","payloadType":"TimePickerChangeEvent<D>","description":"Fires after a committed change — digit + auto-advance, stepper, meridiem toggle, clear, or programmatic write."}],"models":[{"name":"value","type":"D | null","default":"null","description":"Two-way bound current time. `null` when empty. Setting programmatically updates the fields without firing `onChange`."}],"methods":[{"name":"focus","signature":"focus(): void","description":"Moves focus to the hours field."},{"name":"clear","signature":"clear(): void","description":"Clears the value and emits `timeChange` with `source: 'clear'`."}],"formControl":true,"extends":"FormFieldControl"},{"name":"TimePickerVariant","kind":"type","description":"Visual style of the time-picker chrome.","definition":"'default' | 'naked'"},{"name":"TimePickerField","kind":"type","description":"Logical field inside the time-picker.","definition":"'hour' | 'minute' | 'second' | 'meridiem'"},{"name":"TimePickerChangeSource","kind":"type","description":"Origin of a value change — useful when consumers need to distinguish user input from programmatic writes.","definition":"| 'input' | 'stepper' | 'meridiem' | 'clear' | 'programmatic'"},{"name":"TimePickerChangeEvent","kind":"interface","description":"Emitted by `timeChange` after a committed value update.","members":[{"name":"value","type":"D | null","optional":false,"description":"The committed value (`null` when cleared)."},{"name":"previousValue","type":"D | null","optional":false,"description":"The value before this change."},{"name":"source","type":"TimePickerChangeSource","optional":false,"description":"What triggered the change."}]},{"name":"TimePickerInputEvent","kind":"interface","description":"Emitted by `timeInput` before a commit, on every keystroke or stepper press.","members":[{"name":"field","type":"TimePickerField","optional":false,"description":"Which field was edited."},{"name":"rawText","type":"string","optional":false,"description":"The raw text currently in that field (pre-commit)."},{"name":"parsed","type":"D | null","optional":false,"description":"The parsed value if fields form a valid time, else `null`."}]},{"name":"TimePickerValidationErrors","kind":"type","description":"Validation errors a `tw-time-picker` can place on its bound control. Mirrors the shape of `CalendarValidationErrors` so the two pickers report constraint violations the same way: a code keyed by what was violated, with the offending value and the bound it broke.","definition":"Partial<{ timePickerMin: { min: unknown; actual: unknown }; timePickerMax: { max: unknown; actual: unknown }; }>"},{"name":"TimePickerIntl","kind":"service","description":"Localized strings + ARIA announcements consumed by the time-picker. Override per-field via Angular DI — provide a custom instance, or supply a `Partial<TimePickerIntl>` through `provideTimePickerIntl`. Unset fields fall through to the English defaults shipped here. All members take primitives so consumers can write straight string literals or template helpers without depending on internal types.","methods":[{"name":"increaseLabel","signature":"increaseLabel(field: string): string","description":"Aria label template for the up stepper. Receives the active field name (`'hours'`, `'minutes'`, `'seconds'`)."},{"name":"decreaseLabel","signature":"decreaseLabel(field: string): string","description":"Aria label template for the down stepper. Receives the active field name (`'hours'`, `'minutes'`, `'seconds'`)."},{"name":"selectedAnnouncement","signature":"selectedAnnouncement(formattedTime: string): string","description":"Live-region message after a commit. Receives the pre-formatted time string (e.g. `'02:30 PM'`)."}]},{"name":"provideTimePickerIntl","kind":"function","description":"Provides a custom `TimePickerIntl` instance. Place at any injector level — the time-picker resolves the closest one.","signature":"provideTimePickerIntl(custom: Partial<TimePickerIntl>): Provider"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TimePickerVariant = 'default' | 'naked';\ntype TimePickerFormat = '12h' | '24h';\ntype TimePickerMeridiem = 'AM' | 'PM';\ntype TimePickerField = 'hour' | 'minute' | 'second' | 'meridiem';\ntype TimePickerChangeSource =\n | 'input'\n | 'stepper'\n | 'meridiem'\n | 'clear'\n | 'programmatic';\n\ninterface TimePickerChangeEvent<D> {\n value: D | null;\n previousValue: D | null;\n source: TimePickerChangeSource;\n}\n\ninterface TimePickerInputEvent<D> {\n field: TimePickerField;\n rawText: string;\n parsed: D | null;\n}"},{"id":"intlSnippet","title":"Internationalisation","language":"ts","code":"import { provideTimePickerIntl } from '@cdevhub/ngx-tw/time-picker';\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideTimePickerIntl({\n groupLabel: 'Heure',\n hoursLabel: 'Heures',\n minutesLabel: 'Minutes',\n secondsLabel: 'Secondes',\n meridiemGroupLabel: 'AM ou PM',\n amLabel: 'Matin',\n pmLabel: 'Soir',\n clearLabel: 'Effacer l\\'heure',\n clearedAnnouncement: 'Heure effacée',\n selectedAnnouncement: (time) => `${time} sélectionnée`,\n }),\n ],\n};"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"<tw-time-picker variant=\"default\" [(value)]=\"time\" aria-label=\"Default\" />\n<tw-time-picker variant=\"naked\" [(value)]=\"time\" aria-label=\"Naked\" />\n\n<!-- Auto-resolves to naked inside tw-form-field -->\n<tw-form-field>\n <label twLabel>Time</label>\n <tw-time-picker [(value)]=\"time\" />\n</tw-form-field>"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (color of colors; track color) {\n <tw-time-picker\n [color]=\"color\"\n [(value)]=\"colorValues[color]\"\n [attr.aria-label]=\"color\"\n />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (size of sizes; track size) {\n <tw-time-picker\n [size]=\"size\"\n [(value)]=\"sizeValues[size]\"\n [attr.aria-label]=\"'Size ' + size\"\n />\n}"},{"id":"formatSnippet","title":"Format seconds","language":"html","code":"<tw-time-picker format=\"24h\" [(value)]=\"time\" aria-label=\"24-hour time\" />\n<tw-time-picker format=\"12h\" [(value)]=\"time\" aria-label=\"12-hour time\" />\n<tw-time-picker format=\"24h\" [showSeconds]=\"true\" [(value)]=\"time\" aria-label=\"Time with seconds\" />"},{"id":"stepsSnippet","title":"Stepping intervals","language":"html","code":"<tw-time-picker [(value)]=\"time\" [minuteStep]=\"15\" aria-label=\"Quarter-hour time\" />\n\n<tw-time-picker\n [(value)]=\"time\"\n [hourStep]=\"2\"\n [minuteStep]=\"5\"\n aria-label=\"Five-minute time\"\n/>"},{"id":"minMaxSnippet","title":"Min / max time","language":"html","code":"<tw-time-picker\n [(value)]=\"meeting\"\n [minTime]=\"businessOpen\"\n [maxTime]=\"businessClose\"\n aria-label=\"Meeting time\"\n/>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled — dimmed, no interactions -->\n<tw-time-picker [(value)]=\"value\" [disabled]=\"true\" aria-label=\"Disabled time picker\" />\n\n<!-- Readonly — visible and focusable, but refuses edits -->\n<tw-time-picker [(value)]=\"value\" [readonly]=\"true\" aria-label=\"Readonly time picker\" />"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected callTime: Date | null = new Date();"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Preferred call time</label>\n <tw-time-picker name=\"callTime\" [(ngModel)]=\"callTime\" format=\"12h\" />\n <span twHint>When we can reach you.</span>\n</tw-form-field>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly alarmCtrl = new FormControl<Date | null>(\n null,\n [Validators.required],\n);"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Alarm</label>\n <tw-time-picker [formControl]=\"alarmCtrl\" [showSeconds]=\"true\" />\n <span twHint>Required. Pick a time.</span>\n <span twError>Alarm is required.</span>\n</tw-form-field>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly standupModel = signal<{ standupAt: Date | null }>({ standupAt: null });\nprotected readonly standupForm = form(this.standupModel, (path) => {\n required(path.standupAt, { message: 'Standup time is required.' });\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-form-field>\n <label twLabel>Standup time</label>\n <tw-time-picker [formField]=\"standupForm.standupAt\" [minuteStep]=\"15\" />\n <span twHint>Quarter-hour increments.</span>\n <span twError>Standup time is required.</span>\n</tw-form-field>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-time-picker\n [(value)]=\"time\"\n aria-label=\"Basic time\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { TimePickerComponent } from '@cdevhub/ngx-tw/time-picker';\n\n// Required once in your app config — shared with tw-calendar and tw-date-picker.\nimport { provideNativeDateAdapter } from '@cdevhub/ngx-tw/calendar';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideNativeDateAdapter()],\n};"}],"summary":"Segmented time-of-day editor where hours, minutes, optional seconds, and an optional AM/PM toggle are each an individually keyboard-editable spinbutton.","whenToUse":["Capturing a time with no date attached, such as an opening hour or a daily reminder","Keyboard-first time entry where digits auto-advance between fields and arrows step each unit","Restricting entry to a window with minTime and maxTime and surfacing violations through the standard error state","Constraining granularity through per-unit hour, minute, or second steps, or switching between 12h and 24h display","Editing the time half of a timestamp beside a separate date control"],"whenNotToUse":[{"instead":"date-picker","because":"a date is being captured too, and its built-in time fields cover the time half"},{"instead":"select","because":"the user should pick from a short fixed list of slots rather than type any time"},{"instead":"number-input","because":"the value is a plain duration or count of minutes, not a clock time"}],"related":["date-picker","calendar","date-range-picker","form-field","number-input"],"aliases":["timepicker","time input","clock","hour minute","time of day","am pm","24h","time field"],"hasMeta":true,"metaPath":"projects/ngx-tw/time-picker/time-picker.meta.ts"},{"name":"slider","importPath":"@cdevhub/ngx-tw/slider","symbols":[{"name":"SliderComponent","kind":"component","description":"Select a single value or a numeric range from a scale.","selector":"tw-slider","usage":[{"form":"element","selector":"tw-slider","name":"tw-slider"}],"inputs":[{"name":"min","type":"number","default":"0","description":"Lower bound of the slider scale. Defaults to `0`."},{"name":"max","type":"number","default":"100","description":"Upper bound of the slider scale. Defaults to `100`."},{"name":"step","type":"number | null","default":"1","description":"Step increment used for snapping, keyboard nav, and auto-generated marks. Pass `null` for continuous values. Defaults to `1`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color for the filled portion and thumb border. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Overall scale of the rail, thumb, and typography. Defaults to `'md'`."},{"name":"variant","type":"SliderVariant","default":"'solid'","description":"Visual style of the fill: `'solid'` (vivid), `'soft'` (muted), or `'outline'` (rail bordered, fill color-500). Defaults to `'solid'`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, prevents interaction and applies muted styling. Defaults to `false`."},{"name":"required","type":"boolean","default":"false","description":"When true, sets `aria-required=\"true\"` on the thumb for assistive tech. Defaults to `false`."},{"name":"range","type":"boolean","default":"false","description":"When true, the slider selects a `[start, end]` range and renders two thumbs. Defaults to `false`."},{"name":"marks","type":"SliderMark[] | boolean","default":"false","description":"Tick marks: `true` auto-generates one mark per step, an array supplies custom marks, `false` renders no marks. Defaults to `false`."},{"name":"showMarkLabels","type":"boolean","default":"false","description":"When true, renders the mark labels beneath the rail. Requires `marks` to resolve to a list. Defaults to `false`."},{"name":"showMinMax","type":"boolean","default":"false","description":"When true, renders the min and max values at the ends of the scale. Defaults to `false`."},{"name":"showValue","type":"boolean","default":"false","description":"When true, renders a value bubble above the active thumb (on hover/focus/drag) and the current value in the header. Defaults to `false`."},{"name":"label","type":"string | undefined","default":"undefined","description":"Optional visible label rendered above the rail. When set, the slider is wired to the label via `aria-labelledby`."},{"name":"description","type":"string | undefined","default":"undefined","description":"Optional secondary description rendered under the label. Mirrored to `aria-describedby`."},{"name":"valueFormatter","type":"SliderValueFormatter | undefined","default":"undefined","description":"Custom formatter for the value bubble, min/max labels, and `aria-valuetext`. Defaults to an integer or 2-decimal string."},{"name":"name","type":"string | undefined","default":"undefined","description":"Optional form name attribute. Purely informational — the component does not render a native input."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the single-value thumb when no visible label is provided.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external element labelling the single-value thumb.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external element describing the slider.","alias":"aria-describedby"},{"name":"ariaLabelStart","type":"string","default":"'Minimum'","description":"Accessible name for the start (lower) thumb in range mode. Defaults to `\"Minimum\"`."},{"name":"ariaLabelEnd","type":"string","default":"'Maximum'","description":"Accessible name for the end (upper) thumb in range mode. Defaults to `\"Maximum\"`."},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, the component uses the `TW_ERROR_STATE_MATCHER` token's value."}],"outputs":[{"name":"valueInput","payloadType":"SliderValue","description":"Fires continuously while the user drags or holds a key. Payload matches the current `value`. Template event name is `input` — the TS-side identifier is `valueInput` to avoid shadowing the imported `input` factory.","alias":"input"},{"name":"change","payloadType":"SliderValue","description":"Fires when the user commits a change (pointer release, key release, or blur after keyboard change). Payload matches the current `value`."}],"models":[{"name":"value","type":"SliderValue","default":"0","description":"Two-way bound slider value. A `number` for single mode, or `[start, end]` for range mode. Updates on every interaction."}],"methods":[{"name":"onTrackPointerDown","signature":"onTrackPointerDown(event: PointerEvent): void","description":"Track click: move the nearest thumb to the clicked position and begin a drag."},{"name":"onThumbPointerDown","signature":"onThumbPointerDown(event: PointerEvent, thumb: ThumbId): void","description":"Thumb press: capture pointer and begin a drag gesture."},{"name":"onThumbKeyDown","signature":"onThumbKeyDown(event: KeyboardEvent, thumb: ThumbId): void","description":"Keyboard navigation: arrows for step, PageUp/Down for 10% jumps, Home/End for extremes."}],"formControl":true},{"name":"SliderMark","kind":"interface","description":"A tick mark on the slider scale. Optionally carries a display label.","members":[{"name":"value","type":"number","optional":false,"description":"Numeric position of the mark on the slider scale. Must be within `[min, max]`."},{"name":"label","type":"string","optional":true,"description":"Optional label rendered beneath the mark when `showMarkLabels` is true."}]},{"name":"SliderValue","kind":"type","description":"Value emitted by the slider. A plain number for single mode, or `[start, end]` for range mode.","definition":"number | readonly [number, number]"},{"name":"SliderValueFormatter","kind":"type","description":"Format function for the value displayed in the value bubble, min/max labels, and `aria-valuetext`.","definition":"(value: number) => string"},{"name":"SliderVariant","kind":"type","description":"Visual style of the slider fill.","definition":"'solid' | 'soft' | 'outline'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type SliderVariant = 'solid' | 'soft' | 'outline';\n\ntype SliderValue = number | readonly [number, number];\n\ninterface SliderMark {\n value: number;\n label?: string;\n}\n\ntype SliderValueFormatter = (value: number) => string;\n\n// From '@cdevhub/ngx-tw/core':\ntype TwColor =\n | 'primary' | 'secondary' | 'accent' | 'neutral'\n | 'info' | 'success' | 'warning' | 'error';\n\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-slider [variant]=\"v\" [label]=\"v\" [showValue]=\"true\" [(value)]=\"variantValues[v]\" />\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-slider [color]=\"c\" [label]=\"c\" [(value)]=\"colorValues[c]\" />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-slider [size]=\"s\" [label]=\"s\" [(value)]=\"sizeValues[s]\" />\n}"},{"id":"rangeSnippet","title":"Range","language":"html","code":"<tw-slider\n label=\"Price\"\n [range]=\"true\"\n color=\"primary\"\n [showValue]=\"true\"\n [showMinMax]=\"true\"\n [min]=\"0\"\n [max]=\"1000\"\n [step]=\"10\"\n [valueFormatter]=\"currencyFormatter\"\n [(value)]=\"priceRange\"\n/>\n\n<tw-slider\n label=\"Working hours\"\n description=\"Schedule your workday\"\n [range]=\"true\"\n color=\"success\"\n variant=\"soft\"\n [min]=\"0\"\n [max]=\"24\"\n [step]=\"1\"\n [marks]=\"true\"\n [(value)]=\"hoursRange\"\n/>"},{"id":"stepMarksTsSnippet","title":"Step Marks","language":"ts","code":"import type { SliderMark, SliderValue } from '@cdevhub/ngx-tw/slider';\n\nconst BRIGHTNESS_MARKS: SliderMark[] = [\n { value: 0, label: 'Off' },\n { value: 25, label: 'Low' },\n { value: 50, label: 'Med' },\n { value: 75, label: 'High' },\n { value: 100, label: 'Max' },\n];\n\nprotected readonly brightnessMarks = BRIGHTNESS_MARKS;\nprotected readonly brightnessValue = signal<SliderValue>(50);\nprotected readonly modeValue = signal<SliderValue>(25);\nprotected readonly continuousValue = signal<SliderValue>(42.7);"},{"id":"stepMarksHtmlSnippet","title":"Step Marks","language":"html","code":"<tw-slider\n label=\"Brightness\"\n color=\"warning\"\n [step]=\"25\"\n [marks]=\"true\"\n [showMarkLabels]=\"true\"\n [(value)]=\"brightnessValue\"\n/>\n\n<tw-slider\n label=\"Mode\"\n description=\"Custom marks with labels\"\n color=\"accent\"\n [step]=\"25\"\n [marks]=\"brightnessMarks\"\n [showMarkLabels]=\"true\"\n [(value)]=\"modeValue\"\n/>\n\n<tw-slider\n label=\"Continuous\"\n description=\"No step snapping — any value in range\"\n color=\"info\"\n [step]=\"null\"\n [showValue]=\"true\"\n [(value)]=\"continuousValue\"\n/>"},{"id":"formatterTsSnippet","title":"Value Formatters","language":"ts","code":"import type { SliderValueFormatter } from '@cdevhub/ngx-tw/slider';\n\nconst PERCENT_FORMATTER: SliderValueFormatter = (value) => `${Math.round(value)}%`;\nconst TEMP_FORMATTER: SliderValueFormatter = (value) => `${Math.round(value)}°C`;\n\nprotected readonly percentFormatter = PERCENT_FORMATTER;\nprotected readonly tempFormatter = TEMP_FORMATTER;"},{"id":"formatterHtmlSnippet","title":"Value Formatters","language":"html","code":"<tw-slider\n label=\"Completion\"\n color=\"success\"\n [showValue]=\"true\"\n [showMinMax]=\"true\"\n [valueFormatter]=\"percentFormatter\"\n [(value)]=\"completionValue\"\n/>\n\n<tw-slider\n label=\"Temperature\"\n color=\"error\"\n variant=\"soft\"\n [min]=\"-10\"\n [max]=\"40\"\n [showValue]=\"true\"\n [showMinMax]=\"true\"\n [valueFormatter]=\"tempFormatter\"\n [(value)]=\"tempValue\"\n/>"},{"id":"statesSnippet","title":"States","language":"html","code":"<tw-slider label=\"Disabled\" [disabled]=\"true\" [value]=\"35\" />\n\n<tw-slider label=\"Disabled range\" [range]=\"true\" [disabled]=\"true\" [value]=\"[20, 70]\" />"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"import { signal } from '@angular/core';\nimport type { SliderValue } from '@cdevhub/ngx-tw/slider';\n\nprotected readonly tdBrightnessValue = signal<SliderValue>(50);"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-slider\n label=\"Brightness\"\n color=\"warning\"\n [showValue]=\"true\"\n name=\"tdBrightness\"\n [(ngModel)]=\"tdBrightnessValue\"\n/>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"import { FormControl } from '@angular/forms';\n\nprotected readonly qualityControl = new FormControl<number>(50, { nonNullable: true });\n\nprotected toggleQualityDisabled(): void {\n if (this.qualityControl.disabled) {\n this.qualityControl.enable();\n } else {\n this.qualityControl.disable();\n }\n}"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-slider\n label=\"Quality\"\n color=\"info\"\n [showValue]=\"true\"\n [formControl]=\"qualityControl\"\n/>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"import { signal } from '@angular/core';\nimport { form } from '@angular/forms/signals';\n\nprotected readonly signalModel = signal<{ fontSize: number }>({ fontSize: 16 });\nprotected readonly signalForm = form(this.signalModel);"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-slider\n label=\"Font size\"\n color=\"accent\"\n [showValue]=\"true\"\n [formField]=\"signalForm.fontSize\"\n/>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-slider label=\"Volume\" [showValue]=\"true\" [(value)]=\"volume\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { SliderComponent } from '@cdevhub/ngx-tw/slider';"}],"summary":"Draggable track for choosing a numeric value, or a contiguous range of two values, from a continuous or stepped scale.","whenToUse":["Picking an approximate value where the relative position matters more than the exact number — volume, brightness, opacity, zoom","A two-thumb range filter such as a price or date span, via [range]","A scale that needs tick marks, either derived from step or supplied as custom SliderMark[] entries with labels","A value that reads better with a formatted bubble and min/max end labels, driven by one valueFormatter that also feeds aria-valuetext","A continuous scale with no snapping, by setting step to null","Vertical orientation or RTL layouts, where arrow keys follow the ambient CDK Directionality"],"whenNotToUse":[{"instead":"number-input","because":"the user needs to type an exact value, or the scale is too wide to target by dragging"},{"instead":"progress-bar","because":"the bar reports progress rather than accepting a value from the user"},{"instead":"select","because":"the choices are a short enumerated list rather than points on a numeric scale"}],"related":["number-input","progress-bar","form-field","core"],"aliases":["range","range slider","range input","track","thumb","volume control","dual slider","price range","scrubber"],"hasMeta":true,"metaPath":"projects/ngx-tw/slider/slider.meta.ts"},{"name":"stepper","importPath":"@cdevhub/ngx-tw/stepper","symbols":[{"name":"StepperComponent","kind":"component","description":"","selector":"tw-stepper","usage":[{"form":"element","selector":"tw-stepper","name":"tw-stepper"}],"exportAs":"twStepper","inputs":[{"name":"variant","type":"StepperVariant","default":"'default'","description":"Visual style of the indicator strip. `'default'` = numbered circles, `'dot'` = compact filled dots, `'simple'` = indicators only (labels hidden visually). Defaults to `'default'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color for active and completed indicators and connectors. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls indicator size and label typography. Defaults to `'md'`."},{"name":"showError","type":"boolean","default":"true","description":"When true, steps with `hasError` render error styling, icon, and `aria-invalid`. Defaults to `true`."},{"name":"headerInteractive","type":"boolean","default":"true","description":"When true, clicking a navigable step header selects it. Set to `false` to only allow advancement via `twStepperNext` / `twStepperPrevious`. Defaults to `true`."}],"extends":"CdkStepper"},{"name":"StepComponent","kind":"component","description":"","selector":"tw-step","usage":[{"form":"element","selector":"tw-step","name":"tw-step"}],"exportAs":"twStep","contentSlots":[{"select":null}],"inputs":[{"name":"description","type":"string","default":"''","description":"Optional descriptive text shown under the step label in the `'default'` variant."}],"extends":"CdkStep"},{"name":"StepLabelDirective","kind":"directive","description":"Structural-style directive on an `<ng-template>` used as a custom step header label. Consumers write `<ng-template twStepLabel>…</ng-template>` inside `<tw-step>`.","selector":"ng-template[twStepLabel]","usage":[{"form":"element-with-attribute","selector":"ng-template[twStepLabel]","name":"ng-template"}],"extends":"CdkStepLabel"},{"name":"StepperIconDirective","kind":"directive","description":"Structural-style directive on an `<ng-template>` that replaces the default indicator icon for a given step state.","selector":"ng-template[twStepperIcon]","usage":[{"form":"element-with-attribute","selector":"ng-template[twStepperIcon]","name":"ng-template"}],"inputs":[{"name":"state","type":"StepState | undefined","default":"undefined","description":"Step state this template overrides. Matches CDK's `StepState` values (`'number' | 'edit' | 'done' | 'error'`)."}]},{"name":"StepperNextDirective","kind":"directive","description":"","selector":"button[twStepperNext]","usage":[{"form":"element-with-attribute","selector":"button[twStepperNext]","name":"button"}],"inputs":[{"name":"type","type":"unknown","description":"Re-exposed from the `CdkStepperNext` host directive.","from":"CdkStepperNext"}]},{"name":"StepperPreviousDirective","kind":"directive","description":"","selector":"button[twStepperPrevious]","usage":[{"form":"element-with-attribute","selector":"button[twStepperPrevious]","name":"button"}],"inputs":[{"name":"type","type":"unknown","description":"Re-exposed from the `CdkStepperPrevious` host directive.","from":"CdkStepperPrevious"}]},{"name":"provideTwStepperOptions","kind":"function","description":"Provides app-wide stepper defaults via `STEPPER_GLOBAL_OPTIONS`.","signature":"provideTwStepperOptions(options: StepperOptions): Provider[]"},{"name":"StepperVariant","kind":"type","description":"Visual style of the step indicator strip.","definition":"'default' | 'dot' | 'simple'"},{"name":"StepperIconContext","kind":"interface","description":"Context passed to custom `*twStepperIcon` templates.","members":[{"name":"$implicit","type":"{ index: number; active: boolean }","optional":false,"description":""}]}],"snippets":[{"id":"provideSnippet","title":"provideTwStepperOptions()","language":"ts","code":"import { provideTwStepperOptions } from '@cdevhub/ngx-tw/stepper';\n\nbootstrapApplication(App, {\n providers: [\n provideTwStepperOptions({ showError: false }),\n ],\n});"},{"id":"typesSnippet","title":"Types","language":"ts","code":"type StepperVariant = 'default' | 'dot' | 'simple';\n\ninterface StepperIconContext {\n $implicit: { index: number; active: boolean };\n}\n\n// Re-exported from @angular/cdk/stepper\ntype StepState = 'number' | 'edit' | 'done' | 'error' | string;\ntype StepperOrientation = 'horizontal' | 'vertical';\n\ninterface StepperOptions {\n showError?: boolean;\n displayDefaultIndicatorType?: boolean;\n}\n\ninterface StepperSelectionEvent {\n selectedIndex: number;\n previouslySelectedIndex: number;\n selectedStep: CdkStep;\n previouslySelectedStep: CdkStep;\n}"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-stepper\n [variant]=\"v\"\n [selectedIndex]=\"variantIndex[v]()\"\n (selectedIndexChange)=\"variantIndex[v].set($event)\"\n >\n <tw-step label=\"Create\" description=\"Start a new project\">…</tw-step>\n <tw-step label=\"Configure\" description=\"Settings and options\">…</tw-step>\n <tw-step label=\"Deploy\" description=\"Ship it\">…</tw-step>\n </tw-stepper>\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-stepper\n [color]=\"c\"\n [selectedIndex]=\"colorIndex[c]()\"\n (selectedIndexChange)=\"colorIndex[c].set($event)\"\n >\n <tw-step label=\"Plan\">…</tw-step>\n <tw-step label=\"Build\">…</tw-step>\n <tw-step label=\"Ship\">…</tw-step>\n </tw-stepper>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-stepper\n [size]=\"s\"\n [selectedIndex]=\"sizeIndex[s]()\"\n (selectedIndexChange)=\"sizeIndex[s].set($event)\"\n >\n <tw-step label=\"Alpha\">…</tw-step>\n <tw-step label=\"Beta\">…</tw-step>\n <tw-step label=\"Gamma\">…</tw-step>\n </tw-stepper>\n}"},{"id":"verticalSnippet","title":"Vertical orientation","language":"html","code":"<tw-stepper\n orientation=\"vertical\"\n [selectedIndex]=\"index()\"\n (selectedIndexChange)=\"index.set($event)\"\n>\n <tw-step label=\"Upload files\" description=\"Drop anything in\">\n <p>Upload a collection of files to process.</p>\n <button twButton twStepperNext size=\"sm\">Next</button>\n </tw-step>\n <tw-step label=\"Transcode\" description=\"Media pipeline\">\n <p>Run the transcoder over your uploads.</p>\n <button twButton variant=\"ghost\" twStepperPrevious size=\"sm\">Back</button>\n <button twButton twStepperNext size=\"sm\">Next</button>\n </tw-step>\n <tw-step label=\"Publish\" description=\"Make it live\">\n <p>Ready to publish to the world.</p>\n <button twButton variant=\"ghost\" twStepperPrevious size=\"sm\">Back</button>\n <button twButton color=\"success\" size=\"sm\">Publish</button>\n </tw-step>\n</tw-stepper>"},{"id":"linearTsSnippet","title":"Linear mode with reactive forms","language":"ts","code":"protected readonly emailControl = new FormControl('', {\n nonNullable: true,\n validators: [Validators.required, Validators.email],\n});\nprotected readonly passwordControl = new FormControl('', {\n nonNullable: true,\n validators: [Validators.required, Validators.minLength(8)],\n});"},{"id":"linearHtmlSnippet","title":"Linear mode with reactive forms","language":"html","code":"<tw-stepper\n linear\n [selectedIndex]=\"index()\"\n (selectedIndexChange)=\"index.set($event)\"\n>\n <tw-step label=\"Email\" [stepControl]=\"emailControl\" errorMessage=\"Enter a valid email\">\n <tw-form-field>\n <label twLabel>Email</label>\n <input twInput type=\"email\" [formControl]=\"emailControl\" />\n </tw-form-field>\n <button twButton twStepperNext size=\"sm\">Next</button>\n </tw-step>\n <tw-step label=\"Password\" [stepControl]=\"passwordControl\" errorMessage=\"Password must be 8+ chars\">\n <tw-form-field>\n <label twLabel>Password</label>\n <input twInput type=\"password\" [formControl]=\"passwordControl\" />\n </tw-form-field>\n <button twButton variant=\"ghost\" twStepperPrevious size=\"sm\">Back</button>\n <button twButton twStepperNext size=\"sm\">Next</button>\n </tw-step>\n <tw-step label=\"Confirm\">\n <button twButton variant=\"ghost\" twStepperPrevious size=\"sm\">Back</button>\n <button twButton color=\"success\" size=\"sm\">Submit</button>\n </tw-step>\n</tw-stepper>"},{"id":"errorSnippet","title":"Error state","language":"html","code":"<tw-stepper\n [selectedIndex]=\"index()\"\n (selectedIndexChange)=\"index.set($event)\"\n>\n <tw-step label=\"Connect\" [hasError]=\"true\" errorMessage=\"Could not reach the server\">\n <p>There was a problem connecting. Check your network.</p>\n </tw-step>\n <tw-step label=\"Verify\">…</tw-step>\n <tw-step label=\"Done\">…</tw-step>\n</tw-stepper>"},{"id":"optionalSnippet","title":"Optional step","language":"html","code":"<tw-stepper\n linear\n [selectedIndex]=\"index()\"\n (selectedIndexChange)=\"index.set($event)\"\n>\n <tw-step label=\"Basics\">\n <button twButton twStepperNext size=\"sm\">Next</button>\n </tw-step>\n <tw-step label=\"Extras\" optional>\n <button twButton variant=\"ghost\" twStepperPrevious size=\"sm\">Back</button>\n <button twButton twStepperNext size=\"sm\">Skip / Next</button>\n </tw-step>\n <tw-step label=\"Review\">…</tw-step>\n</tw-stepper>"},{"id":"customIconsSnippet","title":"Custom icons","language":"html","code":"<tw-stepper\n color=\"accent\"\n [selectedIndex]=\"index()\"\n (selectedIndexChange)=\"index.set($event)\"\n>\n <tw-step label=\"Upload\">\n <ng-template twStepperIcon state=\"number\">\n <svg class=\"size-3/5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5…\" />\n </svg>\n </ng-template>\n <p>Pick files to upload.</p>\n </tw-step>\n <tw-step label=\"Process\">\n <ng-template twStepperIcon state=\"number\">\n <svg class=\"size-3/5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10 18a8 8 0 1 0 0-16…\" />\n </svg>\n </ng-template>\n <p>Processing…</p>\n </tw-step>\n <tw-step label=\"Deliver\">\n <ng-template twStepperIcon state=\"number\">\n <svg class=\"size-3/5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M3.105 2.289…\" />\n </svg>\n </ng-template>\n <p>Delivered.</p>\n </tw-step>\n</tw-stepper>"},{"id":"customLabelsSnippet","title":"Custom labels","language":"html","code":"<tw-stepper\n [selectedIndex]=\"index()\"\n (selectedIndexChange)=\"index.set($event)\"\n>\n <tw-step>\n <ng-template twStepLabel>\n <span class=\"font-semibold text-primary-700\">Step one</span>\n <span class=\"text-xs text-fg-muted ml-1\">— required</span>\n </ng-template>\n <p>Custom label template.</p>\n </tw-step>\n <tw-step>\n <ng-template twStepLabel>\n <span class=\"font-semibold\">Step two</span>\n <span class=\"text-xs text-fg-muted ml-1\">— optional</span>\n </ng-template>\n <p>Mix plain and custom.</p>\n </tw-step>\n <tw-step label=\"Step three\">\n <p>Plain string label for comparison.</p>\n </tw-step>\n</tw-stepper>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-stepper\n [selectedIndex]=\"index()\"\n (selectedIndexChange)=\"index.set($event)\"\n>\n <tw-step label=\"Account\">Account content</tw-step>\n <tw-step label=\"Profile\">Profile content</tw-step>\n <tw-step label=\"Review\">Review content</tw-step>\n</tw-stepper>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n StepperComponent,\n StepComponent,\n StepLabelDirective,\n StepperIconDirective,\n StepperNextDirective,\n StepperPreviousDirective,\n provideTwStepperOptions,\n} from '@cdevhub/ngx-tw/stepper';"}],"summary":"Guides a user through an ordered sequence of steps — a wizard, an onboarding flow, a multi-page checkout — built on Angular CDK CdkStepper.","whenToUse":["A long form split across several screens the user completes in order","Linear flows that must block advancing until the current step validates, via a per-step stepControl (reactive, template-driven, or signal forms)","Checkout, onboarding, or setup wizards that need a visible \"step n of total\" indicator","Flows with optional or non-editable steps, or steps that must surface an error state to screen readers","Vertical step lists with stacked panels on narrow layouts, horizontal strips on wide ones"],"whenNotToUse":[{"instead":"tabs","because":"the views are parallel alternatives the user browses freely, not a sequence with order"},{"instead":"progress-bar","because":"progress is continuous and there are no discrete, navigable steps to select"},{"instead":"timeline","because":"the steps are a read-only record of past events rather than a flow being completed"}],"related":["tabs","progress-bar","form-field","input","button","timeline"],"aliases":["wizard","multi-step form","step indicator","progress steps","onboarding flow","checkout flow","guided flow","step by step"],"hasMeta":true,"metaPath":"projects/ngx-tw/stepper/stepper.meta.ts"},{"name":"paginator","importPath":"@cdevhub/ngx-tw/paginator","symbols":[{"name":"PaginatorComponent","kind":"component","description":"","selector":"tw-paginator","usage":[{"form":"element","selector":"tw-paginator","name":"tw-paginator"}],"exportAs":"twPaginator","inputs":[{"name":"totalItems","type":"number","default":"0","description":"Total number of items across all pages. Defaults to `0`."},{"name":"type","type":"TwPaginatorType","default":"'numbered'","description":"Rendering type. Defaults to `'numbered'`."},{"name":"layout","type":"TwPaginatorLayout","default":"'compact'","description":"Layout density. Defaults to `'compact'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls padding, font size, and icon size. Defaults to `'md'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color used for the active page indicator. Defaults to `'primary'`."},{"name":"siblingCount","type":"number","default":"1","description":"How many sibling pages to show on each side of the current page. Defaults to `1`."},{"name":"boundaryCount","type":"number","default":"1","description":"How many pages to always show at the start and end boundaries. Defaults to `1`."},{"name":"showFirstLastButtons","type":"boolean","default":"true","description":"When true, renders jump-to-first and jump-to-last buttons. Defaults to `true`."},{"name":"showPageSizeSelector","type":"boolean","default":"false","description":"When true, renders the page-size selector region. Defaults to `false`."},{"name":"pageSizeOptions","type":"readonly number[]","default":"[10, 25, 50, 100]","description":"Options for the default page-size selector. Ignored when `*twPaginatorPageSizeSelector` is projected. Defaults to `[10, 25, 50, 100]`."},{"name":"showPageInfo","type":"boolean","default":"true","description":"When true, renders the page-info text region. Defaults to `true`."},{"name":"hideOnEmpty","type":"boolean","default":"true","description":"When true, renders nothing when `totalItems === 0`. Defaults to `true`."},{"name":"hideOnSinglePage","type":"boolean","default":"false","description":"When true, renders nothing when `totalPages <= 1`. Defaults to `false`."},{"name":"responsive","type":"TwPaginatorResponsive","default":"'auto'","description":"Responsive collapse mode. Defaults to `'auto'`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, every button is disabled. Defaults to `false`."},{"name":"labels","type":"Partial<TwPaginatorLabels>","default":"{}","description":"Partial string labels object for i18n. Unset keys fall back to English defaults. Defaults to `{}`."},{"name":"linkFactory","type":"((page: number) => string) | undefined","default":"undefined","description":"When provided, buttons render as anchor links using the returned `href`. Defaults to `undefined` (renders as buttons)."},{"name":"customAriaLabel","type":"string | undefined","default":"undefined","description":"Overrides `labels.ariaLabel` for the root `<nav>` element.","alias":"aria-label"}],"outputs":[{"name":"paginated","payloadType":"TwPaginatorPageChangeEvent","description":"Fires when `page` or `pageSize` changes. Payload includes derived helpers (`start`, `end`, `totalPages`, `source`). Use `[(page)]` / `[(pageSize)]` for two-way binding; subscribe to this for the rich event payload."}],"models":[{"name":"pageSize","type":"number","default":"10","description":"Items per page. Two-way bindable via `[(pageSize)]`. Defaults to `10`."},{"name":"page","type":"number","default":"1","description":"1-based current page. Two-way bindable via `[(page)]`. Clamped internally. Defaults to `1`."}]},{"name":"PaginatorLabelDirective","kind":"directive","description":"Structural directive on an `<ng-template>` that replaces a specific label slot. Usage: `<ng-template twPaginatorLabel slot=\"pageInfo\" let-ctx> … </ng-template>`.","selector":"ng-template[twPaginatorLabel]","usage":[{"form":"element-with-attribute","selector":"ng-template[twPaginatorLabel]","name":"ng-template"}],"inputs":[{"name":"slot","type":"TwPaginatorLabelSlot","required":true,"description":"The label slot this template overrides."}]},{"name":"PaginatorEmptyDirective","kind":"directive","description":"Structural directive on an `<ng-template>` that renders when `totalItems === 0` and `hideOnEmpty` is `false`. When absent, `labels.empty` renders as fallback.","selector":"ng-template[twPaginatorEmpty]","usage":[{"form":"element-with-attribute","selector":"ng-template[twPaginatorEmpty]","name":"ng-template"}]},{"name":"PaginatorPageSizeSelectorDirective","kind":"directive","description":"Structural directive on an `<ng-template>` that replaces the default page-size selector UI entirely. Context `$implicit` exposes `{ pageSize, options, setPageSize }`.","selector":"ng-template[twPaginatorPageSizeSelector]","usage":[{"form":"element-with-attribute","selector":"ng-template[twPaginatorPageSizeSelector]","name":"ng-template"}]},{"name":"TwPaginatorType","kind":"type","description":"Rendering type. `'basic'` shows prev/next + page info only. `'numbered'` shows page buttons with ellipsis range.","definition":"'basic' | 'numbered'"},{"name":"TwPaginatorLayout","kind":"type","description":"Layout density. `'compact'` stacks regions left-to-right. `'spread'` distributes regions across the full container width.","definition":"'compact' | 'spread'"},{"name":"TwPaginatorResponsive","kind":"type","description":"Responsive mode. `'auto'` collapses numbered pages to basic visuals on narrow containers via CSS container queries. `'off'` disables collapsing.","definition":"'auto' | 'off'"},{"name":"TwPaginatorLabelSlot","kind":"type","description":"Named slot accepted by `*twPaginatorLabel`.","definition":"| 'pageInfo' | 'previous' | 'next' | 'first' | 'last' | 'pageSizeLabel'"},{"name":"TwPaginatorLabels","kind":"interface","description":"String labels used throughout the paginator. All are optional on the `labels` input — unset keys fall back to the English defaults.","members":[{"name":"ariaLabel","type":"string","optional":false,"description":"Accessible name for the `<nav>` landmark."},{"name":"previous","type":"string","optional":false,"description":"Label on the Previous button."},{"name":"next","type":"string","optional":false,"description":"Label on the Next button."},{"name":"first","type":"string","optional":false,"description":"Label on the First-page button."},{"name":"last","type":"string","optional":false,"description":"Label on the Last-page button."},{"name":"pageInfo","type":"string","optional":false,"description":"Visible label before the current-page indicator. Used as `\"{pageInfo} {page}{pageInfoSeparator}{totalPages}\"`."},{"name":"pageInfoSeparator","type":"string","optional":false,"description":"Text that joins the current page and total."},{"name":"pageRange","type":"string","optional":false,"description":"Range-style page info template. Variables: `{start}`, `{end}`, `{total}`."},{"name":"pageSizeLabel","type":"string","optional":false,"description":"Visible label next to the page-size selector."},{"name":"announcement","type":"string","optional":false,"description":"`LiveAnnouncer` template used on every page change. Variables: `{page}`, `{totalPages}`, `{start}`, `{end}`, `{total}`."},{"name":"pageButtonAriaLabel","type":"string","optional":false,"description":"Accessible label per numbered page button. Variable: `{page}`."},{"name":"currentPageAriaLabel","type":"string","optional":false,"description":"Accessible label for the current numbered page button. Variable: `{page}`."},{"name":"ellipsis","type":"string","optional":false,"description":"Accessible label applied to ellipsis items."},{"name":"empty","type":"string","optional":false,"description":"Rendered when `hideOnEmpty` is `false` and `totalItems === 0`."}]},{"name":"TwPaginatorPageChangeEvent","kind":"interface","description":"Emitted by `pageChange`.","members":[{"name":"page","type":"number","optional":false,"description":"The new 1-based page."},{"name":"pageSize","type":"number","optional":false,"description":"The new items-per-page."},{"name":"previousPage","type":"number","optional":false,"description":"The previous 1-based page."},{"name":"previousPageSize","type":"number","optional":false,"description":"The previous items-per-page."},{"name":"totalItems","type":"number","optional":false,"description":"Total number of items."},{"name":"totalPages","type":"number","optional":false,"description":"Total number of pages given the current `totalItems` and `pageSize`."},{"name":"start","type":"number","optional":false,"description":"1-based index of the first item on the new page (inclusive)."},{"name":"end","type":"number","optional":false,"description":"1-based index of the last item on the new page (inclusive)."},{"name":"source","type":"'click' | 'keyboard' | 'pageSizeChange' | 'programmatic'","optional":false,"description":"What triggered the change."}]},{"name":"TwPaginatorLabelContext","kind":"interface","description":"Template context provided to every `*twPaginatorLabel` and `*twPaginatorEmpty` template.","members":[{"name":"page","type":"number","optional":false,"description":"1-based current page."},{"name":"totalPages","type":"number","optional":false,"description":"Total pages."},{"name":"start","type":"number","optional":false,"description":"1-based index of the first item on the current page (inclusive)."},{"name":"end","type":"number","optional":false,"description":"1-based index of the last item on the current page (inclusive)."},{"name":"totalItems","type":"number","optional":false,"description":"Total number of items."},{"name":"pageSize","type":"number","optional":false,"description":"Current items per page."},{"name":"disabled","type":"boolean","optional":false,"description":"Whether the paginator is globally disabled."}]},{"name":"TwPaginatorPageSizeSelectorContext","kind":"interface","description":"Template context provided to `*twPaginatorPageSizeSelector`.","members":[{"name":"pageSize","type":"number","optional":false,"description":"Current page size."},{"name":"options","type":"readonly number[]","optional":false,"description":"Available page-size options."},{"name":"setPageSize","type":"(size: number) => void","optional":false,"description":"Updates the page size and re-anchors the current page to keep the same first visible item."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TwPaginatorType = 'basic' | 'numbered';\ntype TwPaginatorLayout = 'compact' | 'spread';\ntype TwPaginatorResponsive = 'auto' | 'off';\ntype TwPaginatorLabelSlot =\n | 'pageInfo'\n | 'previous'\n | 'next'\n | 'first'\n | 'last'\n | 'pageSizeLabel';\n\ninterface TwPaginatorLabels {\n ariaLabel: string;\n previous: string;\n next: string;\n first: string;\n last: string;\n pageInfo: string;\n pageInfoSeparator: string;\n pageRange: string;\n pageSizeLabel: string;\n announcement: string;\n pageButtonAriaLabel: string;\n currentPageAriaLabel: string;\n ellipsis: string;\n empty: string;\n}\n\ninterface TwPaginatorPageChangeEvent {\n page: number;\n pageSize: number;\n previousPage: number;\n previousPageSize: number;\n totalItems: number;\n totalPages: number;\n start: number;\n end: number;\n source: 'click' | 'keyboard' | 'pageSizeChange' | 'programmatic';\n}\n\ninterface TwPaginatorLabelContext {\n page: number;\n totalPages: number;\n start: number;\n end: number;\n totalItems: number;\n pageSize: number;\n disabled: boolean;\n}\n\ninterface TwPaginatorPageSizeSelectorContext {\n pageSize: number;\n options: readonly number[];\n setPageSize: (size: number) => void;\n}\n\n// Shared library types (re-exported from '@cdevhub/ngx-tw/core'):\ntype TwColor = 'primary' | 'secondary' | 'accent' | 'neutral'\n | 'info' | 'success' | 'warning' | 'error';\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"typesSnippet","title":"Types","language":"html","code":"@for (t of types; track t) {\n <tw-paginator [type]=\"t\" [totalItems]=\"100\" [(page)]=\"typePage[t]\" />\n}"},{"id":"layoutsSnippet","title":"Layouts","language":"html","code":"@for (l of layouts; track l) {\n <tw-paginator\n [layout]=\"l\"\n [totalItems]=\"250\"\n [(page)]=\"layoutPage[l]\"\n [(pageSize)]=\"layoutSize[l]\"\n [showPageSizeSelector]=\"true\"\n />\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-paginator [size]=\"s\" [totalItems]=\"100\" [(page)]=\"sizePage[s]\" />\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-paginator [color]=\"c\" [totalItems]=\"80\" [(page)]=\"colorPage[c]\" />\n}"},{"id":"rangeSnippet","title":"Ellipsis Range","language":"html","code":"<tw-paginator\n [totalItems]=\"500\"\n [siblingCount]=\"2\"\n [boundaryCount]=\"1\"\n [(page)]=\"page\"\n/>\n\n<tw-paginator\n [totalItems]=\"500\"\n [siblingCount]=\"0\"\n [boundaryCount]=\"2\"\n [(page)]=\"page\"\n/>"},{"id":"firstLastSnippet","title":"First Last Buttons","language":"html","code":"<tw-paginator\n [totalItems]=\"200\"\n [showFirstLastButtons]=\"false\"\n [(page)]=\"page\"\n/>"},{"id":"pageSizeTsSnippet","title":"Page Size Selector","language":"ts","code":"protected readonly products = PRODUCTS;\nprotected readonly productPage = signal(1);\nprotected readonly productPageSize = signal(5);\n\nprotected readonly visibleProducts = computed(() => {\n const start = (this.productPage() - 1) * this.productPageSize();\n return this.products.slice(start, start + this.productPageSize());\n});"},{"id":"pageSizeHtmlSnippet","title":"Page Size Selector","language":"html","code":"<div class=\"overflow-hidden rounded-lg border border-border\">\n <!-- …table header… -->\n @for (p of visibleProducts(); track p.id) {\n <div class=\"…row…\">\n <span>{{ p.sku }}</span>\n <span>{{ p.name }}</span>\n <!-- … -->\n </div>\n }\n</div>\n\n<tw-paginator\n layout=\"spread\"\n [totalItems]=\"products.length\"\n [showPageSizeSelector]=\"true\"\n [pageSizeOptions]=\"[5, 10, 25]\"\n [(page)]=\"productPage\"\n [(pageSize)]=\"productPageSize\"\n (paginated)=\"onProductPaginated($event)\"\n/>"},{"id":"linkTsSnippet","title":"Link Mode","language":"ts","code":"protected readonly linkHref = (p: number): string => `/products?page=${p}`;\n\nprotected onLinkPaginated(event: TwPaginatorPageChangeEvent): void {\n // client-side state update\n}"},{"id":"linkHtmlSnippet","title":"Link Mode","language":"html","code":"<tw-paginator\n [totalItems]=\"120\"\n [page]=\"linkPage()\"\n [linkFactory]=\"linkHref\"\n (paginated)=\"onLinkPaginated($event)\"\n/>"},{"id":"responsiveSnippet","title":"Responsive Collapse","language":"html","code":"<div class=\"resize-x overflow-auto min-w-64 max-w-full\">\n <tw-paginator\n layout=\"spread\"\n [totalItems]=\"300\"\n [showPageSizeSelector]=\"true\"\n [(page)]=\"page\"\n [(pageSize)]=\"pageSize\"\n />\n</div>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Disabled while a request is in flight -->\n<tw-paginator [totalItems]=\"100\" [disabled]=\"true\" [page]=\"3\" />\n\n<!-- Empty with a default message -->\n<tw-paginator [totalItems]=\"0\" [hideOnEmpty]=\"false\" />\n\n<!-- Empty with a custom template -->\n<tw-paginator [totalItems]=\"0\" [hideOnEmpty]=\"false\">\n <ng-template twPaginatorEmpty>\n <span>No orders match your filters — try broadening the date range.</span>\n </ng-template>\n</tw-paginator>\n\n<!-- Hide when totalPages <= 1 -->\n<tw-paginator [totalItems]=\"5\" [hideOnSinglePage]=\"true\" />\n<tw-paginator [totalItems]=\"50\" [hideOnSinglePage]=\"true\" [(page)]=\"page\" />"},{"id":"labelsTsSnippet","title":"Custom Labels (i18n)","language":"ts","code":"protected readonly frenchLabels = {\n ariaLabel: 'Pagination',\n previous: 'Précédent',\n next: 'Suivant',\n first: 'Première page',\n last: 'Dernière page',\n pageInfo: 'Page',\n pageInfoSeparator: ' sur ',\n pageSizeLabel: 'Par page :',\n announcement: 'Page {page} sur {totalPages}',\n};"},{"id":"labelsHtmlSnippet","title":"Custom Labels (i18n)","language":"html","code":"<tw-paginator\n layout=\"spread\"\n [totalItems]=\"150\"\n [showPageSizeSelector]=\"true\"\n [pageSizeOptions]=\"[10, 25, 50]\"\n [(page)]=\"page\"\n [(pageSize)]=\"pageSize\"\n [labels]=\"frenchLabels\"\n/>"},{"id":"customInfoSnippet","title":"Custom Page-Info Template","language":"html","code":"<tw-paginator\n layout=\"spread\"\n [totalItems]=\"250\"\n [(page)]=\"page\"\n [(pageSize)]=\"pageSize\"\n>\n <ng-template twPaginatorLabel slot=\"pageInfo\" let-ctx>\n Showing\n <strong>{{ ctx.start }}–{{ ctx.end }}</strong>\n of\n <strong>{{ ctx.totalItems }}</strong>\n results\n </ng-template>\n</tw-paginator>"},{"id":"customSelectorSnippet","title":"Custom Page-Size Selector","language":"html","code":"<tw-paginator\n [totalItems]=\"200\"\n [showPageSizeSelector]=\"true\"\n [pageSizeOptions]=\"[10, 25, 50, 100]\"\n [(page)]=\"page\"\n [(pageSize)]=\"pageSize\"\n>\n <ng-template twPaginatorPageSizeSelector let-ctx>\n <div class=\"inline-flex items-center gap-1\">\n @for (opt of ctx.options; track opt) {\n <button\n type=\"button\"\n [class.bg-primary-600]=\"opt === ctx.pageSize\"\n [class.text-white]=\"opt === ctx.pageSize\"\n (click)=\"ctx.setPageSize(opt)\"\n >{{ opt }}</button>\n }\n </div>\n </ng-template>\n</tw-paginator>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-paginator [totalItems]=\"100\" [(page)]=\"page\" />"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n PaginatorComponent,\n PaginatorLabelDirective,\n PaginatorEmptyDirective,\n PaginatorPageSizeSelectorDirective,\n} from '@cdevhub/ngx-tw/paginator';"}],"summary":"Navigates a large dataset one page at a time, as either a compact prev/next control or a full numbered page strip with ellipsis collapsing.","whenToUse":["Paging through the rows of a table, list, or card grid — typically mounted directly below it","Server-side paging where the (paginated) event supplies the exact slice: page, pageSize, start, end, and the previous values","Letting the user change how many rows are shown, via the built-in page-size selector","Narrow containers that must degrade from the numbered strip to compact visuals automatically (container queries, no JS)","SSR or crawler-friendly pagination where each page must be a real anchor, via linkFactory","Localized pagination — every label is overridable through the labels input with token substitution"],"whenNotToUse":[{"instead":"breadcrumbs","because":"the navigation is a hierarchy of ancestors rather than a flat numbered sequence"},{"instead":"progress-bar","because":"the goal is only to display how far along a process is, with nothing to navigate"}],"related":["table","select","button","skeleton","sort"],"aliases":["pagination","pager","page navigation","page numbers","next previous","page size","rows per page","per page","load pages"],"hasMeta":true,"metaPath":"projects/ngx-tw/paginator/paginator.meta.ts"},{"name":"sort","importPath":"@cdevhub/ngx-tw/sort","symbols":[{"name":"SortDirective","kind":"directive","description":"Container directive that holds the current sort state (`active` id + `direction`) and coordinates with child `SortHeaderComponent` instances. Compose with any rendering layer — tables, lists, custom grids — by placing child headers inside an element marked `[twSort]`.","selector":"[twSort]","usage":[{"form":"attribute","selector":"[twSort]","name":"twSort"}],"exportAs":"twSort","inputs":[{"name":"start","type":"'asc' | 'desc'","default":"'asc'","description":"Starting direction used when a header becomes active for the first time. Per-header `start` overrides this. Defaults to `'asc'`.","alias":"twSortStart"},{"name":"disableClear","type":"boolean","default":"false","description":"When true, the direction cycle skips the cleared (`null`) state — headers toggle between `'asc'` and `'desc'` only. Per-header `disableClear` overrides this. Defaults to `false`.","alias":"twSortDisableClear"},{"name":"disabled","type":"boolean","default":"false","description":"When true, all child sort headers are disabled. Defaults to `false`.","alias":"twSortDisabled"}],"outputs":[{"name":"sortChange","payloadType":"TwSortEvent","description":"Fires whenever the user changes `active` or `direction` by interacting with a header. Programmatic writes to `[(twSortActive)]` / `[(twSortDirection)]` do NOT emit.","alias":"twSortChange"}],"models":[{"name":"active","type":"string | null","default":"null","description":"The id of the currently sorted header, or `null` when nothing is sorted. Two-way bindable via `[(twSortActive)]`. Defaults to `null`.","alias":"twSortActive"},{"name":"direction","type":"SortDirection","default":"null","description":"Current sort direction. `null` represents the cleared state. Two-way bindable via `[(twSortDirection)]`. Defaults to `null`.","alias":"twSortDirection"}],"methods":[{"name":"register","signature":"register(id: string): void","description":"Registers a header id so duplicates can be detected. Called by `SortHeaderComponent` on init. Throws in dev mode if another header with the same id is already registered."},{"name":"deregister","signature":"deregister(id: string): void","description":"Deregisters a header id. Called on destroy."},{"name":"sort","signature":"sort(sortable: TwSortable): void","description":"Cycles the direction for the given header and emits `sortChange`. No-op when the directive or the header is disabled."},{"name":"getNextSortDirection","signature":"getNextSortDirection(sortable: TwSortable): SortDirection","description":"Returns the next direction in the cycle for the given header, based on the current state and header/parent overrides."}]},{"name":"SortHeaderComponent","kind":"component","description":"Turns any element (e.g., `<th>`, `<div>`, `<button>`) into a sortable header under a parent `SortDirective`. Renders the projected label plus an arrow that reflects the current direction. Triggers a sort cycle on click or Enter/Space.","selector":"[tw-sort-header]","usage":[{"form":"attribute","selector":"[tw-sort-header]","name":"tw-sort-header"}],"exportAs":"twSortHeader","contentSlots":[{"select":null},{"select":"[twSortHeaderIcon]"}],"inputs":[{"name":"id","type":"string","required":true,"description":"Unique id for this header. Required — identifies the field/column this header sorts."},{"name":"start","type":"'asc' | 'desc' | undefined","default":"undefined","description":"Overrides the parent directive's `start` for this header only. `undefined` inherits from the parent."},{"name":"disableClear","type":"boolean | undefined","default":"undefined","description":"Overrides the parent directive's `disableClear` for this header only. `undefined` inherits from the parent."},{"name":"headerDisabled","type":"boolean","default":"false","description":"When true, this header is disabled — clicks and keyboard activation are ignored. Defaults to `false`.","alias":"disabled"},{"name":"arrowPosition","type":"TwSortArrowPosition","default":"'after'","description":"Whether the sort arrow renders `'before'` or `'after'` the projected label. Defaults to `'after'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color used to tint the arrow when this header is active. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Controls padding and font size. Uses the shared `TwSize` scale. Defaults to `'md'`."},{"name":"sortActionDescription","type":"string","default":"'Sort'","description":"Accessible description applied via `AriaDescriber` so screen readers announce the sort action alongside the header text. Defaults to `'Sort'`."}]},{"name":"SortDirection","kind":"type","description":"Sort direction. `null` represents the cleared (unsorted) state.","definition":"'asc' | 'desc' | null"},{"name":"TwSortable","kind":"interface","description":"Shape that `SortHeaderComponent` implements when registering with a parent `SortDirective`.","members":[{"name":"id","type":"string","optional":false,"description":"Unique id identifying the column or field this header sorts."},{"name":"start","type":"'asc' | 'desc' | undefined","optional":false,"description":"Header-level starting direction. `undefined` falls back to the parent directive's `start`."},{"name":"disableClear","type":"boolean | undefined","optional":false,"description":"Header-level override for disabling the cleared state in the cycle. `undefined` falls back to the parent directive's `disableClear`."},{"name":"disabled","type":"boolean","optional":false,"description":"Whether this header is disabled."}]},{"name":"TwSortEvent","kind":"interface","description":"Payload emitted by `SortDirective.sortChange` whenever a user interaction changes the sort state.","members":[{"name":"active","type":"string | null","optional":false,"description":"The new active header id, or `null` when the sort was cleared."},{"name":"direction","type":"SortDirection","optional":false,"description":"The new sort direction."},{"name":"previous","type":"{ /** Previous active header id. */ active: string | null; /** Previous sort direction. */ direction: SortDirection; }","optional":false,"description":"Snapshot of the previous state, before this change."}]},{"name":"TwSortArrowPosition","kind":"type","description":"Position of the sort arrow relative to the header label.","definition":"'before' | 'after'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type SortDirection = 'asc' | 'desc' | null;\n\ntype TwSortArrowPosition = 'before' | 'after';\n\ninterface TwSortEvent {\n active: string | null;\n direction: SortDirection;\n previous: {\n active: string | null;\n direction: SortDirection;\n };\n}\n\ninterface TwSortable {\n readonly id: string;\n readonly start: 'asc' | 'desc' | undefined;\n readonly disableClear: boolean | undefined;\n readonly disabled: boolean;\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <div twSort [twSortActive]=\"c\" twSortDirection=\"asc\">\n <span tw-sort-header [id]=\"c\" [color]=\"c\">{{ c }}</span>\n </div>\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <div twSort twSortActive=\"name\" twSortDirection=\"asc\" class=\"inline-flex gap-1\">\n <span tw-sort-header id=\"name\" [size]=\"s\">Name</span>\n <span tw-sort-header id=\"age\" [size]=\"s\">Age</span>\n <span tw-sort-header id=\"role\" [size]=\"s\">Role</span>\n </div>\n}"},{"id":"arrowPositionSnippet","title":"Arrow Position","language":"html","code":"<!-- after (default) -->\n<div twSort twSortActive=\"label\" twSortDirection=\"asc\">\n <span tw-sort-header id=\"label\" arrowPosition=\"after\">Column label</span>\n</div>\n\n<!-- before -->\n<div twSort twSortActive=\"label\" twSortDirection=\"desc\">\n <span tw-sort-header id=\"label\" arrowPosition=\"before\">Column label</span>\n</div>"},{"id":"startSnippet","title":"Starting Direction","language":"html","code":"<div twSort twSortStart=\"asc\">\n <span tw-sort-header id=\"name\">Name (asc)</span>\n <span tw-sort-header id=\"amount\" start=\"desc\">Amount (desc)</span>\n <span tw-sort-header id=\"created\" start=\"desc\">Created (desc)</span>\n</div>"},{"id":"disableClearSnippet","title":"Disable Clear (asc ⇄ desc)","language":"html","code":"<tr\n twSort\n twSortActive=\"customer\"\n twSortDirection=\"asc\"\n [twSortDisableClear]=\"true\"\n>\n <th tw-sort-header id=\"customer\">Customer</th>\n <th tw-sort-header id=\"amount\" start=\"desc\">Amount</th>\n</tr>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Entire directive disabled -->\n<div twSort [twSortDisabled]=\"true\">\n <span tw-sort-header id=\"a\">Name</span>\n <span tw-sort-header id=\"b\">Age</span>\n</div>\n\n<!-- Single header disabled -->\n<div twSort>\n <span tw-sort-header id=\"a\">Name</span>\n <span tw-sort-header id=\"b\" [disabled]=\"true\">Age (locked)</span>\n <span tw-sort-header id=\"c\">Role</span>\n</div>"},{"id":"customIconSnippet","title":"Custom Arrow Icon","language":"html","code":"<div twSort twSortActive=\"name\" twSortDirection=\"asc\">\n <span tw-sort-header id=\"name\">\n Name\n <svg twSortHeaderIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" class=\"size-4\">\n <path d=\"M10 17a.75.75 0 0 1-.75-.75V5.66L7.3 7.7a.75.75 0 …\" />\n </svg>\n </span>\n <span tw-sort-header id=\"rating\">\n Rating\n <svg twSortHeaderIcon viewBox=\"0 0 20 20\" fill=\"currentColor\" class=\"size-4\">\n <path d=\"M10 2.5l2.39 4.84 5.34.77-3.87 3.77 .91 5.32L10 14.77 …\" />\n </svg>\n </span>\n</div>"},{"id":"tableTsSnippet","title":"Composing with a Table","language":"ts","code":"interface Order { id: string; customer: string; amount: number; status: string; created: string; }\n\nprotected readonly active = signal<string | null>(null);\nprotected readonly direction = signal<SortDirection>(null);\nprotected readonly rows = computed<readonly Order[]>(() =>\n [...ORDERS].sort(compareOrders(this.active(), this.direction())),\n);"},{"id":"tableHtmlSnippet","title":"Composing with a Table","language":"html","code":"<table>\n <thead>\n <tr twSort [(twSortActive)]=\"active\" [(twSortDirection)]=\"direction\">\n <th tw-sort-header id=\"id\">Order</th>\n <th tw-sort-header id=\"customer\">Customer</th>\n <th tw-sort-header id=\"amount\" start=\"desc\">Amount</th>\n <th tw-sort-header id=\"status\">Status</th>\n <th tw-sort-header id=\"created\" start=\"desc\">Created</th>\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.id) {\n <tr>\n <td>{{ row.id }}</td>\n <td>{{ row.customer }}</td>\n <td>{{ row.amount }}</td>\n <td>{{ row.status }}</td>\n <td>{{ row.created }}</td>\n </tr>\n }\n </tbody>\n</table>"},{"id":"listSnippet","title":"Composing with a List","language":"html","code":"<div twSort [(twSortActive)]=\"active\" [(twSortDirection)]=\"direction\">\n <button tw-sort-header id=\"customer\" type=\"button\">Customer</button>\n <button tw-sort-header id=\"amount\" start=\"desc\" type=\"button\">Amount</button>\n <button tw-sort-header id=\"created\" start=\"desc\" type=\"button\">Created</button>\n</div>\n<ul>\n @for (row of rows(); track row.id) {\n <li>…</li>\n }\n</ul>"},{"id":"eventSnippet","title":"Sort Change Event","language":"html","code":"<div twSort (twSortChange)=\"logEvent($event)\">\n <span tw-sort-header id=\"name\">Name</span>\n <span tw-sort-header id=\"created\" start=\"desc\">Created</span>\n <span tw-sort-header id=\"amount\" start=\"desc\">Amount</span>\n</div>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tr twSort [(twSortActive)]=\"active\" [(twSortDirection)]=\"direction\">\n <th tw-sort-header id=\"name\">Name</th>\n <th tw-sort-header id=\"role\">Role</th>\n <th tw-sort-header id=\"age\">Age</th>\n</tr>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n SortDirective,\n SortHeaderComponent,\n type SortDirection,\n type TwSortEvent,\n} from '@cdevhub/ngx-tw/sort';"}],"summary":"Composable sorting primitive — not a widget: a container directive holds the active column id and direction while child header components turn any element into a clickable sort trigger with a rotating arrow and correct `aria-sort`; the consumer still sorts the data.","whenToUse":["Making the header cells of a `tw-table` sortable — the canonical pairing","Adding sortable headers to a hand-rolled list, card grid, or button group that is not a table","Syncing sort state to the URL or to a server query via two-way `[(twSortActive)]` / `[(twSortDirection)]`","Server-side or custom sorting, where you want the interaction and ARIA but own the ordering yourself","A sort cycle that can return to unsorted (`null → asc → desc → null`) or one locked to `asc ⇄ desc`"],"related":["table","paginator"],"aliases":["sortable","sort header","order by","ordering","ascending","descending","column sort","aria-sort","MatSort"],"hasMeta":true,"metaPath":"projects/ngx-tw/sort/sort.meta.ts"},{"name":"split","importPath":"@cdevhub/ngx-tw/split","symbols":[{"name":"SplitComponent","kind":"component","description":"Container component that lays out two or more panes along a single axis and lets the user resize them by dragging the gutters between them.","selector":"tw-split","usage":[{"form":"element","selector":"tw-split","name":"tw-split"}],"contentSlots":[{"select":null}],"inputs":[{"name":"direction","type":"SplitDirection","default":"'horizontal'","description":"Axis along which panes are laid out. `'horizontal'` = side-by-side; `'vertical'` = stacked. Defaults to `'horizontal'`."},{"name":"unit","type":"SplitUnit","default":"'percent'","description":"How sizes are expressed and reported. A single unit governs the whole container. Defaults to `'percent'`."},{"name":"gutterSize","type":"number","default":"6","description":"Thickness of each gutter in pixels, perpendicular to the split axis. Defaults to `6`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, all resize interactions are disabled. Gutters are not focusable and do not respond to input. Defaults to `false`."},{"name":"keyboardStep","type":"number","default":"10","description":"How much to move the gutter per arrow-key press, in the container's declared unit. Defaults to `10`."},{"name":"keyboardStepLarge","type":"number","default":"50","description":"Step size for `PageUp` / `PageDown` key presses, in the container's declared unit. Defaults to `50`."},{"name":"storageKey","type":"string | null","default":"null","description":"If non-null, pane sizes are persisted to `localStorage` under this key and restored on init. Defaults to `null` (persistence disabled)."},{"name":"rtl","type":"boolean | null","default":"null","description":"When true, horizontal direction is visually reversed (RTL layout). Defaults to `null`, which means the value is inherited from the nearest ancestor `dir` attribute."}],"outputs":[{"name":"sizesChange","payloadType":"number[]","description":"Fires after any committed resize with the ordered array of current pane sizes. Does **not** fire on every pointer move during drag — only on commit (release, keyboard step, programmatic call)."},{"name":"resizeStart","payloadType":"SplitResizeEvent","description":"Fires on pointer/touch down or keyboard-initiated resize start."},{"name":"resizeEnd","payloadType":"SplitResizeEvent","description":"Fires on pointer/touch up, blur, or keyboard resize commit."},{"name":"collapseChange","payloadType":"SplitCollapseEvent","description":"Fires when a pane collapses or expands via snap, keyboard, or programmatic API."}],"methods":[{"name":"setSizes","signature":"setSizes(sizes: number[]): void","description":"Programmatically set all pane sizes. The array length must equal the current pane count and sizes must be compatible with the declared unit. Throws on mismatch."},{"name":"collapse","signature":"collapse(paneIndex: number): void","description":"Collapse the pane at `paneIndex` to its `collapsedSize`. The pane must have `collapsible = true`. Throws if the index is out of range."},{"name":"expand","signature":"expand(paneIndex: number): void","description":"Restore the pane at `paneIndex` to its pre-collapse size, or its `defaultSize` if none is recorded. Throws if the index is out of range."},{"name":"reset","signature":"reset(): void","description":"Restore all panes to their declared `defaultSize` values. Clears any persisted sizes if `storageKey` is set."}]},{"name":"SplitPaneComponent","kind":"component","description":"A single resizable pane inside `<tw-split>`. Declare size constraints here; the parent container drives the actual sizing.","selector":"tw-split-pane","usage":[{"form":"element","selector":"tw-split-pane","name":"tw-split-pane"}],"contentSlots":[{"select":null}],"inputs":[{"name":"defaultSize","type":"number | undefined","default":"undefined","description":"Initial size of this pane in the container's unit. Omit to use even distribution."},{"name":"minSize","type":"number","default":"0","description":"Minimum size in the container's unit. The gutter will not move past this. Defaults to `0`."},{"name":"maxSize","type":"unknown","default":"Infinity","description":"Maximum size in the container's unit. The gutter will not move past this. Defaults to `Infinity`."},{"name":"collapsible","type":"boolean","default":"false","description":"When true, the pane may collapse to `collapsedSize` via snap, keyboard, or API. Defaults to `false`."},{"name":"collapsedSize","type":"number","default":"0","description":"Size to use when the pane is collapsed. May be `> 0` for rail-style collapse. Defaults to `0`."},{"name":"snapSize","type":"number","default":"0","description":"If `> 0`, dragging within `snapSize` units of `collapsedSize` snaps the pane closed. Dragging back out past `snapSize` re-expands it. Defaults to `0` (snap disabled)."},{"name":"order","type":"number | undefined","default":"undefined","description":"Stable ordering token used by the container when content-projection order and resize math must stay consistent across change detection. Defaults to declaration order."}],"outputs":[{"name":"sizeChange","payloadType":"number","description":"Fires when this pane's size changes, in the container's unit."},{"name":"collapsedChange","payloadType":"boolean","description":"Fires when this pane's collapsed state changes."}]},{"name":"SplitGutterDirective","kind":"directive","description":"Marker directive for a custom gutter projection slot. Attach to content inside `<tw-split>` to provide custom gutter visuals. The container still owns all interaction logic.","selector":"[twSplitGutter]","usage":[{"form":"attribute","selector":"[twSplitGutter]","name":"twSplitGutter"}]},{"name":"SplitPaneHeaderDirective","kind":"directive","description":"Marker directive for an optional header region inside `<tw-split-pane>`. A parent Dock or Panel component may key off this region; the split pane itself does not apply any styling to it.","selector":"[twSplitPaneHeader]","usage":[{"form":"attribute","selector":"[twSplitPaneHeader]","name":"twSplitPaneHeader"}]},{"name":"SplitResizeEvent","kind":"interface","description":"Payload emitted on resizeStart and resizeEnd.","members":[{"name":"sizes","type":"number[]","optional":false,"description":"Ordered array of current pane sizes in the container's declared unit."},{"name":"unit","type":"'percent' | 'pixel'","optional":false,"description":"The unit used by the container."},{"name":"originPaneIndex","type":"number","optional":false,"description":"Index of the pane immediately before the gutter being dragged."},{"name":"cause","type":"'pointer' | 'touch' | 'keyboard' | 'programmatic'","optional":false,"description":"What triggered the resize."}]},{"name":"SplitCollapseEvent","kind":"interface","description":"Payload emitted on collapseChange.","members":[{"name":"paneIndex","type":"number","optional":false,"description":"Index of the pane whose collapsed state changed."},{"name":"collapsed","type":"boolean","optional":false,"description":"True when the pane just collapsed, false when it just expanded."},{"name":"cause","type":"'snap' | 'keyboard' | 'programmatic'","optional":false,"description":"What triggered the collapse or expand."}]},{"name":"SplitDirection","kind":"type","description":"Axis along which panes are laid out.","definition":"'horizontal' | 'vertical'"},{"name":"SplitUnit","kind":"type","description":"Unit in which pane sizes are expressed and reported.","definition":"'percent' | 'pixel'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type SplitDirection = 'horizontal' | 'vertical';\n\ntype SplitUnit = 'percent' | 'pixel';\n\ninterface SplitResizeEvent {\n sizes: number[];\n unit: SplitUnit;\n originPaneIndex: number;\n cause: 'pointer' | 'touch' | 'keyboard' | 'programmatic';\n}\n\ninterface SplitCollapseEvent {\n paneIndex: number;\n collapsed: boolean;\n cause: 'snap' | 'keyboard' | 'programmatic';\n}"},{"id":"horizontalSnippet","title":"Horizontal","language":"html","code":"<tw-split>\n <tw-split-pane [defaultSize]=\"40\" [minSize]=\"15\">Left</tw-split-pane>\n <tw-split-pane [defaultSize]=\"60\">Right</tw-split-pane>\n</tw-split>"},{"id":"verticalSnippet","title":"Vertical","language":"html","code":"<tw-split direction=\"vertical\">\n <tw-split-pane [defaultSize]=\"35\">Top</tw-split-pane>\n <tw-split-pane [defaultSize]=\"65\">Bottom</tw-split-pane>\n</tw-split>"},{"id":"threePaneSnippet","title":"Three panes","language":"html","code":"<tw-split>\n <tw-split-pane [defaultSize]=\"25\" [minSize]=\"10\">Files</tw-split-pane>\n <tw-split-pane [defaultSize]=\"50\" [minSize]=\"20\">Editor</tw-split-pane>\n <tw-split-pane [defaultSize]=\"25\" [minSize]=\"10\">Preview</tw-split-pane>\n</tw-split>"},{"id":"minMaxSnippet","title":"Min / Max constraints","language":"html","code":"<tw-split>\n <tw-split-pane [defaultSize]=\"30\" [minSize]=\"20\" [maxSize]=\"50\">Constrained</tw-split-pane>\n <tw-split-pane>Free</tw-split-pane>\n</tw-split>"},{"id":"collapsibleSnippet","title":"Collapsible pane","language":"html","code":"<tw-split>\n <tw-split-pane\n [defaultSize]=\"30\"\n [minSize]=\"20\"\n [collapsible]=\"true\"\n [collapsedSize]=\"6\"\n [snapSize]=\"6\"\n >Sidebar</tw-split-pane>\n <tw-split-pane>Main</tw-split-pane>\n</tw-split>\n\n// In the component:\nsplit.collapse(0);\nsplit.expand(0);"},{"id":"persistenceSnippet","title":"Persisted sizes","language":"html","code":"<tw-split storageKey=\"my-app-split\">\n <tw-split-pane [defaultSize]=\"50\">Left</tw-split-pane>\n <tw-split-pane [defaultSize]=\"50\">Right</tw-split-pane>\n</tw-split>"},{"id":"pixelSnippet","title":"Pixel mode","language":"html","code":"<tw-split unit=\"pixel\">\n <tw-split-pane [defaultSize]=\"240\" [minSize]=\"120\">240 px</tw-split-pane>\n <tw-split-pane [defaultSize]=\"320\">Remainder</tw-split-pane>\n</tw-split>"},{"id":"rtlSnippet","title":"RTL","language":"html","code":"<div dir=\"rtl\">\n <tw-split>\n <tw-split-pane [defaultSize]=\"35\">First in source order</tw-split-pane>\n <tw-split-pane>Second in source order</tw-split-pane>\n </tw-split>\n</div>"},{"id":"programmaticSnippet","title":"Programmatic control","language":"ts","code":"@Component({ /* ... */ })\nclass MyComponent {\n private readonly split = viewChild.required(SplitComponent);\n\n resize80_20(): void {\n this.split().setSizes([80, 20]);\n }\n\n resetSplit(): void {\n this.split().reset();\n }\n}"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-split direction=\"horizontal\">\n <tw-split-pane [defaultSize]=\"30\" [minSize]=\"15\">\n <div class=\"p-4\">Sidebar</div>\n </tw-split-pane>\n <tw-split-pane [defaultSize]=\"70\">\n <div class=\"p-4\">Main content</div>\n </tw-split-pane>\n</tw-split>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { SplitComponent, SplitPaneComponent } from '@cdevhub/ngx-tw/split';"}],"summary":"Resizable pane layout where the user drags or keyboard-adjusts the gutter between adjacent regions, following the WAI-ARIA window-splitter pattern.","whenToUse":["An IDE-style shell where a file tree, an editor, and a terminal each need a user-controlled share of the viewport","A list-and-detail layout where the reader wants a wider list or a wider detail pane","A side-by-side editor and live preview whose ratio the user tunes","Pane sizes must survive a reload, via the storageKey persistence input","A sidebar that collapses to a narrow rail when dragged past a snap threshold, and reopens with Enter or Space on the gutter","Sizes must be expressed in fixed pixels rather than percentages, or constrained by per-pane minSize/maxSize"],"whenNotToUse":[{"instead":"tabs","because":"each region is a whole screen of distinct content that should be shown one at a time, not simultaneously"},{"instead":"collapsible","because":"there is a single region to show or hide and no ratio for the user to control"},{"instead":"separator","because":"the divider is purely visual and there is nothing to resize"},{"instead":"sheet","because":"the secondary region is a temporary panel that slides in over the page rather than a permanent pane"}],"related":["separator","tabs","collapsible","sheet","tree"],"aliases":["splitter","resizable panes","split pane","split view","resizable panels","gutter","drag to resize","sidebar resize","window splitter"],"hasMeta":true,"metaPath":"projects/ngx-tw/split/split.meta.ts"},{"name":"table","importPath":"@cdevhub/ngx-tw/table","symbols":[{"name":"ColumnComponent","kind":"component","description":"Declares a single column of a `<tw-table>`. Pure-metadata component — its host element is `display: none`; the column's `<th>` / `<td>` / `<tfoot>` renderers are emitted by the parent `<tw-table>` using the projected cell templates (`*twHeaderCellDef`, `*twCellDef`, `*twFooterCellDef`).","selector":"tw-column","usage":[{"form":"element","selector":"tw-column","name":"tw-column"}],"inputs":[{"name":"name","type":"string","required":true,"description":"Unique identifier for this column. Required — referenced by `*twRowDef` / `*twHeaderRowDef` and used as the CDK `cdkColumnDef` name."},{"name":"display","type":"TwColumnDisplay","default":"{}","description":"Visual configuration: sticky, align, numeric, hideBelow, width. Accepts any subset; unset keys fall back to the defaults."},{"name":"hidden","type":"boolean","default":"false","description":"When true, removes this column from the visible column set. Defaults to `false`."},{"name":"priority","type":"number","default":"0","description":"Ordering hint for the default visible-columns list when no `*twRowDef` is declared. Lower renders first. Defaults to `0`."},{"name":"headerLabel","type":"string | undefined","default":"undefined","description":"Plain text header label. Used when no `*twHeaderCellDef` template is projected. Defaults to `undefined`."},{"name":"stackLabel","type":"string | undefined","default":"undefined","description":"Label used as `data-label` on cells in responsive `'stack'` mode. Falls back to `headerLabel`, then `name`. Defaults to `undefined`."},{"name":"sortState","type":"TwColumnAriaSort","default":"null","description":"Explicit override for the column header's `aria-sort` attribute. When unset (the default), the column derives its `aria-sort` from a parent `[twSort]` directive via the `TW_SORT_HANDLE` token — the column is treated as active when the directive's `active` id matches this column's `name`. Set explicitly to `'ascending'` / `'descending'` / `'none'` to override; `null` disables the attribute entirely. Defaults to `null`."}]},{"name":"DEFAULT_TABLE_LABELS","kind":"const","description":"Default English labels for the table.","type":"Readonly<TwTableLabels>"},{"name":"TableComponent","kind":"component","description":"A highly customizable data-table component wrapping `@angular/cdk/table`. Composes with other ngx-tw primitives: - Sortable headers: place `[twSort]` on `<tw-table>` and `[tw-sort-header]` inside `*twHeaderCellDef`. - Pagination: project `<tw-paginator slot=\"pagination\">` as a child. - Loading spinner: project `<tw-spinner slot=\"loading\">` or rely on the built-in fallback.","selector":"tw-table","usage":[{"form":"element","selector":"tw-table","name":"tw-table"}],"exportAs":"twTable","contentSlots":[{"select":"[slot="},{"select":"[slot="},{"select":null},{"select":"[slot="},{"select":"[slot="},{"select":"[slot="},{"select":"[slot="},{"select":"[slot="}],"inputs":[{"name":"data","type":"TwTableDataSourceInput<T>","default":"[] as readonly T[]","description":"The table's rows. Accepts a plain array, an `Observable<readonly T[]>`, or a CDK `DataSource<T>`. Wired directly to `CdkTable.dataSource`. Defaults to an empty array."},{"name":"trackBy","type":"TrackByFunction<T> | undefined","default":"undefined","description":"Tracking function used to identify rows across data changes. When unset, rows are identified by object reference. This drives **selection and expansion membership** as well as CDK's DOM diffing. Supply it when the data source re-emits equal-but-new objects — an HTTP refetch, an immutable store — or the user's selection empties on every refresh with no event and no error. It is called with `-1` as the index for membership lookups, so return a value derived from the row alone (`row.id`), not from the index."},{"name":"loading","type":"boolean","default":"false","description":"When true, renders the loading slot as an overlay and applies `opacity-60 pointer-events-none` to the body. Defaults to `false`."},{"name":"error","type":"unknown | null","default":"null","description":"When non-null, renders the error slot in place of the body. Defaults to `null`."},{"name":"appearance","type":"TwTableAppearance","default":"{}","description":"Visual configuration — `variant`, `density`, `size`, `layout`, `rowAnimations`. Accepts a partial; unset keys fall back to the defaults."},{"name":"sticky","type":"TwTableSticky","default":"{}","description":"Sticky configuration — `header`, `footer`, `scrollHeight`. Accepts a partial; unset keys fall back to the defaults."},{"name":"responsive","type":"TwTableResponsive","default":"{}","description":"Responsive configuration — `mode`, `stackBelow`. Accepts a partial; unset keys fall back to the defaults."},{"name":"selection","type":"TwTableSelection","default":"{}","description":"Selection configuration — `enabled`. Accepts a partial; unset keys fall back to the defaults."},{"name":"multiTemplateRows","type":"boolean","default":"false","description":"Whether multiple row templates may render per data object. Required for `*twRowExpansion` and advanced `*twRowDef [when]` usage. Defaults to `false`."},{"name":"labels","type":"Partial<TwTableLabels>","default":"{}","description":"Overrides for user-facing strings. Unset keys fall back to the English defaults. Defaults to `{}`."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the `<table>`. Required when no visible `<caption slot=\"caption\">` is provided.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"Id of an external element labelling the table. Mirrored to `aria-labelledby` on the `<table>`.","alias":"aria-labelledby"}],"outputs":[{"name":"rowClicked","payloadType":"TwRowClickEvent<T>","description":"Fires when a row is clicked. Suppressed when the click originated inside an interactive descendant (button, link, input, etc.)."},{"name":"selectionChange","payloadType":"TwSelectionChangeEvent<T>","description":"Fires after `selected` changes via user interaction (not on programmatic writes)."},{"name":"expansionChange","payloadType":"TwRowExpansionChangeEvent<T>","description":"Fires after a row is expanded or collapsed by user interaction."}],"models":[{"name":"expandedRows","type":"ReadonlySet<T>","default":"new Set<T>()","description":"Two-way bound set of rows currently expanded. Immutable — set a new `Set` on every change; do not mutate in place."},{"name":"selected","type":"readonly T[]","default":"[]","description":"Two-way bound list of selected rows. Set a new array on every change; do not mutate. Only used when `selection.enabled` is `true`."}],"methods":[{"name":"expand","signature":"expand(row: T): void","description":"Expands the given row if not already expanded and emits `expansionChange`."},{"name":"collapse","signature":"collapse(row: T): void","description":"Collapses the given row if currently expanded and emits `expansionChange`."},{"name":"toggleExpansion","signature":"toggleExpansion(row: T): void","description":"Toggles the expansion state of the given row."},{"name":"isSelected","signature":"isSelected(row: T): boolean","description":"Whether a given row is currently in the `selected` list."},{"name":"setSelected","signature":"setSelected(row: T, nextSelected: boolean): void","description":"Selects or deselects a row and emits `selectionChange`."},{"name":"selectAll","signature":"selectAll(): void","description":"Adds every row in the current data snapshot to `selected` that isn't already selected. Emits `selectionChange` if anything changed. Only the array data-source path can compute a full snapshot — for `Observable<T[]>` or `DataSource<T>` data inputs this is a no-op."},{"name":"clearSelection","signature":"clearSelection(): void","description":"Clears every currently-selected row. Emits `selectionChange` if anything was selected."}]},{"name":"CellDefDirective","kind":"directive","description":"Structural directive on an `<ng-template>` (or a star-directive host) defining a column's data-cell template. Typed as `TwCellContext<T>`.","selector":"[twCellDef]","usage":[{"form":"attribute","selector":"[twCellDef]","name":"twCellDef"}]},{"name":"FooterCellDefDirective","kind":"directive","description":"Structural directive defining a column's footer-cell template. Typed as `TwFooterCellContext<T>`.","selector":"[twFooterCellDef]","usage":[{"form":"attribute","selector":"[twFooterCellDef]","name":"twFooterCellDef"}]},{"name":"HeaderCellDefDirective","kind":"directive","description":"Structural directive defining a column's header-cell template. Typed as `TwHeaderCellContext`.","selector":"[twHeaderCellDef]","usage":[{"form":"attribute","selector":"[twHeaderCellDef]","name":"twHeaderCellDef"}]},{"name":"NoDataRowDirective","kind":"directive","description":"Structural directive on an `<ng-template>` declaring the no-data row fallback. Takes precedence over `[slot=\"empty\"]` when both are present.","selector":"ng-template[twNoDataRow]","usage":[{"form":"element-with-attribute","selector":"ng-template[twNoDataRow]","name":"ng-template"}],"extends":"CdkNoDataRow"},{"name":"RowExpansionDirective","kind":"directive","description":"Structural directive on an `<ng-template>` declaring the row-expansion template. Requires `[multiTemplateRows]=\"true\"` on the parent `<tw-table>`.","selector":"ng-template[twRowExpansion]","usage":[{"form":"element-with-attribute","selector":"ng-template[twRowExpansion]","name":"ng-template"}],"inputs":[{"name":"predicate","type":"((row: T, index: number) => boolean) | undefined","default":"undefined","description":"Optional predicate that decides whether this expansion renders for a given row. Defaults to `undefined` (render for every expanded row).","alias":"twRowExpansionWhen"}]},{"name":"TwCellContext","kind":"interface","description":"Context provided to every data-cell template (`*twCellDef`). Generic over the row type `T`.","members":[{"name":"$implicit","type":"T","optional":false,"description":"The row data (implicit `let-row`)."},{"name":"row","type":"T","optional":false,"description":"The row, aliased for readability."},{"name":"column","type":"string","optional":false,"description":"The column's declared `name`."},{"name":"index","type":"number","optional":false,"description":"Zero-based index within the rendered row list."},{"name":"columnIndex","type":"number","optional":false,"description":"Zero-based column index in the visible column set."},{"name":"first","type":"boolean","optional":false,"description":"True when this cell is in the first row."},{"name":"last","type":"boolean","optional":false,"description":"True when this cell is in the last row."},{"name":"even","type":"boolean","optional":false,"description":"True when the row index is even."},{"name":"odd","type":"boolean","optional":false,"description":"True when the row index is odd."},{"name":"count","type":"number","optional":false,"description":"Total number of rendered rows."}]},{"name":"TwColumnAlign","kind":"type","description":"Horizontal alignment of a column's cells.","definition":"'start' | 'center' | 'end'"},{"name":"TwColumnAriaSort","kind":"type","description":"Possible values for the `aria-sort` attribute on a sortable column header.","definition":"'ascending' | 'descending' | 'none' | null"},{"name":"TwColumnDisplay","kind":"interface","description":"Per-column display configuration.","members":[{"name":"sticky","type":"TwColumnSticky","optional":true,"description":"Sticky positioning. `'start'` pins to the leading edge; `'end'` pins to the trailing edge; `false` disables stickiness. Defaults to `false`."},{"name":"align","type":"TwColumnAlign","optional":true,"description":"Horizontal text alignment. `'end'` is idiomatic for numeric columns. Defaults to `'start'`."},{"name":"numeric","type":"boolean","optional":true,"description":"Convenience flag equivalent to `align: 'end'` plus tabular numerals. Overridden by an explicit `align`. Defaults to `false`."},{"name":"hideBelow","type":"TwBreakpoint | null","optional":true,"description":"Responsive visibility: when set and the viewport is below this breakpoint, the column is hidden (applies when the table's `responsive.mode === 'hide'`). Defaults to `null`."},{"name":"width","type":"string | number | null","optional":true,"description":"CSS column width applied to header and data cells. A number is treated as pixels; a string is passed through. Only honoured when the table's `appearance.layout === 'fixed'`. Defaults to `null`."}]},{"name":"TwColumnSticky","kind":"type","description":"Column stickiness. `'start'` pins to leading edge; `'end'` pins to trailing edge; `false` disables.","definition":"'start' | 'end' | false"},{"name":"TwFooterCellContext","kind":"interface","description":"Context provided to every footer-cell template (`*twFooterCellDef`). Generic over the row type `T`.","members":[{"name":"$implicit","type":"string","optional":false,"description":"The column's declared `name` (implicit)."},{"name":"column","type":"string","optional":false,"description":"The column's declared `name`."},{"name":"columnIndex","type":"number","optional":false,"description":"Zero-based column index in the visible column set."},{"name":"rows","type":"readonly T[]","optional":false,"description":"Snapshot of all rows (useful for total/summary computation)."}]},{"name":"TwHeaderCellContext","kind":"interface","description":"Context provided to every header-cell template (`*twHeaderCellDef`).","members":[{"name":"$implicit","type":"string","optional":false,"description":"The column's declared `name` (implicit `let-column`)."},{"name":"column","type":"string","optional":false,"description":"The column's declared `name`, aliased."},{"name":"columnIndex","type":"number","optional":false,"description":"Zero-based column index in the visible column set."}]},{"name":"TwRowClickEvent","kind":"interface","description":"Payload emitted by `rowClicked`.","members":[{"name":"row","type":"T","optional":false,"description":"The clicked row."},{"name":"index","type":"number","optional":false,"description":"Zero-based index in the rendered data."},{"name":"event","type":"MouseEvent","optional":false,"description":"The original DOM event."}]},{"name":"TwRowContext","kind":"interface","description":"Context surfaced to a `*twRowDef` template. Mirrors `TwCellContext<T>` minus the column-specific fields.","members":[{"name":"$implicit","type":"T","optional":false,"description":"The row data (implicit `let-row`)."},{"name":"row","type":"T","optional":false,"description":"The row, aliased."},{"name":"index","type":"number","optional":false,"description":"Zero-based index within the rendered row list."},{"name":"first","type":"boolean","optional":false,"description":"True when this row is the first rendered row."},{"name":"last","type":"boolean","optional":false,"description":"True when this row is the last rendered row."},{"name":"even","type":"boolean","optional":false,"description":"True when the row index is even."},{"name":"odd","type":"boolean","optional":false,"description":"True when the row index is odd."},{"name":"count","type":"number","optional":false,"description":"Total rendered row count."}]},{"name":"TwRowExpansionChangeEvent","kind":"interface","description":"Payload emitted by `expansionChange`.","members":[{"name":"row","type":"T","optional":false,"description":"The row whose expansion state changed."},{"name":"expanded","type":"boolean","optional":false,"description":"Whether the row is now expanded."},{"name":"expandedRows","type":"ReadonlySet<T>","optional":false,"description":"The full set of expanded rows after the change."}]},{"name":"TwRowExpansionContext","kind":"interface","description":"Context provided to `*twRowExpansion`.","members":[{"name":"$implicit","type":"T","optional":false,"description":"The row whose expansion panel this template renders."},{"name":"row","type":"T","optional":false,"description":"The row, aliased."},{"name":"index","type":"number","optional":false,"description":"The data index."},{"name":"collapse","type":"() => void","optional":false,"description":"Collapses this row (removes it from `expandedRows`)."}]},{"name":"TwSelectionChangeEvent","kind":"interface","description":"Payload emitted by `selectionChange`.","members":[{"name":"selected","type":"readonly T[]","optional":false,"description":"The full current selection."},{"name":"added","type":"readonly T[]","optional":false,"description":"Rows added since the previous selection."},{"name":"removed","type":"readonly T[]","optional":false,"description":"Rows removed since the previous selection."},{"name":"previous","type":"readonly T[]","optional":false,"description":"The previous selection."}]},{"name":"TwTableAppearance","kind":"interface","description":"Visual configuration. Pass any subset; unset keys fall back to the defaults.","members":[{"name":"variant","type":"TwTableVariant","optional":true,"description":"Visual variant. Defaults to `'default'`."},{"name":"density","type":"TwTableDensity","optional":true,"description":"Row density (vertical padding only — independent of font size). Defaults to `'comfortable'`."},{"name":"size","type":"TwSize","optional":true,"description":"Base font-size scale for header and data cells. Defaults to `'md'`."},{"name":"layout","type":"TwTableLayout","optional":true,"description":"Table layout algorithm. Defaults to `'auto'`."},{"name":"rowAnimations","type":"boolean","optional":true,"description":"When `true`, rows fade in on enter via `animate.enter=\"fade-in\"`. Off by default to avoid flicker on frequent data updates. Defaults to `false`."}]},{"name":"TwTableDataSourceInput","kind":"type","description":"Re-export of CDK's accepted data-source shapes: plain array, Observable, or `DataSource<T>`.","definition":"CdkTableDataSourceInput<T>"},{"name":"TwTableDensity","kind":"type","description":"Row density. `'comfortable'` — larger vertical padding. `'compact'` — tighter padding for dense data.","definition":"'comfortable' | 'compact'"},{"name":"TwTableLabels","kind":"interface","description":"String labels used by the table. All keys optional on the `labels` input; unset keys fall back to the English defaults.","members":[{"name":"ariaLabel","type":"string","optional":false,"description":"Accessible name used when neither `<caption>`, `ariaLabel`, nor `ariaLabelledby` is provided. Triggers a dev-mode warning."},{"name":"empty","type":"string","optional":false,"description":"Default empty-state message rendered when no `[slot=\"empty\"]` content is projected."},{"name":"loading","type":"string","optional":false,"description":"Default loading message announced politely via `LiveAnnouncer`."},{"name":"errorPrefix","type":"string","optional":false,"description":"Prefix prepended to `String(error)` when no `[slot=\"error\"]` content is projected."},{"name":"rowsUpdatedAnnouncement","type":"string","optional":false,"description":"`LiveAnnouncer` template announced on row-count changes. Variable: `{count}`."},{"name":"selectionAnnouncement","type":"string","optional":false,"description":"`LiveAnnouncer` template for selection-change announcements. Variable: `{count}`."},{"name":"expandRowLabel","type":"string","optional":false,"description":"Accessible label for the row-expansion toggle button."},{"name":"collapseRowLabel","type":"string","optional":false,"description":"Accessible label for the collapse-row toggle button."},{"name":"selectAllLabel","type":"string","optional":false,"description":"Accessible label for the master \"select all\" checkbox in the leading selection column."},{"name":"selectRowLabel","type":"string","optional":false,"description":"Accessible label template for each per-row selection checkbox. Variable: `{index}` (1-based)."}]},{"name":"TwTableLayout","kind":"type","description":"Table layout algorithm. `'auto'` — content-sized columns. `'fixed'` — respects `<tw-column display.width>`.","definition":"'auto' | 'fixed'"},{"name":"TwTableResponsive","kind":"interface","description":"Responsive configuration — how the table adapts to narrow viewports.","members":[{"name":"mode","type":"TwTableResponsiveMode","optional":true,"description":"Narrow-viewport strategy. Defaults to `'scroll'`."},{"name":"stackBelow","type":"TwBreakpoint","optional":true,"description":"Breakpoint below which the `'stack'` mode engages. Ignored when `mode !== 'stack'`. Defaults to `'md'`."}]},{"name":"TwTableResponsiveMode","kind":"type","description":"Responsive behaviour mode. `'scroll'` — horizontal overflow. `'stack'` — cards per row below a breakpoint. `'hide'` — columns with `hideBelow` are hidden.","definition":"'scroll' | 'stack' | 'hide'"},{"name":"TwTableSelection","kind":"interface","description":"Selection configuration.","members":[{"name":"enabled","type":"boolean","optional":true,"description":"When `true`, exposes a leading `_selection` column slot for checkbox rendering. Defaults to `false`."}]},{"name":"TwTableSticky","kind":"interface","description":"Sticky configuration — pinned header/footer rows and an internal scroll region.","members":[{"name":"header","type":"boolean","optional":true,"description":"When `true`, the `<thead>` row stays visible while the body scrolls. Requires `scrollHeight` or a scrolling ancestor. Defaults to `false`."},{"name":"footer","type":"boolean","optional":true,"description":"When `true`, the `<tfoot>` row stays pinned while the body scrolls. Defaults to `false`."},{"name":"scrollHeight","type":"string | number | null","optional":true,"description":"Max-height of the internal scroll container. A number is treated as pixels; a string is passed through as a CSS length. When `null`, the table flows with its content. Defaults to `null`."}]},{"name":"TwTableVariant","kind":"type","description":"Visual variant of the table. `'default'` — clean row dividers. `'striped'` — alternating rows. `'bordered'` — full grid with outer rounded border.","definition":"'default' | 'striped' | 'bordered'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TwTableVariant = 'default' | 'striped' | 'bordered';\ntype TwTableDensity = 'comfortable' | 'compact';\ntype TwTableResponsiveMode = 'scroll' | 'stack' | 'hide';\ntype TwTableLayout = 'auto' | 'fixed';\ntype TwColumnAlign = 'start' | 'center' | 'end';\ntype TwColumnSticky = 'start' | 'end' | false;\ntype TwColumnAriaSort = 'ascending' | 'descending' | 'none' | null;\n\ntype TwTableDataSourceInput<T> = readonly T[] | Observable<readonly T[]> | DataSource<T>;\n\ninterface TwTableAppearance {\n variant?: TwTableVariant; // default: 'default'\n density?: TwTableDensity; // default: 'comfortable'\n size?: TwSize; // default: 'md'\n layout?: TwTableLayout; // default: 'auto'\n rowAnimations?: boolean; // default: false\n}\n\ninterface TwTableSticky {\n header?: boolean; // default: false\n footer?: boolean; // default: false\n scrollHeight?: string | number | null; // default: null\n}\n\ninterface TwTableResponsive {\n mode?: TwTableResponsiveMode; // default: 'scroll'\n stackBelow?: TwBreakpoint; // default: 'md'\n}\n\ninterface TwTableSelection {\n enabled?: boolean; // default: false\n}\n\ninterface TwColumnDisplay {\n sticky?: TwColumnSticky; // default: false\n align?: TwColumnAlign; // default: 'start'\n numeric?: boolean; // default: false\n hideBelow?: TwBreakpoint | null; // default: null\n width?: string | number | null; // default: null\n}\n\ninterface TwTableLabels {\n ariaLabel: string;\n empty: string;\n loading: string;\n errorPrefix: string;\n rowsUpdatedAnnouncement: string; // '{count} rows loaded'\n selectionAnnouncement: string; // '{count} rows selected'\n expandRowLabel: string;\n collapseRowLabel: string;\n selectAllLabel: string; // 'Select all rows'\n selectRowLabel: string; // 'Select row {index}'\n}\n\ninterface TwCellContext<T> {\n $implicit: T;\n row: T;\n column: string;\n index: number;\n columnIndex: number;\n first: boolean;\n last: boolean;\n even: boolean;\n odd: boolean;\n count: number;\n}\n\ninterface TwHeaderCellContext {\n $implicit: string;\n column: string;\n columnIndex: number;\n}\n\ninterface TwFooterCellContext<T> {\n $implicit: string;\n column: string;\n columnIndex: number;\n rows: readonly T[];\n}\n\ninterface TwRowExpansionContext<T> {\n $implicit: T;\n row: T;\n index: number;\n collapse: () => void;\n}\n\ninterface TwRowClickEvent<T> {\n row: T;\n index: number;\n event: MouseEvent;\n}\n\ninterface TwRowExpansionChangeEvent<T> {\n row: T;\n expanded: boolean;\n expandedRows: ReadonlySet<T>;\n}\n\ninterface TwSelectionChangeEvent<T> {\n selected: readonly T[];\n added: readonly T[];\n removed: readonly T[];\n previous: readonly T[];\n}\n\n// Shared library types (re-exported from '@cdevhub/ngx-tw/core'):\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\ntype TwBreakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"@for (v of variants; track v) {\n <tw-table [data]=\"orders\" [appearance]=\"{ variant: v }\" [attr.aria-label]=\"'Orders — ' + v\">\n <tw-column name=\"id\" headerLabel=\"Order\" [display]=\"{ numeric: true, width: '90px' }\">\n <ng-template twCellDef let-row>#{{ row.id }}</ng-template>\n </tw-column>\n <tw-column name=\"customer\" headerLabel=\"Customer\">\n <ng-template twCellDef let-row>{{ row.customer }}</ng-template>\n </tw-column>\n <!-- … -->\n </tw-table>\n}"},{"id":"densitySnippet","title":"Density","language":"html","code":"@for (d of densities; track d) {\n <tw-table [data]=\"orders\" [appearance]=\"{ variant: 'bordered', density: d }\" [attr.aria-label]=\"'Density — ' + d\">\n <tw-column name=\"id\" headerLabel=\"ID\" [display]=\"{ numeric: true }\">\n <ng-template twCellDef let-row>{{ row.id }}</ng-template>\n </tw-column>\n <!-- … -->\n </tw-table>\n}"},{"id":"statesSnippet","title":"States","language":"html","code":"<tw-table\n [data]=\"statesData()\"\n [loading]=\"stateMode() === 'loading'\"\n [error]=\"stateMode() === 'error' ? 'Failed to load orders' : null\"\n aria-label=\"State demo\"\n>\n <tw-column name=\"id\" headerLabel=\"ID\" [display]=\"{ numeric: true, width: '80px' }\">\n <ng-template twCellDef let-row>#{{ row.id }}</ng-template>\n </tw-column>\n <!-- … -->\n</tw-table>"},{"id":"stickySnippet","title":"Sticky Header Columns","language":"html","code":"<tw-table\n [data]=\"orders\"\n [appearance]=\"{ variant: 'bordered' }\"\n [sticky]=\"{ header: true, scrollHeight: '280px' }\"\n aria-label=\"Sticky demo\"\n>\n <tw-column name=\"id\" headerLabel=\"ID\" [display]=\"{ numeric: true, sticky: 'start', width: '80px' }\">\n <ng-template twCellDef let-row>#{{ row.id }}</ng-template>\n </tw-column>\n <!-- middle columns scroll horizontally -->\n <tw-column name=\"actions\" headerLabel=\"\" [display]=\"{ sticky: 'end', width: '100px' }\">\n <ng-template twCellDef>\n <button twButton variant=\"ghost\" color=\"primary\" size=\"xs\">View</button>\n </ng-template>\n </tw-column>\n</tw-table>"},{"id":"expansionSnippet","title":"Row Expansion","language":"html","code":"<tw-table\n [data]=\"orders\"\n [multiTemplateRows]=\"true\"\n [(expandedRows)]=\"expandedOrders\"\n aria-label=\"Expandable orders\"\n>\n <tw-column name=\"toggle\" headerLabel=\"\" [display]=\"{ width: '44px' }\">\n <ng-template twCellDef let-row>\n <button\n twButton variant=\"ghost\" color=\"neutral\" size=\"xs\"\n [attr.aria-expanded]=\"isExpanded(row)\"\n (click)=\"toggleExpanded(row, $event)\"\n >▸</button>\n </ng-template>\n </tw-column>\n <!-- data columns -->\n\n <ng-template twRowExpansion let-row let-collapse=\"collapse\">\n <div class=\"p-4 bg-surface-sunken\">\n <p><strong>Notes:</strong> {{ row.notes || 'No notes for this order.' }}</p>\n <button twButton variant=\"outline\" size=\"xs\" (click)=\"collapse()\">Hide</button>\n </div>\n </ng-template>\n</tw-table>"},{"id":"footerSnippet","title":"Footer Row Totals","language":"html","code":"<tw-table [data]=\"orders\" [appearance]=\"{ variant: 'bordered' }\" aria-label=\"Totals demo\">\n <tw-column name=\"id\" headerLabel=\"ID\" [display]=\"{ numeric: true, width: '80px' }\">\n <ng-template twCellDef let-row>{{ row.id }}</ng-template>\n <ng-template twFooterCellDef>\n <span class=\"font-semibold text-fg-muted\">Totals</span>\n </ng-template>\n </tw-column>\n <tw-column name=\"customer\" headerLabel=\"Customer\">\n <ng-template twCellDef let-row>{{ row.customer }}</ng-template>\n <ng-template twFooterCellDef let-rows=\"rows\">\n <span class=\"text-xs text-fg-muted\">{{ rows.length }} orders</span>\n </ng-template>\n </tw-column>\n <tw-column name=\"total\" headerLabel=\"Total\" [display]=\"{ numeric: true }\">\n <ng-template twCellDef let-row>${{ row.total.toFixed(2) }}</ng-template>\n <ng-template twFooterCellDef let-rows=\"rows\">\n <span class=\"font-semibold\">${{ sumTotal(rows).toFixed(2) }}</span>\n </ng-template>\n </tw-column>\n</tw-table>"},{"id":"noDataSnippet","title":"Custom No-Data Row","language":"html","code":"<tw-table [data]=\"[]\" [appearance]=\"{ variant: 'bordered' }\" aria-label=\"Empty orders\">\n <tw-column name=\"id\" headerLabel=\"ID\" [display]=\"{ numeric: true }\">\n <ng-template twCellDef let-row>{{ row.id }}</ng-template>\n </tw-column>\n <!-- … -->\n\n <ng-template twNoDataRow>\n <tr>\n <td colspan=\"3\" class=\"px-4 py-10 text-center\">\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"size-8 text-fg-subtle\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">…</svg>\n <p class=\"text-sm text-fg-muted\">All caught up — no orders to review.</p>\n </div>\n </td>\n </tr>\n </ng-template>\n</tw-table>"},{"id":"headerTemplateSnippet","title":"Custom Header Template","language":"html","code":"<tw-table [data]=\"orders\" [appearance]=\"{ variant: 'bordered' }\" aria-label=\"Custom headers\">\n <tw-column name=\"id\">\n <ng-template twHeaderCellDef>\n <span class=\"inline-flex items-center gap-1.5 text-fg\">\n <svg class=\"size-3.5 text-fg-muted\" aria-hidden=\"true\">…</svg>\n Order ID\n </span>\n </ng-template>\n <ng-template twCellDef let-row>#{{ row.id }}</ng-template>\n </tw-column>\n <!-- … -->\n</tw-table>"},{"id":"sortTsSnippet","title":"Sortable Columns","language":"ts","code":"protected readonly sortActive = signal<string | null>(null);\nprotected readonly sortDirection = signal<SortDirection>(null);\n\nprotected readonly sortedOrders = computed<readonly Order[]>(() => {\n const rows = this.orders();\n const active = this.sortActive();\n const direction = this.sortDirection();\n if (!active || direction === null) return rows;\n const sign = direction === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const av = a[active as keyof Order];\n const bv = b[active as keyof Order];\n if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * sign;\n return String(av ?? '').localeCompare(String(bv ?? '')) * sign;\n });\n});"},{"id":"sortHtmlSnippet","title":"Sortable Columns","language":"html","code":"<tw-table\n twSort\n [(twSortActive)]=\"sortActive\"\n [(twSortDirection)]=\"sortDirection\"\n [data]=\"sortedOrders()\"\n [appearance]=\"{ variant: 'bordered' }\"\n aria-label=\"Sortable orders\"\n>\n <tw-column name=\"id\" [display]=\"{ numeric: true, width: '100px' }\">\n <ng-template twHeaderCellDef>\n <span tw-sort-header id=\"id\">Order</span>\n </ng-template>\n <ng-template twCellDef let-row>#{{ row.id }}</ng-template>\n </tw-column>\n <tw-column name=\"total\" [display]=\"{ numeric: true }\">\n <ng-template twHeaderCellDef>\n <span tw-sort-header id=\"total\" start=\"desc\">Total</span>\n </ng-template>\n <ng-template twCellDef let-row>${{ row.total.toFixed(2) }}</ng-template>\n </tw-column>\n</tw-table>"},{"id":"selectionSnippet","title":"Selection","language":"html","code":"<tw-table\n [data]=\"orders\"\n [appearance]=\"{ variant: 'bordered' }\"\n [selection]=\"{ enabled: true }\"\n [(selected)]=\"selectedOrders\"\n aria-label=\"Selectable orders\"\n>\n <tw-column name=\"id\" headerLabel=\"Order\" [display]=\"{ numeric: true, width: '90px' }\">\n <ng-template twCellDef let-row>#{{ row.id }}</ng-template>\n </tw-column>\n <tw-column name=\"customer\" headerLabel=\"Customer\">\n <ng-template twCellDef let-row>{{ row.customer }}</ng-template>\n </tw-column>\n <!-- … -->\n</tw-table>\n\n<!-- Component class -->\nprotected readonly selectedOrders = signal<readonly Order[]>([]);"},{"id":"responsiveSnippet","title":"Responsive — Stack Below Breakpoint","language":"html","code":"<tw-table\n [data]=\"orders\"\n [responsive]=\"{ mode: 'stack', stackBelow: 'md' }\"\n aria-label=\"Responsive orders\"\n>\n <tw-column name=\"id\" headerLabel=\"ID\" stackLabel=\"Order\" [display]=\"{ numeric: true }\">\n <ng-template twCellDef let-row>#{{ row.id }}</ng-template>\n </tw-column>\n <tw-column name=\"customer\" headerLabel=\"Customer\" stackLabel=\"Customer\">\n <ng-template twCellDef let-row>{{ row.customer }}</ng-template>\n </tw-column>\n <tw-column name=\"total\" headerLabel=\"Total\" stackLabel=\"Total\" [display]=\"{ numeric: true }\">\n <ng-template twCellDef let-row>${{ row.total.toFixed(2) }}</ng-template>\n </tw-column>\n</tw-table>"},{"id":"adminSnippet","title":"Admin Pattern — Toolbar, Sort Pagination","language":"html","code":"<tw-table\n twSort\n [(twSortActive)]=\"sortActive\"\n [(twSortDirection)]=\"sortDirection\"\n [data]=\"pagedOrders()\"\n [appearance]=\"{ variant: 'bordered' }\"\n [sticky]=\"{ header: true, scrollHeight: '320px' }\"\n aria-label=\"Orders admin\"\n>\n <div slot=\"toolbar\" class=\"flex items-center justify-between w-full\">\n <div class=\"flex items-center gap-3\">\n <input type=\"search\" [value]=\"search()\" (input)=\"onSearch($event)\" placeholder=\"Search customers…\" />\n <span class=\"text-xs text-fg-muted\">{{ filteredOrders().length }} results</span>\n </div>\n <button twButton variant=\"outline\" color=\"neutral\" size=\"sm\">Export CSV</button>\n </div>\n\n <tw-column name=\"id\" [display]=\"{ numeric: true, sticky: 'start', width: '90px' }\">\n <ng-template twHeaderCellDef><span tw-sort-header id=\"id\">Order</span></ng-template>\n <ng-template twCellDef let-row>#{{ row.id }}</ng-template>\n </tw-column>\n <!-- …other sortable columns -->\n\n <tw-paginator\n slot=\"pagination\"\n [totalItems]=\"filteredOrders().length\"\n [(page)]=\"page\"\n [pageSize]=\"pageSize\"\n type=\"basic\"\n layout=\"spread\"\n />\n</tw-table>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-table [data]=\"team\" aria-label=\"Team members\">\n <tw-column name=\"id\" headerLabel=\"ID\" [display]=\"{ numeric: true, width: '80px' }\">\n <ng-template twCellDef let-row>{{ row.id }}</ng-template>\n </tw-column>\n <tw-column name=\"name\" headerLabel=\"Name\">\n <ng-template twCellDef let-row>{{ row.name }}</ng-template>\n </tw-column>\n <tw-column name=\"email\" headerLabel=\"Email\">\n <ng-template twCellDef let-row>{{ row.email }}</ng-template>\n </tw-column>\n <tw-column name=\"role\" headerLabel=\"Role\">\n <ng-template twCellDef let-row>{{ row.role }}</ng-template>\n </tw-column>\n</tw-table>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n TableComponent,\n ColumnComponent,\n CellDefDirective,\n HeaderCellDefDirective,\n FooterCellDefDirective,\n NoDataRowDirective,\n RowExpansionDirective,\n} from '@cdevhub/ngx-tw/table';"}],"summary":"Data table over Angular CDK `CdkTable` that renders a native `<table>` with typed cell templates, sticky regions, selection, row expansion, and semantic empty / loading / error states.","whenToUse":["Displaying rows of structured records where each field belongs in its own labelled column","A result set that needs sticky headers, sticky start/end columns, or an internal scroll container","Rows the user can select (tri-state master checkbox) or expand into a detail panel","A grid that must degrade on narrow screens — scroll, stack one card per row, or hide low-priority columns","A data view whose loading, error, and no-results states should be handled by the component rather than hand-rolled","Feeding rows from a plain array, an `Observable<T[]>`, or a CDK `DataSource<T>`"],"whenNotToUse":[{"instead":"item","because":"each row is a title/description/avatar composition rather than a set of aligned columns"},{"instead":"tree","because":"the records are hierarchical and the user expands parents to reveal nested children"},{"instead":"transfer","because":"the point of the list is moving entries between an available set and a chosen set"}],"related":["sort","paginator","skeleton","empty-state","checkbox","select","input"],"aliases":["grid","datagrid","data grid","data table","datatable","rows","columns","spreadsheet","list view","tabular"],"hasMeta":true,"metaPath":"projects/ngx-tw/table/table.meta.ts"},{"name":"item","importPath":"@cdevhub/ngx-tw/item","symbols":[{"name":"ItemComponent","kind":"component","description":"","selector":"tw-item","usage":[{"form":"element","selector":"tw-item","name":"tw-item"}],"contentSlots":[{"select":"[twItemLeading]"},{"select":"[twItemTitle]"},{"select":"[twItemDescription]"},{"select":"[twItemTrailing]"}],"inputs":[{"name":"size","type":"ItemSize","default":"'md'","description":"Density and typography scale. `'sm'` is compact with truncated single-line title and description (table rows). `'md'` is the default list-item size. `'lg'` is the section-header scale with a larger title. Defaults to `'md'`."},{"name":"align","type":"ItemAlign","default":"'start'","description":"Vertical alignment of the leading and trailing slots relative to the content stack. `'start'` aligns them with the title baseline (recommended when a description is present). `'center'` vertically centers them on the whole block (recommended for single-line items). Defaults to `'start'`."},{"name":"interactive","type":"boolean","default":"false","description":"When `true`, the item is keyboard-activatable: adds `role=\"button\"`, `tabindex=\"0\"`, a hover background, pointer cursor, and a visible focus ring. Click and Enter/Space emit `selected`. Defaults to `false`. Do not project additional focusable elements (buttons, links, inputs) into `[twItemLeading]` or `[twItemTrailing]` when this is `true` — they create nested interactive controls that fail AXE. For action-row patterns, keep `interactive` as `false` and let the projected control carry the semantics instead. For `role=\"menuitem\"`, `role=\"option\"`, or other listbox/menu semantics, do not use `tw-item` — the menu, select, and command-palette components own those roles and the keyboard contracts that come with them.","transform":"booleanAttribute"},{"name":"disabled","type":"boolean","default":"false","description":"Disables an interactive item. Applies `opacity-50`, `pointer-events-none`, and sets `aria-disabled`. Only meaningful when `interactive` is `true`. Defaults to `false`.","transform":"booleanAttribute"},{"name":"current","type":"boolean","default":"false","description":"When `true`, marks the item as the visually highlighted \"current\" row (e.g. selected list entry, active settings tab). Applies a low-prominence primary tint, an inset ring, and `aria-current=\"true\"`. Stacks cleanly with `interactive` (the focus ring sits on top of the current ring). Defaults to `false`.","transform":"booleanAttribute"}],"outputs":[{"name":"selected","payloadType":"Event","description":"Fires when an interactive item is activated via click, Enter, or Space. Payload is the originating DOM event. Does not emit when `interactive` is `false` or `disabled` is `true`."}]},{"name":"ItemLeadingDirective","kind":"directive","description":"","selector":"[twItemLeading]","usage":[{"form":"attribute","selector":"[twItemLeading]","name":"twItemLeading"}]},{"name":"ItemTitleDirective","kind":"directive","description":"","selector":"[twItemTitle]","usage":[{"form":"attribute","selector":"[twItemTitle]","name":"twItemTitle"}]},{"name":"ItemDescriptionDirective","kind":"directive","description":"","selector":"[twItemDescription]","usage":[{"form":"attribute","selector":"[twItemDescription]","name":"twItemDescription"}]},{"name":"ItemTrailingDirective","kind":"directive","description":"","selector":"[twItemTrailing]","usage":[{"form":"attribute","selector":"[twItemTrailing]","name":"twItemTrailing"}]},{"name":"ItemSize","kind":"type","description":"Density / typography scale for `tw-item`. A narrower subset of `TwSize` — the three sizes that match real use cases (table row, list item, section header).","definition":"Extract<TwSize, 'sm' | 'md' | 'lg'>"},{"name":"ItemAlign","kind":"type","description":"Vertical alignment of the leading and trailing slots relative to the title/description stack.","definition":"'start' | 'center'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type ItemSize = 'sm' | 'md' | 'lg';\n\ntype ItemAlign = 'start' | 'center';"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-item [size]=\"s\">\n <div twItemLeading class=\"flex items-center justify-center rounded-lg bg-info-50 text-info-600\"\n [class.size-8]=\"s !== 'lg'\" [class.size-10]=\"s === 'lg'\">\n <tw-icon name=\"calendar\" [size]=\"s === 'lg' ? 'sm' : 'xs'\" />\n </div>\n <span twItemTitle>Scheduled report</span>\n <span twItemDescription>Ships every Monday at 09:00 UTC</span>\n </tw-item>\n}"},{"id":"alignmentSnippet","title":"Alignment","language":"html","code":"<!-- align=\"start\" — default, aligns leading slot to title baseline -->\n<tw-item align=\"start\">\n <tw-avatar twItemLeading initials=\"AL\" size=\"md\" />\n <span twItemTitle>Ada Lovelace</span>\n <span twItemDescription>\n Founder of scientific computing — authored the first published algorithm\n intended to be processed by a machine.\n </span>\n</tw-item>\n\n<!-- align=\"center\" — vertically centres leading/trailing on the whole row -->\n<tw-item align=\"center\">\n <tw-avatar twItemLeading initials=\"GH\" size=\"md\" color=\"primary\" />\n <span twItemTitle>Grace Hopper</span>\n <span twItemDescription>Pioneer of machine-independent programming.</span>\n</tw-item>"},{"id":"interactiveSnippet","title":"Interactive","language":"html","code":"<ul class=\"divide-y divide-border rounded-lg border border-border bg-surface-raised overflow-hidden\">\n @for (person of people; track person.name) {\n <li class=\"px-4\">\n <tw-item\n align=\"center\"\n [interactive]=\"true\"\n [disabled]=\"person.status === 'suspended'\"\n (selected)=\"onSelect(person)\"\n >\n <tw-avatar twItemLeading [initials]=\"person.initials\" size=\"sm\" />\n <span twItemTitle class=\"flex items-center gap-2\">\n <span>{{ person.name }}</span>\n @if (person.status === 'invited') {\n <span twBadge color=\"warning\" size=\"xs\">Invited</span>\n }\n @if (person.status === 'suspended') {\n <span twBadge color=\"error\" size=\"xs\">Suspended</span>\n }\n </span>\n <span twItemDescription>{{ person.role }}</span>\n <tw-icon twItemTrailing name=\"chevron-right\" size=\"sm\" color=\"neutral\" />\n </tw-item>\n </li>\n }\n</ul>"},{"id":"tableSnippet","title":"Table cell composition","language":"html","code":"<table class=\"w-full text-sm\">\n <thead>\n <tr class=\"bg-surface-muted text-left border-b border-border\">\n <th class=\"px-4 py-2 font-medium text-fg-muted\">Item</th>\n <th class=\"px-4 py-2 font-medium text-fg-muted\">Owner</th>\n <th class=\"px-4 py-2 font-medium text-fg-muted\">Updated</th>\n </tr>\n </thead>\n <tbody class=\"divide-y divide-border\">\n @for (row of rows; track row.id) {\n <tr>\n <td class=\"px-4 py-2\">\n <tw-item size=\"sm\" align=\"center\">\n <div twItemLeading class=\"flex size-8 items-center justify-center rounded-lg bg-info-50 text-info-600\">\n <tw-icon name=\"file-text\" size=\"xs\" />\n </div>\n <span twItemTitle>{{ row.title }}</span>\n <span twItemDescription>{{ row.code }}</span>\n </tw-item>\n </td>\n <td class=\"px-4 py-2 text-fg\">{{ row.owner }}</td>\n <td class=\"px-4 py-2 text-fg-muted\">{{ row.updatedAt }}</td>\n </tr>\n }\n </tbody>\n</table>"},{"id":"currentSnippet","title":"Current / selected state","language":"html","code":"<ul class=\"divide-y divide-border rounded-lg border border-border bg-surface-raised overflow-hidden\">\n @for (route of routes; track route.id) {\n <li class=\"px-4\">\n <tw-item\n align=\"center\"\n [interactive]=\"true\"\n [current]=\"activeRoute() === route.id\"\n (selected)=\"activeRoute.set(route.id)\"\n >\n <tw-icon twItemLeading [name]=\"route.icon\" size=\"sm\" color=\"neutral\" />\n <span twItemTitle>{{ route.label }}</span>\n <span twItemDescription>{{ route.description }}</span>\n </tw-item>\n </li>\n }\n</ul>"},{"id":"richSlotsSnippet","title":"Rich content in slots","language":"html","code":"<tw-item align=\"center\">\n <div twItemLeading class=\"flex size-10 items-center justify-center rounded-lg bg-success-50 text-success-600\">\n <tw-icon name=\"package\" size=\"sm\" />\n </div>\n <span twItemTitle class=\"flex items-center gap-2\">\n <span>Release v1.4.0</span>\n <span twBadge color=\"success\" variant=\"soft\" size=\"xs\">Latest</span>\n <span twBadge color=\"info\" variant=\"outline\" size=\"xs\">stable</span>\n </span>\n <span twItemDescription>Published 2 hours ago by the release bot.</span>\n <button twItemTrailing twButton size=\"xs\" variant=\"outline\" color=\"neutral\">\n View notes\n </button>\n</tw-item>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-item size=\"lg\">\n <div twItemLeading class=\"flex size-10 items-center justify-center rounded-lg bg-primary-50 text-primary-600\">\n <tw-icon name=\"arrow-down-wide-narrow\" size=\"sm\" />\n </div>\n <h3 twItemTitle>Sort</h3>\n <p twItemDescription>\n Composable sorting primitive — a container directive plus a sortable header\n component. Use with tables, lists, or any data view.\n </p>\n</tw-item>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n ItemComponent,\n ItemLeadingDirective,\n ItemTitleDirective,\n ItemDescriptionDirective,\n ItemTrailingDirective,\n} from '@cdevhub/ngx-tw/item';"}],"summary":"Layout-only row primitive composing four regions — leading, title, description, and trailing — into a horizontal row with a vertical text stack in the middle, at three densities and with an optional keyboard-activatable mode.","whenToUse":["List rows built from an avatar or icon, a title, a supporting line, and a trailing action or badge","Section and page headers, so heading rhythm matches the list rows below them","Rows that must be clickable — `interactive` adds `role=\"button\"`, tabindex, hover background, focus ring, and Enter/Space activation","Marking the active settings tab, routed nav entry, or selected row with the `current` highlight and `aria-current`","Keeping density and spacing consistent across page headers, list items, and table-cell compositions","Row content projected inside another container — a timeline item, a card body, a menu-like list"],"whenNotToUse":[{"instead":"card","because":"the composition needs its own enclosing surface with padding, border, or elevation"},{"instead":"table","because":"the fields must align into labelled columns across every row"},{"instead":"menu","because":"the rows are an overlay command list needing roving focus and menu ARIA roles"}],"related":["card","avatar","badge","button","icon","separator"],"aliases":["list item","list row","row","ListTile","media object","section header","page header","leading trailing","title description","list tile"],"hasMeta":true,"metaPath":"projects/ngx-tw/item/item.meta.ts"},{"name":"breadcrumbs","importPath":"@cdevhub/ngx-tw/breadcrumbs","symbols":[{"name":"BreadcrumbsComponent","kind":"component","description":"Breadcrumb trail rendered as a `<nav>` landmark with an ordered list of navigation hops. The last entry in `items` is the current page (gets `aria-current=\"page\"`, never an anchor). Items beyond `maxItems` are collapsed behind an ellipsis trigger that opens an overflow menu. Customize per-item rendering with `*twBreadcrumbsItem` (consumer projects router-aware anchors), and the separator with `*twBreadcrumbsSeparator` or the `separator` input (Lucide icon name).","selector":"tw-breadcrumbs","usage":[{"form":"element","selector":"tw-breadcrumbs","name":"tw-breadcrumbs"}],"inputs":[{"name":"items","type":"readonly TwBreadcrumbsItem<T>[]","default":"[]","description":"The full breadcrumb trail. The last entry is treated as the current page. Defaults to `[]`."},{"name":"size","type":"TwSize","default":"'md'","description":"Density of the trail — drives font size, gap, icon size, and the overflow-button square. Defaults to `'md'`."},{"name":"maxItems","type":"number","default":"0","description":"When greater than `0`, the trail collapses any middle items past this threshold behind an overflow menu. The first item and the current (last) item are always visible. Values `< 2` are clamped to `2`. Defaults to `0` (no collapsing)."},{"name":"separator","type":"string","default":"'chevron-right'","description":"Lucide icon name used for the default separator between items. Ignored when a `*twBreadcrumbsSeparator` template is projected. Defaults to `'chevron-right'`. Requires the consumer to register the icon via `provideTwLucideIcons({ ChevronRight, ... })`."},{"name":"ariaLabel","type":"string","default":"'Breadcrumb'","description":"Accessible label applied to the `<nav>` landmark. Defaults to `'Breadcrumb'`. Aliased as `aria-label`.","alias":"aria-label"}]},{"name":"BreadcrumbsItemTemplateDirective","kind":"directive","description":"Structural directive applied to an `<ng-template>` projected into `tw-breadcrumbs`. Replaces the default per-item rendering so consumers can wire their own anchors (typically `routerLink`-bound). The template receives `$implicit` (the item), `item`, `index`, and `isCurrent` in its context.","selector":"[twBreadcrumbsItem]","usage":[{"form":"attribute","selector":"[twBreadcrumbsItem]","name":"twBreadcrumbsItem"}],"methods":[{"name":"ngTemplateContextGuard","signature":"ngTemplateContextGuard(_dir: BreadcrumbsItemTemplateDirective<T>, _ctx: unknown): _ctx is TwBreadcrumbsItemContext<T>","description":"Type-narrows the template context for `let-` destructuring in templates."}]},{"name":"BreadcrumbsLinkDirective","kind":"directive","description":"Applies the parent `tw-breadcrumbs` link / current / disabled styling to a consumer-projected anchor (or span) inside `*twBreadcrumbsItem`. Without this directive, projected content renders unstyled — only the layout `<li>` is provided by the component. The directive sets `aria-current=\"page\"` when `current` is true. It does NOT itself swap the element tag; the consumer should still pick `<a>` for link items and `<span>` for the current item.","selector":"[twBreadcrumbsLink]","usage":[{"form":"attribute","selector":"[twBreadcrumbsLink]","name":"twBreadcrumbsLink"}],"inputs":[{"name":"current","type":"boolean","default":"false","description":"When true, this element is the current page: styled bold and gets `aria-current=\"page\"`. Defaults to `false`."},{"name":"disabled","type":"boolean","default":"false","description":"When true, applies disabled styling (muted, not-allowed cursor). Defaults to `false`."}]},{"name":"BreadcrumbsSeparatorTemplateDirective","kind":"directive","description":"Structural directive applied to an `<ng-template>` projected into `tw-breadcrumbs`. Replaces the default chevron separator with custom content (any element — icon, text, dot, etc.).","selector":"[twBreadcrumbsSeparator]","usage":[{"form":"attribute","selector":"[twBreadcrumbsSeparator]","name":"twBreadcrumbsSeparator"}]},{"name":"TwBreadcrumbsItem","kind":"interface","description":"Describes a single hop in a breadcrumb trail. The last entry in `items` is always treated as the current page (rendered with `aria-current=\"page\"`, never as an anchor). The generic `T` types the optional `data` payload — useful for forwarding routerLink commands or any other consumer-defined metadata to the custom item template without unsafe casts.","members":[{"name":"label","type":"string","optional":false,"description":"Visible label shown for this hop."},{"name":"href","type":"string","optional":true,"description":"Optional href used to render the default anchor. Omit on the current item (the last entry). Ignored when a custom `*twBreadcrumbsItem` template is projected — the consumer owns the anchor in that case."},{"name":"data","type":"T","optional":true,"description":"Opaque payload forwarded to the consumer's `*twBreadcrumbsItem` template via the template context. Use this to carry router commands or any other data your template needs."},{"name":"disabled","type":"boolean","optional":true,"description":"When true, the item renders as muted text without an anchor (even if `href` is set). Receives `aria-disabled=\"true\"`."}]},{"name":"TwBreadcrumbsItemContext","kind":"interface","description":"Template context passed to `*twBreadcrumbsItem` for each rendered item.","members":[{"name":"$implicit","type":"TwBreadcrumbsItem<T>","optional":false,"description":"The item record (also exposed via `let-item=\"$implicit\"`)."},{"name":"item","type":"TwBreadcrumbsItem<T>","optional":false,"description":"Same as `$implicit`. Available via `let-item=\"item\"` for readability."},{"name":"index","type":"number","optional":false,"description":"Zero-based index of this item in the original `items` input."},{"name":"isCurrent","type":"boolean","optional":false,"description":"`true` only for the last entry — the current page."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"interface TwBreadcrumbsItem<T = unknown> {\n /** Visible label shown for this hop. */\n label: string;\n /** Optional href for the default anchor. Omit on the current item. */\n href?: string;\n /** Opaque payload forwarded to the consumer's *twBreadcrumbsItem template. */\n data?: T;\n /** When true, the item renders muted with aria-disabled=\"true\". */\n disabled?: boolean;\n}\n\ninterface TwBreadcrumbsItemContext<T = unknown> {\n /** Implicit: the item record. */\n $implicit: TwBreadcrumbsItem<T>;\n /** Alias of $implicit; for readable let- bindings. */\n item: TwBreadcrumbsItem<T>;\n /** Zero-based index of this item in the original items input. */\n index: number;\n /** True only for the last (current) entry. */\n isCurrent: boolean;\n}"},{"id":"basicSnippet","title":"Basic 3-level trail","language":"html","code":"<tw-breadcrumbs [items]=\"items\" />"},{"id":"separatorIconSnippet","title":"Custom separator (icon name)","language":"html","code":"<tw-breadcrumbs [items]=\"items\" separator=\"slash\" />"},{"id":"separatorTemplateSnippet","title":"Custom separator (template)","language":"html","code":"<tw-breadcrumbs [items]=\"items\">\n <ng-template twBreadcrumbsSeparator>\n <span class=\"text-fg-subtle\">/</span>\n </ng-template>\n</tw-breadcrumbs>"},{"id":"overflowSnippet","title":"Truncation with overflow menu","language":"html","code":"<tw-breadcrumbs [items]=\"longTrail\" [maxItems]=\"3\" />\n<!-- 6 items, maxItems=3 → first + ellipsis menu + last 2 -->"},{"id":"routerSnippet","title":"Angular Router integration","language":"html","code":"<tw-breadcrumbs [items]=\"trail\">\n <ng-template twBreadcrumbsItem let-item let-isCurrent=\"isCurrent\">\n @if (isCurrent) {\n <span twBreadcrumbsLink [current]=\"true\">{{ item.label }}</span>\n } @else {\n <a twBreadcrumbsLink [routerLink]=\"item.data?.routerLink\">{{ item.label }}</a>\n }\n </ng-template>\n</tw-breadcrumbs>"},{"id":"rtlSnippet","title":"RTL (right-to-left)","language":"html","code":"<div dir=\"rtl\">\n <tw-breadcrumbs [items]=\"items\" aria-label=\"مسار التنقل\" />\n</div>"},{"id":"iconSnippet","title":"Leading icon via custom template","language":"html","code":"<tw-breadcrumbs [items]=\"items\">\n <ng-template twBreadcrumbsItem let-item let-isCurrent=\"isCurrent\" let-index=\"index\">\n @if (isCurrent) {\n <span twBreadcrumbsLink [current]=\"true\">{{ item.label }}</span>\n } @else {\n <a twBreadcrumbsLink [attr.href]=\"item.href\" class=\"gap-1.5\">\n @if (index === 0) {\n <tw-icon name=\"home\" size=\"sm\" />\n }\n <span>{{ item.label }}</span>\n </a>\n }\n </ng-template>\n</tw-breadcrumbs>"},{"id":"disabledSnippet","title":"Disabled hop","language":"html","code":"<tw-breadcrumbs\n [items]=\"[\n { label: 'Home', href: '/' },\n { label: 'Billing', href: '/billing', disabled: true },\n { label: 'Invoice 1024' },\n ]\"\n/>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-breadcrumbs [items]=\"items\" />\n\n// items\n[\n { label: 'Home', href: '/' },\n { label: 'Library', href: '/library' },\n { label: 'The Pragmatic Programmer' }, // current page\n]"},{"id":"routerSnippet","title":"Router Integration","language":"html","code":"<tw-breadcrumbs [items]=\"trail\" ariaLabel=\"Section navigation\">\n <ng-template twBreadcrumbsItem let-item let-isCurrent=\"isCurrent\">\n @if (isCurrent) {\n <span twBreadcrumbsLink [current]=\"true\">{{ item.label }}</span>\n } @else {\n <a twBreadcrumbsLink [routerLink]=\"item.data?.routerLink\">{{ item.label }}</a>\n }\n </ng-template>\n</tw-breadcrumbs>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n BreadcrumbsComponent,\n BreadcrumbsItemTemplateDirective,\n BreadcrumbsLinkDirective,\n BreadcrumbsSeparatorTemplateDirective,\n type TwBreadcrumbsItem,\n} from '@cdevhub/ngx-tw/breadcrumbs';"}],"summary":"Horizontal trail of navigation hops ending in the current page, rendered as a <nav> landmark wrapping an ordered list.","whenToUse":["Showing where the current page sits in a hierarchy and letting the user jump back to any ancestor","Deep nested structures (file paths, catalog categories, admin sections) where the ancestry is not obvious from the page itself","Long trails that must auto-collapse — set maxItems and the middle hops move into an ellipsis overflow menu","Router-driven trails: project a *twBreadcrumbsItem template and bind your own routerLink on each anchor","RTL layouts, where the default chevron separator flips automatically"],"whenNotToUse":[{"instead":"tab-nav","because":"the links switch between sibling sections instead of ascending a hierarchy"},{"instead":"paginator","because":"the navigation surface is a flat numbered sequence rather than a nested path"},{"instead":"stepper","because":"the trail represents progress through a sequence the user is completing"}],"related":["menu","tab-nav","paginator","icon"],"aliases":["breadcrumb","crumbs","trail","navigation trail","path","hierarchy navigation","ancestry","you are here"],"hasMeta":true,"metaPath":"projects/ngx-tw/breadcrumbs/breadcrumbs.meta.ts"},{"name":"timeline","importPath":"@cdevhub/ngx-tw/timeline","symbols":[{"name":"TimelineComponent","kind":"component","description":"Presentational chronological-sequence layout primitive. Renders a list of `<tw-timeline-item>` children connected by a line that runs through their markers. The container owns orientation, alignment, density, and line-style decisions; items style their own marker, state, and connector colors. **Not interactive.** Unlike `tw-stepper`, the timeline does not install a keyboard map, does not own panels, does not trap focus, and item hosts are not focusable. Consumers needing row-level activation project an interactive primitive (`<tw-item interactive>`, `<button>`, an anchor) inside the default slot.","selector":"tw-timeline","usage":[{"form":"element","selector":"tw-timeline","name":"tw-timeline"}],"contentSlots":[{"select":null}],"inputs":[{"name":"orientation","type":"TwOrientation","default":"'vertical'","description":"Axis along which items are laid out. `'vertical'` stacks items top-to-bottom; `'horizontal'` lays them out left-to-right (RTL-aware). Defaults to `'vertical'`."},{"name":"align","type":"TimelineAlign","default":"'left'","description":"Vertical layout strategy. `'left'` / `'right'` place the marker on that side; `'alternate'` centers the marker and flips the body left ↔ right per item; `'split'` centers the marker with body on the right and the opposite slot on the left. Ignored when orientation is `'horizontal'`. Defaults to `'left'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Density and typography scale. Controls marker diameter, gap between items, and body typography step. Defaults to `'md'`."},{"name":"lineStyle","type":"TimelineLineStyle","default":"'solid'","description":"Connector line style applied to every gap between items. Defaults to `'solid'`."},{"name":"scrollControls","type":"TimelineScrollControls","default":"'auto'","description":"Visibility policy for the horizontal-overflow chevron buttons. `'auto'` shows them only when the inner scroll region can scroll in that direction; `'always'` renders both regardless of scroll state (disabled when at an edge); `'never'` hides them entirely (consumer manages overflow externally). Ignored when orientation is `'vertical'`. Defaults to `'auto'`. Counts as the 5th container input, justified by the \"Overflow-control axis on layout primitives\" cap exception: a layout primitive whose primary axes (orientation, alignment, size, line/style) already saturate ≤ 4 inputs MAY add a single additional input that toggles overflow-navigation affordances when overflow is a real concern for at least one axis value. The added input MUST be a single tri-state and MUST be inert on axis values where overflow cannot occur (here: `orientation === 'vertical'`)."}]},{"name":"TimelineItemComponent","kind":"component","description":"A single event in a `tw-timeline`. Renders a marker (dot or circle) plus content. Connectors leading into and out of the marker are emitted by this component and conditionally elided when the item is first or last in the timeline. The item is not focusable — row activation belongs to projected interactive children.","selector":"tw-timeline-item","usage":[{"form":"element","selector":"tw-timeline-item","name":"tw-timeline-item"}],"contentSlots":[{"select":"[twTimelineMarker]"},{"select":null},{"select":"[twTimelineTimestamp]"},{"select":"[twTimelineOpposite]"}],"inputs":[{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color for the marker fill and the trailing connector when the item is in a reached state. Ignored when state is `'error'` (which forces the error palette). Defaults to `'primary'`."},{"name":"marker","type":"TimelineMarker","default":"'dot'","description":"Marker geometry. `'dot'` is a small filled circle. `'circle'` is a larger ring that may contain a projected icon, avatar, or an auto-computed 1-based index. Defaults to `'dot'`."},{"name":"state","type":"TimelineState","default":"'reached'","description":"Semantic state of the event. Drives marker fill, ring, and trailing-connector color, and applies `aria-current=\"step\"` when `'current'`. Defaults to `'reached'`."},{"name":"timestamp","type":"string | Date | null","default":"null","description":"Timestamp shown in the timestamp slot. A `Date` is formatted via `Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' })`; a string is rendered verbatim; `null` omits the timestamp element. Overridden by projected `[twTimelineTimestamp]` content when present. Defaults to `null`."},{"name":"dateTime","type":"string | null","default":"null","description":"Machine-readable ISO 8601 datetime for the rendered `<time datetime=\"…\">` attribute. Derived from `timestamp` when it is a `Date` and this input is `null`; required (explicit) when `timestamp` is a string and machine-readability is desired. When `null` with a string timestamp, the timestamp renders as a `<span>` instead. Defaults to `null`."}]},{"name":"TimelineMarkerDirective","kind":"directive","description":"Marker-slot directive. Apply on any element projected inside a `<tw-timeline-item>` to render that element inside the marker bubble (only when `marker=\"circle\"`).","selector":"[twTimelineMarker]","usage":[{"form":"attribute","selector":"[twTimelineMarker]","name":"twTimelineMarker"}]},{"name":"TimelineTimestampDirective","kind":"directive","description":"Timestamp-slot directive. Replaces the rendered `timestamp` / `dateTime` output. Use for relative-time components (\"2 hours ago\").","selector":"[twTimelineTimestamp]","usage":[{"form":"attribute","selector":"[twTimelineTimestamp]","name":"twTimelineTimestamp"}]},{"name":"TimelineOppositeDirective","kind":"directive","description":"Opposite-slot directive. Renders on the side opposite the body in `align=\"alternate\"` / `align=\"split\"` (vertical orientation only).","selector":"[twTimelineOpposite]","usage":[{"form":"attribute","selector":"[twTimelineOpposite]","name":"twTimelineOpposite"}]},{"name":"TW_TIMELINE_SCROLL_LABELS","kind":"token","description":"Injection token carrying localisable `aria-label` strings for the timeline's horizontal-overflow chevron buttons. The container uses these labels exclusively on the prev/next buttons; they do not affect any other rendered text. Provide via `provideTwTimelineScrollLabels({ ... })` at the root or feature level. If both keys are present they override the English defaults; if only one is present the other falls back to the English default. First concrete token under the `TW_TIMELINE_I18N` reservation in `docs/requirements/timeline.requirements.md` § 11.2."},{"name":"DEFAULT_TW_TIMELINE_SCROLL_LABELS","kind":"const","description":"Default English labels used when `TW_TIMELINE_SCROLL_LABELS` is not provided.","type":"TwTimelineScrollLabels"},{"name":"provideTwTimelineScrollLabels","kind":"function","description":"Configures the localised labels used by `tw-timeline` overflow chevrons. Add to `bootstrapApplication`'s `providers` (or any feature/route provider array).","signature":"provideTwTimelineScrollLabels(labels: Partial<TwTimelineScrollLabels>): EnvironmentProviders"},{"name":"TimelineMarker","kind":"type","description":"Marker geometry of a `tw-timeline-item`.","definition":"'dot' | 'circle'"},{"name":"TimelineState","kind":"type","description":"Semantic state of a `tw-timeline-item`. Drives marker fill, ring, and trailing-connector color.","definition":"'reached' | 'pending' | 'current' | 'error'"},{"name":"TimelineAlign","kind":"type","description":"Vertical layout strategy of a `tw-timeline`. Ignored for `orientation: 'horizontal'`.","definition":"'left' | 'right' | 'alternate' | 'split'"},{"name":"TimelineLineStyle","kind":"type","description":"Connector line style.","definition":"'solid' | 'dashed'"},{"name":"TimelineScrollControls","kind":"type","description":"Visibility policy for the horizontal-overflow chevron buttons.","definition":"'auto' | 'always' | 'never'"},{"name":"TwTimelineScrollLabels","kind":"interface","description":"Localisable labels for the horizontal-overflow chevron buttons on `tw-timeline`.","members":[{"name":"scrollPrevious","type":"string","optional":false,"description":"Accessible label for the previous-scroll chevron. Used as `aria-label`. Defaults to `'Scroll to previous events'`."},{"name":"scrollNext","type":"string","optional":false,"description":"Accessible label for the next-scroll chevron. Used as `aria-label`. Defaults to `'Scroll to next events'`."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"import type { TwColor, TwOrientation, TwSize } from '@cdevhub/ngx-tw/core';\n\ntype TimelineMarker = 'dot' | 'circle';\ntype TimelineState = 'reached' | 'pending' | 'current' | 'error';\ntype TimelineAlign = 'left' | 'right' | 'alternate' | 'split';\ntype TimelineLineStyle = 'solid' | 'dashed';\ntype TimelineScrollControls = 'auto' | 'always' | 'never';"},{"id":"orientationSnippet","title":"Orientation","language":"html","code":"<!-- Vertical (default) -->\n<tw-timeline>\n @for (h of hops; track h.id) {\n <tw-timeline-item\n marker=\"circle\"\n [color]=\"h.color\"\n [state]=\"h.state\"\n [timestamp]=\"h.when || null\"\n >\n <tw-icon twTimelineMarker [name]=\"h.icon\" size=\"sm\" />\n <p class=\"text-sm font-semibold\">{{ h.label }}</p>\n </tw-timeline-item>\n }\n</tw-timeline>\n\n<!-- Horizontal -->\n<tw-timeline orientation=\"horizontal\" size=\"sm\">\n @for (h of hops; track h.id) { ... }\n</tw-timeline>"},{"id":"longHorizontalSnippet","title":"Orientation","language":"html","code":"<!-- Long horizontal timeline — chevrons appear when overflow exists -->\n<div class=\"max-w-full overflow-hidden\">\n <tw-timeline orientation=\"horizontal\" size=\"sm\">\n @for (h of buildPipeline; track h.id) {\n <tw-timeline-item\n marker=\"circle\"\n [color]=\"h.color\"\n [state]=\"h.state\"\n [timestamp]=\"h.when || null\"\n >\n <tw-icon twTimelineMarker [name]=\"h.icon\" size=\"xs\" />\n <p class=\"text-xs font-medium\">{{ h.label }}</p>\n </tw-timeline-item>\n }\n </tw-timeline>\n</div>\n\n<!-- Force chevrons even at edges (disabled when there's nowhere to scroll) -->\n<tw-timeline orientation=\"horizontal\" scrollControls=\"always\">…</tw-timeline>\n\n<!-- Hide chevrons entirely (consumer-managed overflow) -->\n<tw-timeline orientation=\"horizontal\" scrollControls=\"never\">…</tw-timeline>"},{"id":"alignmentSnippet","title":"Alignment","language":"html","code":"<!-- Alternate -->\n<tw-timeline align=\"alternate\">\n <tw-timeline-item marker=\"circle\" color=\"primary\" state=\"reached\">\n <span twTimelineOpposite class=\"text-sm font-semibold\">Q1 2026</span>\n <p class=\"text-sm font-semibold\">Auth rewrite</p>\n </tw-timeline-item>\n <!-- ... -->\n</tw-timeline>\n\n<!-- Split -->\n<tw-timeline align=\"split\">\n <tw-timeline-item marker=\"circle\" color=\"info\" state=\"reached\">\n <span twTimelineOpposite class=\"text-xs font-mono\">14:00:02</span>\n <p class=\"text-sm\">Lint check passed.</p>\n </tw-timeline-item>\n <!-- ... -->\n</tw-timeline>\n\n<!-- Right -->\n<tw-timeline align=\"right\">\n <tw-timeline-item color=\"success\" state=\"reached\" timestamp=\"Apr 12\">\n <p class=\"text-sm\">Alice merged PR #421.</p>\n </tw-timeline-item>\n <!-- ... -->\n</tw-timeline>"},{"id":"markersSnippet","title":"Markers","language":"html","code":"<!-- Dot (default, compact) -->\n<tw-timeline size=\"sm\">\n <tw-timeline-item color=\"success\" state=\"reached\" timestamp=\"08:42\">\n <p class=\"text-sm\">Build succeeded.</p>\n </tw-timeline-item>\n</tw-timeline>\n\n<!-- Circle with auto-number -->\n<tw-timeline>\n <tw-timeline-item marker=\"circle\" color=\"primary\" state=\"reached\">\n <p class=\"text-sm font-semibold\">Sign up</p>\n </tw-timeline-item>\n</tw-timeline>\n\n<!-- Circle with projected icon -->\n<tw-timeline-item marker=\"circle\" color=\"success\" state=\"reached\">\n <tw-icon twTimelineMarker name=\"check-circle\" size=\"sm\" />\n <p class=\"text-sm font-semibold\">Order placed</p>\n</tw-timeline-item>\n\n<!-- Circle with projected avatar -->\n<tw-timeline-item marker=\"circle\" color=\"neutral\">\n <tw-avatar twTimelineMarker initials=\"AM\" size=\"sm\" />\n <p class=\"text-sm\"><strong>Alice Morgan</strong> rotated API keys.</p>\n</tw-timeline-item>"},{"id":"statesSnippet","title":"States","language":"html","code":"<tw-timeline>\n <tw-timeline-item marker=\"circle\" color=\"success\" state=\"reached\" timestamp=\"14:00\">\n <tw-icon twTimelineMarker name=\"check-circle\" size=\"sm\" />\n <p class=\"text-sm font-semibold\">Lint passed</p>\n </tw-timeline-item>\n <tw-timeline-item marker=\"circle\" color=\"primary\" state=\"current\" timestamp=\"14:05\">\n <tw-icon twTimelineMarker name=\"play-circle\" size=\"sm\" />\n <p class=\"text-sm font-semibold\">Deploying to staging</p>\n </tw-timeline-item>\n <tw-timeline-item marker=\"circle\" color=\"neutral\" state=\"pending\">\n <tw-icon twTimelineMarker name=\"eye\" size=\"sm\" />\n <p class=\"text-sm text-fg-muted\">Smoke tests</p>\n </tw-timeline-item>\n <tw-timeline-item marker=\"circle\" color=\"primary\" state=\"error\" timestamp=\"14:14\">\n <p class=\"text-sm font-semibold text-error-fg\">Canary aborted</p>\n </tw-timeline-item>\n</tw-timeline>"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"<tw-timeline size=\"sm\">\n <tw-timeline-item color=\"success\" state=\"reached\" timestamp=\"now\">\n <p class=\"text-xs\">Created</p>\n </tw-timeline-item>\n <tw-timeline-item color=\"success\" state=\"current\">\n <p class=\"text-xs\">In progress</p>\n </tw-timeline-item>\n</tw-timeline>\n\n@for (c of colors; track c) { ... }"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-timeline [size]=\"s\">\n <tw-timeline-item marker=\"circle\" color=\"primary\" state=\"reached\" timestamp=\"Mar 14\">\n <p>Order placed</p>\n </tw-timeline-item>\n <tw-timeline-item marker=\"circle\" color=\"primary\" state=\"current\" timestamp=\"Mar 15\">\n <p>Shipped</p>\n </tw-timeline-item>\n <tw-timeline-item marker=\"circle\" color=\"neutral\" state=\"pending\">\n <p>Delivered</p>\n </tw-timeline-item>\n </tw-timeline>\n}"},{"id":"lineStyleSnippet","title":"Line styles","language":"html","code":"<!-- Solid (recorded chronology) -->\n<tw-timeline lineStyle=\"solid\">\n <tw-timeline-item color=\"success\" state=\"reached\" timestamp=\"Mar 14\">\n <p class=\"text-sm\">Tag v2.4.0 cut.</p>\n </tw-timeline-item>\n</tw-timeline>\n\n<!-- Dashed (projected schedule) -->\n<tw-timeline lineStyle=\"dashed\">\n <tw-timeline-item color=\"primary\" state=\"current\">\n <p class=\"text-sm font-semibold\">Today — UX review</p>\n </tw-timeline-item>\n <tw-timeline-item color=\"neutral\" state=\"pending\">\n <p class=\"text-sm text-fg-muted\">+3 days — eng kickoff</p>\n </tw-timeline-item>\n</tw-timeline>"},{"id":"timestampsSnippet","title":"Timestamps","language":"html","code":"<!-- Date input → <time datetime=\"…\"> -->\n<tw-timeline-item color=\"primary\" state=\"reached\" [timestamp]=\"postedAt\">\n <p class=\"text-sm\">Date input — machine-readable via &lt;time&gt;.</p>\n</tw-timeline-item>\n\n<!-- String + explicit dateTime -->\n<tw-timeline-item color=\"primary\" state=\"reached\" timestamp=\"Apr 12, 2026\" dateTime=\"2026-04-12\">\n <p class=\"text-sm\">Verbatim text with machine-readable attribute.</p>\n</tw-timeline-item>\n\n<!-- Plain string (no datetime) -->\n<tw-timeline-item color=\"primary\" state=\"current\" timestamp=\"last week\">\n <p class=\"text-sm\">Renders as a &lt;span&gt;.</p>\n</tw-timeline-item>\n\n<!-- Projected slot (full DOM control) -->\n<tw-timeline-item color=\"info\" state=\"reached\">\n <span twTimelineTimestamp class=\"text-xs text-fg-muted\">2 hours ago</span>\n <p class=\"text-sm\">Custom relative-time component.</p>\n</tw-timeline-item>"},{"id":"interactiveSnippet","title":"Interactive items","language":"html","code":"<tw-timeline>\n @for (e of auditLog; track e.id) {\n <tw-timeline-item marker=\"circle\" color=\"neutral\" [timestamp]=\"e.when\">\n <tw-avatar twTimelineMarker [initials]=\"e.initials\" size=\"sm\" />\n <tw-item interactive (selected)=\"onAuditSelected(e.id)\">\n <span twItemTitle><strong>{{ e.actor }}</strong> {{ e.action }}</span>\n <span twItemDescription>Click to view full audit record.</span>\n </tw-item>\n </tw-timeline-item>\n }\n</tw-timeline>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-timeline>\n <tw-timeline-item color=\"success\" state=\"reached\" timestamp=\"Apr 10, 2026\">\n <p class=\"text-sm\">Alice Morgan opened pull request <strong>#421</strong>.</p>\n </tw-timeline-item>\n <tw-timeline-item color=\"primary\" state=\"reached\" timestamp=\"Apr 11, 2026\">\n <p class=\"text-sm\">Ben Rivera left a review with 3 comments.</p>\n </tw-timeline-item>\n <tw-timeline-item color=\"success\" state=\"current\" timestamp=\"Apr 12, 2026\">\n <p class=\"text-sm\">CI checks running on commit <code>e83a4c1</code>.</p>\n </tw-timeline-item>\n <tw-timeline-item color=\"neutral\" state=\"pending\">\n <p class=\"text-sm text-fg-muted\">Merge to <code>main</code>.</p>\n </tw-timeline-item>\n</tw-timeline>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n TimelineComponent,\n TimelineItemComponent,\n TimelineMarkerDirective,\n TimelineTimestampDirective,\n TimelineOppositeDirective,\n} from '@cdevhub/ngx-tw/timeline';"}],"summary":"Presentational chronological sequence — events laid out along a single vertical or horizontal axis, each pairing a dot or circle marker with content and connected by a line running through the markers.","whenToUse":["A history the user reads rather than navigates: audit logs, activity feeds, comment threads, changelogs","Process progress that is reported, not driven — order tracking, build pipelines, deployment stages","Marking where a sequence currently stands with `aria-current=\"step\"` and per-item reached / pending / current / error states","Alternating or split layouts where dates sit opposite the event body","Feeds whose markers carry an avatar or an icon rather than a plain dot"],"whenNotToUse":[{"instead":"stepper","because":"the user actually navigates through the steps and each step owns a panel or form section"},{"instead":"item","because":"the rows need no chronological axis or connector line and are just a list of compositions"}],"related":["stepper","item","avatar","icon","badge"],"aliases":["activity feed","history","changelog","audit log","event log","chronology","feed","milestones","progress trail","vertical steps"],"hasMeta":true,"metaPath":"projects/ngx-tw/timeline/timeline.meta.ts"},{"name":"carousel","importPath":"@cdevhub/ngx-tw/carousel","symbols":[{"name":"CarouselComponent","kind":"component","description":"Slide / swipe gallery primitive. Renders projected `<tw-carousel-slide>` children in a horizontally (or vertically) scrolling viewport with native CSS scroll-snap, optional autoplay, mouse-pointer drag, keyboard navigation, prev/next directive hosts, and an `<tw-carousel-indicators>` companion. Implements the W3C APG carousel pattern: `role=\"region\"` + `aria-roledescription=\"carousel\"` on the host; the inner viewport carries `tabindex=\"0\"` and a polite `aria-live` region when autoplay is off.","selector":"tw-carousel","usage":[{"form":"element","selector":"tw-carousel","name":"tw-carousel"}],"contentSlots":[{"select":"tw-carousel-slide"},{"select":"[twCarouselPrev], [twCarouselNext], tw-carousel-indicators"},{"select":null}],"inputs":[{"name":"orientation","type":"TwOrientation","default":"'horizontal'","description":"Axis along which slides flow. `'horizontal'` is the canonical case; `'vertical'` is supported for tickers and feature lists. Defaults to `'horizontal'`."},{"name":"slidesPerView","type":"number","default":"1","description":"Number of slides visible in the viewport. May be fractional (e.g. `1.2` for a \"peek\" of the next slide). Values below `0.5` are clamped to `0.5`; values above the slide count are clamped to the slide count. Defaults to `1`."},{"name":"slidesToScroll","type":"number","default":"1","description":"Number of slides advanced per navigation action (button click, keyboard, indicator). Non-integer values are floored. Defaults to `1`."},{"name":"gap","type":"TwSize","default":"'md'","description":"Inter-slide gap on the scroll axis. Mapped via the canonical spacing scale. Defaults to `'md'`."},{"name":"loop","type":"boolean","default":"false","description":"When `true`, navigation wraps around at the boundaries. Prev at slide 0 jumps to the last page; Next at the last page jumps to slide 0. Implementation is jumpless via a brief opacity mask. Defaults to `false`."},{"name":"autoplay","type":"boolean","default":"false","description":"When `true`, the carousel auto-advances by `slidesToScroll` every `autoplayInterval` ms. Pauses on hover, focus-in, drag, document hidden, or user interaction. Defaults to `false`."},{"name":"autoplayInterval","type":"number","default":"5000","description":"Milliseconds between autoplay advances. Values below `1000` are clamped to `1000` per WCAG 2.2.2. Defaults to `5000`."},{"name":"pauseOnHover","type":"boolean","default":"true","description":"Pauses autoplay while the pointer is over the container. Defaults to `true` because losing autoplay on hover is the expected gallery behavior — opt-out is the special case."},{"name":"pauseOnFocusIn","type":"boolean","default":"true","description":"Pauses autoplay while keyboard focus is anywhere inside the container. Defaults to `true` for WCAG 2.2.2 compliance — opt-out is the special case."},{"name":"draggable","type":"boolean","default":"true","description":"When `true`, the user may pan the slides via mouse pointer drag; touch is left to native scroll. Pointer events are intercepted only when the drag exceeds a 6-pixel threshold so clicks inside slides still work. Defaults to `true` because galleries are draggable by user expectation — opt-out is the special case."},{"name":"keyboard","type":"boolean","default":"true","description":"When `true`, the viewport responds to Arrow / Home / End / PageUp / PageDown when focus is inside it. Defaults to `true` for keyboard accessibility — opt-out is the special case."},{"name":"snapAlign","type":"'start' | 'center' | 'end'","default":"'start'","description":"CSS `scroll-snap-align` value applied to each slide. `'start'` is the standard gallery behavior; `'center'` is used for peek/preview layouts. Defaults to `'start'`."},{"name":"ariaLabel","type":"string | null","default":"null","description":"Accessible name for the carousel region. If both this and `ariaLabelledBy` are `null`, a one-time dev-mode `console.warn` is logged (production builds never log). Defaults to `null`."},{"name":"ariaLabelledBy","type":"string | null","default":"null","description":"ID of an element labeling the carousel. Either `ariaLabel` or `ariaLabelledBy` SHOULD be provided. Defaults to `null`."},{"name":"labels","type":"Partial<TwCarouselLabels>","default":"{}","description":"Localizable strings for prev/next/pause/resume/indicator/slide-of templates. Unset keys fall back to the English defaults in `DEFAULT_CAROUSEL_LABELS`. Defaults to `{}`."}],"outputs":[{"name":"slideChange","payloadType":"TwCarouselSlideChangeEvent","description":"Fires when the active index changes. Payload identifies the previous and new index plus the trigger source."},{"name":"autoplayPaused","payloadType":"TwCarouselAutoplayReason","description":"Fires when autoplay transitions from running to paused. Payload is the pause reason."},{"name":"autoplayResumed","payloadType":"void","description":"Fires when autoplay transitions from paused to running."}],"models":[{"name":"activeIndex","type":"number","default":"0","description":"Two-way bound 0-based index of the first visible slide in the current page. Setting from the parent scrolls the viewport smoothly to align that slide; reading reflects user-driven scroll position after `scrollend`. Defaults to `0`."}],"methods":[{"name":"next","signature":"next(): void","description":"Advance by `slidesToScroll`. Wraps if `loop` is `true`; no-op at the last page when `loop` is `false`. Emits `slideChange` with trigger `'programmatic'` when called externally."},{"name":"prev","signature":"prev(): void","description":"Retreat by `slidesToScroll`. Wraps if `loop` is `true`; no-op at slide 0 when `loop` is `false`. Emits `slideChange` with trigger `'programmatic'` when called externally."},{"name":"scrollTo","signature":"scrollTo(index: number, opts?: { behavior?: 'smooth' | 'instant' }): void","description":"Jump to a specific 0-based slide index. `opts.behavior` is `'smooth' | 'instant'`; default is `'smooth'` unless `prefers-reduced-motion: reduce` is set, in which case the default is `'instant'`."},{"name":"pause","signature":"pause(_reason: TwCarouselAutoplayReason = 'manual'): void","description":"Pause autoplay. `reason` defaults to `'manual'`."},{"name":"resume","signature":"resume(): void","description":"Resume autoplay if `autoplay` input is `true`."}]},{"name":"CarouselSlideComponent","kind":"component","description":"A single slide inside a `<tw-carousel>`. Projects arbitrary content via its default slot. Reports its visibility back to the carousel container via a shared `IntersectionObserver`; hidden slides receive `aria-hidden=\"true\"` and the `inert` attribute so their focusable descendants are removed from the tab order.","selector":"tw-carousel-slide","usage":[{"form":"element","selector":"tw-carousel-slide","name":"tw-carousel-slide"}],"contentSlots":[{"select":null}],"inputs":[{"name":"label","type":"string | null","default":"null","description":"Optional human-readable label for the slide. When provided, used in the slide's `aria-label` as `\"{index + 1} of {total}: {label}\"`. When `null`, uses `\"{index + 1} of {total}\"` only. Defaults to `null`."},{"name":"disabled","type":"boolean","default":"false","description":"When `true`, the slide is rendered but skipped by Prev/Next, Indicators, keyboard nav, and autoplay. Programmatic `scrollTo` still lands on it. Visually muted (`opacity-50`, `cursor-not-allowed`). Defaults to `false`."}]},{"name":"CarouselIndicatorsComponent","kind":"component","description":"Renders one button per **page** (not per slide) inside a `<tw-carousel>`. The active button is marked with `aria-current=\"true\"` and a distinguishing scale / width / fill so it is identifiable beyond color alone (WCAG 1.4.1).","selector":"tw-carousel-indicators","usage":[{"form":"element","selector":"tw-carousel-indicators","name":"tw-carousel-indicators"}],"inputs":[{"name":"variant","type":"TwCarouselIndicatorVariant","default":"'dots'","description":"Visual style. `'dots'` = small filled circles; `'lines'` = short horizontal/vertical bars; `'numbers'` = text 1, 2, 3 inside small pills. Defaults to `'dots'`."},{"name":"color","type":"TwColor","default":"'primary'","description":"Color of the active indicator. Inactive indicators use neutral `fg-muted` tokens. Defaults to `'primary'`."},{"name":"size","type":"TwSize","default":"'md'","description":"Indicator size (diameter/length and gap between indicators). Defaults to `'md'`."},{"name":"position","type":"TwCarouselIndicatorPosition","default":"'below'","description":"When `'overlay'`, the indicators float absolutely-positioned over the carousel with a `bg-overlay-control` translucent capsule backdrop for contrast. When `'below'`, they sit below the carousel as a normal block. Defaults to `'below'`."}]},{"name":"CarouselPrevDirective","kind":"directive","description":"Apply to any focusable element (typically `<button>`) inside a `<tw-carousel>` to navigate to the previous page. Auto-disables at the first slide when the carousel is not looping. Sets `aria-label` to `labels.previous` unless the host already carries `aria-label` or `aria-labelledby`.","selector":"[twCarouselPrev]","usage":[{"form":"attribute","selector":"[twCarouselPrev]","name":"twCarouselPrev"}]},{"name":"CarouselNextDirective","kind":"directive","description":"Apply to any focusable element (typically `<button>`) inside a `<tw-carousel>` to navigate to the next page. Auto-disables at the last page when the carousel is not looping. Sets `aria-label` to `labels.next` unless the host already carries `aria-label` or `aria-labelledby`.","selector":"[twCarouselNext]","usage":[{"form":"attribute","selector":"[twCarouselNext]","name":"twCarouselNext"}]},{"name":"DEFAULT_CAROUSEL_LABELS","kind":"const","description":"Default English labels used when consumers do not override via the `labels` input.","type":"Readonly<TwCarouselLabels>"},{"name":"TwCarouselIndicatorVariant","kind":"type","description":"Visual style of the indicators row rendered by `<tw-carousel-indicators>`.","definition":"'dots' | 'lines' | 'numbers'"},{"name":"TwCarouselIndicatorPosition","kind":"type","description":"Where the indicators sit relative to the carousel viewport.","definition":"'overlay' | 'below'"},{"name":"TwCarouselSlideChangeTrigger","kind":"type","description":"What triggered an `activeIndex` change.","definition":"| 'pointer' | 'keyboard' | 'autoplay' | 'indicator' | 'button' | 'programmatic'"},{"name":"TwCarouselSlideChangeEvent","kind":"interface","description":"Payload emitted by `CarouselComponent.slideChange`.","members":[{"name":"from","type":"number","optional":false,"description":"Previous active slide index (0-based)."},{"name":"to","type":"number","optional":false,"description":"New active slide index (0-based)."},{"name":"trigger","type":"TwCarouselSlideChangeTrigger","optional":false,"description":"What triggered the change."}]},{"name":"TwCarouselAutoplayReason","kind":"type","description":"Reason emitted when autoplay transitions running → paused.","definition":"| 'hover' | 'focus' | 'interaction' | 'visibility' | 'manual'"},{"name":"TwCarouselLabels","kind":"interface","description":"Localizable strings for the carousel.","members":[{"name":"previous","type":"string","optional":false,"description":"Accessible label for the Previous-slide directive host. Default: `'Previous slide'`."},{"name":"next","type":"string","optional":false,"description":"Accessible label for the Next-slide directive host. Default: `'Next slide'`."},{"name":"pauseAutoplay","type":"string","optional":false,"description":"Accessible label for the autoplay pause control. Default: `'Pause autoplay'`."},{"name":"resumeAutoplay","type":"string","optional":false,"description":"Accessible label for the autoplay resume control. Default: `'Resume autoplay'`."},{"name":"indicator","type":"string","optional":false,"description":"Template for indicator-button accessible names. Variable: `{page}` (1-based). Default: `'Go to slide {page}'`."},{"name":"slideOfWithLabel","type":"string","optional":false,"description":"Template for per-slide accessible names with a custom label. Variables: `{index}` (1-based), `{total}`, `{label}`. Default: `'{index} of {total}: {label}'`."},{"name":"slideOf","type":"string","optional":false,"description":"Template for per-slide accessible names without a custom label. Variables: `{index}` (1-based), `{total}`. Default: `'{index} of {total}'`."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type TwCarouselIndicatorVariant = 'dots' | 'lines' | 'numbers';\n\ntype TwCarouselIndicatorPosition = 'overlay' | 'below';\n\ntype TwCarouselSlideChangeTrigger =\n | 'pointer'\n | 'keyboard'\n | 'autoplay'\n | 'indicator'\n | 'button'\n | 'programmatic';\n\ninterface TwCarouselSlideChangeEvent {\n /** Previous active slide index (0-based). */\n from: number;\n /** New active slide index (0-based). */\n to: number;\n /** What triggered the change. */\n trigger: TwCarouselSlideChangeTrigger;\n}\n\ntype TwCarouselAutoplayReason =\n | 'hover'\n | 'focus'\n | 'interaction'\n | 'visibility'\n | 'manual';\n\ninterface TwCarouselLabels {\n previous: string;\n next: string;\n pauseAutoplay: string;\n resumeAutoplay: string;\n /** Template — variable: {page}. */\n indicator: string;\n /** Template — variables: {index}, {total}, {label}. */\n slideOfWithLabel: string;\n /** Template — variables: {index}, {total}. */\n slideOf: string;\n}\n\n// English defaults exported as DEFAULT_CAROUSEL_LABELS."},{"id":"heroSnippet","title":"Hero gallery","language":"html","code":"<tw-carousel\n ariaLabel=\"Featured promotions\"\n [autoplay]=\"true\"\n [autoplayInterval]=\"4000\"\n [loop]=\"true\"\n>\n @for (hero of heroes; track hero.id) {\n <tw-carousel-slide [label]=\"hero.title\">\n <div class=\"flex h-56 flex-col items-start justify-end rounded-lg p-6\n bg-gradient-to-br text-white\" [class]=\"hero.tint\">\n <p class=\"text-2xl font-semibold mb-1\">{{ '{{' }} hero.title {{ '}}' }}</p>\n <p class=\"text-sm opacity-90\">{{ '{{' }} hero.subtitle {{ '}}' }}</p>\n </div>\n </tw-carousel-slide>\n }\n <tw-carousel-indicators position=\"overlay\" color=\"neutral\" />\n</tw-carousel>"},{"id":"productSnippet","title":"Product gallery","language":"html","code":"<tw-carousel ariaLabel=\"Featured products\">\n @for (product of products; track product.id) {\n <tw-carousel-slide [label]=\"product.name\">\n <div class=\"flex flex-col items-center gap-4 rounded-lg border\n border-border bg-surface p-8\">\n <!-- product cell -->\n </div>\n </tw-carousel-slide>\n }\n <button twCarouselPrev class=\"absolute start-2 top-1/2 -translate-y-1/2 z-10\n size-9 rounded-full bg-surface-raised/95 ...\">\n <!-- chevron-left -->\n </button>\n <button twCarouselNext class=\"absolute end-2 top-1/2 -translate-y-1/2 z-10\n size-9 rounded-full bg-surface-raised/95 ...\">\n <!-- chevron-right -->\n </button>\n <tw-carousel-indicators />\n</tw-carousel>"},{"id":"peekSnippet","title":"Peek layout (testimonials)","language":"html","code":"<tw-carousel\n ariaLabel=\"Customer testimonials\"\n [slidesPerView]=\"1.2\"\n [slidesToScroll]=\"1\"\n gap=\"md\"\n>\n @for (t of testimonials; track t.id) {\n <tw-carousel-slide [label]=\"t.author\">\n <figure class=\"flex h-full flex-col gap-4 rounded-lg border\n border-border bg-surface p-6\">\n <blockquote class=\"text-sm text-fg leading-relaxed\">\n {{ '{{' }} t.quote {{ '}}' }}\n </blockquote>\n <figcaption>{{ '{{' }} t.author {{ '}}' }}</figcaption>\n </figure>\n </tw-carousel-slide>\n }\n</tw-carousel>"},{"id":"onboardingSnippet","title":"Onboarding deck","language":"html","code":"<tw-carousel\n ariaLabel=\"Onboarding tour\"\n [(activeIndex)]=\"step\"\n>\n @for (s of onboardingSteps; track s.id) {\n <tw-carousel-slide [label]=\"s.title\">\n <div class=\"...\">{{ '{{' }} s.body {{ '}}' }}</div>\n </tw-carousel-slide>\n }\n\n <!-- Footer controls nest inside <tw-carousel> so the prev/next\n directives find the parent carousel via DI. -->\n <div class=\"flex items-center justify-between mt-5\">\n <button twButton twCarouselPrev variant=\"ghost\" color=\"neutral\">Back</button>\n <tw-carousel-indicators variant=\"lines\" />\n <button twButton twCarouselNext>Next</button>\n </div>\n</tw-carousel>"},{"id":"tickerSnippet","title":"Vertical news ticker","language":"html","code":"<tw-carousel\n ariaLabel=\"Latest updates\"\n orientation=\"vertical\"\n [autoplay]=\"true\"\n [autoplayInterval]=\"3500\"\n [loop]=\"true\"\n class=\"h-16\"\n>\n @for (h of headlines; track h.id) {\n <tw-carousel-slide [label]=\"h.title\">\n <p class=\"text-sm text-fg truncate\">{{ '{{' }} h.title {{ '}}' }}</p>\n </tw-carousel-slide>\n }\n</tw-carousel>"},{"id":"indicatorVariantsSnippet","title":"Indicator variants","language":"html","code":"<tw-carousel ariaLabel=\"Demo\">\n <!-- slides -->\n <tw-carousel-indicators variant=\"dots\" />\n <tw-carousel-indicators variant=\"lines\" />\n <tw-carousel-indicators variant=\"numbers\" />\n</tw-carousel>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-carousel ariaLabel=\"Featured promotions\">\n <tw-carousel-slide label=\"Spring sale\">\n <img src=\"/hero-spring.jpg\" alt=\"Spring sale — 30% off\" />\n </tw-carousel-slide>\n <tw-carousel-slide label=\"New collection\">\n <img src=\"/hero-collection.jpg\" alt=\"New collection live\" />\n </tw-carousel-slide>\n\n <button twCarouselPrev>‹</button>\n <button twCarouselNext>›</button>\n <tw-carousel-indicators />\n</tw-carousel>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n CarouselComponent,\n CarouselSlideComponent,\n CarouselIndicatorsComponent,\n CarouselPrevDirective,\n CarouselNextDirective,\n} from '@cdevhub/ngx-tw/carousel';"}],"summary":"Slide and swipe gallery built on native CSS scroll-snap, with looping, autoplay, drag, keyboard paging, indicators, and an APG-compliant pause control.","whenToUse":["A hero banner rotating through promotional images or announcements","A product image gallery the user swipes through on touch and drags with the mouse","A row of cards or testimonials showing several per view and paging sideways","Content that advances on its own but must satisfy WCAG 2.2.2 — the pause button renders automatically when autoplay is on","A vertical slide track, or a horizontal one that must flip direction under dir=\"rtl\"","Prev/next controls styled entirely by the consumer, applied as attribute directives to their own button primitive"],"whenNotToUse":[{"instead":"tabs","because":"the user picks one panel out of many explicitly rather than paging through them in order"},{"instead":"paginator","because":"the user is navigating discrete pages of data rather than a visual slide track"},{"instead":"stepper","because":"the sequence is a wizard with completion state and form validation between steps"},{"instead":"slider","because":"the control is a numeric range input rather than a gallery of slides"}],"related":["tabs","stepper","paginator","dialog","aspect-ratio"],"aliases":["slider","image slider","gallery","slideshow","swiper","slides","lightbox","banner rotator","scroll snap","filmstrip"],"hasMeta":true,"metaPath":"projects/ngx-tw/carousel/carousel.meta.ts"},{"name":"stat","importPath":"@cdevhub/ngx-tw/stat","symbols":[{"name":"StatComponent","kind":"component","description":"KPI tile — a compact display tile surfacing a single key performance indicator with an optional trend delta. Composes a `<dl>` / `<dt>` / `<dd>` definition list internally so the label-to-value pairing is announced naturally by assistive technology. Slots: - `[twStatLabel]` — caption (rendered as `<dt>`). - `[twStatValue]` — dominant value (rendered as `<dd>`). - `[twStatDescription]` — optional secondary text (second `<dd>`). - `[twStatIcon]` — optional leading icon (switches to icon-leading layout). - `[twStatFooter]` — optional auxiliary region (sparklines, tags). - `<tw-stat-delta>` — projected trend indicator alongside the value. Loading state replaces label/value/delta with `<tw-skeleton>` placeholders sized to match the density. Footer always renders during loading so consumers can compose their own skeleton charts.","selector":"tw-stat","usage":[{"form":"element","selector":"tw-stat","name":"tw-stat"}],"contentSlots":[{"select":"[twStatIcon]"},{"select":"[twStatFooter]"},{"select":"[twStatLabel]"},{"select":"[twStatValue]"},{"select":"tw-stat-delta"},{"select":"[twStatDescription]"}],"inputs":[{"name":"variant","type":"StatVariant","default":"'outlined'","description":"Surface treatment. `'plain'` removes border and background; `'outlined'` (default) adds a border on the surface token; `'elevated'` adds shadow and uses the raised surface; `'filled'` uses the muted surface with no border."},{"name":"size","type":"TwSize","default":"'md'","description":"Density scale — drives padding, internal gaps, value/label typography, and the skeleton placeholder dimensions. Defaults to `'md'`."},{"name":"loading","type":"boolean","default":"false","description":"When true, replaces label, value, and delta regions with `<tw-skeleton>` placeholders and toggles `aria-busy=\"true\"` on the host. Projected footer content still renders. Defaults to `false`.","transform":"booleanAttribute"}]},{"name":"StatDeltaComponent","kind":"component","description":"Compact trend indicator — direction + projected delta text + optional comparison label. Usable standalone or projected into a `<tw-stat>` tile. Sentiment is conveyed by both icon direction and color; consumers running \"lower is better\" metrics (bounce rate, latency, churn) set `inverted` to swap the success/error color mapping without changing the literal direction or the announced verb.","selector":"tw-stat-delta","usage":[{"form":"element","selector":"tw-stat-delta","name":"tw-stat-delta"}],"contentSlots":[{"select":null}],"inputs":[{"name":"direction","type":"StatDeltaDirection","default":"'neutral'","description":"Direction of change. `'up'` renders an up-chevron and (by default) the `success` color; `'down'` renders a down-chevron and the `error` color; `'neutral'` renders a horizontal-line glyph and the neutral color. Defaults to `'neutral'`."},{"name":"inverted","type":"boolean","default":"false","description":"When true, swaps success/error semantics so `down` reads as success and `up` reads as error — use for metrics where lower is better (bounce rate, error rate, latency, churn). `neutral` direction is unaffected. Defaults to `false`.","transform":"booleanAttribute"},{"name":"variant","type":"StatDeltaVariant","default":"'badge'","description":"Display style. `'badge'` (default) wraps the delta in a pill chip; `'inline'` is icon + text only with no chip; `'icon-only'` shows just the chevron for ultra-dense layouts."},{"name":"comparisonLabel","type":"string","description":"Optional comparison label rendered next to the delta value (e.g. `\"vs last week\"`, `\"since launch\"`). Defaults to `undefined`."},{"name":"ariaLabel","type":"string","description":"Explicit accessible label. When omitted, the component composes one from `direction` + projected text + `comparisonLabel`. Override when projected content is purely symbolic or already localized. Defaults to `undefined`."}]},{"name":"StatLabelDirective","kind":"directive","description":"Label slot — short caption (\"Revenue\", \"Active users\"). Projected into `<dt>`.","selector":"[twStatLabel]","usage":[{"form":"attribute","selector":"[twStatLabel]","name":"twStatLabel"}]},{"name":"StatValueDirective","kind":"directive","description":"Value slot — the dominant numeric/text element. Projected into `<dd>`.","selector":"[twStatValue]","usage":[{"form":"attribute","selector":"[twStatValue]","name":"twStatValue"}]},{"name":"StatDescriptionDirective","kind":"directive","description":"Description slot — optional secondary text under the value. Projected into a second `<dd>`.","selector":"[twStatDescription]","usage":[{"form":"attribute","selector":"[twStatDescription]","name":"twStatDescription"}]},{"name":"StatIconDirective","kind":"directive","description":"Icon slot — optional leading icon. Triggers the icon-leading layout.","selector":"[twStatIcon]","usage":[{"form":"attribute","selector":"[twStatIcon]","name":"twStatIcon"}]},{"name":"StatFooterDirective","kind":"directive","description":"Footer slot — optional auxiliary region (sparklines, tags, metadata). Always renders, even during loading.","selector":"[twStatFooter]","usage":[{"form":"attribute","selector":"[twStatFooter]","name":"twStatFooter"}]},{"name":"StatVariant","kind":"type","description":"Surface treatment for the stat tile.","definition":"'plain' | 'outlined' | 'elevated' | 'filled'"},{"name":"StatDeltaDirection","kind":"type","description":"Direction of trend conveyed by the delta indicator.","definition":"'up' | 'down' | 'neutral'"},{"name":"StatDeltaVariant","kind":"type","description":"Display style of the trend delta.","definition":"'badge' | 'inline' | 'icon-only'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"export type StatVariant = 'plain' | 'outlined' | 'elevated' | 'filled';\n\nexport type StatDeltaDirection = 'up' | 'down' | 'neutral';\n\nexport type StatDeltaVariant = 'badge' | 'inline' | 'icon-only';\n\n// Imported from '@cdevhub/ngx-tw/core' — included here for reference:\nexport type TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"<!-- Outlined (default) -->\n<tw-stat variant=\"outlined\">\n <span twStatLabel>Revenue</span>\n <span twStatValue>$24,580</span>\n <tw-stat-delta direction=\"up\" comparisonLabel=\"vs last week\">+12.5%</tw-stat-delta>\n</tw-stat>\n\n<!-- Elevated -->\n<tw-stat variant=\"elevated\">\n <span twStatLabel>Orders</span>\n <span twStatValue>1,284</span>\n <tw-stat-delta direction=\"up\" comparisonLabel=\"vs last week\">+8.1%</tw-stat-delta>\n</tw-stat>\n\n<!-- Filled -->\n<tw-stat variant=\"filled\">\n <span twStatLabel>MRR</span>\n <span twStatValue>$48.2k</span>\n <tw-stat-delta direction=\"up\" comparisonLabel=\"MoM\">+6.0%</tw-stat-delta>\n</tw-stat>\n\n<!-- Plain (no chrome) -->\n<tw-stat variant=\"plain\">\n <span twStatLabel>Churn</span>\n <span twStatValue>2.1%</span>\n <tw-stat-delta direction=\"down\" inverted comparisonLabel=\"MoM\">−0.4pp</tw-stat-delta>\n</tw-stat>"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"<tw-stat size=\"xs\"><!-- 27 tickets, neutral --></tw-stat>\n<tw-stat size=\"sm\"><!-- 3,412 sessions, +4.8% --></tw-stat>\n<tw-stat size=\"md\"><!-- 84,217 page views, +11.0% --></tw-stat>\n<tw-stat size=\"lg\"><!-- 12,894 active users, +5.2% --></tw-stat>\n<tw-stat size=\"xl\"><!-- $128,400 revenue, +18.6% --></tw-stat>"},{"id":"deltaSnippet","title":"Trend delta","language":"html","code":"<tw-stat>\n <span twStatLabel>Signups</span>\n <span twStatValue>1,402</span>\n <tw-stat-delta direction=\"up\" comparisonLabel=\"vs last week\">+14.2%</tw-stat-delta>\n</tw-stat>\n\n<tw-stat>\n <span twStatLabel>Trial conversions</span>\n <span twStatValue>312</span>\n <tw-stat-delta direction=\"down\" comparisonLabel=\"vs last week\">−6.8%</tw-stat-delta>\n</tw-stat>\n\n<tw-stat>\n <span twStatLabel>Active subscribers</span>\n <span twStatValue>8,940</span>\n <tw-stat-delta direction=\"neutral\" comparisonLabel=\"vs last week\">No change</tw-stat-delta>\n</tw-stat>"},{"id":"invertedSnippet","title":"Inverted sentiment","language":"html","code":"<!-- Bounce rate fell — down is GOOD here -->\n<tw-stat>\n <span twStatLabel>Bounce rate</span>\n <span twStatValue>38.2%</span>\n <tw-stat-delta direction=\"down\" inverted comparisonLabel=\"WoW\">−4.1pp</tw-stat-delta>\n</tw-stat>\n\n<!-- Error rate rose — up is BAD here -->\n<tw-stat>\n <span twStatLabel>Error rate</span>\n <span twStatValue>0.42%</span>\n <tw-stat-delta direction=\"up\" inverted comparisonLabel=\"WoW\">+0.12pp</tw-stat-delta>\n</tw-stat>\n\n<!-- p95 latency dropped — down is GOOD here -->\n<tw-stat>\n <span twStatLabel>p95 latency</span>\n <span twStatValue>184ms</span>\n <tw-stat-delta direction=\"down\" inverted comparisonLabel=\"vs last deploy\">−22ms</tw-stat-delta>\n</tw-stat>"},{"id":"deltaVariantsSnippet","title":"Delta display variants standalone use","language":"html","code":"<!-- Badge (default) — pill chip with colored background -->\n<tw-stat-delta direction=\"up\" comparisonLabel=\"WoW\">+12.5%</tw-stat-delta>\n\n<!-- Inline — no chip, colored chevron + colored text -->\n<tw-stat-delta direction=\"up\" variant=\"inline\" comparisonLabel=\"WoW\">+12.5%</tw-stat-delta>\n\n<!-- Icon-only — chevron alone, text + comparison stay in aria-label -->\n<tw-stat-delta direction=\"up\" variant=\"icon-only\" comparisonLabel=\"WoW\">+12.5%</tw-stat-delta>"},{"id":"standaloneDeltaSnippet","title":"Delta display variants standalone use","language":"html","code":"<table>\n <thead>\n <tr><th>Channel</th><th>Sessions</th><th>7d trend</th></tr>\n </thead>\n <tbody>\n <tr>\n <td>Organic search</td>\n <td>21,408</td>\n <td><tw-stat-delta direction=\"up\" variant=\"inline\">+5.4%</tw-stat-delta></td>\n </tr>\n <tr>\n <td>Paid social</td>\n <td>8,912</td>\n <td><tw-stat-delta direction=\"down\" variant=\"inline\">−3.1%</tw-stat-delta></td>\n </tr>\n </tbody>\n</table>"},{"id":"iconLeadingSnippet","title":"Icon-leading layout","language":"html","code":"<tw-stat variant=\"elevated\">\n <span twStatIcon class=\"flex size-10 items-center justify-center rounded-lg bg-primary-50 text-primary-600\">\n <tw-icon name=\"user\" size=\"md\" aria-hidden=\"true\" />\n </span>\n <span twStatLabel>Active users</span>\n <span twStatValue>12,894</span>\n <tw-stat-delta direction=\"up\" variant=\"inline\" comparisonLabel=\"WoW\">+5.2%</tw-stat-delta>\n</tw-stat>"},{"id":"loadingSnippet","title":"Loading state","language":"html","code":"<div class=\"grid grid-cols-4 gap-4\">\n @for (tile of tiles(); track tile.id) {\n <tw-stat [loading]=\"isLoading()\">\n <span twStatLabel>{{ tile.label }}</span>\n <span twStatValue>{{ tile.value }}</span>\n <tw-stat-delta [direction]=\"tile.direction\" comparisonLabel=\"vs last week\">\n {{ tile.delta }}\n </tw-stat-delta>\n </tw-stat>\n }\n</div>"},{"id":"footerSnippet","title":"Footer slot","language":"html","code":"<tw-stat variant=\"elevated\" size=\"lg\">\n <span twStatLabel>Revenue</span>\n <span twStatValue>$128,400</span>\n <tw-stat-delta direction=\"up\" variant=\"inline\">+18.6%</tw-stat-delta>\n <div twStatFooter>\n <svg viewBox=\"0 0 100 28\" preserveAspectRatio=\"none\" class=\"w-full h-7 text-success-500\" aria-hidden=\"true\">\n <path d=\"M0,22 L12,18 L24,20 L36,14 L48,16 L60,10 L72,12 L84,6 L100,4\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </div>\n</tw-stat>\n\n<tw-stat variant=\"elevated\" size=\"lg\">\n <span twStatLabel>Active subscribers</span>\n <span twStatValue>8,940</span>\n <tw-stat-delta direction=\"neutral\" variant=\"inline\">±0</tw-stat-delta>\n <div twStatFooter class=\"flex items-center gap-2\">\n <span twBadge color=\"success\" size=\"sm\">Live</span>\n <span class=\"text-xs text-fg-muted\">Updated 2 min ago</span>\n </div>\n</tw-stat>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-stat>\n <span twStatLabel>Revenue</span>\n <span twStatValue>$24,580</span>\n <span twStatDescription>Past 30 days</span>\n <tw-stat-delta direction=\"up\" comparisonLabel=\"vs prior period\">+12.5%</tw-stat-delta>\n</tw-stat>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n StatComponent,\n StatDeltaComponent,\n StatLabelDirective,\n StatValueDirective,\n StatDescriptionDirective,\n StatIconDirective,\n StatFooterDirective,\n} from '@cdevhub/ngx-tw/stat';"}],"summary":"KPI tile presenting a single dominant numeric value with a short label, optional description, and an optional trend delta against a comparison period.","whenToUse":["Analytics dashboards, admin overviews, and reporting surfaces where four to twelve metrics share a grid","Showing a metric alongside its movement (\"+12.5% vs prior period\") with direction conveyed by icon, color, and an announced label","\"Lower is better\" metrics — bounce rate, latency, error rate, churn — where the success/error colors need inverting without flipping the literal direction","A metric block that must show placeholders while its number is still loading","Pairing the number with a sparkline, status badge, or auxiliary metadata in a footer slot"],"whenNotToUse":[{"instead":"progress-bar","because":"the KPI reads as progress toward a target rather than a change versus a previous period"},{"instead":"badge","because":"the number annotates some other content (a count on a tab or row) instead of being the block itself"},{"instead":"card","because":"you need the frame around a whole dashboard section — stat is the tile, not the section container"}],"related":["card","badge","skeleton","progress-bar","icon"],"aliases":["kpi","metric","metrics","statistic","stat card","metric card","number tile","trend","delta","scorecard","dashboard tile"],"hasMeta":true,"metaPath":"projects/ngx-tw/stat/stat.meta.ts"},{"name":"combobox","importPath":"@cdevhub/ngx-tw/combobox","symbols":[{"name":"ComboboxComponent","kind":"component","description":"Editable single-select typeahead form control. Follows the WAI-ARIA 1.2 combobox + listbox pattern with `aria-activedescendant` (DOM focus stays on the `<input>`). Supports local filtering, async result streams via `queryChange`, grouped options, strict and free-text modes, and full integration with template-driven, reactive, and signal forms plus `tw-form-field`.","selector":"tw-combobox","usage":[{"form":"element","selector":"tw-combobox","name":"tw-combobox"}],"contentSlots":[{"select":"[twComboboxPrefix]"},{"select":"[twComboboxSuffix]"},{"select":null}],"inputs":[{"name":"options","type":"readonly unknown[]","default":"[]","description":"Array of options to render in the popover. Accepts plain records or `TwComboboxOption<T>`."},{"name":"optionLabel","type":"(option: unknown) => string","default":"defaultOptionLabel","description":"Accessor returning the visible label for an option. Used by the default filter and the trigger label resolver."},{"name":"optionValue","type":"(option: unknown) => T","default":"defaultOptionValue as (option: unknown) => T","description":"Accessor returning the value emitted via `valueCommit` when this option is picked."},{"name":"optionDisabled","type":"(option: unknown) => boolean","default":"defaultOptionDisabled","description":"Accessor returning whether an option is non-interactive."},{"name":"optionGroup","type":"(option: unknown) => string | undefined","default":"defaultOptionGroup","description":"Accessor returning a group label. Options sharing a group render under a labelled `role=\"group\"` region."},{"name":"optionDescription","type":"(option: unknown) => string | undefined","default":"defaultOptionDescription","description":"Accessor returning an optional secondary description rendered under the label in the default option row."},{"name":"filterFn","type":"((option: unknown, query: string) => boolean) | null","default":"defaultStartsWithFilter","description":"Filter function applied client-side whenever `inputValue` changes. Pass `null` to disable client filtering (async mode). Defaults to case-insensitive `startsWith` on the label."},{"name":"strict","type":"boolean","default":"false","description":"When `true`, free-text commits are rejected — the input reverts to the last committed label on blur."},{"name":"placeholder","type":"string | undefined","default":"undefined","description":"Placeholder shown when the input is empty."},{"name":"disabledInput","type":"boolean","default":"false","description":"Disables the input and prevents the popover from opening.","alias":"disabled"},{"name":"requiredInput","type":"boolean","default":"false","description":"Sets `aria-required=\"true\"` on the input.","alias":"required"},{"name":"size","type":"TwSize","default":"'md'","description":"Controls trigger padding and font size per the inline padding scale."},{"name":"color","type":"TwColor","default":"'primary'","description":"Semantic color for the focus ring."},{"name":"showChevron","type":"boolean","default":"true","description":"Whether the trailing chevron affordance is rendered."},{"name":"clearable","type":"boolean","default":"true","description":"Whether the inline clear (×) button appears while `inputValue` is non-empty."},{"name":"loading","type":"boolean","default":"false","description":"When `true`, shows an in-popover spinner and an inline spinner in the trigger while the popover is open."},{"name":"queryDebounce","type":"number","default":"150","description":"Debounce window (ms) before `queryChange` emits. Local filtering is not debounced."},{"name":"minQueryLength","type":"number","default":"0","description":"Minimum query length before the popover opens automatically. `0` opens on focus."},{"name":"openOnFocus","type":"boolean","default":"true","description":"Whether the popover opens automatically when the input receives focus."},{"name":"panelMaxHeight","type":"number","default":"256","description":"Maximum height (px) of the popover scroll region."},{"name":"panelWidth","type":"'trigger' | 'auto' | number | string","default":"'trigger'","description":"Overlay width strategy. `'trigger'` matches input width; `'auto'` lets content decide; a number is applied as px; a string is passed as a CSS length."},{"name":"panelClass","type":"string | readonly string[]","default":"''","description":"Extra class(es) appended to the overlay panel for consumer customization."},{"name":"scrollStrategy","type":"'reposition' | 'close' | 'block'","default":"'reposition'","description":"CDK overlay scroll strategy."},{"name":"offset","type":"number","default":"4","description":"Pixel offset between the input and the popover."},{"name":"emptyMessage","type":"string","default":"'No results'","description":"Fallback empty-state message when no `*twComboboxEmpty` template is projected."},{"name":"compareWith","type":"(a: T, b: T) => boolean","default":"Object.is","description":"Equality comparator used to reconcile `value` with options during `writeValue`."},{"name":"ariaLabel","type":"string | undefined","default":"undefined","description":"Accessible name for the combobox input.","alias":"aria-label"},{"name":"ariaLabelledby","type":"string | undefined","default":"undefined","description":"ID of an external label element.","alias":"aria-labelledby"},{"name":"ariaDescribedby","type":"string | undefined","default":"undefined","description":"ID of an external descriptor element.","alias":"aria-describedby"},{"name":"errorStateMatcher","type":"ErrorStateMatcher | undefined","default":"undefined","description":"Per-instance override of the ErrorStateMatcher. When omitted, the combobox uses the `TW_ERROR_STATE_MATCHER` token's value."}],"outputs":[{"name":"queryChange","payloadType":"string","description":"Fires after the query text changes, debounced by `queryDebounce`. Async-mode consumers subscribe to this to fetch results."},{"name":"optionSelected","payloadType":"TwComboboxOptionSelectedEvent<T>","description":"Fires when the user picks an option from the list (not on free-text commit)."},{"name":"valueCommit","payloadType":"TwComboboxValueCommitEvent<T>","description":"Fires whenever `value` changes, with a `source` discriminator distinguishing option / free-text / reset / programmatic origin."},{"name":"openedChange","payloadType":"TwComboboxOpenedEvent","description":"Fires when the popover opens or closes."}],"models":[{"name":"value","type":"T | string | null","default":"null","description":"Two-way bound committed value. May be an option's value (`T`), a typed string (free-text mode), or `null`."},{"name":"inputValue","type":"string","default":"''","description":"Two-way bound visible text in the input. Bound separately from `value` so async consumers can drive the query."},{"name":"open","type":"boolean","default":"false","description":"Two-way bound open state of the popover."}],"methods":[{"name":"openPanel","signature":"openPanel(): void","description":"Opens the popover. No-op when disabled or already open."},{"name":"closePanel","signature":"closePanel(): void","description":"Closes the popover. No-op when already closed."},{"name":"focus","signature":"focus(): void","description":"Programmatically focuses the input."},{"name":"clear","signature":"clear(): void","description":"Clears the input and committed value, emitting `valueCommit({ source: 'reset' })`."}],"formControl":true},{"name":"ComboboxOptionTemplateDirective","kind":"directive","description":"Structural directive projecting a custom template for each option row. Context: `TwComboboxOptionContext<T>`.","selector":"[twComboboxOption]","usage":[{"form":"attribute","selector":"[twComboboxOption]","name":"twComboboxOption"}]},{"name":"ComboboxEmptyTemplateDirective","kind":"directive","description":"Structural directive projecting a custom template for the empty-results state. Context: `{ $implicit: query }`.","selector":"[twComboboxEmpty]","usage":[{"form":"attribute","selector":"[twComboboxEmpty]","name":"twComboboxEmpty"}]},{"name":"ComboboxLoadingTemplateDirective","kind":"directive","description":"Structural directive projecting a custom template above the list while `loading=true`.","selector":"[twComboboxLoading]","usage":[{"form":"attribute","selector":"[twComboboxLoading]","name":"twComboboxLoading"}]},{"name":"ComboboxPrefixDirective","kind":"directive","description":"Attribute directive marking a leading adornment projected inside the input row.","selector":"[twComboboxPrefix]","usage":[{"form":"attribute","selector":"[twComboboxPrefix]","name":"twComboboxPrefix"}]},{"name":"ComboboxSuffixDirective","kind":"directive","description":"Attribute directive marking a trailing adornment projected inside the input row, before the clear button.","selector":"[twComboboxSuffix]","usage":[{"form":"attribute","selector":"[twComboboxSuffix]","name":"twComboboxSuffix"}]},{"name":"TwComboboxOption","kind":"interface","description":"Canonical option shape for `tw-combobox`. Consumers using arbitrary records override the accessor inputs instead.","members":[{"name":"label","type":"string","optional":false,"description":"Visible label. Used by the default filter and trigger render."},{"name":"value","type":"T","optional":false,"description":"Value emitted via `valueCommit` / `optionSelected` when this option is picked."},{"name":"disabled","type":"boolean","optional":true,"description":"When true, the option cannot be highlighted, selected, or matched by the resolver."},{"name":"group","type":"string","optional":true,"description":"Optional group label. Options sharing a group render under a labelled `role=\"group\"` region."},{"name":"description","type":"string","optional":true,"description":"Optional secondary description rendered under the label in the default option row."}]},{"name":"TwComboboxOptionContext","kind":"interface","description":"Template context passed to `*twComboboxOption` consumer templates.","members":[{"name":"$implicit","type":"O","optional":false,"description":"Raw option object (or arbitrary record)."},{"name":"option","type":"O","optional":false,"description":"Same as `$implicit`, named for clarity."},{"name":"label","type":"string","optional":false,"description":"Resolved label."},{"name":"value","type":"T","optional":false,"description":"Resolved value."},{"name":"selected","type":"boolean","optional":false,"description":"Whether this option is the currently committed selection."},{"name":"active","type":"boolean","optional":false,"description":"Whether this option is the active descendant (keyboard highlight)."},{"name":"disabled","type":"boolean","optional":false,"description":"Whether this option is disabled."},{"name":"index","type":"number","optional":false,"description":"Index within the visible (filtered) options."}]},{"name":"TwComboboxValueSource","kind":"type","description":"Discriminator for the origin of a `valueCommit` emission.","definition":"'option' | 'free-text' | 'reset' | 'programmatic'"},{"name":"TwComboboxFilterFn","kind":"type","description":"Filter callback applied to `options` whenever `inputValue` changes. Return `true` to keep the option visible.","definition":"(option: unknown, query: string) => boolean"},{"name":"TwComboboxOptionSelectedEvent","kind":"interface","description":"Payload of the `optionSelected` output.","members":[{"name":"option","type":"unknown","optional":false,"description":"The raw option object (or arbitrary record when accessors are overridden)."},{"name":"value","type":"T","optional":false,"description":"Resolved value from `optionValue`."},{"name":"label","type":"string","optional":false,"description":"Resolved label from `optionLabel`."}]},{"name":"TwComboboxValueCommitEvent","kind":"interface","description":"Payload of the `valueCommit` output. Distinguishes selection / free-text / reset / programmatic.","members":[{"name":"value","type":"T | string | null","optional":false,"description":"The committed value."},{"name":"source","type":"TwComboboxValueSource","optional":false,"description":"What triggered the commit."}]},{"name":"TwComboboxOpenedEvent","kind":"interface","description":"Payload of the `openedChange` output.","members":[{"name":"open","type":"boolean","optional":false,"description":"Whether the popover is now open."},{"name":"trigger","type":"HTMLElement","optional":false,"description":"The combobox input element."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"interface TwComboboxOption<T> {\n label: string;\n value: T;\n disabled?: boolean;\n group?: string;\n description?: string;\n}\n\ntype TwComboboxValueSource = 'option' | 'free-text' | 'reset' | 'programmatic';\n\ntype TwComboboxFilterFn = (option: unknown, query: string) => boolean;\n\ninterface TwComboboxOptionSelectedEvent<T> {\n option: unknown;\n value: T;\n label: string;\n}\n\ninterface TwComboboxValueCommitEvent<T> {\n value: T | string | null;\n source: TwComboboxValueSource;\n}\n\ninterface TwComboboxOpenedEvent {\n open: boolean;\n trigger: HTMLElement;\n}\n\ninterface TwComboboxOptionContext<T, O = TwComboboxOption<T>> {\n $implicit: O;\n option: O;\n label: string;\n value: T;\n selected: boolean;\n active: boolean;\n disabled: boolean;\n index: number;\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-combobox\n [options]=\"fruits\"\n [size]=\"s\"\n [placeholder]=\"'Type a fruit (' + s + ')'\"\n [attr.aria-label]=\"'Size ' + s\"\n />\n}"},{"id":"colorsSnippet","title":"Colors","language":"html","code":"@for (c of colors; track c) {\n <tw-combobox\n [options]=\"fruits\"\n [color]=\"c\"\n [placeholder]=\"c\"\n [attr.aria-label]=\"'Color ' + c\"\n />\n}"},{"id":"asyncTsSnippet","title":"Async server search","language":"ts","code":"protected readonly query = signal('');\nprotected readonly loading = signal(false);\nprotected readonly results = signal<readonly { label: string; value: string }[]>([]);\n\nprotected onQueryChange(q: string): void {\n this.query.set(q);\n if (!q) { this.results.set([]); this.loading.set(false); return; }\n this.loading.set(true);\n // Replace with your real HTTP call. queryChange is already debounced\n // by [queryDebounce] (default 150ms), so no extra debouncing is needed.\n setTimeout(() => {\n this.results.set(fetchPeople(q));\n this.loading.set(false);\n }, 350);\n}"},{"id":"asyncHtmlSnippet","title":"Async server search","language":"html","code":"<tw-combobox\n [options]=\"results()\"\n [filterFn]=\"null\"\n [loading]=\"loading()\"\n (queryChange)=\"onQueryChange($event)\"\n placeholder=\"Search computer scientists…\"\n aria-label=\"Computer scientist\"\n>\n <ng-template twComboboxLoading>\n <div class=\"p-3 text-sm text-fg-muted\">Searching the directory…</div>\n </ng-template>\n</tw-combobox>"},{"id":"groupedSnippet","title":"Grouped options","language":"html","code":"// Each option carries a `group` field; options sharing\n// a group render under a labelled role=\"group\" header.\nconst countries = [\n { label: 'United States', value: 'us', group: 'Americas' },\n { label: 'Germany', value: 'de', group: 'Europe' },\n { label: 'Japan', value: 'jp', group: 'Asia' },\n // …\n];\n\n<tw-combobox\n [options]=\"countries\"\n placeholder=\"Type a country…\"\n aria-label=\"Country\"\n/>"},{"id":"strictSnippet","title":"Strict mode","language":"html","code":"<tw-combobox\n [options]=\"fruits\"\n [(value)]=\"fruit\"\n [strict]=\"true\"\n placeholder=\"Pick a fruit (strict)\"\n aria-label=\"Strict fruit\"\n/>"},{"id":"freeTextTsSnippet","title":"Free-text creation","language":"ts","code":"protected readonly tagInput = signal('');\nprotected readonly tags = signal<readonly string[]>([]);\n\nprotected onTagCommit(event: TwComboboxValueCommitEvent<unknown>): void {\n if (event.source !== 'free-text') return;\n const raw = typeof event.value === 'string' ? event.value.trim() : '';\n if (!raw || this.tags().includes(raw)) return;\n this.tags.update(list => [...list, raw]);\n this.tagInput.set('');\n}"},{"id":"freeTextHtmlSnippet","title":"Free-text creation","language":"html","code":"<tw-combobox\n [options]=\"fruits\"\n [(inputValue)]=\"tagInput\"\n (valueCommit)=\"onTagCommit($event)\"\n placeholder=\"Type a tag and press Enter…\"\n aria-label=\"Add a tag\"\n/>\n\n@for (tag of tags(); track tag) {\n <span class=\"tag-chip\">{{ tag }}</span>\n}"},{"id":"customOptionTsSnippet","title":"Custom option template","language":"ts","code":"interface User {\n id: number;\n name: string;\n role: string;\n}\n\nprotected readonly userLabel = (u: unknown) => (u as User).name;\nprotected readonly userValue = (u: unknown) => (u as User).id;"},{"id":"customOptionHtmlSnippet","title":"Custom option template","language":"html","code":"<tw-combobox\n [options]=\"users\"\n [optionLabel]=\"userLabel\"\n [optionValue]=\"userValue\"\n placeholder=\"Assign a teammate…\"\n aria-label=\"Assignee\"\n>\n <ng-template twComboboxOption let-u let-selected=\"selected\">\n <span class=\"avatar-chip\">{{ initials(u) }}</span>\n <span class=\"flex-1 min-w-0\">\n <span class=\"block truncate text-sm text-fg\">{{ u.name }}</span>\n <span class=\"block truncate text-xs text-fg-muted\">{{ u.role }}</span>\n </span>\n @if (selected) { <svg class=\"size-4 text-primary-600\">…</svg> }\n </ng-template>\n</tw-combobox>"},{"id":"reactiveTsSnippet","title":"Reactive Forms","language":"ts","code":"protected readonly fruitCtrl = new FormControl<string | null>('apple');"},{"id":"reactiveHtmlSnippet","title":"Reactive Forms","language":"html","code":"<tw-combobox\n [options]=\"fruits\"\n [formControl]=\"fruitCtrl\"\n placeholder=\"Choose a fruit\"\n aria-label=\"Fruit\"\n/>"},{"id":"tdTsSnippet","title":"Template-Driven Forms","language":"ts","code":"protected readonly fruit = signal<string | null>('apricot');"},{"id":"tdHtmlSnippet","title":"Template-Driven Forms","language":"html","code":"<tw-combobox\n name=\"fruit\"\n [options]=\"fruits\"\n [(ngModel)]=\"fruit\"\n placeholder=\"Choose a fruit\"\n aria-label=\"Fruit\"\n/>"},{"id":"signalTsSnippet","title":"Signal Forms","language":"ts","code":"protected readonly model = signal<{ fruit: string | null }>({ fruit: null });\nprotected readonly fruitForm = form(this.model, (p) => {\n required(p.fruit);\n});"},{"id":"signalHtmlSnippet","title":"Signal Forms","language":"html","code":"<tw-combobox\n [options]=\"fruits\"\n [formField]=\"fruitForm.fruit\"\n placeholder=\"Choose a fruit\"\n aria-label=\"Fruit\"\n/>"},{"id":"formFieldSnippet","title":"Inside tw-form-field (auto-naked)","language":"html","code":"<tw-form-field>\n <label twLabel>Favourite fruit</label>\n <tw-combobox [options]=\"fruits\" [(value)]=\"value\" aria-label=\"Fruit\" />\n <span twHint>Pick anything sweet; free-text is allowed.</span>\n</tw-form-field>\n\n<tw-form-field>\n <label twLabel>Required fruit</label>\n <tw-combobox [options]=\"fruits\" [formControl]=\"ctrl\" aria-label=\"Required fruit\" />\n @if (ctrl.touched && ctrl.hasError('required')) {\n <span twError>Pick a fruit before submitting.</span>\n }\n</tw-form-field>"},{"id":"prefilledSnippet","title":"Prefilled value","language":"ts","code":"protected readonly value = signal<string | null>('grape');\n// The combobox resolves the value against `options` on first render and writes\n// \"Grape\" into the input. Late-arriving options are reconciled automatically."},{"id":"longListSnippet","title":"Long list with internal scroll","language":"html","code":"<tw-combobox\n [options]=\"longList\"\n [panelMaxHeight]=\"280\"\n placeholder=\"Search 120 items…\"\n aria-label=\"Long list\"\n/>"},{"id":"customFilterSnippet","title":"Custom filter (fuzzy match)","language":"ts","code":"// Match anywhere in the label, not just the prefix.\nprotected readonly includesFilter = (option: unknown, query: string): boolean => {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (option as FruitOption).label.toLowerCase().includes(q);\n};\n\n// <tw-combobox [options]=\"fruits\" [filterFn]=\"includesFilter\" />"},{"id":"disabledOptionsSnippet","title":"Disabled options","language":"ts","code":"const fruits = [\n { label: 'Apple', value: 'apple' },\n { label: 'Apricot', value: 'apricot', disabled: true },\n { label: 'Banana', value: 'banana' },\n { label: 'Blueberry', value: 'blueberry', disabled: true },\n // …\n];\n// Disabled options skip during arrow navigation and refuse to commit."},{"id":"minQuerySnippet","title":"Min query length","language":"html","code":"<tw-combobox\n [options]=\"fruits\"\n [minQueryLength]=\"2\"\n placeholder=\"Type at least 2 characters…\"\n aria-label=\"Min query length\"\n/>"},{"id":"linkedTsSnippet","title":"Linked comboboxes","language":"ts","code":"protected readonly country = signal<string | null>(null);\nprotected readonly city = signal<string | null>(null);\nprotected readonly cities = computed(() => COUNTRY_CITIES[this.country() ?? ''] ?? []);\n\nprotected onCountryCommit(): void {\n // Reset the dependent value whenever the source changes.\n this.city.set(null);\n}"},{"id":"linkedHtmlSnippet","title":"Linked comboboxes","language":"html","code":"<tw-combobox\n [options]=\"countries\"\n [(value)]=\"country\"\n (valueCommit)=\"onCountryCommit()\"\n aria-label=\"Country\"\n/>\n<tw-combobox\n [options]=\"cities()\"\n [(value)]=\"city\"\n [disabled]=\"!country()\"\n [placeholder]=\"country() ? 'Pick a city' : 'Pick a country first'\"\n aria-label=\"City\"\n/>"},{"id":"statesSnippet","title":"States","language":"html","code":"<!-- Static disabled -->\n<tw-combobox [options]=\"fruits\" [disabled]=\"true\" placeholder=\"Not available\" aria-label=\"Disabled\" />\n\n<!-- Interactive toggle -->\n<tw-combobox [options]=\"fruits\" [disabled]=\"off()\" placeholder=\"Type a fruit\" aria-label=\"Toggle\" />\n<button twButton (click)=\"off.update(v => !v)\">{{ off() ? 'Enable' : 'Disable' }}</button>\n\n<!-- Loading: input stays editable; popover shows the loading template -->\n<tw-combobox [options]=\"fruits\" [loading]=\"true\" placeholder=\"Loading results…\" aria-label=\"Loading\" />"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-combobox\n [options]=\"fruits\"\n [(value)]=\"fruit\"\n placeholder=\"Type a fruit…\"\n aria-label=\"Fruit\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { ComboboxComponent } from '@cdevhub/ngx-tw/combobox';"}],"summary":"Editable typeahead text input paired with a popover listbox, where the typed text filters suggestions and may itself be committed as the value.","whenToUse":["A single-value field whose option list is too long to scroll and needs type-ahead filtering","Suggestions fetched from a server as the user types, driven by a debounced queryChange with client-side filtering turned off","Fields that accept a value outside the suggestion list, such as a tag or city that may not exist yet","Locking entry to the known set instead, via strict mode, while keeping the typing affordance"],"whenNotToUse":[{"instead":"select","because":"the value must come from a closed set with no typing, or the user needs to pick several values at once"},{"instead":"input","because":"the field is free text with no suggestion list behind it"},{"instead":"tags-input","because":"the user is building a list of free-text values rather than choosing one"},{"instead":"command-palette","because":"the typed query runs a global action rather than filling in a form field, and is reached by keyboard from anywhere"}],"related":["select","input","form-field","command-palette","tags-input"],"aliases":["autocomplete","typeahead","auto-complete","suggest","search input","dropdown","lookup","filterable select"],"hasMeta":true,"metaPath":"projects/ngx-tw/combobox/combobox.meta.ts"},{"name":"empty-state","importPath":"@cdevhub/ngx-tw/empty-state","symbols":[{"name":"EmptyStateComponent","kind":"component","description":"Zero-data layout primitive. Renders an icon, title, description, and actions for surfaces that have nothing to display. The component is intentionally neutral — accent comes from projected action buttons. It does not announce itself: consumers that want a live announcement (e.g. \"no search results found\") wrap the component with `<div role=\"status\" aria-live=\"polite\">…</div>` themselves.","selector":"tw-empty-state","usage":[{"form":"element","selector":"tw-empty-state","name":"tw-empty-state"}],"contentSlots":[{"select":"[twEmptyStateIcon]"},{"select":"[twEmptyStateActions]"}],"inputs":[{"name":"size","type":"TwSize","default":"'md'","description":"Controls overall spacing and icon scale. Defaults to `'md'`."},{"name":"variant","type":"EmptyStateVariant","default":"'centered'","description":"Layout style. `'centered'` stacks icon/title/description/actions vertically with center alignment for full-region usage; `'inline'` arranges them horizontally for compact rows. Defaults to `'centered'`."},{"name":"title","type":"string","description":"Primary heading text. Projected `*twEmptyStateTitle` content takes precedence. Defaults to `undefined`."},{"name":"description","type":"string","description":"Secondary descriptive text. Projected `*twEmptyStateDescription` content takes precedence. Defaults to `undefined`."},{"name":"titleLevel","type":"EmptyStateTitleLevel","default":"3","description":"Heading level used for the title element. Set to match the surrounding document outline. Defaults to `3`."}]},{"name":"EmptyStateIconDirective","kind":"directive","description":"Icon-slot marker. Project an icon element with `twEmptyStateIcon` to replace the fallback `<tw-icon name=\"inbox\">`.","selector":"[twEmptyStateIcon]","usage":[{"form":"attribute","selector":"[twEmptyStateIcon]","name":"twEmptyStateIcon"}]},{"name":"EmptyStateTitleDirective","kind":"directive","description":"Structural title slot. Captures a `TemplateRef` so the heading can be rendered into the dynamic `<h1>`–`<h6>` wrapper at runtime.","selector":"[twEmptyStateTitle]","usage":[{"form":"attribute","selector":"[twEmptyStateTitle]","name":"twEmptyStateTitle"}]},{"name":"EmptyStateDescriptionDirective","kind":"directive","description":"Structural description slot. Captures a `TemplateRef` so the content can be rendered into the `<p>` wrapper.","selector":"[twEmptyStateDescription]","usage":[{"form":"attribute","selector":"[twEmptyStateDescription]","name":"twEmptyStateDescription"}]},{"name":"EmptyStateActionsDirective","kind":"directive","description":"Actions-slot directive. Carries the actions row classes (`flex gap-2` etc.) on its host.","selector":"[twEmptyStateActions]","usage":[{"form":"attribute","selector":"[twEmptyStateActions]","name":"twEmptyStateActions"}]},{"name":"EmptyStateVariant","kind":"type","description":"Layout style of the empty state.","definition":"'centered' | 'inline'"},{"name":"EmptyStateTitleLevel","kind":"type","description":"Heading level for the title element. Matches native `<h1>`–`<h6>`.","definition":"1 | 2 | 3 | 4 | 5 | 6"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type EmptyStateVariant = 'centered' | 'inline';\n\ntype EmptyStateTitleLevel = 1 | 2 | 3 | 4 | 5 | 6;\n\n// Shared from '@cdevhub/ngx-tw/core':\ntype TwSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';"},{"id":"variantsSnippet","title":"Variants","language":"html","code":"<!-- Centered (default) -->\n<tw-empty-state\n title=\"No projects yet\"\n description=\"Create your first project to start tracking work.\"\n>\n <button twButton variant=\"solid\" color=\"primary\" twEmptyStateActions>\n New project\n </button>\n</tw-empty-state>\n\n<!-- Inline -->\n<tw-empty-state\n variant=\"inline\"\n title=\"No matching tasks\"\n description=\"Adjust filters to broaden the search.\"\n>\n <button twButton variant=\"ghost\" size=\"sm\" twEmptyStateActions>\n Reset filters\n </button>\n</tw-empty-state>"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (s of sizes; track s) {\n <tw-empty-state [size]=\"s\" title=\"No items\" description=\"Nothing to show yet.\" />\n}"},{"id":"iconsSnippet","title":"With Icons","language":"html","code":"<tw-empty-state title=\"Inbox zero\" description=\"Everything's caught up.\">\n <tw-icon twEmptyStateIcon name=\"check-circle\" size=\"xl\" color=\"success\" aria-hidden=\"true\" />\n</tw-empty-state>\n\n<tw-empty-state title=\"No results\" description=\"Try a different search term.\">\n <tw-icon twEmptyStateIcon name=\"search\" size=\"xl\" color=\"neutral\" aria-hidden=\"true\" />\n</tw-empty-state>"},{"id":"customTitleSnippet","title":"Custom title slot","language":"html","code":"<tw-empty-state description=\"Search returned nothing matching that query.\">\n <span *twEmptyStateTitle class=\"inline-flex items-center gap-2\">\n No results\n <span twBadge color=\"neutral\" size=\"sm\">0</span>\n </span>\n</tw-empty-state>"},{"id":"cardSnippet","title":"Inside a card","language":"html","code":"<tw-card variant=\"outlined\">\n <div twCardBody>\n <tw-empty-state\n title=\"No team members\"\n description=\"Invite collaborators to share this workspace.\"\n >\n <div twEmptyStateActions>\n <button twButton variant=\"solid\" color=\"primary\">Invite people</button>\n <button twButton variant=\"ghost\">Learn more</button>\n </div>\n </tw-empty-state>\n </div>\n</tw-card>"},{"id":"tableSnippet","title":"Inline in a table empty row","language":"html","code":"<table>\n <thead>\n <tr><th>Title</th><th>Owner</th><th>Status</th><th>Updated</th></tr>\n </thead>\n <tbody>\n <tr>\n <td colspan=\"4\">\n <tw-empty-state\n variant=\"inline\"\n size=\"sm\"\n title=\"No matching rows\"\n description=\"Adjust filters to see results.\"\n >\n <button twButton variant=\"ghost\" size=\"sm\" twEmptyStateActions>\n Reset filters\n </button>\n </tw-empty-state>\n </td>\n </tr>\n </tbody>\n</table>"},{"id":"liveSnippet","title":"Live-announcement wrapper","language":"html","code":"<div role=\"status\" aria-live=\"polite\">\n @if (results().length === 0) {\n <tw-empty-state\n title=\"No results\"\n description=\"No items match your search.\"\n >\n <tw-icon twEmptyStateIcon name=\"search\" size=\"lg\" color=\"neutral\" aria-hidden=\"true\" />\n </tw-empty-state>\n }\n</div>"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<tw-empty-state\n title=\"No messages\"\n description=\"When you receive a message it'll appear here.\"\n/>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n EmptyStateComponent,\n EmptyStateIconDirective,\n EmptyStateTitleDirective,\n EmptyStateDescriptionDirective,\n EmptyStateActionsDirective,\n} from '@cdevhub/ngx-tw/empty-state';"}],"summary":"Zero-data layout that fills the space where records normally live — a centered icon, heading, description, and optional action buttons for an empty inbox, a search with no matches, or a list before its first record exists.","whenToUse":["A request succeeded but returned zero rows, and the region would otherwise render blank","Onboarding a brand-new list with a call to action (\"Create your first project\") projected as buttons","A search or filter that matched nothing, offering a \"Clear filters\" action","Inside a table's no-results row, using the compact `inline` variant in a colspan'd cell","A heading that must participate in the document outline, via the `titleLevel` input"],"whenNotToUse":[{"instead":"skeleton","because":"data is still loading and the response has not yet confirmed there is nothing to show"},{"instead":"alert","because":"the message is an error or a transient notice that needs color emphasis and a live region"}],"related":["skeleton","alert","card","table","button","icon"],"aliases":["no results","no data","zero state","blank slate","blank state","nothing here","placeholder","empty list","first run","no records"],"hasMeta":true,"metaPath":"projects/ngx-tw/empty-state/empty-state.meta.ts"},{"name":"sheet","importPath":"@cdevhub/ngx-tw/sheet","symbols":[{"name":"Sheet","kind":"service","description":"Opens edge-anchored sheet (drawer) overlays. Composes `@angular/cdk/dialog` for focus trapping, portals, and overlay plumbing — adds a `GlobalPositionStrategy` pinned to the requested viewport edge, a Tailwind container with axis-aware sizing, slide enter/exit animations, and split close-behavior flags. The rendering layer (`@angular/cdk/dialog` + the Tailwind container) is loaded through a dynamic `import()` on the first `open()` call, so merely registering this service costs nothing in the initial bundle. `open()` still returns its SheetRef synchronously — the sheet is rendered once the chunk lands. Read the rendered component via SheetRef.whenComponentReady. Not `providedIn: 'root'` — register it via provideSheet.","methods":[{"name":"open","signature":"open(content: ComponentType<C> | TemplateRef<C>, config?: SheetConfig<D, R>): SheetRef<R, C>","description":"Opens a sheet using the given component or template."},{"name":"closeAll","signature":"closeAll(): void","description":"Closes every open sheet managed by this service (and child services)."},{"name":"getSheetById","signature":"getSheetById(id: string): SheetRef<R, C> | undefined","description":"Looks up an open sheet by its id."}]},{"name":"provideSheet","kind":"function","description":"Registers the Sheet service for dependency injection.","signature":"provideSheet(defaultOptions?: Partial<SheetConfig>): EnvironmentProviders"},{"name":"SheetRef","kind":"class","description":"Reference to a sheet opened via Sheet.open. Drives the sheet lifecycle (close, state, observables) and forwards useful overlay streams. The ref is returned **synchronously** from `open()`, but the sheet's render layer (`@angular/cdk/dialog` + the Tailwind container) is loaded through a dynamic `import()`. The ref therefore starts *detached*: `id`, `state`, `close()`, the lifecycle observables, and panel mutations all work immediately (mutations are buffered and replayed on attach), but the rendered component instance does not exist yet — read it via whenComponentReady instead of a synchronous `componentInstance` field.","methods":[{"name":"whenComponentReady","signature":"whenComponentReady(): Promise<C | null>","description":"Resolves with the rendered content-component instance once the sheet's render chunk has loaded and attached. Resolves `null` for template sheets, and for a sheet closed before it ever opened. Replaces the former synchronous `componentInstance` field, which cannot be populated before the deferred render chunk lands."},{"name":"close","signature":"close(result?: R): void","description":"Closes the sheet. The exit slide animation runs before the overlay is disposed."},{"name":"afterOpened","signature":"afterOpened(): Observable<void>","description":"Observable that emits once after the enter animation finishes."},{"name":"beforeClosed","signature":"beforeClosed(): Observable<R | undefined>","description":"Observable that emits once when the close animation starts."},{"name":"afterClosed","signature":"afterClosed(): Observable<R | undefined>","description":"Observable that emits once after the sheet has fully closed and the overlay is disposed."},{"name":"backdropClick","signature":"backdropClick(): Observable<MouseEvent>","description":"Backdrop click stream (emits even when `closeOnBackdropClick` / `disableClose` is set). Buffered — a subscription made before the sheet attaches receives events once it does."},{"name":"keydownEvents","signature":"keydownEvents(): Observable<KeyboardEvent>","description":"Keydown event stream for the overlay. Buffered — a subscription made before the sheet attaches receives events once it does."},{"name":"addPanelClass","signature":"addPanelClass(classes: string | string[]): this","description":"Adds CSS classes to the overlay panel. Buffered until attach."},{"name":"removePanelClass","signature":"removePanelClass(classes: string | string[]): this","description":"Removes CSS classes from the overlay panel. Buffered until attach."}]},{"name":"SheetAnimationEvent","kind":"type","description":"Event emitted when the sheet's animation state transitions.","definition":"OverlayContainerAnimationEvent"},{"name":"SheetState","kind":"type","description":"Lifecycle states a sheet passes through.","definition":"OverlayContainerState"},{"name":"SheetConfig","kind":"class","description":"Configuration for opening a sheet with Sheet.open. Extends `@angular/cdk/dialog`'s `DialogConfig` with sheet-specific options (`side`, `size`, animation durations, split close flags).","extends":"CdkDialogConfig"},{"name":"SHEET_DATA","kind":"token","description":"Injection token carrying the `data` value passed via SheetConfig.data."},{"name":"SHEET_DEFAULT_OPTIONS","kind":"token","description":"Injection token for application-wide default sheet options. Set via `provideSheet()`."},{"name":"SheetSide","kind":"type","description":"Edge the sheet anchors against.","definition":"'top' | 'right' | 'bottom' | 'left'"},{"name":"SheetSize","kind":"type","description":"Preset size for the sheet panel. Sizing is axis-dependent: - `'left' | 'right'` — controls panel width (height is always `100vh`). - `'top' | 'bottom'` — controls panel height (width is always `100vw`).","definition":"'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'"},{"name":"SheetRole","kind":"type","description":"ARIA role of the sheet element. Use `'alertdialog'` for destructive confirmation surfaces.","definition":"DialogRole"},{"name":"SheetAutoFocus","kind":"type","description":"Where to move focus when the sheet opens.","definition":"AutoFocusTarget | string | boolean"},{"name":"SheetRestoreFocus","kind":"type","description":"How to restore focus on close. `true` restores to the previously focused element; a selector or element targets a specific node.","definition":"RestoreFocusValue"},{"name":"SheetScrollStrategy","kind":"type","description":"Scroll behavior for content underneath the sheet.","definition":"'block' | 'close' | 'reposition' | 'noop'"},{"name":"SheetTitleDirective","kind":"directive","description":"Sheet title. Registers its ID with the container's `aria-labelledby` queue so screen readers announce it automatically.","selector":"[twSheetTitle], tw-sheet-title","usage":[{"form":"attribute","selector":"[twSheetTitle]","name":"twSheetTitle"},{"form":"element","selector":"tw-sheet-title","name":"tw-sheet-title"}],"inputs":[{"name":"id","type":"string","default":"this.generatedId","description":"Custom id for the title element. Defaults to a generated unique id."}]},{"name":"SheetSubtitleDirective","kind":"directive","description":"Secondary line beneath the sheet title — intended for a short description.","selector":"[twSheetSubtitle], tw-sheet-subtitle","usage":[{"form":"attribute","selector":"[twSheetSubtitle]","name":"twSheetSubtitle"},{"form":"element","selector":"tw-sheet-subtitle","name":"tw-sheet-subtitle"}]},{"name":"SheetDescriptionDirective","kind":"directive","description":"Sheet description. Registers its ID with the container's `aria-describedby` queue so screen readers announce the descriptive paragraph after the title. Mirrors SheetTitleDirective but for `aria-describedby`.","selector":"[twSheetDescription], tw-sheet-description","usage":[{"form":"attribute","selector":"[twSheetDescription]","name":"twSheetDescription"},{"form":"element","selector":"tw-sheet-description","name":"tw-sheet-description"}],"inputs":[{"name":"id","type":"string","default":"this.generatedId","description":"Custom id for the description element. Defaults to a generated unique id."}]},{"name":"SheetContentDirective","kind":"directive","description":"Scrollable content region of the sheet. Apply between the header and the actions bar. Inherits CDK's `CdkScrollable` to play nicely with scroll strategies and nested scrollables.","selector":"[twSheetContent], tw-sheet-content","usage":[{"form":"attribute","selector":"[twSheetContent]","name":"twSheetContent"},{"form":"element","selector":"tw-sheet-content","name":"tw-sheet-content"}]},{"name":"SheetActionsDirective","kind":"directive","description":"Bottom action bar of a sheet. Use inside the sheet content or template to host Cancel/Confirm buttons. Stays pinned below scrollable content.","selector":"[twSheetActions], tw-sheet-actions","usage":[{"form":"attribute","selector":"[twSheetActions]","name":"twSheetActions"},{"form":"element","selector":"tw-sheet-actions","name":"tw-sheet-actions"}],"inputs":[{"name":"align","type":"SheetActionsAlign","default":"'end'","description":"Horizontal alignment of the action buttons. Defaults to `'end'`."}]},{"name":"SheetCloseDirective","kind":"directive","description":"Closes the enclosing sheet when the host button is clicked. Provide a `[twSheetClose]` value to pass a result to `afterClosed()` subscribers.","selector":"[twSheetClose]","usage":[{"form":"attribute","selector":"[twSheetClose]","name":"twSheetClose"}],"inputs":[{"name":"twSheetClose","type":"unknown","default":"undefined","description":"Value passed to `afterClosed()` when the button is clicked."},{"name":"type","type":"'button' | 'submit' | 'reset'","default":"'button'","description":"Native button `type`. Defaults to `'button'` to avoid accidental form submission."}]},{"name":"SheetIconDirective","kind":"directive","description":"Decorative leading icon for a sheet header. Use with a semantic `color` to match destructive / informational / success sheets.","selector":"[twSheetIcon]","usage":[{"form":"attribute","selector":"[twSheetIcon]","name":"twSheetIcon"}],"inputs":[{"name":"color","type":"TwColor | undefined","default":"undefined","description":"Semantic color for the icon container. Defaults to a neutral surface."}]},{"name":"SheetHeaderDirective","kind":"directive","description":"Header wrapper for a sheet. Provides consistent padding and spacing for title + subtitle + leading icon, and separates the header from scrollable content below.","selector":"[twSheetHeader], tw-sheet-header","usage":[{"form":"attribute","selector":"[twSheetHeader]","name":"twSheetHeader"},{"form":"element","selector":"tw-sheet-header","name":"tw-sheet-header"}]},{"name":"SheetActionsAlign","kind":"type","description":"Horizontal alignment for sheet action buttons.","definition":"'start' | 'center' | 'end'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"type SheetSide = 'top' | 'right' | 'bottom' | 'left';\ntype SheetSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';\ntype SheetState = 'opening' | 'open' | 'closing' | 'closed';\ntype SheetRole = 'dialog' | 'alertdialog';\ntype SheetScrollStrategy = 'block' | 'reposition' | 'close' | 'noop';\ntype SheetAutoFocus = AutoFocusTarget | string | boolean;\ntype SheetRestoreFocus = boolean | string | HTMLElement;\ntype SheetActionsAlign = 'start' | 'center' | 'end';\n\ninterface SheetAnimationEvent {\n state: SheetState;\n totalTime: number;\n}"},{"id":"provideSnippet","title":"Import","language":"ts","code":"import { provideSheet } from '@cdevhub/ngx-tw/sheet';\n\nbootstrapApplication(AppComponent, {\n providers: [\n provideSheet({ side: 'right', size: 'md' }), // optional defaults\n ],\n});"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n Sheet,\n SHEET_DATA,\n SheetHeaderDirective,\n SheetIconDirective,\n SheetTitleDirective,\n SheetSubtitleDirective,\n SheetDescriptionDirective,\n SheetContentDirective,\n SheetActionsDirective,\n SheetCloseDirective,\n} from '@cdevhub/ngx-tw/sheet';"}],"summary":"Modal panel opened from a service and docked to one of the four viewport edges, sliding in along the docking axis with focus trapping and the WAI-ARIA dialog contract.","whenToUse":["A side panel for filters, details, or settings that should not lose the page behind it","A long form or record editor that reads better in a tall edge-anchored column than a centred box","A mobile-style bottom panel of actions or content pulled up from the bottom edge","A navigation drawer slid in from the left or right on small screens","Stacked panels — drilling from a list panel into a detail panel"],"whenNotToUse":[{"instead":"dialog","because":"the surface should be centred in the viewport rather than docked to an edge"},{"instead":"popover","because":"the panel belongs anchored to its trigger and should not block the page"},{"instead":"collapsible","because":"the content can expand inline in the page instead of covering it"}],"related":["dialog","popover","button","collapsible","split"],"aliases":["drawer","slide-over","side panel","off-canvas","panel","bottom sheet","flyout"],"hasMeta":true,"metaPath":"projects/ngx-tw/sheet/sheet.meta.ts"},{"name":"aspect-ratio","importPath":"@cdevhub/ngx-tw/aspect-ratio","symbols":[{"name":"AspectRatioDirective","kind":"directive","description":"Sets the native CSS `aspect-ratio` property on its host element, standardizing the `aspect-[16/9]` pattern consumers otherwise hand-roll across cards, thumbnails, video, and image grids. The directive sets **only** `aspect-ratio` — it does not set `width`, `display`, or any other layout property (matching Tailwind's `aspect-*` utility). Block-level hosts already fill their container width; for replaced elements (`<img>`, `<video>`) pair it with `w-full` / `object-cover` so the box has a definite cross-axis size. Purely presentational: it adds no `role` or `aria-*` and leaves the host media's own accessibility (`alt`, `aria-*`, `title`) untouched.","selector":"[twAspectRatio]","usage":[{"form":"attribute","selector":"[twAspectRatio]","name":"twAspectRatio"}],"exportAs":"twAspectRatio","inputs":[{"name":"twAspectRatio","type":"number | string","default":"DEFAULT_RATIO","description":"Sets the host's aspect ratio. Accepts a unitless number (e.g. `1.7777`) or a ratio string using `/` or `:` (e.g. `'16/9'` or `'16:9'`). The value is normalized to CSS `'<w> / <h>'` form (a bare number `n` becomes `'n / 1'`). Invalid or non-positive values fall back to the default. Defaults to a square (`1 / 1`)."}]}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"// The twAspectRatio input accepts (no exported type to import):\n// • a unitless number → [twAspectRatio]=\"1.7777\"\n// • a '/' ratio string → twAspectRatio=\"16/9\"\n// • a ':' ratio string → twAspectRatio=\"16:9\"\n// Every value normalizes to the CSS 'w / h' form; invalid input → '1 / 1'."},{"id":"commonRatiosSnippet","title":"Common Ratios","language":"html","code":"@for (r of commonRatios; track r.ratio) {\n <div [twAspectRatio]=\"r.ratio\" class=\"w-full\"></div>\n}"},{"id":"imagesSnippet","title":"Images","language":"html","code":"<img\n twAspectRatio=\"16/9\"\n src=\"/preview-wide.jpg\"\n alt=\"Landscape preview, 16 by 9\"\n class=\"w-full object-cover rounded-lg\"\n/>\n\n<img\n twAspectRatio=\"4/3\"\n src=\"/preview-classic.jpg\"\n alt=\"Landscape preview, 4 by 3\"\n class=\"w-full object-cover rounded-lg\"\n/>"},{"id":"videoSnippet","title":"Video Embeds","language":"html","code":"<div twAspectRatio=\"21/9\" class=\"w-full\">\n <iframe\n src=\"https://www.youtube.com/embed/VIDEO_ID\"\n title=\"Embedded video\"\n class=\"w-full h-full rounded-lg\"\n allowfullscreen\n ></iframe>\n</div>"},{"id":"syntaxSnippet","title":"Numeric Colon Syntax","language":"html","code":"<!-- Unitless number (bound) -->\n<div [twAspectRatio]=\"1.7777\" class=\"w-full\"></div>\n\n<!-- Slash string (native CSS syntax) -->\n<div twAspectRatio=\"16/9\" class=\"w-full\"></div>\n\n<!-- Colon string (normalized to 16 / 9) -->\n<div twAspectRatio=\"16:9\" class=\"w-full\"></div>"},{"id":"gridSnippet","title":"Image Grid","language":"html","code":"@for (tile of gridTiles; track tile.id) {\n <img\n twAspectRatio=\"1/1\"\n [src]=\"tile.src\"\n [alt]=\"tile.alt\"\n class=\"w-full object-cover rounded-md\"\n />\n}"},{"id":"basicUsageSnippet","title":"Basic Usage","language":"html","code":"<!-- Square — bare attribute, zero config -->\n<div twAspectRatio class=\"w-40\"></div>\n\n<!-- 16:9 — pass a ratio string -->\n<div twAspectRatio=\"16/9\" class=\"w-64\"></div>"},{"id":"importSnippet","title":"Import","language":"ts","code":"import { AspectRatioDirective } from '@cdevhub/ngx-tw/aspect-ratio';"}],"summary":"Attribute directive that pins any element to a fixed width-to-height ratio using the native CSS aspect-ratio property, accepting a number or a \"16/9\"-style string.","whenToUse":["Card cover images that must all render at the same shape regardless of the source file","A responsive video or iframe embed that has to hold 16/9 as the column width changes","Thumbnail and image grids where mismatched intrinsic sizes would make rows ragged","Every slide in a gallery needs a uniform footprint so paging does not jump the layout","A placeholder that must reserve the media box before the asset loads","Replacing hand-rolled aspect-[16/9] utility classes with one bindable input"],"related":["card","avatar","skeleton","carousel","icon"],"aliases":["ratio","aspect","aspect ratio box","16:9","4:3","square","intrinsic ratio","responsive embed","video wrapper","image ratio"],"hasMeta":true,"metaPath":"projects/ngx-tw/aspect-ratio/aspect-ratio.meta.ts"},{"name":"tree","importPath":"@cdevhub/ngx-tw/tree","symbols":[{"name":"TreeComponent","kind":"component","description":"A hierarchical, keyboard-navigable tree-view wrapping `@angular/cdk/tree` (children-accessor model). Consumers supply the per-node template via the `*twTreeNode` structural directive and receive a typed context exposing the node, its depth/expansion/children flags, its selection state, and action functions (`toggle`, `expand`, `collapse`, `toggleSelection`). Selection is optional and managed internally (`'none' | 'single' | 'multiple'`). In `'multiple'` mode with `cascade`, selecting a branch selects all its leaf descendants and the branch reports `'indeterminate'` when partially selected. This is NOT a form control — selection is exposed via the `selectionChange` output and the public `toggleSelection` / `selectionState` methods.","selector":"tw-tree","usage":[{"form":"element","selector":"tw-tree","name":"tw-tree"}],"exportAs":"twTree","inputs":[{"name":"data","type":"readonly T[]","default":"[]","description":"Root nodes of the tree. Each node's children are resolved via `childrenAccessor`. Defaults to `[]`."},{"name":"childrenAccessor","type":"(node: T) => readonly T[]","required":true,"description":"Resolves a node's direct children; return an empty array for leaf nodes. Required."},{"name":"trackBy","type":"(node: T) => unknown","description":"Identifies a node across data changes and keys both expansion and selection state. When unset, node identity is used."},{"name":"selection","type":"Partial<TwTreeSelectionConfig>","default":"{}","description":"Node-selection behavior — `mode`, `cascade`, `initialKeys`. Accepts a partial; unset keys fall back to the defaults. Defaults to selection disabled."},{"name":"display","type":"Partial<TwTreeDisplayConfig>","default":"{}","description":"Display configuration — `size`, `indent`, `showLines`. Accepts a partial; unset keys fall back to the defaults."}],"outputs":[{"name":"selectionChange","payloadType":"readonly T[]","description":"Fires after the selection changes by user interaction. Payload is every node whose `selectionState` is `'checked'` (selected leaves plus fully-checked branches in cascade mode)."},{"name":"expandedChange","payloadType":"{ node: T; expanded: boolean }","description":"Fires after a node is expanded or collapsed by user interaction. Payload identifies the node and its new expansion state."}],"models":[{"name":"expandedKeys","type":"readonly unknown[]","default":"[]","description":"Two-way bound list of expansion key values (resolved via `trackBy`) for currently-expanded nodes. Set a new array on every change; do not mutate in place. Changes when a node is expanded or collapsed. Defaults to `[]` (all collapsed)."}],"methods":[{"name":"hasChildren","signature":"hasChildren(node: T): boolean","description":"Whether the node has at least one child (is a branch)."},{"name":"isExpanded","signature":"isExpanded(node: T): boolean","description":"Whether the node is currently expanded."},{"name":"expand","signature":"expand(node: T): void","description":"Expands the node if collapsed. Emits `expandedChange` and updates `expandedKeys` via the CDK callback."},{"name":"collapse","signature":"collapse(node: T): void","description":"Collapses the node if expanded. Emits `expandedChange` and updates `expandedKeys` via the CDK callback."},{"name":"toggle","signature":"toggle(node: T): void","description":"Toggles the node's expand/collapse state. No-op on leaf nodes."},{"name":"selectionState","signature":"selectionState(node: T): TwTreeSelectionState","description":"Returns the tri-state selection status of a node. `'unchecked'` when selection is disabled. For cascade branches this walks the node's leaf descendants on each call and is invoked a few times per node per change-detection pass (row class, ARIA, context). This assumes small-to-medium trees; the component has no virtualization and is not tuned for very large data sets."},{"name":"toggleSelection","signature":"toggleSelection(node: T): void","description":"Toggles selection for a node (cascades to leaf descendants in `'multiple'` mode with `cascade` on). No-op when `selection.mode === 'none'`."}]},{"name":"TreeNodeDefDirective","kind":"directive","description":"Structural directive (`*twTreeNode=\"let node\"`) declaring the per-node template. Typed as `TwTreeNodeContext<T>`.","selector":"[twTreeNode]","usage":[{"form":"attribute","selector":"[twTreeNode]","name":"twTreeNode"}]},{"name":"TreeNodeToggleDirective","kind":"directive","description":"Attribute directive (`[twTreeNodeToggle]=\"node\"`) that toggles the given node's expansion on click.","selector":"[twTreeNodeToggle]","usage":[{"form":"attribute","selector":"[twTreeNodeToggle]","name":"twTreeNodeToggle"}],"inputs":[{"name":"node","type":"T","required":true,"description":"The node whose expansion this control toggles.","alias":"twTreeNodeToggle"}]},{"name":"TwTreeSelectionConfig","kind":"interface","description":"Configuration for node selection behavior. Pass any subset; unset keys fall back to the defaults.","members":[{"name":"mode","type":"'none' | 'single' | 'multiple'","optional":false,"description":"Selection mode. `'single'` is scalar (one node, no cascade/indeterminate); `'multiple'` supports cascade + tri-state. `'none'` disables selection. Defaults to `'none'`."},{"name":"cascade","type":"boolean","optional":true,"description":"In `'multiple'` mode, selecting a branch selects all its leaf descendants and a branch renders `'indeterminate'` when only some are selected. Ignored in `'single'`/`'none'`. Defaults to `true`."},{"name":"initialKeys","type":"readonly unknown[]","optional":true,"description":"Pre-selected node keys (matched via `trackBy`). In cascade mode these are leaf-node keys. Defaults to `[]`."}]},{"name":"TwTreeDisplayConfig","kind":"interface","description":"Display configuration for the tree. Pass any subset; unset keys fall back to the defaults.","members":[{"name":"size","type":"TwSize","optional":true,"description":"Row vertical density. Defaults to `'md'`."},{"name":"indent","type":"number","optional":true,"description":"Indentation per level, in pixels. Defaults to `16`."},{"name":"showLines","type":"boolean","optional":true,"description":"Render connecting guide lines down the indent gutter. Defaults to `false`."}]},{"name":"TwTreeNodeContext","kind":"interface","description":"Context surfaced to a `*twTreeNode` template. Generic over the node type `T`.","members":[{"name":"$implicit","type":"T","optional":false,"description":"The node data (implicit `let-node`)."},{"name":"node","type":"T","optional":false,"description":"The node data, aliased for readability."},{"name":"level","type":"number","optional":false,"description":"Zero-based depth of the node."},{"name":"expanded","type":"boolean","optional":false,"description":"Whether the node is currently expanded. Always `false` for leaf nodes."},{"name":"hasChildren","type":"boolean","optional":false,"description":"Whether the node has children (is a branch, not a leaf)."},{"name":"selectionState","type":"TwTreeSelectionState","optional":false,"description":"Tri-state selection status of this node. `'unchecked'` when selection is disabled."},{"name":"toggle","type":"() => void","optional":false,"description":"Toggles this node's expand/collapse state. No-op on leaf nodes."},{"name":"expand","type":"() => void","optional":false,"description":"Expands this node. No-op on leaf nodes or when already expanded."},{"name":"collapse","type":"() => void","optional":false,"description":"Collapses this node. No-op when already collapsed."},{"name":"toggleSelection","type":"() => void","optional":false,"description":"Toggles this node's selection (cascades in `'multiple'` mode with `cascade` on). No-op when selection is disabled."}]},{"name":"TwTreeSelectionState","kind":"type","description":"Tri-state selection status of a tree node.","definition":"'checked' | 'unchecked' | 'indeterminate'"}],"snippets":[{"id":"typesSnippet","title":"Types","language":"ts","code":"interface TwTreeSelectionConfig {\n /** 'none' | 'single' | 'multiple'. Defaults to 'none'. */\n mode: 'none' | 'single' | 'multiple';\n /** Cascade parent → leaf descendants (multiple mode). Defaults to true. */\n cascade?: boolean;\n /** Pre-selected node keys (leaf keys in cascade mode). Defaults to []. */\n initialKeys?: readonly unknown[];\n}\n\ninterface TwTreeDisplayConfig {\n /** Row density. Defaults to 'md'. */\n size?: TwSize;\n /** Indentation per level, in px. Defaults to 16. */\n indent?: number;\n /** Render per-level connector guide lines. Defaults to false. */\n showLines?: boolean;\n}\n\ntype TwTreeSelectionState = 'checked' | 'unchecked' | 'indeterminate';\n\ninterface TwTreeNodeContext<T> {\n $implicit: T;\n node: T;\n level: number;\n expanded: boolean;\n hasChildren: boolean;\n selectionState: TwTreeSelectionState;\n toggle: () => void;\n expand: () => void;\n collapse: () => void;\n toggleSelection: () => void;\n}"},{"id":"sizesSnippet","title":"Sizes","language":"html","code":"@for (size of sizes; track size) {\n <tw-tree\n [data]=\"project\"\n [childrenAccessor]=\"fileChildren\"\n [trackBy]=\"fileTrackBy\"\n [display]=\"{ size: size }\"\n [expandedKeys]=\"['src', 'app']\"\n >\n <ng-template twTreeNode let-node let-hasChildren=\"hasChildren\"\n let-isExpanded=\"expanded\" let-toggle=\"toggle\">\n @if (hasChildren) {\n <button type=\"button\" tabindex=\"-1\" (click)=\"toggle()\"\n [attr.aria-label]=\"isExpanded ? 'Collapse' : 'Expand'\">\n <tw-icon name=\"chevron-right\" size=\"xs\" [class.rotate-90]=\"isExpanded\" />\n </button>\n } @else {\n <span class=\"size-5\"></span>\n }\n <span>{{ '{{' }} node.label {{ '}}' }}</span>\n </ng-template>\n </tw-tree>\n}"},{"id":"singleSnippet","title":"Single Selection","language":"html","code":"<tw-tree\n [data]=\"docs\"\n [childrenAccessor]=\"navChildren\"\n [trackBy]=\"navTrackBy\"\n [selection]=\"{ mode: 'single' }\"\n [expandedKeys]=\"['guides', 'components']\"\n (selectionChange)=\"activeDoc.set($event[0]?.label ?? null)\"\n>\n <ng-template twTreeNode let-node let-hasChildren=\"hasChildren\"\n let-isExpanded=\"expanded\" let-toggle=\"toggle\"\n let-toggleSelection=\"toggleSelection\">\n <!-- chevron button (same as Sizes) -->\n <span class=\"flex-1\" (click)=\"toggleSelection()\">{{ '{{' }} node.label {{ '}}' }}</span>\n </ng-template>\n</tw-tree>"},{"id":"cascadeHtmlSnippet","title":"Multiple Selection Cascade","language":"html","code":"<tw-tree\n [data]=\"permissions\"\n [childrenAccessor]=\"permChildren\"\n [trackBy]=\"permTrackBy\"\n [selection]=\"{ mode: 'multiple', cascade: true }\"\n [expandedKeys]=\"['billing', 'members']\"\n (selectionChange)=\"grantedCount.set($event.length)\"\n>\n <ng-template twTreeNode let-node let-hasChildren=\"hasChildren\"\n let-state=\"selectionState\" let-toggle=\"toggle\"\n let-toggleSelection=\"toggleSelection\">\n <!-- chevron button for branches -->\n <span aria-hidden=\"true\" [class]=\"checkboxClass(state)\" (click)=\"toggleSelection()\">\n @if (state === 'checked') { <!-- check icon --> }\n @else if (state === 'indeterminate') { <!-- dash icon --> }\n </span>\n <span (click)=\"toggleSelection()\">{{ '{{' }} node.label {{ '}}' }}</span>\n </ng-template>\n</tw-tree>"},{"id":"cascadeTsSnippet","title":"Multiple Selection Cascade","language":"ts","code":"import type { TwTreeSelectionState } from '@cdevhub/ngx-tw/tree';\n\n// aria-checked=\"mixed\" lives on the treeitem, so the visual box is aria-hidden.\ncheckboxClass(state: TwTreeSelectionState): string {\n return state === 'unchecked'\n ? 'border-border bg-surface text-transparent'\n : 'border-primary-500 bg-primary-500 text-white';\n}"},{"id":"linesSnippet","title":"Indentation Guide Lines","language":"html","code":"<tw-tree\n [data]=\"project\"\n [childrenAccessor]=\"fileChildren\"\n [trackBy]=\"fileTrackBy\"\n [display]=\"{ indent: 24, showLines: true }\"\n [expandedKeys]=\"['src', 'app']\"\n>\n <ng-template twTreeNode let-node let-hasChildren=\"hasChildren\"\n let-isExpanded=\"expanded\" let-toggle=\"toggle\">\n <!-- chevron button (same as Sizes) -->\n <span>{{ '{{' }} node.label {{ '}}' }}</span>\n </ng-template>\n</tw-tree>"},{"id":"controlledHtmlSnippet","title":"Controlled Expansion","language":"html","code":"<button twButton variant=\"outline\" size=\"sm\" (click)=\"expandAll()\">Expand all</button>\n<button twButton variant=\"outline\" size=\"sm\" (click)=\"collapseAll()\">Collapse all</button>\n\n<tw-tree\n [data]=\"project\"\n [childrenAccessor]=\"fileChildren\"\n [trackBy]=\"fileTrackBy\"\n [(expandedKeys)]=\"controlledKeys\"\n>\n <!-- node template -->\n</tw-tree>"},{"id":"controlledTsSnippet","title":"Controlled Expansion","language":"ts","code":"protected readonly controlledKeys = signal<unknown[]>(['src']);\n\nexpandAll() {\n const keys: unknown[] = [];\n const walk = (nodes: FileNode[]) => {\n for (const n of nodes) {\n if (n.children?.length) { keys.push(n.id); walk(n.children); }\n }\n };\n walk(this.project);\n this.controlledKeys.set(keys);\n}\n\ncollapseAll() {\n this.controlledKeys.set([]);\n}"},{"id":"explorerSnippet","title":"File Explorer","language":"html","code":"<tw-tree\n [data]=\"explorer\"\n [childrenAccessor]=\"fileChildren\"\n [trackBy]=\"fileTrackBy\"\n [display]=\"{ size: 'sm' }\"\n [expandedKeys]=\"['components', 'button']\"\n>\n <ng-template twTreeNode let-node let-hasChildren=\"hasChildren\"\n let-isExpanded=\"expanded\" let-toggle=\"toggle\">\n @if (hasChildren) {\n <button type=\"button\" tabindex=\"-1\" (click)=\"toggle()\"\n [attr.aria-label]=\"isExpanded ? 'Collapse' : 'Expand'\">\n <tw-icon name=\"chevron-right\" size=\"xs\" [class.rotate-90]=\"isExpanded\" />\n </button>\n <tw-icon name=\"folder\" size=\"xs\" class=\"text-warning-500\" />\n } @else {\n <span class=\"size-5\"></span>\n <tw-icon name=\"file\" size=\"xs\" class=\"text-fg-subtle\" />\n }\n <span>{{ '{{' }} node.label {{ '}}' }}</span>\n </ng-template>\n</tw-tree>"},{"id":"basicUsageHtmlSnippet","title":"Basic Usage","language":"html","code":"<tw-tree\n [data]=\"project\"\n [childrenAccessor]=\"childrenOf\"\n [trackBy]=\"trackById\"\n [(expandedKeys)]=\"expanded\"\n>\n <ng-template\n twTreeNode\n let-node\n let-hasChildren=\"hasChildren\"\n let-isExpanded=\"expanded\"\n let-toggle=\"toggle\"\n >\n @if (hasChildren) {\n <button type=\"button\" tabindex=\"-1\" (click)=\"toggle()\"\n [attr.aria-label]=\"isExpanded ? 'Collapse' : 'Expand'\">\n <tw-icon name=\"chevron-right\" size=\"xs\" [class.rotate-90]=\"isExpanded\" />\n </button>\n } @else {\n <span class=\"size-5\"></span>\n }\n <span>{{ $any(node).label }}</span>\n </ng-template>\n</tw-tree>"},{"id":"basicUsageTsSnippet","title":"Basic Usage","language":"ts","code":"interface FileNode {\n id: string;\n label: string;\n children?: FileNode[];\n}\n\nprotected readonly project: FileNode[] = [/* … */];\nprotected readonly expanded = signal<unknown[]>(['src', 'app']);\nprotected readonly childrenOf = (n: FileNode) => n.children ?? [];\nprotected readonly trackById = (n: FileNode) => n.id;"},{"id":"importSnippet","title":"Import","language":"ts","code":"import {\n TreeComponent,\n TreeNodeDefDirective,\n TreeNodeToggleDirective,\n} from '@cdevhub/ngx-tw/tree';"}],"summary":"Renders nested data as an accessible, keyboard-navigable hierarchy implementing the WAI-ARIA Tree pattern, built on Angular CDK CdkTree.","whenToUse":["Arbitrarily deep nested data — file explorers, folder structures, category or org hierarchies, nested navigation","Supplying nesting through a single childrenAccessor function instead of maintaining a flattened list yourself","Fully custom row appearance via the *twTreeNode template, with a typed context exposing node, level, expanded, hasChildren, selectionState, and actions","Checkbox-style multi-select over a hierarchy where selecting a branch cascades to its leaves and partially-selected branches report indeterminate","Controlled expansion driven from the parent via two-way expandedKeys"],"whenNotToUse":[{"instead":"accordion","because":"the content is one flat level of expand/collapse panels rather than a nested hierarchy"},{"instead":"table","because":"the data is tabular with columns, and expansion only ever goes one row deep"},{"instead":"select","because":"the user is picking from a flat option list in an overlay as a form value"},{"instead":"transfer","because":"the task is moving items between two lists rather than exploring a hierarchy in place"}],"related":["accordion","table","select","checkbox","transfer","icon"],"aliases":["tree view","treeview","hierarchy","nested list","file explorer","folder tree","node tree","expandable hierarchy","directory tree"],"hasMeta":true,"metaPath":"projects/ngx-tw/tree/tree.meta.ts"},{"name":"theme","importPath":"@cdevhub/ngx-tw/theme","symbols":[{"name":"ThemeService","kind":"service","description":"Stateful runtime service that owns the active theme, reacts to OS `prefers-color-scheme` changes, persists the user selection to `localStorage`, and writes the resolved theme onto the configured DOM target as a `data-theme` attribute. The selected theme may be `'system'` (defer to the OS); the resolvedTheme computed from it is always one of `'light'`, `'dark'`, or `'high-contrast'` — never `'system'`. Register via provideTheme in the app's environment providers.","methods":[{"name":"setTheme","signature":"setTheme(theme: TwTheme): void","description":"Sets the selected theme. Pass `'system'` to follow the OS preference."},{"name":"cycleTheme","signature":"cycleTheme(): void","description":"Advances the selected theme to the next entry in TW_THEMES, wrapping around."},{"name":"applyToElement","signature":"applyToElement(element: HTMLElement, theme: TwResolvedTheme): void","description":"Writes the configured theme attribute onto an arbitrary element — used to scope themes to a subtree."}]},{"name":"ThemeDirective","kind":"directive","description":"","selector":"[twTheme]","usage":[{"form":"attribute","selector":"[twTheme]","name":"twTheme"}],"inputs":[{"name":"twTheme","type":"TwResolvedTheme","required":true,"description":"Scopes a subtree to a specific resolved theme by writing `data-theme` on the host. Required."}]},{"name":"THEME_CONFIG","kind":"token","description":"Injection token carrying the resolved TwThemeConfig (storage key, attribute, target element, default theme)."},{"name":"provideTheme","kind":"function","description":"Registers ThemeService and a THEME_CONFIG value built by merging `config` over DEFAULT_TW_THEME_CONFIG. Call once in the app's environment providers.","signature":"provideTheme(config?: Partial<TwThemeConfig>): EnvironmentProviders"},{"name":"TwTheme","kind":"type","description":"The user-selectable theme. `'system'` defers to the OS `prefers-color-scheme` setting.","definition":"'light' | 'dark' | 'high-contrast' | 'system'"},{"name":"TwResolvedTheme","kind":"type","description":"The theme actually applied to the DOM after resolving `'system'` against the OS preference.","definition":"'light' | 'dark' | 'high-contrast'"},{"name":"TwThemeConfig","kind":"interface","description":"Runtime configuration for provideTheme; controls storage, attribute name, target element, and default.","members":[{"name":"defaultTheme","type":"TwTheme","optional":false,"description":"The default theme when no preference is stored. Defaults to `'system'`."},{"name":"storageKey","type":"string","optional":false,"description":"localStorage key for persisting theme preference. Defaults to `'ngx-tw-theme'`."},{"name":"attribute","type":"string","optional":false,"description":"The HTML attribute written to the target element. Defaults to `'data-theme'`."},{"name":"target","type":"'documentElement' | 'body'","optional":false,"description":"Which element receives the theme attribute. Defaults to `'documentElement'`."}]},{"name":"TwThemeState","kind":"interface","description":"Composite snapshot of `ThemeService` state — selected, resolved, system, and boolean flags.","members":[{"name":"theme","type":"TwTheme","optional":false,"description":"The user-selected theme — may be `'system'`."},{"name":"resolvedTheme","type":"TwResolvedTheme","optional":false,"description":"The theme actually applied to the DOM — never `'system'`."},{"name":"systemTheme","type":"TwResolvedTheme","optional":false,"description":"The OS color-scheme preference detected via `prefers-color-scheme`."},{"name":"isDark","type":"boolean","optional":false,"description":"True when resolvedTheme is `'dark'`."},{"name":"isLight","type":"boolean","optional":false,"description":"True when resolvedTheme is `'light'`."},{"name":"isHighContrast","type":"boolean","optional":false,"description":"True when resolvedTheme is `'high-contrast'`."}]},{"name":"TW_THEMES","kind":"const","description":"Ordered list of every TwTheme value, used by `cycleTheme()` and for UI iteration."},{"name":"TW_RESOLVED_THEMES","kind":"const","description":"Ordered list of every TwResolvedTheme value (i.e. TW_THEMES minus `'system'`)."},{"name":"DEFAULT_TW_THEME_CONFIG","kind":"const","description":"Built-in defaults merged under any user-provided TwThemeConfig by `provideTheme()`.","type":"TwThemeConfig"}],"snippets":[],"summary":"Runtime theming API — provideTheme, ThemeService, THEME_CONFIG, and a [twTheme] directive — that switches between light, dark, high-contrast, and system modes and persists the choice, alongside the default theme CSS.","whenToUse":["Building a theme toggle or a light/dark/high-contrast picker: inject ThemeService and call setTheme() or cycleTheme()","Reading the active appearance in component logic via the theme, resolvedTheme, systemTheme, isDark, isLight, and isHighContrast signals","The choice must survive a reload — the service persists to localStorage under ngx-tw-theme by default","Following the OS preference by default and re-resolving live when the user changes it, via the \"system\" theme","Customizing where and how the theme is written: provideTheme({ defaultTheme, storageKey, attribute, target }) changes the data-theme attribute name or targets body instead of documentElement","Scoping a different theme to one subtree of the page (a preview pane, an always-dark hero) with the [twTheme] directive","Rebranding the whole library: import the theme CSS and override the semantic tokens (--color-primary-500, surface, fg, border) in your own @theme block","Note: plain dark mode needs no JavaScript at all — the imported theme CSS already falls back to prefers-color-scheme when no data-theme attribute is set, so provideTheme is only for explicit, persisted switching"],"related":["core","switch","segmented-control","menu","button","icon"],"aliases":["dark mode","theming","color scheme","tokens","light mode","high contrast","prefers-color-scheme","data-theme","theme toggle","theme switcher","palette","branding","css variables","provideTheme","ThemeService"],"hasMeta":true,"metaPath":"projects/ngx-tw/theme/theme.meta.ts"}],"themeTokens":[{"token":"surface","property":"--color-surface","kind":"color","group":"surface","value":"var(--color-white)","utilities":["bg-surface","text-surface","border-surface","ring-surface","fill-surface"]},{"token":"surface-raised","property":"--color-surface-raised","kind":"color","group":"surface","value":"var(--color-white)","utilities":["bg-surface-raised","text-surface-raised","border-surface-raised","ring-surface-raised","fill-surface-raised"]},{"token":"surface-overlay","property":"--color-surface-overlay","kind":"color","group":"surface","value":"var(--color-white)","utilities":["bg-surface-overlay","text-surface-overlay","border-surface-overlay","ring-surface-overlay","fill-surface-overlay"]},{"token":"surface-sunken","property":"--color-surface-sunken","kind":"color","group":"surface","value":"var(--color-gray-50)","utilities":["bg-surface-sunken","text-surface-sunken","border-surface-sunken","ring-surface-sunken","fill-surface-sunken"]},{"token":"surface-muted","property":"--color-surface-muted","kind":"color","group":"surface","value":"var(--color-gray-100)","utilities":["bg-surface-muted","text-surface-muted","border-surface-muted","ring-surface-muted","fill-surface-muted"]},{"token":"overlay-control","property":"--color-overlay-control","kind":"color","group":"surface","value":"oklch(0 0 0 / 0.4)","utilities":["bg-overlay-control","text-overlay-control","border-overlay-control","ring-overlay-control","fill-overlay-control"]},{"token":"overlay-control-hover","property":"--color-overlay-control-hover","kind":"color","group":"surface","value":"oklch(0 0 0 / 0.6)","utilities":["bg-overlay-control-hover","text-overlay-control-hover","border-overlay-control-hover","ring-overlay-control-hover","fill-overlay-control-hover"]},{"token":"fg","property":"--color-fg","kind":"color","group":"foreground","value":"var(--color-gray-900)","utilities":["bg-fg","text-fg","border-fg","ring-fg","fill-fg"]},{"token":"fg-muted","property":"--color-fg-muted","kind":"color","group":"foreground","value":"var(--color-gray-600)","utilities":["bg-fg-muted","text-fg-muted","border-fg-muted","ring-fg-muted","fill-fg-muted"]},{"token":"fg-subtle","property":"--color-fg-subtle","kind":"color","group":"foreground","value":"var(--color-gray-500)","utilities":["bg-fg-subtle","text-fg-subtle","border-fg-subtle","ring-fg-subtle","fill-fg-subtle"]},{"token":"border","property":"--color-border","kind":"color","group":"border","value":"var(--color-gray-300)","utilities":["bg-border","text-border","border-border","ring-border","fill-border"]},{"token":"border-muted","property":"--color-border-muted","kind":"color","group":"border","value":"var(--color-gray-200)","utilities":["bg-border-muted","text-border-muted","border-border-muted","ring-border-muted","fill-border-muted"]},{"token":"border-strong","property":"--color-border-strong","kind":"color","group":"border","value":"var(--color-gray-400)","utilities":["bg-border-strong","text-border-strong","border-border-strong","ring-border-strong","fill-border-strong"]},{"token":"table-sticky","property":"--shadow-table-sticky","kind":"shadow","group":"shadow","value":"0 1px 0 0 var(--color-border)","utilities":["shadow-table-sticky"]},{"token":"table-sticky-cell-start","property":"--shadow-table-sticky-cell-start","kind":"shadow","group":"shadow","value":"1px 0 0 0 var(--color-border)","utilities":["shadow-table-sticky-cell-start"]},{"token":"table-sticky-cell-end","property":"--shadow-table-sticky-cell-end","kind":"shadow","group":"shadow","value":"-1px 0 0 0 var(--color-border)","utilities":["shadow-table-sticky-cell-end"]},{"token":"calendar-xs","property":"--width-calendar-xs","kind":"width","group":"width","value":"210px","utilities":["w-calendar-xs","max-w-calendar-xs","min-w-calendar-xs"]},{"token":"calendar-sm","property":"--width-calendar-sm","kind":"width","group":"width","value":"240px","utilities":["w-calendar-sm","max-w-calendar-sm","min-w-calendar-sm"]},{"token":"calendar-md","property":"--width-calendar-md","kind":"width","group":"width","value":"292px","utilities":["w-calendar-md","max-w-calendar-md","min-w-calendar-md"]},{"token":"calendar-lg","property":"--width-calendar-lg","kind":"width","group":"width","value":"320px","utilities":["w-calendar-lg","max-w-calendar-lg","min-w-calendar-lg"]},{"token":"calendar-xl","property":"--width-calendar-xl","kind":"width","group":"width","value":"376px","utilities":["w-calendar-xl","max-w-calendar-xl","min-w-calendar-xl"]},{"token":"2xs","property":"--text-2xs","kind":"typography","group":"typography","value":"0.6875rem","utilities":["text-2xs"]},{"token":"primary-50","property":"--color-primary-50","kind":"color","group":"semantic-color","value":"var(--color-blue-50)","utilities":["bg-primary-50","text-primary-50","border-primary-50","ring-primary-50","fill-primary-50"]},{"token":"primary-100","property":"--color-primary-100","kind":"color","group":"semantic-color","value":"var(--color-blue-100)","utilities":["bg-primary-100","text-primary-100","border-primary-100","ring-primary-100","fill-primary-100"]},{"token":"primary-200","property":"--color-primary-200","kind":"color","group":"semantic-color","value":"var(--color-blue-200)","utilities":["bg-primary-200","text-primary-200","border-primary-200","ring-primary-200","fill-primary-200"]},{"token":"primary-300","property":"--color-primary-300","kind":"color","group":"semantic-color","value":"var(--color-blue-300)","utilities":["bg-primary-300","text-primary-300","border-primary-300","ring-primary-300","fill-primary-300"]},{"token":"primary-400","property":"--color-primary-400","kind":"color","group":"semantic-color","value":"var(--color-blue-400)","utilities":["bg-primary-400","text-primary-400","border-primary-400","ring-primary-400","fill-primary-400"]},{"token":"primary-500","property":"--color-primary-500","kind":"color","group":"semantic-color","value":"var(--color-blue-500)","utilities":["bg-primary-500","text-primary-500","border-primary-500","ring-primary-500","fill-primary-500"]},{"token":"primary-600","property":"--color-primary-600","kind":"color","group":"semantic-color","value":"var(--color-blue-600)","utilities":["bg-primary-600","text-primary-600","border-primary-600","ring-primary-600","fill-primary-600"]},{"token":"primary-700","property":"--color-primary-700","kind":"color","group":"semantic-color","value":"var(--color-blue-700)","utilities":["bg-primary-700","text-primary-700","border-primary-700","ring-primary-700","fill-primary-700"]},{"token":"primary-800","property":"--color-primary-800","kind":"color","group":"semantic-color","value":"var(--color-blue-800)","utilities":["bg-primary-800","text-primary-800","border-primary-800","ring-primary-800","fill-primary-800"]},{"token":"primary-900","property":"--color-primary-900","kind":"color","group":"semantic-color","value":"var(--color-blue-900)","utilities":["bg-primary-900","text-primary-900","border-primary-900","ring-primary-900","fill-primary-900"]},{"token":"primary-950","property":"--color-primary-950","kind":"color","group":"semantic-color","value":"var(--color-blue-950)","utilities":["bg-primary-950","text-primary-950","border-primary-950","ring-primary-950","fill-primary-950"]},{"token":"secondary-50","property":"--color-secondary-50","kind":"color","group":"semantic-color","value":"var(--color-slate-50)","utilities":["bg-secondary-50","text-secondary-50","border-secondary-50","ring-secondary-50","fill-secondary-50"]},{"token":"secondary-100","property":"--color-secondary-100","kind":"color","group":"semantic-color","value":"var(--color-slate-100)","utilities":["bg-secondary-100","text-secondary-100","border-secondary-100","ring-secondary-100","fill-secondary-100"]},{"token":"secondary-200","property":"--color-secondary-200","kind":"color","group":"semantic-color","value":"var(--color-slate-200)","utilities":["bg-secondary-200","text-secondary-200","border-secondary-200","ring-secondary-200","fill-secondary-200"]},{"token":"secondary-300","property":"--color-secondary-300","kind":"color","group":"semantic-color","value":"var(--color-slate-300)","utilities":["bg-secondary-300","text-secondary-300","border-secondary-300","ring-secondary-300","fill-secondary-300"]},{"token":"secondary-400","property":"--color-secondary-400","kind":"color","group":"semantic-color","value":"var(--color-slate-400)","utilities":["bg-secondary-400","text-secondary-400","border-secondary-400","ring-secondary-400","fill-secondary-400"]},{"token":"secondary-500","property":"--color-secondary-500","kind":"color","group":"semantic-color","value":"var(--color-slate-500)","utilities":["bg-secondary-500","text-secondary-500","border-secondary-500","ring-secondary-500","fill-secondary-500"]},{"token":"secondary-600","property":"--color-secondary-600","kind":"color","group":"semantic-color","value":"var(--color-slate-600)","utilities":["bg-secondary-600","text-secondary-600","border-secondary-600","ring-secondary-600","fill-secondary-600"]},{"token":"secondary-700","property":"--color-secondary-700","kind":"color","group":"semantic-color","value":"var(--color-slate-700)","utilities":["bg-secondary-700","text-secondary-700","border-secondary-700","ring-secondary-700","fill-secondary-700"]},{"token":"secondary-800","property":"--color-secondary-800","kind":"color","group":"semantic-color","value":"var(--color-slate-800)","utilities":["bg-secondary-800","text-secondary-800","border-secondary-800","ring-secondary-800","fill-secondary-800"]},{"token":"secondary-900","property":"--color-secondary-900","kind":"color","group":"semantic-color","value":"var(--color-slate-900)","utilities":["bg-secondary-900","text-secondary-900","border-secondary-900","ring-secondary-900","fill-secondary-900"]},{"token":"secondary-950","property":"--color-secondary-950","kind":"color","group":"semantic-color","value":"var(--color-slate-950)","utilities":["bg-secondary-950","text-secondary-950","border-secondary-950","ring-secondary-950","fill-secondary-950"]},{"token":"accent-50","property":"--color-accent-50","kind":"color","group":"semantic-color","value":"var(--color-violet-50)","utilities":["bg-accent-50","text-accent-50","border-accent-50","ring-accent-50","fill-accent-50"]},{"token":"accent-100","property":"--color-accent-100","kind":"color","group":"semantic-color","value":"var(--color-violet-100)","utilities":["bg-accent-100","text-accent-100","border-accent-100","ring-accent-100","fill-accent-100"]},{"token":"accent-200","property":"--color-accent-200","kind":"color","group":"semantic-color","value":"var(--color-violet-200)","utilities":["bg-accent-200","text-accent-200","border-accent-200","ring-accent-200","fill-accent-200"]},{"token":"accent-300","property":"--color-accent-300","kind":"color","group":"semantic-color","value":"var(--color-violet-300)","utilities":["bg-accent-300","text-accent-300","border-accent-300","ring-accent-300","fill-accent-300"]},{"token":"accent-400","property":"--color-accent-400","kind":"color","group":"semantic-color","value":"var(--color-violet-400)","utilities":["bg-accent-400","text-accent-400","border-accent-400","ring-accent-400","fill-accent-400"]},{"token":"accent-500","property":"--color-accent-500","kind":"color","group":"semantic-color","value":"var(--color-violet-500)","utilities":["bg-accent-500","text-accent-500","border-accent-500","ring-accent-500","fill-accent-500"]},{"token":"accent-600","property":"--color-accent-600","kind":"color","group":"semantic-color","value":"var(--color-violet-600)","utilities":["bg-accent-600","text-accent-600","border-accent-600","ring-accent-600","fill-accent-600"]},{"token":"accent-700","property":"--color-accent-700","kind":"color","group":"semantic-color","value":"var(--color-violet-700)","utilities":["bg-accent-700","text-accent-700","border-accent-700","ring-accent-700","fill-accent-700"]},{"token":"accent-800","property":"--color-accent-800","kind":"color","group":"semantic-color","value":"var(--color-violet-800)","utilities":["bg-accent-800","text-accent-800","border-accent-800","ring-accent-800","fill-accent-800"]},{"token":"accent-900","property":"--color-accent-900","kind":"color","group":"semantic-color","value":"var(--color-violet-900)","utilities":["bg-accent-900","text-accent-900","border-accent-900","ring-accent-900","fill-accent-900"]},{"token":"accent-950","property":"--color-accent-950","kind":"color","group":"semantic-color","value":"var(--color-violet-950)","utilities":["bg-accent-950","text-accent-950","border-accent-950","ring-accent-950","fill-accent-950"]},{"token":"neutral-50","property":"--color-neutral-50","kind":"color","group":"semantic-color","value":"var(--color-gray-50)","utilities":["bg-neutral-50","text-neutral-50","border-neutral-50","ring-neutral-50","fill-neutral-50"]},{"token":"neutral-100","property":"--color-neutral-100","kind":"color","group":"semantic-color","value":"var(--color-gray-100)","utilities":["bg-neutral-100","text-neutral-100","border-neutral-100","ring-neutral-100","fill-neutral-100"]},{"token":"neutral-200","property":"--color-neutral-200","kind":"color","group":"semantic-color","value":"var(--color-gray-200)","utilities":["bg-neutral-200","text-neutral-200","border-neutral-200","ring-neutral-200","fill-neutral-200"]},{"token":"neutral-300","property":"--color-neutral-300","kind":"color","group":"semantic-color","value":"var(--color-gray-300)","utilities":["bg-neutral-300","text-neutral-300","border-neutral-300","ring-neutral-300","fill-neutral-300"]},{"token":"neutral-400","property":"--color-neutral-400","kind":"color","group":"semantic-color","value":"var(--color-gray-400)","utilities":["bg-neutral-400","text-neutral-400","border-neutral-400","ring-neutral-400","fill-neutral-400"]},{"token":"neutral-500","property":"--color-neutral-500","kind":"color","group":"semantic-color","value":"var(--color-gray-500)","utilities":["bg-neutral-500","text-neutral-500","border-neutral-500","ring-neutral-500","fill-neutral-500"]},{"token":"neutral-600","property":"--color-neutral-600","kind":"color","group":"semantic-color","value":"var(--color-gray-600)","utilities":["bg-neutral-600","text-neutral-600","border-neutral-600","ring-neutral-600","fill-neutral-600"]},{"token":"neutral-700","property":"--color-neutral-700","kind":"color","group":"semantic-color","value":"var(--color-gray-700)","utilities":["bg-neutral-700","text-neutral-700","border-neutral-700","ring-neutral-700","fill-neutral-700"]},{"token":"neutral-800","property":"--color-neutral-800","kind":"color","group":"semantic-color","value":"var(--color-gray-800)","utilities":["bg-neutral-800","text-neutral-800","border-neutral-800","ring-neutral-800","fill-neutral-800"]},{"token":"neutral-900","property":"--color-neutral-900","kind":"color","group":"semantic-color","value":"var(--color-gray-900)","utilities":["bg-neutral-900","text-neutral-900","border-neutral-900","ring-neutral-900","fill-neutral-900"]},{"token":"neutral-950","property":"--color-neutral-950","kind":"color","group":"semantic-color","value":"var(--color-gray-950)","utilities":["bg-neutral-950","text-neutral-950","border-neutral-950","ring-neutral-950","fill-neutral-950"]},{"token":"info-50","property":"--color-info-50","kind":"color","group":"semantic-color","value":"var(--color-sky-50)","utilities":["bg-info-50","text-info-50","border-info-50","ring-info-50","fill-info-50"]},{"token":"info-100","property":"--color-info-100","kind":"color","group":"semantic-color","value":"var(--color-sky-100)","utilities":["bg-info-100","text-info-100","border-info-100","ring-info-100","fill-info-100"]},{"token":"info-200","property":"--color-info-200","kind":"color","group":"semantic-color","value":"var(--color-sky-200)","utilities":["bg-info-200","text-info-200","border-info-200","ring-info-200","fill-info-200"]},{"token":"info-300","property":"--color-info-300","kind":"color","group":"semantic-color","value":"var(--color-sky-300)","utilities":["bg-info-300","text-info-300","border-info-300","ring-info-300","fill-info-300"]},{"token":"info-400","property":"--color-info-400","kind":"color","group":"semantic-color","value":"var(--color-sky-400)","utilities":["bg-info-400","text-info-400","border-info-400","ring-info-400","fill-info-400"]},{"token":"info-500","property":"--color-info-500","kind":"color","group":"semantic-color","value":"var(--color-sky-500)","utilities":["bg-info-500","text-info-500","border-info-500","ring-info-500","fill-info-500"]},{"token":"info-600","property":"--color-info-600","kind":"color","group":"semantic-color","value":"var(--color-sky-600)","utilities":["bg-info-600","text-info-600","border-info-600","ring-info-600","fill-info-600"]},{"token":"info-700","property":"--color-info-700","kind":"color","group":"semantic-color","value":"var(--color-sky-700)","utilities":["bg-info-700","text-info-700","border-info-700","ring-info-700","fill-info-700"]},{"token":"info-800","property":"--color-info-800","kind":"color","group":"semantic-color","value":"var(--color-sky-800)","utilities":["bg-info-800","text-info-800","border-info-800","ring-info-800","fill-info-800"]},{"token":"info-900","property":"--color-info-900","kind":"color","group":"semantic-color","value":"var(--color-sky-900)","utilities":["bg-info-900","text-info-900","border-info-900","ring-info-900","fill-info-900"]},{"token":"info-950","property":"--color-info-950","kind":"color","group":"semantic-color","value":"var(--color-sky-950)","utilities":["bg-info-950","text-info-950","border-info-950","ring-info-950","fill-info-950"]},{"token":"success-50","property":"--color-success-50","kind":"color","group":"semantic-color","value":"var(--color-green-50)","utilities":["bg-success-50","text-success-50","border-success-50","ring-success-50","fill-success-50"]},{"token":"success-100","property":"--color-success-100","kind":"color","group":"semantic-color","value":"var(--color-green-100)","utilities":["bg-success-100","text-success-100","border-success-100","ring-success-100","fill-success-100"]},{"token":"success-200","property":"--color-success-200","kind":"color","group":"semantic-color","value":"var(--color-green-200)","utilities":["bg-success-200","text-success-200","border-success-200","ring-success-200","fill-success-200"]},{"token":"success-300","property":"--color-success-300","kind":"color","group":"semantic-color","value":"var(--color-green-300)","utilities":["bg-success-300","text-success-300","border-success-300","ring-success-300","fill-success-300"]},{"token":"success-400","property":"--color-success-400","kind":"color","group":"semantic-color","value":"var(--color-green-400)","utilities":["bg-success-400","text-success-400","border-success-400","ring-success-400","fill-success-400"]},{"token":"success-500","property":"--color-success-500","kind":"color","group":"semantic-color","value":"var(--color-green-500)","utilities":["bg-success-500","text-success-500","border-success-500","ring-success-500","fill-success-500"]},{"token":"success-600","property":"--color-success-600","kind":"color","group":"semantic-color","value":"var(--color-green-600)","utilities":["bg-success-600","text-success-600","border-success-600","ring-success-600","fill-success-600"]},{"token":"success-700","property":"--color-success-700","kind":"color","group":"semantic-color","value":"var(--color-green-700)","utilities":["bg-success-700","text-success-700","border-success-700","ring-success-700","fill-success-700"]},{"token":"success-800","property":"--color-success-800","kind":"color","group":"semantic-color","value":"var(--color-green-800)","utilities":["bg-success-800","text-success-800","border-success-800","ring-success-800","fill-success-800"]},{"token":"success-900","property":"--color-success-900","kind":"color","group":"semantic-color","value":"var(--color-green-900)","utilities":["bg-success-900","text-success-900","border-success-900","ring-success-900","fill-success-900"]},{"token":"success-950","property":"--color-success-950","kind":"color","group":"semantic-color","value":"var(--color-green-950)","utilities":["bg-success-950","text-success-950","border-success-950","ring-success-950","fill-success-950"]},{"token":"warning-50","property":"--color-warning-50","kind":"color","group":"semantic-color","value":"var(--color-amber-50)","utilities":["bg-warning-50","text-warning-50","border-warning-50","ring-warning-50","fill-warning-50"]},{"token":"warning-100","property":"--color-warning-100","kind":"color","group":"semantic-color","value":"var(--color-amber-100)","utilities":["bg-warning-100","text-warning-100","border-warning-100","ring-warning-100","fill-warning-100"]},{"token":"warning-200","property":"--color-warning-200","kind":"color","group":"semantic-color","value":"var(--color-amber-200)","utilities":["bg-warning-200","text-warning-200","border-warning-200","ring-warning-200","fill-warning-200"]},{"token":"warning-300","property":"--color-warning-300","kind":"color","group":"semantic-color","value":"var(--color-amber-300)","utilities":["bg-warning-300","text-warning-300","border-warning-300","ring-warning-300","fill-warning-300"]},{"token":"warning-400","property":"--color-warning-400","kind":"color","group":"semantic-color","value":"var(--color-amber-400)","utilities":["bg-warning-400","text-warning-400","border-warning-400","ring-warning-400","fill-warning-400"]},{"token":"warning-500","property":"--color-warning-500","kind":"color","group":"semantic-color","value":"var(--color-amber-500)","utilities":["bg-warning-500","text-warning-500","border-warning-500","ring-warning-500","fill-warning-500"]},{"token":"warning-600","property":"--color-warning-600","kind":"color","group":"semantic-color","value":"var(--color-amber-600)","utilities":["bg-warning-600","text-warning-600","border-warning-600","ring-warning-600","fill-warning-600"]},{"token":"warning-700","property":"--color-warning-700","kind":"color","group":"semantic-color","value":"var(--color-amber-700)","utilities":["bg-warning-700","text-warning-700","border-warning-700","ring-warning-700","fill-warning-700"]},{"token":"warning-800","property":"--color-warning-800","kind":"color","group":"semantic-color","value":"var(--color-amber-800)","utilities":["bg-warning-800","text-warning-800","border-warning-800","ring-warning-800","fill-warning-800"]},{"token":"warning-900","property":"--color-warning-900","kind":"color","group":"semantic-color","value":"var(--color-amber-900)","utilities":["bg-warning-900","text-warning-900","border-warning-900","ring-warning-900","fill-warning-900"]},{"token":"warning-950","property":"--color-warning-950","kind":"color","group":"semantic-color","value":"var(--color-amber-950)","utilities":["bg-warning-950","text-warning-950","border-warning-950","ring-warning-950","fill-warning-950"]},{"token":"error-50","property":"--color-error-50","kind":"color","group":"semantic-color","value":"var(--color-red-50)","utilities":["bg-error-50","text-error-50","border-error-50","ring-error-50","fill-error-50"]},{"token":"error-100","property":"--color-error-100","kind":"color","group":"semantic-color","value":"var(--color-red-100)","utilities":["bg-error-100","text-error-100","border-error-100","ring-error-100","fill-error-100"]},{"token":"error-200","property":"--color-error-200","kind":"color","group":"semantic-color","value":"var(--color-red-200)","utilities":["bg-error-200","text-error-200","border-error-200","ring-error-200","fill-error-200"]},{"token":"error-300","property":"--color-error-300","kind":"color","group":"semantic-color","value":"var(--color-red-300)","utilities":["bg-error-300","text-error-300","border-error-300","ring-error-300","fill-error-300"]},{"token":"error-400","property":"--color-error-400","kind":"color","group":"semantic-color","value":"var(--color-red-400)","utilities":["bg-error-400","text-error-400","border-error-400","ring-error-400","fill-error-400"]},{"token":"error-500","property":"--color-error-500","kind":"color","group":"semantic-color","value":"var(--color-red-500)","utilities":["bg-error-500","text-error-500","border-error-500","ring-error-500","fill-error-500"]},{"token":"error-600","property":"--color-error-600","kind":"color","group":"semantic-color","value":"var(--color-red-600)","utilities":["bg-error-600","text-error-600","border-error-600","ring-error-600","fill-error-600"]},{"token":"error-700","property":"--color-error-700","kind":"color","group":"semantic-color","value":"var(--color-red-700)","utilities":["bg-error-700","text-error-700","border-error-700","ring-error-700","fill-error-700"]},{"token":"error-800","property":"--color-error-800","kind":"color","group":"semantic-color","value":"var(--color-red-800)","utilities":["bg-error-800","text-error-800","border-error-800","ring-error-800","fill-error-800"]},{"token":"error-900","property":"--color-error-900","kind":"color","group":"semantic-color","value":"var(--color-red-900)","utilities":["bg-error-900","text-error-900","border-error-900","ring-error-900","fill-error-900"]},{"token":"error-950","property":"--color-error-950","kind":"color","group":"semantic-color","value":"var(--color-red-950)","utilities":["bg-error-950","text-error-950","border-error-950","ring-error-950","fill-error-950"]},{"token":"primary-soft","property":"--color-primary-soft","kind":"color","group":"semantic-color","value":"var(--color-primary-50)","utilities":["bg-primary-soft","text-primary-soft","border-primary-soft","ring-primary-soft","fill-primary-soft"]},{"token":"primary-soft-hover","property":"--color-primary-soft-hover","kind":"color","group":"semantic-color","value":"var(--color-primary-100)","utilities":["bg-primary-soft-hover","text-primary-soft-hover","border-primary-soft-hover","ring-primary-soft-hover","fill-primary-soft-hover"]},{"token":"primary-soft-fg","property":"--color-primary-soft-fg","kind":"color","group":"semantic-color","value":"var(--color-primary-900)","utilities":["bg-primary-soft-fg","text-primary-soft-fg","border-primary-soft-fg","ring-primary-soft-fg","fill-primary-soft-fg"]},{"token":"primary-soft-fg-muted","property":"--color-primary-soft-fg-muted","kind":"color","group":"semantic-color","value":"var(--color-primary-800)","utilities":["bg-primary-soft-fg-muted","text-primary-soft-fg-muted","border-primary-soft-fg-muted","ring-primary-soft-fg-muted","fill-primary-soft-fg-muted"]},{"token":"primary-solid","property":"--color-primary-solid","kind":"color","group":"semantic-color","value":"var(--color-primary-600)","utilities":["bg-primary-solid","text-primary-solid","border-primary-solid","ring-primary-solid","fill-primary-solid"]},{"token":"primary-solid-hover","property":"--color-primary-solid-hover","kind":"color","group":"semantic-color","value":"var(--color-primary-700)","utilities":["bg-primary-solid-hover","text-primary-solid-hover","border-primary-solid-hover","ring-primary-solid-hover","fill-primary-solid-hover"]},{"token":"primary-solid-fg","property":"--color-primary-solid-fg","kind":"color","group":"semantic-color","value":"var(--color-white)","utilities":["bg-primary-solid-fg","text-primary-solid-fg","border-primary-solid-fg","ring-primary-solid-fg","fill-primary-solid-fg"]},{"token":"primary-border","property":"--color-primary-border","kind":"color","group":"semantic-color","value":"var(--color-primary-300)","utilities":["bg-primary-border","text-primary-border","border-primary-border","ring-primary-border","fill-primary-border"]},{"token":"primary-border-strong","property":"--color-primary-border-strong","kind":"color","group":"semantic-color","value":"var(--color-primary-500)","utilities":["bg-primary-border-strong","text-primary-border-strong","border-primary-border-strong","ring-primary-border-strong","fill-primary-border-strong"]},{"token":"primary-fg","property":"--color-primary-fg","kind":"color","group":"semantic-color","value":"var(--color-primary-700)","utilities":["bg-primary-fg","text-primary-fg","border-primary-fg","ring-primary-fg","fill-primary-fg"]},{"token":"primary-icon","property":"--color-primary-icon","kind":"color","group":"semantic-color","value":"var(--color-primary-600)","utilities":["bg-primary-icon","text-primary-icon","border-primary-icon","ring-primary-icon","fill-primary-icon"]},{"token":"secondary-soft","property":"--color-secondary-soft","kind":"color","group":"semantic-color","value":"var(--color-secondary-50)","utilities":["bg-secondary-soft","text-secondary-soft","border-secondary-soft","ring-secondary-soft","fill-secondary-soft"]},{"token":"secondary-soft-hover","property":"--color-secondary-soft-hover","kind":"color","group":"semantic-color","value":"var(--color-secondary-100)","utilities":["bg-secondary-soft-hover","text-secondary-soft-hover","border-secondary-soft-hover","ring-secondary-soft-hover","fill-secondary-soft-hover"]},{"token":"secondary-soft-fg","property":"--color-secondary-soft-fg","kind":"color","group":"semantic-color","value":"var(--color-secondary-900)","utilities":["bg-secondary-soft-fg","text-secondary-soft-fg","border-secondary-soft-fg","ring-secondary-soft-fg","fill-secondary-soft-fg"]},{"token":"secondary-soft-fg-muted","property":"--color-secondary-soft-fg-muted","kind":"color","group":"semantic-color","value":"var(--color-secondary-800)","utilities":["bg-secondary-soft-fg-muted","text-secondary-soft-fg-muted","border-secondary-soft-fg-muted","ring-secondary-soft-fg-muted","fill-secondary-soft-fg-muted"]},{"token":"secondary-solid","property":"--color-secondary-solid","kind":"color","group":"semantic-color","value":"var(--color-secondary-700)","utilities":["bg-secondary-solid","text-secondary-solid","border-secondary-solid","ring-secondary-solid","fill-secondary-solid"]},{"token":"secondary-solid-hover","property":"--color-secondary-solid-hover","kind":"color","group":"semantic-color","value":"var(--color-secondary-800)","utilities":["bg-secondary-solid-hover","text-secondary-solid-hover","border-secondary-solid-hover","ring-secondary-solid-hover","fill-secondary-solid-hover"]},{"token":"secondary-solid-fg","property":"--color-secondary-solid-fg","kind":"color","group":"semantic-color","value":"var(--color-white)","utilities":["bg-secondary-solid-fg","text-secondary-solid-fg","border-secondary-solid-fg","ring-secondary-solid-fg","fill-secondary-solid-fg"]},{"token":"secondary-border","property":"--color-secondary-border","kind":"color","group":"semantic-color","value":"var(--color-secondary-300)","utilities":["bg-secondary-border","text-secondary-border","border-secondary-border","ring-secondary-border","fill-secondary-border"]},{"token":"secondary-border-strong","property":"--color-secondary-border-strong","kind":"color","group":"semantic-color","value":"var(--color-secondary-500)","utilities":["bg-secondary-border-strong","text-secondary-border-strong","border-secondary-border-strong","ring-secondary-border-strong","fill-secondary-border-strong"]},{"token":"secondary-fg","property":"--color-secondary-fg","kind":"color","group":"semantic-color","value":"var(--color-secondary-700)","utilities":["bg-secondary-fg","text-secondary-fg","border-secondary-fg","ring-secondary-fg","fill-secondary-fg"]},{"token":"secondary-icon","property":"--color-secondary-icon","kind":"color","group":"semantic-color","value":"var(--color-secondary-600)","utilities":["bg-secondary-icon","text-secondary-icon","border-secondary-icon","ring-secondary-icon","fill-secondary-icon"]},{"token":"accent-soft","property":"--color-accent-soft","kind":"color","group":"semantic-color","value":"var(--color-accent-50)","utilities":["bg-accent-soft","text-accent-soft","border-accent-soft","ring-accent-soft","fill-accent-soft"]},{"token":"accent-soft-hover","property":"--color-accent-soft-hover","kind":"color","group":"semantic-color","value":"var(--color-accent-100)","utilities":["bg-accent-soft-hover","text-accent-soft-hover","border-accent-soft-hover","ring-accent-soft-hover","fill-accent-soft-hover"]},{"token":"accent-soft-fg","property":"--color-accent-soft-fg","kind":"color","group":"semantic-color","value":"var(--color-accent-900)","utilities":["bg-accent-soft-fg","text-accent-soft-fg","border-accent-soft-fg","ring-accent-soft-fg","fill-accent-soft-fg"]},{"token":"accent-soft-fg-muted","property":"--color-accent-soft-fg-muted","kind":"color","group":"semantic-color","value":"var(--color-accent-800)","utilities":["bg-accent-soft-fg-muted","text-accent-soft-fg-muted","border-accent-soft-fg-muted","ring-accent-soft-fg-muted","fill-accent-soft-fg-muted"]},{"token":"accent-solid","property":"--color-accent-solid","kind":"color","group":"semantic-color","value":"var(--color-accent-600)","utilities":["bg-accent-solid","text-accent-solid","border-accent-solid","ring-accent-solid","fill-accent-solid"]},{"token":"accent-solid-hover","property":"--color-accent-solid-hover","kind":"color","group":"semantic-color","value":"var(--color-accent-700)","utilities":["bg-accent-solid-hover","text-accent-solid-hover","border-accent-solid-hover","ring-accent-solid-hover","fill-accent-solid-hover"]},{"token":"accent-solid-fg","property":"--color-accent-solid-fg","kind":"color","group":"semantic-color","value":"var(--color-white)","utilities":["bg-accent-solid-fg","text-accent-solid-fg","border-accent-solid-fg","ring-accent-solid-fg","fill-accent-solid-fg"]},{"token":"accent-border","property":"--color-accent-border","kind":"color","group":"semantic-color","value":"var(--color-accent-300)","utilities":["bg-accent-border","text-accent-border","border-accent-border","ring-accent-border","fill-accent-border"]},{"token":"accent-border-strong","property":"--color-accent-border-strong","kind":"color","group":"semantic-color","value":"var(--color-accent-500)","utilities":["bg-accent-border-strong","text-accent-border-strong","border-accent-border-strong","ring-accent-border-strong","fill-accent-border-strong"]},{"token":"accent-fg","property":"--color-accent-fg","kind":"color","group":"semantic-color","value":"var(--color-accent-700)","utilities":["bg-accent-fg","text-accent-fg","border-accent-fg","ring-accent-fg","fill-accent-fg"]},{"token":"accent-icon","property":"--color-accent-icon","kind":"color","group":"semantic-color","value":"var(--color-accent-600)","utilities":["bg-accent-icon","text-accent-icon","border-accent-icon","ring-accent-icon","fill-accent-icon"]},{"token":"neutral-soft","property":"--color-neutral-soft","kind":"color","group":"semantic-color","value":"var(--color-surface-muted)","utilities":["bg-neutral-soft","text-neutral-soft","border-neutral-soft","ring-neutral-soft","fill-neutral-soft"]},{"token":"neutral-soft-hover","property":"--color-neutral-soft-hover","kind":"color","group":"semantic-color","value":"var(--color-surface-sunken)","utilities":["bg-neutral-soft-hover","text-neutral-soft-hover","border-neutral-soft-hover","ring-neutral-soft-hover","fill-neutral-soft-hover"]},{"token":"neutral-soft-fg","property":"--color-neutral-soft-fg","kind":"color","group":"semantic-color","value":"var(--color-fg)","utilities":["bg-neutral-soft-fg","text-neutral-soft-fg","border-neutral-soft-fg","ring-neutral-soft-fg","fill-neutral-soft-fg"]},{"token":"neutral-soft-fg-muted","property":"--color-neutral-soft-fg-muted","kind":"color","group":"semantic-color","value":"var(--color-fg-muted)","utilities":["bg-neutral-soft-fg-muted","text-neutral-soft-fg-muted","border-neutral-soft-fg-muted","ring-neutral-soft-fg-muted","fill-neutral-soft-fg-muted"]},{"token":"neutral-solid","property":"--color-neutral-solid","kind":"color","group":"semantic-color","value":"var(--color-fg)","utilities":["bg-neutral-solid","text-neutral-solid","border-neutral-solid","ring-neutral-solid","fill-neutral-solid"]},{"token":"neutral-solid-hover","property":"--color-neutral-solid-hover","kind":"color","group":"semantic-color","value":"var(--color-gray-700)","utilities":["bg-neutral-solid-hover","text-neutral-solid-hover","border-neutral-solid-hover","ring-neutral-solid-hover","fill-neutral-solid-hover"]},{"token":"neutral-solid-fg","property":"--color-neutral-solid-fg","kind":"color","group":"semantic-color","value":"var(--color-surface)","utilities":["bg-neutral-solid-fg","text-neutral-solid-fg","border-neutral-solid-fg","ring-neutral-solid-fg","fill-neutral-solid-fg"]},{"token":"neutral-border","property":"--color-neutral-border","kind":"color","group":"semantic-color","value":"var(--color-border)","utilities":["bg-neutral-border","text-neutral-border","border-neutral-border","ring-neutral-border","fill-neutral-border"]},{"token":"neutral-border-strong","property":"--color-neutral-border-strong","kind":"color","group":"semantic-color","value":"var(--color-border-strong)","utilities":["bg-neutral-border-strong","text-neutral-border-strong","border-neutral-border-strong","ring-neutral-border-strong","fill-neutral-border-strong"]},{"token":"neutral-fg","property":"--color-neutral-fg","kind":"color","group":"semantic-color","value":"var(--color-fg)","utilities":["bg-neutral-fg","text-neutral-fg","border-neutral-fg","ring-neutral-fg","fill-neutral-fg"]},{"token":"neutral-icon","property":"--color-neutral-icon","kind":"color","group":"semantic-color","value":"var(--color-fg-muted)","utilities":["bg-neutral-icon","text-neutral-icon","border-neutral-icon","ring-neutral-icon","fill-neutral-icon"]},{"token":"info-soft","property":"--color-info-soft","kind":"color","group":"semantic-color","value":"var(--color-info-50)","utilities":["bg-info-soft","text-info-soft","border-info-soft","ring-info-soft","fill-info-soft"]},{"token":"info-soft-hover","property":"--color-info-soft-hover","kind":"color","group":"semantic-color","value":"var(--color-info-100)","utilities":["bg-info-soft-hover","text-info-soft-hover","border-info-soft-hover","ring-info-soft-hover","fill-info-soft-hover"]},{"token":"info-soft-fg","property":"--color-info-soft-fg","kind":"color","group":"semantic-color","value":"var(--color-info-900)","utilities":["bg-info-soft-fg","text-info-soft-fg","border-info-soft-fg","ring-info-soft-fg","fill-info-soft-fg"]},{"token":"info-soft-fg-muted","property":"--color-info-soft-fg-muted","kind":"color","group":"semantic-color","value":"var(--color-info-800)","utilities":["bg-info-soft-fg-muted","text-info-soft-fg-muted","border-info-soft-fg-muted","ring-info-soft-fg-muted","fill-info-soft-fg-muted"]},{"token":"info-solid","property":"--color-info-solid","kind":"color","group":"semantic-color","value":"var(--color-info-700)","utilities":["bg-info-solid","text-info-solid","border-info-solid","ring-info-solid","fill-info-solid"]},{"token":"info-solid-hover","property":"--color-info-solid-hover","kind":"color","group":"semantic-color","value":"var(--color-info-800)","utilities":["bg-info-solid-hover","text-info-solid-hover","border-info-solid-hover","ring-info-solid-hover","fill-info-solid-hover"]},{"token":"info-solid-fg","property":"--color-info-solid-fg","kind":"color","group":"semantic-color","value":"var(--color-white)","utilities":["bg-info-solid-fg","text-info-solid-fg","border-info-solid-fg","ring-info-solid-fg","fill-info-solid-fg"]},{"token":"info-border","property":"--color-info-border","kind":"color","group":"semantic-color","value":"var(--color-info-300)","utilities":["bg-info-border","text-info-border","border-info-border","ring-info-border","fill-info-border"]},{"token":"info-border-strong","property":"--color-info-border-strong","kind":"color","group":"semantic-color","value":"var(--color-info-500)","utilities":["bg-info-border-strong","text-info-border-strong","border-info-border-strong","ring-info-border-strong","fill-info-border-strong"]},{"token":"info-fg","property":"--color-info-fg","kind":"color","group":"semantic-color","value":"var(--color-info-700)","utilities":["bg-info-fg","text-info-fg","border-info-fg","ring-info-fg","fill-info-fg"]},{"token":"info-icon","property":"--color-info-icon","kind":"color","group":"semantic-color","value":"var(--color-info-600)","utilities":["bg-info-icon","text-info-icon","border-info-icon","ring-info-icon","fill-info-icon"]},{"token":"success-soft","property":"--color-success-soft","kind":"color","group":"semantic-color","value":"var(--color-success-50)","utilities":["bg-success-soft","text-success-soft","border-success-soft","ring-success-soft","fill-success-soft"]},{"token":"success-soft-hover","property":"--color-success-soft-hover","kind":"color","group":"semantic-color","value":"var(--color-success-100)","utilities":["bg-success-soft-hover","text-success-soft-hover","border-success-soft-hover","ring-success-soft-hover","fill-success-soft-hover"]},{"token":"success-soft-fg","property":"--color-success-soft-fg","kind":"color","group":"semantic-color","value":"var(--color-success-900)","utilities":["bg-success-soft-fg","text-success-soft-fg","border-success-soft-fg","ring-success-soft-fg","fill-success-soft-fg"]},{"token":"success-soft-fg-muted","property":"--color-success-soft-fg-muted","kind":"color","group":"semantic-color","value":"var(--color-success-800)","utilities":["bg-success-soft-fg-muted","text-success-soft-fg-muted","border-success-soft-fg-muted","ring-success-soft-fg-muted","fill-success-soft-fg-muted"]},{"token":"success-solid","property":"--color-success-solid","kind":"color","group":"semantic-color","value":"var(--color-success-700)","utilities":["bg-success-solid","text-success-solid","border-success-solid","ring-success-solid","fill-success-solid"]},{"token":"success-solid-hover","property":"--color-success-solid-hover","kind":"color","group":"semantic-color","value":"var(--color-success-800)","utilities":["bg-success-solid-hover","text-success-solid-hover","border-success-solid-hover","ring-success-solid-hover","fill-success-solid-hover"]},{"token":"success-solid-fg","property":"--color-success-solid-fg","kind":"color","group":"semantic-color","value":"var(--color-white)","utilities":["bg-success-solid-fg","text-success-solid-fg","border-success-solid-fg","ring-success-solid-fg","fill-success-solid-fg"]},{"token":"success-border","property":"--color-success-border","kind":"color","group":"semantic-color","value":"var(--color-success-300)","utilities":["bg-success-border","text-success-border","border-success-border","ring-success-border","fill-success-border"]},{"token":"success-border-strong","property":"--color-success-border-strong","kind":"color","group":"semantic-color","value":"var(--color-success-500)","utilities":["bg-success-border-strong","text-success-border-strong","border-success-border-strong","ring-success-border-strong","fill-success-border-strong"]},{"token":"success-fg","property":"--color-success-fg","kind":"color","group":"semantic-color","value":"var(--color-success-700)","utilities":["bg-success-fg","text-success-fg","border-success-fg","ring-success-fg","fill-success-fg"]},{"token":"success-icon","property":"--color-success-icon","kind":"color","group":"semantic-color","value":"var(--color-success-600)","utilities":["bg-success-icon","text-success-icon","border-success-icon","ring-success-icon","fill-success-icon"]},{"token":"warning-soft","property":"--color-warning-soft","kind":"color","group":"semantic-color","value":"var(--color-warning-50)","utilities":["bg-warning-soft","text-warning-soft","border-warning-soft","ring-warning-soft","fill-warning-soft"]},{"token":"warning-soft-hover","property":"--color-warning-soft-hover","kind":"color","group":"semantic-color","value":"var(--color-warning-100)","utilities":["bg-warning-soft-hover","text-warning-soft-hover","border-warning-soft-hover","ring-warning-soft-hover","fill-warning-soft-hover"]},{"token":"warning-soft-fg","property":"--color-warning-soft-fg","kind":"color","group":"semantic-color","value":"var(--color-warning-900)","utilities":["bg-warning-soft-fg","text-warning-soft-fg","border-warning-soft-fg","ring-warning-soft-fg","fill-warning-soft-fg"]},{"token":"warning-soft-fg-muted","property":"--color-warning-soft-fg-muted","kind":"color","group":"semantic-color","value":"var(--color-warning-800)","utilities":["bg-warning-soft-fg-muted","text-warning-soft-fg-muted","border-warning-soft-fg-muted","ring-warning-soft-fg-muted","fill-warning-soft-fg-muted"]},{"token":"warning-solid","property":"--color-warning-solid","kind":"color","group":"semantic-color","value":"var(--color-warning-500)","utilities":["bg-warning-solid","text-warning-solid","border-warning-solid","ring-warning-solid","fill-warning-solid"]},{"token":"warning-solid-hover","property":"--color-warning-solid-hover","kind":"color","group":"semantic-color","value":"var(--color-warning-600)","utilities":["bg-warning-solid-hover","text-warning-solid-hover","border-warning-solid-hover","ring-warning-solid-hover","fill-warning-solid-hover"]},{"token":"warning-solid-fg","property":"--color-warning-solid-fg","kind":"color","group":"semantic-color","value":"var(--color-warning-950)","utilities":["bg-warning-solid-fg","text-warning-solid-fg","border-warning-solid-fg","ring-warning-solid-fg","fill-warning-solid-fg"]},{"token":"warning-border","property":"--color-warning-border","kind":"color","group":"semantic-color","value":"var(--color-warning-300)","utilities":["bg-warning-border","text-warning-border","border-warning-border","ring-warning-border","fill-warning-border"]},{"token":"warning-border-strong","property":"--color-warning-border-strong","kind":"color","group":"semantic-color","value":"var(--color-warning-500)","utilities":["bg-warning-border-strong","text-warning-border-strong","border-warning-border-strong","ring-warning-border-strong","fill-warning-border-strong"]},{"token":"warning-fg","property":"--color-warning-fg","kind":"color","group":"semantic-color","value":"var(--color-warning-800)","utilities":["bg-warning-fg","text-warning-fg","border-warning-fg","ring-warning-fg","fill-warning-fg"]},{"token":"warning-icon","property":"--color-warning-icon","kind":"color","group":"semantic-color","value":"var(--color-warning-600)","utilities":["bg-warning-icon","text-warning-icon","border-warning-icon","ring-warning-icon","fill-warning-icon"]},{"token":"error-soft","property":"--color-error-soft","kind":"color","group":"semantic-color","value":"var(--color-error-50)","utilities":["bg-error-soft","text-error-soft","border-error-soft","ring-error-soft","fill-error-soft"]},{"token":"error-soft-hover","property":"--color-error-soft-hover","kind":"color","group":"semantic-color","value":"var(--color-error-100)","utilities":["bg-error-soft-hover","text-error-soft-hover","border-error-soft-hover","ring-error-soft-hover","fill-error-soft-hover"]},{"token":"error-soft-fg","property":"--color-error-soft-fg","kind":"color","group":"semantic-color","value":"var(--color-error-900)","utilities":["bg-error-soft-fg","text-error-soft-fg","border-error-soft-fg","ring-error-soft-fg","fill-error-soft-fg"]},{"token":"error-soft-fg-muted","property":"--color-error-soft-fg-muted","kind":"color","group":"semantic-color","value":"var(--color-error-800)","utilities":["bg-error-soft-fg-muted","text-error-soft-fg-muted","border-error-soft-fg-muted","ring-error-soft-fg-muted","fill-error-soft-fg-muted"]},{"token":"error-solid","property":"--color-error-solid","kind":"color","group":"semantic-color","value":"var(--color-error-600)","utilities":["bg-error-solid","text-error-solid","border-error-solid","ring-error-solid","fill-error-solid"]},{"token":"error-solid-hover","property":"--color-error-solid-hover","kind":"color","group":"semantic-color","value":"var(--color-error-700)","utilities":["bg-error-solid-hover","text-error-solid-hover","border-error-solid-hover","ring-error-solid-hover","fill-error-solid-hover"]},{"token":"error-solid-fg","property":"--color-error-solid-fg","kind":"color","group":"semantic-color","value":"var(--color-white)","utilities":["bg-error-solid-fg","text-error-solid-fg","border-error-solid-fg","ring-error-solid-fg","fill-error-solid-fg"]},{"token":"error-border","property":"--color-error-border","kind":"color","group":"semantic-color","value":"var(--color-error-300)","utilities":["bg-error-border","text-error-border","border-error-border","ring-error-border","fill-error-border"]},{"token":"error-border-strong","property":"--color-error-border-strong","kind":"color","group":"semantic-color","value":"var(--color-error-500)","utilities":["bg-error-border-strong","text-error-border-strong","border-error-border-strong","ring-error-border-strong","fill-error-border-strong"]},{"token":"error-fg","property":"--color-error-fg","kind":"color","group":"semantic-color","value":"var(--color-error-700)","utilities":["bg-error-fg","text-error-fg","border-error-fg","ring-error-fg","fill-error-fg"]},{"token":"error-icon","property":"--color-error-icon","kind":"color","group":"semantic-color","value":"var(--color-error-600)","utilities":["bg-error-icon","text-error-icon","border-error-icon","ring-error-icon","fill-error-icon"]},{"token":"on-info","property":"--color-on-info","kind":"color","group":"other","value":"var(--color-info-solid-fg)","utilities":["bg-on-info","text-on-info","border-on-info","ring-on-info","fill-on-info"]},{"token":"on-success","property":"--color-on-success","kind":"color","group":"other","value":"var(--color-success-solid-fg)","utilities":["bg-on-success","text-on-success","border-on-success","ring-on-success","fill-on-success"]},{"token":"on-warning","property":"--color-on-warning","kind":"color","group":"other","value":"var(--color-warning-solid-fg)","utilities":["bg-on-warning","text-on-warning","border-on-warning","ring-on-warning","fill-on-warning"]},{"token":"on-error","property":"--color-on-error","kind":"color","group":"other","value":"var(--color-error-solid-fg)","utilities":["bg-on-error","text-on-error","border-on-error","ring-on-error","fill-on-error"]},{"token":"on-primary","property":"--color-on-primary","kind":"color","group":"other","value":"var(--color-primary-solid-fg)","utilities":["bg-on-primary","text-on-primary","border-on-primary","ring-on-primary","fill-on-primary"]},{"token":"on-secondary","property":"--color-on-secondary","kind":"color","group":"other","value":"var(--color-secondary-solid-fg)","utilities":["bg-on-secondary","text-on-secondary","border-on-secondary","ring-on-secondary","fill-on-secondary"]},{"token":"on-accent","property":"--color-on-accent","kind":"color","group":"other","value":"var(--color-accent-solid-fg)","utilities":["bg-on-accent","text-on-accent","border-on-accent","ring-on-accent","fill-on-accent"]},{"token":"on-neutral","property":"--color-on-neutral","kind":"color","group":"other","value":"var(--color-neutral-solid-fg)","utilities":["bg-on-neutral","text-on-neutral","border-on-neutral","ring-on-neutral","fill-on-neutral"]},{"token":"sans","property":"--font-sans","kind":"font","group":"font","value":"'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'","utilities":["font-sans"]},{"token":"mono","property":"--font-mono","kind":"font","group":"font","value":"'JetBrains Mono', ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace","utilities":["font-mono"]},{"token":"fast","property":"--duration-fast","kind":"duration","group":"duration","value":"150ms","utilities":["duration-fast"]},{"token":"normal","property":"--duration-normal","kind":"duration","group":"duration","value":"200ms","utilities":["duration-normal"]}],"content":{"conventions":"# ngx-tw conventions\n\nRules for writing code that matches how the library is built and styled. These\napply to consumer code too — following them keeps an application visually\nconsistent with the components it embeds.\n\n## Selectors and imports\n\n- Element selectors are prefixed `tw-` (`tw-card`, `tw-select`).\n- Directive selectors use a camelCase `tw` prefix as an attribute\n (`twBadge`, `twButton`, `twTooltip`).\n- Class names carry **no** `Tw` prefix — `ButtonDirective`, `BadgeComponent`.\n Only shared *types* do (`TwColor`, `TwSize`).\n- Import per component: `import { BadgeComponent } from '@cdevhub/ngx-tw/badge'`.\n- Shared types come from `@cdevhub/ngx-tw/core`.\n\n## Color — semantic tokens only\n\nNever use raw Tailwind palette colors (`bg-blue-50`, `text-red-800`). Use\nsemantic roles on the 50–950 scale so the consumer's theme can retarget them.\n\n| Role | Use for |\n|---|---|\n| `primary` | primary brand actions, key UI elements |\n| `secondary` | secondary actions, supporting UI |\n| `accent` | decorative emphasis, highlights |\n| `neutral` | borders, backgrounds, subdued text |\n| `info` | informational messages, neutral highlights |\n| `success` | positive outcomes, confirmations |\n| `warning` | caution, attention needed |\n| `error` | critical issues, destructive actions |\n\nSo: `bg-info-50`, `text-error-800`, `border-primary-300`.\n\n### Surface, foreground and border tokens\n\nFor neutral/structural styling use these instead of `neutral-*` shades — they\nadapt to dark mode automatically.\n\n| Token | Kind | Use for |\n|---|---|---|\n| `surface` | bg | default page/component background |\n| `surface-raised` | bg | elevated elements (cards, modals) |\n| `surface-overlay` | bg | overlays, popovers |\n| `surface-sunken` | bg | recessed areas (code blocks, wells) |\n| `surface-muted` | bg | subtle backgrounds (gutters, inactive tabs, headers) |\n| `fg` | text | primary text, high contrast |\n| `fg-muted` | text | secondary text, descriptions, subtitles |\n| `fg-subtle` | text | tertiary text, placeholders, line numbers |\n| `border` | border | standard structural dividers, panel edges |\n| `border-muted` | border | very subtle |\n| `border-strong` | border | emphasized |\n\nRule of thumb: color-specific variants use `{color}-{shade}` with explicit\n`dark:` overrides; neutral structural styling uses surface/fg/border tokens,\nwhich need no `dark:` variant.\n\n## Border radius\n\n| Token | Use for |\n|---|---|\n| `rounded-md` | small interactive: buttons, badges, dismiss buttons, pill tab triggers |\n| `rounded-lg` | standard containers: alerts, cards, panels, enclosed tabs, code blocks |\n| `rounded-xl` | outer wrappers with internal rounded children: pill tablists |\n| `rounded-full` | circular: avatars, dot indicators |\n| `rounded-none` | explicit \"no radius\" |\n\nDo not use `rounded`, `rounded-sm`, `rounded-2xl`, or `rounded-3xl`.\n\n## Spacing\n\nContainer padding, mapped to the `size` input:\n\n| Size | Padding |\n|---|---|\n| `xs` | `p-2` |\n| `sm` | `p-3` |\n| `md` | `p-4` |\n| `lg` | `p-6` |\n| `xl` | `p-8` |\n\nInline element padding (buttons, tab triggers, badges):\n\n| Size | Padding |\n|---|---|\n| `xs` | `px-2 py-1` |\n| `sm` | `px-3 py-1.5` |\n| `md` | `px-4 py-2` |\n| `lg` | `px-5 py-2.5` |\n| `xl` | `px-6 py-3` |\n\nGaps: `gap-1` (pill tabs in a tablist), `gap-1.5` (icon + label in a trigger),\n`gap-2` (action button groups, small lists), `gap-3` (icon + content in alerts,\navatar + title in headers). Do not use `gap-0.5`, `gap-4`, or larger — reach for\ncontainer padding instead.\n\n## Typography\n\n| Role | Size | Weight |\n|---|---|---|\n| Body text, alert content | `text-sm` | normal |\n| Titles, header labels | `text-sm` | `font-semibold` |\n| Subtitles, descriptions | `text-sm` | normal + `text-fg-muted` |\n| Interactive triggers (tabs, buttons) | `text-sm` at md, scales with size | `font-medium` |\n| Captions, metadata, footers | `text-xs` | normal |\n| xs-density secondary text | `text-2xs` | normal |\n| Monospace content | `font-mono text-sm` | normal |\n\nTrigger font scale: `xs` → `text-xs`, `sm`–`md` → `text-sm`, `lg`–`xl` → `text-base`.\n\n`text-2xs` (0.6875rem / 11px) is the smallest permitted step. Never use\narbitrary font sizes like `text-[11px]`. Headings inside projected content are\nthe consumer's responsibility — library components do not use `text-lg` or larger.\n\n## Focus rings\n\nEvery interactive element needs a visible focus indicator. The canonical pattern:\n\n```\nfocus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500\n```\n\nAlways `focus-visible`, never bare `focus` — mouse users should not see focus\nrings. For selected/active states that persist, use\n`ring-2 ring-offset-2 ring-primary-500`.\n\nTwo carve-outs use a background shift (`focus-visible:bg-surface-muted`) instead:\nelements with role `menuitem` / `menuitemcheckbox` / `menuitemradio`, and\n`role=\"option\"` inside a combobox+listbox driven by `aria-activedescendant`\n(where the option never receives DOM focus, so `focus-visible:` never fires).\n\n## Icon sizing\n\nFour sub-scales — pick by role, never mix them.\n\n**Glyph icons** (inline alongside text): `size-4` (16px) in captions and small\nbuttons, `size-5` (20px) standard, `size-10` (40px) large standalone/avatars.\n`<tw-icon>` parametrises this: `xs`→`size-3`, `sm`→`size-4`, `md`→`size-5`,\n`lg`→`size-6`, `xl`→`size-8`.\n\n**Square interactive targets** (icon-only buttons where the container is the\ntouch target): `xs`→`size-6`, `sm`→`size-7`, `md`→`size-8`, `lg`→`size-9`.\n\n**Dot indicators** (non-interactive status markers): `xs`→`size-2`,\n`sm`→`size-2.5`, `md`→`size-3`.\n\nAlways add `shrink-0` to icons in flex containers. Use `mt-0.5` on icons beside\nmulti-line text to align with the first line.\n\n## Shadows, borders, states\n\nShadows: `shadow-sm` (subtle lift), `shadow` (standard elevation),\n`shadow-md` (prominent / hover). Never `shadow-lg` or larger — components are\nflat by default and shadows mean explicit elevation.\n\nBorders are 1px: `border-border` structural, `border-border-strong` emphasized,\n`border-border-muted` subtle, `border-{color}-300` for semantic outlines. 2px\n(`border-b-2`, `border-r-2`) is reserved for active-state indicators like tab\nunderlines.\n\nHover: cards deepen shadow (`hover:shadow-md`); outlined surfaces darken border\n(`hover:border-border-strong`); filled/ghost surfaces shift background\n(`hover:bg-surface-muted`); text triggers use `hover:text-fg`.\n\nDisabled: `opacity-50` with `pointer-events-none` or `cursor-not-allowed`. Within\na group, `disabled:opacity-30 disabled:cursor-default`. For subdued text prefer\n`text-fg-muted` / `text-fg-subtle` over opacity.\n\nFlex children that may truncate need `min-w-0`. Rounded containers clipping\nchildren need `overflow-hidden`.\n\n## Transitions and animation\n\n| Duration | Use for |\n|---|---|\n| `duration-150` | fast micro-interactions |\n| `duration-200` | standard hover/focus transitions |\n| `duration-normal` | theme-overridable alias for 200ms |\n\nUse `transition-colors`, `transition-shadow`, or an explicit property list like\n`transition-[color,shadow]`. **Never `transition-all`.** Append\n`motion-reduce:transition-none` for reduced-motion support.\n\nDo **not** use `@angular/animations` (deprecated in v20.2, removed in v23). Use\nAngular's native `animate.enter` / `animate.leave`:\n\n```html\n<div animate.enter=\"fade-in\" animate.leave=\"fade-out\">…</div>\n```\n\nMultiple classes are space-separated (`animate.enter=\"slide-in fade-in\"`).\nKeyframes ship in the theme CSS; components reference class names only.\n\n## Angular idioms\n\n- Standalone components only; no NgModules. Do not set `standalone: true` — it\n is the default in v22.\n- Signal APIs: `input()`, `output()`, `model()` for two-way binding.\n- `computed()` for read-only derived state; `linkedSignal()` for writable state\n that defaults to a source but can be overridden by user interaction.\n- `ChangeDetectionStrategy.OnPush` on every component.\n- `inject()` for DI, not constructor injection.\n- The `host` object for host bindings — never `@HostBinding` / `@HostListener`.\n- Native control flow `@if` / `@for` / `@switch`; class and style bindings\n rather than `ngClass` / `ngStyle`.\n- No arrow functions in templates.\n- Never mutate a signal inside an `effect()` that the same effect reads — that\n cycle is the most common way to freeze a component. Use `computed()` or\n `linkedSignal()` to derive state; reserve `effect()` for side effects that\n leave the signal graph (DOM, focus, announcements, storage).\n\n## Variant styling\n\nVariant-driven classes use `tailwind-variants` (`tv()`), with `slots` for\nmulti-part components, `defaultVariants` always defined, and `twMerge: true` so\nconsumer class overrides resolve correctly. Never concatenate class strings by\nhand.\n\n## Accessibility\n\nEvery component must meet WCAG AA and pass AXE checks. Use Angular CDK's a11y\nutilities (`FocusMonitor`, `FocusTrap`, `LiveAnnouncer`, `AriaDescriber`) rather\nthan reimplementing them. Every interactive component defines keyboard behavior.\nColor alone must never carry meaning — pair it with an icon or text.\n","gettingStarted":"# Getting started with ngx-tw\n\n## Install\n\n```bash\nnpm install @cdevhub/ngx-tw @angular/cdk tailwindcss tailwind-variants\n```\n\nPeer requirements:\n\n| Package | Version |\n|---|---|\n| `@angular/core`, `@angular/common` | `^22.0.0` |\n| `@angular/cdk` | `^22.0.0` |\n| `tailwindcss` | `^4.0.0` |\n| `tailwind-variants` | `^0.3.0` |\n\nForm controls additionally use `@angular/forms` (ships with Angular). Node\n`^22.22.3`, `^24.15.0`, or `>=26.0.0`.\n\n## Theme CSS — one import\n\nTailwind v4 has no JS config file; all customization happens in CSS.\n\n```css\n/* src/styles.css */\n@import '@cdevhub/ngx-tw/theme/index.css';\n```\n\nThat single line pulls in Tailwind itself, the semantic tokens, and a `@source`\ndirective pointing at the library's compiled bundles. The last part matters:\nTailwind v4 never scans `node_modules` on its own, so without it a build emits\ntokens but **no component utilities**. Do not add a `@source` line yourself, and\ndo not add a separate `@import 'tailwindcss'` (a duplicate is harmless if one is\nalready there).\n\nFor any overlay-based component — dialog, sheet, menu, popover, select, tooltip,\ncombobox, date-picker, time-picker, command-palette — also import the CDK\noverlay stylesheet:\n\n```css\n@import '@angular/cdk/overlay-prebuilt.css';\n```\n\n## Providers\n\nSeveral subsystems are provider-based. Register only the ones in use:\n\n```ts\n// src/app/app.config.ts\nimport { ApplicationConfig } from '@angular/core';\nimport { provideTheme } from '@cdevhub/ngx-tw/theme';\nimport { provideNativeDateAdapter } from '@cdevhub/ngx-tw/calendar';\nimport { provideTwLucideIcons } from '@cdevhub/ngx-tw/icon/lucide';\nimport { provideTwDialog } from '@cdevhub/ngx-tw/dialog';\nimport { provideSheet } from '@cdevhub/ngx-tw/sheet';\nimport { provideToast } from '@cdevhub/ngx-tw/toast';\nimport { Star, Search, Settings } from 'lucide';\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideTheme(),\n provideNativeDateAdapter(),\n provideTwLucideIcons({ Star, Search, Settings }),\n provideTwDialog(),\n provideSheet(),\n provideToast({ position: 'bottom-right', duration: 4000 }),\n ],\n};\n```\n\n| Provider | Required for | Entry point |\n|---|---|---|\n| `provideTheme()` | runtime theme switching / `ThemeService` (optional) | `@cdevhub/ngx-tw/theme` |\n| `provideNativeDateAdapter()` | `calendar`, `date-picker`, `date-range-picker` | `@cdevhub/ngx-tw/calendar` |\n| `provideTwLucideIcons()` / `provideTwIcons()` | `<tw-icon>` and any component rendering glyphs | `@cdevhub/ngx-tw/icon/lucide`, `@cdevhub/ngx-tw/icon` |\n| `provideTwDialog()` | imperative dialogs via the `TwDialog` service | `@cdevhub/ngx-tw/dialog` |\n| `provideSheet()` | imperative side sheets via the `Sheet` service | `@cdevhub/ngx-tw/sheet` |\n| `provideToast()` | `ToastService` notifications | `@cdevhub/ngx-tw/toast` |\n\n## Importing components\n\nImport from the per-component entry point for the best tree-shaking. Components\nare standalone — list them in the consuming component's `imports` array.\n\n```ts\nimport { Component } from '@angular/core';\nimport { ButtonDirective } from '@cdevhub/ngx-tw/button';\nimport { AlertComponent } from '@cdevhub/ngx-tw/alert';\n\n@Component({\n selector: 'app-root',\n imports: [ButtonDirective, AlertComponent],\n template: `\n <button twButton color=\"primary\">Save</button>\n <tw-alert color=\"success\">Changes saved.</tw-alert>\n `,\n})\nexport class AppComponent {}\n```\n\nA root barrel (`@cdevhub/ngx-tw`) re-exports everything for convenience, but the\nper-component path is preferred.\n\n## Icons — the registration model, not a catalogue\n\n`<tw-icon>` uses a **registry**: a glyph renders only after its name has been\nregistered by the consuming application. There is no built-in icon set, so\n**valid `name` values are whatever that application registered** — never assume\na name exists.\n\nThe Lucide adapter is the quickest path:\n\n```ts\nimport { provideTwLucideIcons } from '@cdevhub/ngx-tw/icon/lucide';\nimport { Star, Search } from 'lucide';\n\n// in providers:\nprovideTwLucideIcons({ Star, Search });\n```\n\n```html\n<tw-icon name=\"Star\" size=\"md\" />\n```\n\nBring your own SVGs with `provideTwIcons()` / `IconRegistry` from\n`@cdevhub/ngx-tw/icon`.\n\n## Theming\n\nComponents never reference raw palette colors — only semantic roles on a 50–950\nscale (`bg-primary-500`, `text-error-800`) plus structural tokens (`surface`,\n`surface-raised`, `fg`, `fg-muted`, `border`). Re-theme the whole library by\noverriding those tokens:\n\n```css\n@import '@cdevhub/ngx-tw/theme/index.css';\n\n@theme {\n --color-primary-500: oklch(0.55 0.2 260);\n --color-primary-600: oklch(0.48 0.2 260);\n --color-info-50: var(--color-sky-50);\n --color-info-500: var(--color-sky-500);\n}\n```\n\nDark mode is driven by a `data-theme` attribute on `<html>`, with a\n`prefers-color-scheme` fallback when no attribute is set — so the simplest setup\nneeds no JavaScript. For explicit, persisted switching, register `provideTheme()`\nand inject `ThemeService` (`theme`, `resolvedTheme`, `isDark` signals;\n`setTheme()`, `cycleTheme()`). A `[twTheme]` directive scopes a theme to a\nsubtree.\n\n## Date & time\n\nCalendar and date pickers require a date adapter:\n\n```ts\nimport { provideNativeDateAdapter } from '@cdevhub/ngx-tw/calendar';\n// providers: [ provideNativeDateAdapter() ]\n```\n\nA Luxon adapter lives at `@cdevhub/ngx-tw/calendar/luxon`, test helpers at\n`@cdevhub/ngx-tw/calendar/testing`. Calendar UI strings are localizable via\n`CalendarIntl` / `provideCalendarIntl`.\n\n## Form controls\n\n`input`, `textarea`, `select`, `combobox`, `checkbox`, `radio`, `switch`,\n`slider`, `date-picker`, `date-range-picker`, and `time-picker` implement\n`ControlValueAccessor` and work with **all three** Angular form strategies —\ntemplate-driven, reactive, and signal forms. The library does not prescribe one.\nPair them with `tw-form-field` for labels, hints, and error messages.\n"}}